Golang NSQ message queue usage practice

Keywords: Go git Load Balance Programmer message queue

I read a lot on the Internet and copied the official website README. I didn't explain many important things clearly. I had to study it myself.

My blog, keyword Less-Bug.com, welcome to pay attention.

Introduction to NSQ

  • nsqd: daemon, client communication. Default port 4150 (TCP) 4151 (HTTP)
  • Nsqlookupd: equivalent to a router. The client can discover producer and nsqd broadcast topics through it. An nsqlookupd can manage a group of nsqds. Default port: 4160 (TCP), 4161 (HTTP)
  • nsqadmin: online panel, which can be accessed directly through the browser. Default port: 4171

Starting from the command line

Binary files can be downloaded directly. Open three terminals and execute them respectively:

nsqlookupd
nsqd --lookupd-tcp-address=127.0.0.1:4160 --broadcast-address=127.0.0.1
nsqadmin --lookupd-http-address=127.0.0.1:4161

Use of go NSQ

I encapsulated a package:

package mq

import (
    "encoding/json"
    "fmt"
    "time"

    "github.com/nsqio/go-nsq"
    "go.uber.org/zap"
)

type MessageQueueConfig struct {
    NsqAddr         string
    NsqLookupdAddr  string
    SupportedTopics []string
}

type MessageQueue struct {
    config    MessageQueueConfig
    producer  *nsq.Producer
    consumers map[string]*nsq.Consumer
}

func NewMessageQueue(config MessageQueueConfig) (mq *MessageQueue, err error) {
    zap.L().Debug("New message queue")
    producer, err := initProducer(config.NsqAddr)
    if err != nil {
        return nil, err
    }
    consumers := make(map[string]*nsq.Consumer)
    for _, topic := range config.SupportedTopics {
        nsq.Register(topic,"default")
        consumers[topic], err = initConsumer(topic, "default", config.NsqAddr)
        if err != nil {
            return
        }
    }
    return &MessageQueue{
        config:    config,
        producer:  producer,
        consumers: consumers,
    }, nil
}

func (mq *MessageQueue) Run() {
    for name, c := range mq.consumers {
        zap.L().Info("Run consumer for " + name)
        // c.ConnectToNSQLookupd(mq.config.NsqLookupdAddr)
        c.ConnectToNSQD(mq.config.NsqAddr)
    }
}

func initProducer(addr string) (producer *nsq.Producer, err error) {
    zap.L().Debug("initProducer to " + addr)
    config := nsq.NewConfig()
    producer, err = nsq.NewProducer(addr, config)    
    return
}

func initConsumer(topic string, channel string, address string) (c *nsq.Consumer, err error) {
    zap.L().Debug("initConsumer to " + topic + "/" + channel)
    config := nsq.NewConfig()
    config.LookupdPollInterval = 15 * time.Second
    c, err = nsq.NewConsumer(topic, channel, config)
    return
}

func (mq *MessageQueue) Pub(name string, data interface{}) (err error) {
    body, err := json.Marshal(data)
    if err != nil {
        return
    }
    zap.L().Info("Pub " + name + " to mq. data = " + string(body))
    return mq.producer.Publish(name, body)
}

type Messagehandler func(v []byte)

func (mq *MessageQueue) Sub(name string, handler Messagehandler) (err error) {
    zap.L().Info("Subscribe " + name)
    v, ok := mq.consumers[name]
    if !ok {
        err = fmt.Errorf("No such topic: " + name)
        return
    }
    v.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {
        handler(message.Body)
        return nil
    }))
    return
}

Use example:

    m, err := mq.NewMessageQueue(mq.MessageQueueConfig{
        NsqAddr:         "127.0.0.1:4150",
        NsqLookupdAddr:  "127.0.0.1:4161",
        SupportedTopics: []string{"hello"},
    })

    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }

    m.Sub("hello", func(resp []byte) {
        zap.L().Info("S1 Got: " + string(resp))
    })
    m.Sub("hello", func(resp []byte) {
        zap.L().Info("S2 Got: " + string(resp))
    })
    m.Run()
    err = m.Pub("hello", "world")
    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }
    err = m.Pub("hello", "tom")
    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan
    os.Exit(0);

It is mainly for decoupling, so that if we change to a queue such as Kalfa, we can not touch the business code.

Output results:

2021-11-07T19:13:41.886+0800    DEBUG   mq/mq.go:29     New message queue
2021-11-07T19:13:41.886+0800    DEBUG   mq/mq.go:58     initProducer to 127.0.0.1:4150
2021-11-07T19:13:41.887+0800    DEBUG   mq/mq.go:65     initConsumer to hello/default
2021-11-07T19:13:41.887+0800    INFO    mq/mq.go:84     Subscribe hello
2021-11-07T19:13:41.887+0800    INFO    mq/mq.go:84     Subscribe hello
2021-11-07T19:13:41.887+0800    INFO    mq/mq.go:51     Run consumer for hello
2021/11/07 19:13:41 INF    2 [hello/default] (127.0.0.1:4150) connecting to nsqd
2021-11-07T19:13:41.887+0800    INFO    mq/mq.go:77     Pub hello to mq. data = "world"
2021/11/07 19:13:41 INF    1 (127.0.0.1:4150) connecting to nsqd
2021-11-07T19:13:41.888+0800    INFO    mq/mq.go:77     Pub hello to mq. data = "tom"
2021-11-07T19:13:41.888+0800    INFO    buqi-admin-backend/main.go:60   S1 Got: "world"
2021-11-07T19:13:41.888+0800    INFO    buqi-admin-backend/main.go:63   S2 Got: "tom"

