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
内部至于由哪个消费者消费,取决于负载均衡。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。