docker pull nsqio/nsq
服務埠及關係
topic & channel
叢集模式
預準備
因為是在單機上通過 docker
容器實現多節點部署,nsqd/nsqadmin
的容器想要與 nsqlookup
通訊,需要訪問 nsqlookup
在宿主機上暴露的服務埠,所以我們在建立nsqd/nsqadmin
容器時與 nsqlookup
的通訊相關的地址都要填寫宿主機 ip
。
# 獲取宿主機內網ip
ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v inet6|awk '{print $2}'|tr -d "addr:"
比如是我的是 10.10.31.147
,後續注意替換。
nsqlookupd
# 4160 tcp 供 nsqd 註冊用
# 4161 http 供 nsqdadmin 和 consumer 查詢服務名字
docker run -d --name nsqlookupd \
-p 4160:4160 -p 4161:4161 \
nsqio/nsq /nsqlookupd
nsqd
建立兩個 nsqd 節點
nsq 有兩個 producer 服務埠 tcp-address 和 http-address
# --broadcast-address 節點主機地址 用來供外放訪問
# 下文設為宿主機IP 以便admin訪問統計例項狀態
# --tcp-address tcp 協議的 producer 埠
# --http-address http 協議的 producer 埠
# --lookupd-tcp-address lookupd 的 tcp 地址
# --data-path 資料持久化儲存路徑
# nsq0 tcp://127.0.0.1:4150/ http://127.0.0.1:4151/
docker run -d -v /tmp/nsq0:/tmp/nsq \
-p 4150:4150 -p 4151:4151 \
--name nsqd0 nsqio/nsq /nsqd \
--tcp-address :4150 \
--http-address :4151 \
--broadcast-address=10.10.31.147 \
--lookupd-tcp-address=10.10.31.147:4160 \
--data-path /tmp/nsq
# nsq1 tcp://127.0.0.1:4250/ http://127.0.0.1:4251/
docker run -d -v /tmp/nsq1:/tmp/nsq \
-p 4250:4250 -p 4251:4251 \
--name nsqd1 nsqio/nsq /nsqd \
--tcp-address :4250 \
--http-address :4251 \
--broadcast-address=10.10.31.147 \
--lookupd-tcp-address=10.10.31.147:4160 \
--data-path /tmp/nsq
nsqadmin
# 4171 admin管理平臺服務埠
# --lookupd-http-address lookupd 的 http 地址
docker run -d --name nsqadmin \
-p 4171:4171 nsqio/nsq /nsqadmin \
--lookupd-http-address=10.10.31.147:4161
topic
# 建立主題
curl -X POST http://127.0.0.1:4151/topic/create?topic=test
curl -X POST http://127.0.0.1:4251/topic/create?topic=test
channel
# channel 相當於消費組 channel 與 topic 之間相當於訂閱釋出的關係
curl -X POST 'http://127.0.0.1:4151/channel/create?topic=test&channel=chan_4151_1'
curl -X POST 'http://127.0.0.1:4151/channel/create?topic=test&channel=chan_4151_2'
curl -X POST 'http://127.0.0.1:4251/channel/create?topic=test&channel=chan_4251_1'
curl -X POST 'http://127.0.0.1:4251/channel/create?topic=test&channel=chan_4251_2'
消費者
這裡借用 nsqlookupd
容器內的 nsq
相關命令指令碼。nsq_to_file
作為消費者,通過查詢 lookupd
來獲取所有包含指定 topic
的節點,並繫結 channel
,當有生產者向此 topic
傳送訊息時,訂閱的 channel
繼而消費。
# 通過查詢 lookupd 獲取所有的 topic
# 訂閱 topic > channel
docker exec -it nsqlookupd nsq_to_file \
--topic=test --channel=chan_4151_1 \
--output-dir=/tmp/chan_4151_1 \
--lookupd-http-address=127.0.0.1:4161
docker exec -it nsqlookupd nsq_to_file \
--topic=test --channel=chan_4251_1 \
--output-dir=/tmp/chan_4251_1 \
--lookupd-http-address=127.0.0.1:4161
生產者
# topic 釋出訊息 每個 channel 會受到此訊息
# 且負載輪訓分配給channel下的其中一個消費者
curl -d 'hello world 4151' 'http://127.0.0.1:4151/pub?topic=test'
curl -d 'hello world 4251' 'http://127.0.0.1:4251/pub?topic=test'
高可用場景
副本
、冪等消費
高可用叢集,自然少不了副本的概念,但 nsq 的叢集沒有節點資料同步機制,不像其他高階佇列一樣有同步資料維護副本的概念,所以 nsq 的副本需要我們在程式碼層面維護實現。
拿 nsqd0 nsqd1
兩個節點舉例,如何實現叢集高可用呢?在 nsqd0 nsqd1
建立同名的 topic_ha & channel_replic
,並在投遞訊息時,同時向兩個節點都傳送。
消費者通過 lookupd
模式訂閱消費時,可以訂閱所有包含此 topic_ha
的節點的 channel_replic
。這時通過向 nsqd0
和 nsqd1
傳送相同訊息時,topic_ha
就維護出一個備份副本來,做訊息冪等消費,防止重複處理,在其中一個 nsqd
節點掛掉時,我們仍可以正常的投遞和消費業務訊息。
curl -X POST http://127.0.0.1:4151/topic/create?topic=test_ha
curl -X POST http://127.0.0.1:4251/topic/create?topic=test_ha
curl -X POST 'http://127.0.0.1:4151/channel/create?topic=test_ha&channel=chan_replic'
curl -X POST 'http://127.0.0.1:4251/channel/create?topic=test_ha&channel=chan_replic'
# 或者使用文中最後的 go 程式碼體驗叢集投遞/消費的概念
docker exec -it nsqlookupd nsq_to_file \
--topic=test_ha --channel=chan_replic \
--output-dir=/tmp/chan_replic \
--lookupd-http-address=127.0.0.1:4161
可以看到通過 nsqlookupd
獲取到所有含有此 topic&channel
的節點
2022/03/03 09:49:34 INF 1 [test_ha/chan_replic] querying nsqlookupd http://127.0.0.1:4161/lookup?topic=test_ha
2022/03/03 09:49:34 INF 1 [test_ha/chan_replic] (10.10.31.147:4150) connecting to nsqd
2022/03/03 09:49:34 INF 1 [test_ha/chan_replic] (10.10.31.147:4250) connecting to nsqd
基礎概念
- nsq 的高可用叢集,並沒有自動同步副本的功能,即你有N個節點,則你需要在N個節點上建立同名的 topic,在投遞訊息時也需要向這N個節點分別投遞一次訊息。
- nsq 的消費,最佳方法為消費者連線 lookupd 服務,查詢訂閱的 topic 都分佈在哪些節點,消費者以 topic 為主,會訂閱所有的包含 topic 節點的訊息資料。
- channel 就是消費組,組內負載均衡訊息佇列,組間互為訂閱釋出。好比 kafka 的低階消費組一樣。加入相同消費組,負載均衡消費,不同消費組之前互為 topic 的訂閱者。
- 同一節點,訂閱 相同 topic 相同 channel 則為加入消費組,組內負載均衡消費。
- 同一節點,訂閱 相同 topic 不同 channel 則為訂閱釋出,每個消費組內至少有一個消費者能得到訊息。
例項(叢集投遞/消費)
nsqProducer
我這裡也封裝了通過 lookupd
自動獲取所有包含 topic
的 nsqd
並建立 tcpProducer
。這樣向一個分佈在多個 nsqd
節點上 topic
投遞訊息時,就不需要挨個手寫投遞了。
package main
import (
"encoding/json"
"errors"
"flag"
"github.com/nsqio/go-nsq"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
)
var TopicProducers map[string][]*nsq.Producer
type LookupTopicRes struct {
Channels []string `json:"channels"`
Producers []ProducerInfo `json:"producers"`
}
type ProducerInfo struct {
RemoteAddress string `json:"remote_address"`
Hostname string `json:"hostname"`
BroadcastAddress string `json:"broadcast_address"`
TcpPort int `json:"tcp_port"`
HttpPort int `json:"http_port"`
Version string `json:"version"`
}
// nsq 內部日誌
type nsqServerLogger struct {
}
func (nsl *nsqServerLogger) Output(callDepth int, s string) error {
log.Println("nsqServerLogger", callDepth, s[:3], strings.Trim(s[3:], " "))
return nil
}
func main() {
var topic string
flag.StringVar(&topic, "topic", "test", "topic name default test")
flag.Parse()
// 為每個包含topic的nsqd節點 建立1個生產者
NewTopicProducer(topic)
go func() {
timerTicker := time.Tick(2 * time.Second)
for {
<-timerTicker
totalNode, failedNode, err := PublishTopicMsg(topic, []byte("hello nsq "+time.Now().Format("2006-01-02 15:04:05")))
if err != nil {
log.Fatalln("PublishTopicMsg err topic", topic, "err", err.Error())
}
log.Println("PublishTopicMsg ok topic", topic, "totalNode", totalNode, "failedNode", failedNode)
}
}()
// wait for signal to exit
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sigMsg := <-sigChan
log.Println("sigMsg", sigMsg)
// Gracefully stop the producer.
for _, producers := range TopicProducers {
for _, producer := range producers {
producer.Stop()
}
}
}
// NewTopicProducer
// 獲取 topic 所有的 nsqd 節點 並建立 tcp 連結
func NewTopicProducer(topic string) {
TopicProducers = make(map[string][]*nsq.Producer)
config := nsq.NewConfig()
topicNodeAddr := getTopicNodeAddrSet(topic)
var producers []*nsq.Producer
for _, addr := range topicNodeAddr {
producer, err := nsq.NewProducer(addr, config)
if err != nil {
log.Fatalln("newProducer err topic", topic, "err", err.Error())
}
producer.SetLogger(&nsqServerLogger{}, nsq.LogLevelDebug)
producers = append(producers, producer)
}
TopicProducers[topic] = producers
}
// PublishTopicMsg
// 向 topic 傳送訊息 會自動向每一個包含此 topic 的節點傳送 叢集模式
func PublishTopicMsg(topic string, msg []byte) (totalNode int, failedNode int, err error) {
producers, ok := TopicProducers[topic]
if !ok {
return 0, 0, errors.New("PublishTopicMsg err topic not exists")
}
totalNode = len(producers)
for _, producer := range producers {
errPub := producer.Publish(topic, msg)
if nil != errPub {
failedNode++
}
}
return
}
// 獲取 topic 的所在的 nsqd 節點集合
func getTopicNodeAddrSet(topic string) (topicNodeAddrArr []string) {
resp, _ := http.Get("http://127.0.0.1:4161/lookup?topic=" + topic)
defer func() {
_ = resp.Body.Close()
}()
bodyRaw, _ := ioutil.ReadAll(resp.Body)
lookupTopicRes := &LookupTopicRes{}
_ = json.Unmarshal(bodyRaw, &lookupTopicRes)
for _, producer := range lookupTopicRes.Producers {
topicNodeAddrArr = append(topicNodeAddrArr, producer.BroadcastAddress+":"+strconv.Itoa(producer.TcpPort))
}
return topicNodeAddrArr
}
nsqConsumer
使用通過 lookupd
自動獲得 topic
+ channel
的所有 nsqd
節點,並訂閱消費。
package main
import (
"flag"
"github.com/nsqio/go-nsq"
"log"
"os"
"os/signal"
"syscall"
)
type nsqMessageHandler struct{}
// HandleMessage implements the Handler interface.
func (h *nsqMessageHandler) HandleMessage(m *nsq.Message) error {
if len(m.Body) == 0 {
// Returning nil will automatically send a FIN command to NSQ to mark the message as processed.
// In this case, a message with an empty body is simply ignored/discarded.
return nil
}
// do whatever actual message processing is desired
log.Println("HandleMessage nsqd:", m.NSQDAddress, "msg:", string(m.Body))
// Returning a non-nil error will automatically send a REQ command to NSQ to re-queue the message.
return nil
}
func main() {
var topic string
var channel string
var count int
var consumerGroup []*nsq.Consumer
flag.StringVar(&topic, "topic", "test", "topic name default test")
flag.StringVar(&channel, "channel", "test", "channel name default test")
flag.IntVar(&count, "count", 1, "consumer count default 1")
flag.Parse()
// Instantiate a consumer that will subscribe to the provided channel.
config := nsq.NewConfig()
config.MaxInFlight = 10 // 一個消費者可同時接收的最多訊息數量
for i := 0; i < count; i++ {
consumer, err := nsq.NewConsumer(topic, channel, config)
if err != nil {
log.Fatalln("NewConsumer err:", err.Error())
}
// Set the Handler for messages received by this Consumer. Can be called multiple times.
// See also AddConcurrentHandlers.
consumer.AddHandler(&nsqMessageHandler{})
// 併發模式的消費處理 消費者會啟用 n 個協程處理訊息
consumer.AddConcurrentHandlers(&nsqMessageHandler{}, 10)
consumer.SetLogger(&nsqServerLogger{}, nsq.LogLevelDebug)
// Use nsqlookupd to discover nsqd instances.
// See also ConnectToNSQD, ConnectToNSQDs, ConnectToNSQLookupds.
// 會訂閱所有包含當前 topic 的 nsqd 例項
// 多用於叢集模式時 生產者向多個含有topic的例項同時傳送訊息
// 當其中部分例項掛到時 消費者仍可通過其它例項獲得訊息
// !此處要做訊息冪等處理!
err = consumer.ConnectToNSQLookupd("localhost:4161")
if err != nil {
log.Fatalln("ConnectToNSQLookupd err:", err.Error())
} else {
log.Println("ConnectToNSQLookupd success topic:", topic, "channel:", channel)
}
consumerGroup = append(consumerGroup, consumer)
}
// wait for signal to exit
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
sigMsg := <-sigChan
log.Println("sigMsg", sigMsg)
// Gracefully stop the consumer.
for _, consumer := range consumerGroup {
consumer.Stop()
}
}
執行
go run nsqConsumer.go -topic test_ha -channel chan_replic -count=2
go run nsqProducer.go -topic test_ha
兩個消費者同 chan
互為負載均衡構成 消費組A
,消費組A
依次訂閱 nsqd0, nsqd1
的 chan_replic
,生產者向 test_ha
叢集模式投遞,nsqd0, nsqd1
收到訊息後,會分別向 消費組A
投遞一次訊息,消費組A
內部至於由哪個消費者消費,取決於負載均衡。