From the output results, we can confirm the fact that different consumers who subscribe to the same topic and the same channel will be load balanced when messages flood in - each Handler will only receive one message.

Problems encountered

TOPIC_NOT_FOUND

There are two reasons.

One is case sensitivity. Topic names are case sensitive, so hello and hello are two different topics. When writing code, you should standardize the operation: extract constants and maintain a list of all topics.

Second, Topic is not created. After the first pub, the corresponding topic/channel can be created. It is recommended to write a script and call the / topic/create interface to create it at one time, otherwise the message will not be received until the subscription is retried the second time, resulting in unexpected delay.

Discovery client polling HTTP

This is because NsqLookupd itself is an intermediary that can manage a bunch of nsqd s with different IP addresses, so we can't always connect to only one nsq, so we need to poll to confirm which clients there are.

For small projects, you can bypass NsqLookupd:

        // c.ConnectToNSQLookupd(mq.config.NsqLookupdAddr)
        c.ConnectToNSQD(mq.config.NsqAddr)

How to make multiple consumers consume the same topic?

Obviously, according to the mechanism of nsq, we need to let consumers of the same topic use different channels. One way is to randomize the channel, for example, using a recursive increment as the channel name.

The second method is to define the channel name according to the purpose.

The third method: it is said that addconcurrent handlers can be used, which has not been studied.

The fourth method: We mediate the Handler and use a consumer to consume, but manually send the message to a custom pipeline in the application layer and let the pipeline filter process the message. I guess it also avoids some critical zone problems.

Let's try the fourth method. (code published to) GIST , Github username (Pluveto)

Implement pipelined Handler

package mq

import (
    "encoding/json"
    "fmt"
    "time"

    "github.com/nsqio/go-nsq"
    "go.uber.org/zap"
)

type MessageQueueConfig struct {
    NsqAddr         string
    NsqLookupdAddr  string
    EnableLookupd   bool
    SupportedTopics []string
}

type MessageQueue struct {
    subscribers map[string]Subscriber
    config      MessageQueueConfig
    producer    *nsq.Producer
}

type Messagehandler func(v []byte) bool

// The first node of LinkedHandlerNode is the head node, and the Handler must be nil
type LinkedHandlerNode struct {
    Handler  *Messagehandler
    Index    int
    NextNode *LinkedHandlerNode
}

type Subscriber struct {
    HandlerHeadNode *LinkedHandlerNode
    Consumer        *nsq.Consumer
    Handler         nsq.HandlerFunc
}

func createProducer(addr string) (producer *nsq.Producer, err error) {
    zap.L().Debug("initProducer to " + addr)
    config := nsq.NewConfig()
    producer, err = nsq.NewProducer(addr, config)
    return
}

func createConsumer(topic string, channel string, address string) (c *nsq.Consumer, err error) {
    zap.L().Debug("initConsumer to " + topic + "/" + channel)
    config := nsq.NewConfig()
    config.LookupdPollInterval = 15 * time.Second
    c, err = nsq.NewConsumer(topic, channel, config)
    return
}

func NewMessageQueue(config MessageQueueConfig) (mq *MessageQueue, err error) {    
    zap.L().Debug("New message queue")
    producer, err := createProducer(config.NsqAddr)
    if err != nil {
        return nil, err
    }
    subscribers := make(map[string]Subscriber)
    for _, topic := range config.SupportedTopics {
        nsq.Register(topic, "default")
        consumer, err := createConsumer(topic, "default", config.NsqAddr)
        if err != nil {
            return nil, err
        }
        // The header node does not participate in actual use, so Index = -1
        headNode := &LinkedHandlerNode{Index: -1}
        hubHandler := nsq.HandlerFunc(func(message *nsq.Message) error {
            // Call each Handler in a circular chain
            curNode := headNode.NextNode
            // Throw a warning when no user-defined Handler exists
            if(nil == curNode){
                return fmt.Errorf("No handler provided!")
            }
            for nil != curNode {
                msg := message.Body
                zap.S().Debugf("handler[%v] for %v is invoked", curNode.Index, topic)
                stop := (*curNode.Handler)(msg)
                if stop {
                    zap.S().Debugf("the message has stopped spreading ")
                    break
                }
                curNode = curNode.NextNode
            }
            return nil
        })
        consumer.AddHandler(hubHandler)
        subscribers[topic] = Subscriber{
            Consumer:        consumer,
            HandlerHeadNode: headNode,
        }
    }
    return &MessageQueue{
        config:      config,
        producer:    producer,
        subscribers: subscribers,
    }, nil
}

func (mq *MessageQueue) Run() {
    for name, s := range mq.subscribers {
        zap.L().Info("Run consumer for " + name)
        if mq.config.EnableLookupd {
            s.Consumer.ConnectToNSQLookupd(mq.config.NsqLookupdAddr)
        } else {
            s.Consumer.ConnectToNSQD(mq.config.NsqAddr)
        }
    }
}

func (mq *MessageQueue) IsTopicSupported(topic string) bool {

    for _, v := range mq.config.SupportedTopics {
        if v == topic {
            return true
        }
    }
    return false
}

// Pub sends a message to the message queue
func (mq *MessageQueue) Pub(topic string, data interface{}) (err error) {
    if !mq.IsTopicSupported(topic) {
        err = fmt.Errorf("unsupported topic name: " + topic)
        return
    }
    body, err := json.Marshal(data)
    if err != nil {
        return
    }
    zap.L().Info("Pub " + topic + " to mq. data = " + string(body))
    return mq.producer.Publish(topic, body)
}

// Sub subscribes to a message from the message queue
func (mq *MessageQueue) Sub(topic string, handler Messagehandler) (err error) {
    if !mq.IsTopicSupported(topic) {
        err = fmt.Errorf("unsupported topic name: " + topic)
        return
    }
    zap.L().Info("Subscribe " + topic)
    subscriber, ok := mq.subscribers[topic]
    if !ok {
        err = fmt.Errorf("No such topic: " + topic)
        return
    }
    // Arrive at the last valid linked list node
    curNode := subscriber.HandlerHeadNode
    for nil != curNode.NextNode {
        curNode = curNode.NextNode
    }
    // Create node
    curNode.NextNode = &LinkedHandlerNode{
        Handler:  &handler,
        Index:    1 + curNode.Index,
        NextNode: nil,
    }
    return
}

The idea here is to create a unique Handler for each consumer in advance, which will call each specific Handler in the linked list in turn. When a user subscribes to a Topic, the Handler provided by the user is added to the end of the linked list.

Use example:

    m, err := mq.NewMessageQueue(mq.MessageQueueConfig{
        NsqAddr:         "127.0.0.1:4150",
        NsqLookupdAddr:  "127.0.0.1:4161",
        SupportedTopics: []string{"hello"},
        EnableLookupd:   false,
    })

    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }

    m.Sub("hello", func(resp []byte) bool {
        zap.L().Info("S1 Got: " + string(resp))
        return false
    })
    m.Sub("hello", func(resp []byte) bool {
        zap.L().Info("S2 Got: " + string(resp))
        return true
    })
    m.Sub("hello", func(resp []byte) bool {
        zap.L().Info("S3 Got: " + string(resp))
        return false
    })
    m.Run()
    err = m.Pub("hello", "world")
    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }
    err = m.Pub("hello", "tom")
    if err != nil {
        zap.L().Fatal("Message queue error: " + err.Error())
    }

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    <-sigChan
    os.Exit(0)

Output:

2021-11-07T20:30:38.448+0800    DEBUG   mq/mq.go:40     New message queue
2021-11-07T20:30:38.448+0800    DEBUG   mq/mq.go:89     initProducer to 127.0.0.1:4150
2021-11-07T20:30:38.448+0800    DEBUG   mq/mq.go:96     initConsumer to hello/default
2021-11-07T20:30:38.448+0800    INFO    mq/mq.go:113    Subscribe hello
2021-11-07T20:30:38.448+0800    INFO    mq/mq.go:113    Subscribe hello
2021-11-07T20:30:38.448+0800    INFO    mq/mq.go:113    Subscribe hello
2021-11-07T20:30:38.448+0800    INFO    mq/mq.go:82     Run consumer for hello
2021/11/07 20:30:38 INF    2 [hello/default] (127.0.0.1:4150) connecting to nsqd
2021-11-07T20:30:38.454+0800    INFO    mq/mq.go:108    Pub hello to mq. data = "world"
2021/11/07 20:30:38 INF    1 (127.0.0.1:4150) connecting to nsqd
2021-11-07T20:30:38.455+0800    INFO    mq/mq.go:108    Pub hello to mq. data = "tom"
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:57     handler[0] for hello is invoked
2021-11-07T20:30:38.455+0800    INFO    buqi-admin-backend/main.go:60   S1 Got: "world"
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:57     handler[1] for hello is invoked
2021-11-07T20:30:38.455+0800    INFO    buqi-admin-backend/main.go:64   S2 Got: "world"
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:60     the message has stopped spreading 
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:57     handler[0] for hello is invoked
2021-11-07T20:30:38.455+0800    INFO    buqi-admin-backend/main.go:60   S1 Got: "tom"
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:57     handler[1] for hello is invoked
2021-11-07T20:30:38.455+0800    INFO    buqi-admin-backend/main.go:64   S2 Got: "tom"
2021-11-07T20:30:38.455+0800    DEBUG   mq/mq.go:60     the message has stopped spreading 
^C

You can see that when the Handler returns true, the message propagation can be blocked.

Posted by mabans on Mon, 08 Nov 2021 23:18:21 -0800