分布式实时消息平台NSQ- 整体架构
介绍
NSQ是一个基于Go语言的分布式实时消息平台,其设计目标是为在分布式环境下运行的去中心化服务提供一个强大的基础架构。
NSQ具有分布式、去中心化的拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。NSQ非常容易配置和部署,且具有最大的灵活性,支持众多消息协议。
术语
- Topic: 一个可供订阅的话题,是消息的分类概念。
- channel: 一个通道(channel)是消费者订阅了某个话题的逻辑分组. 一个Topic有可以分为多个Channel,每当一个发布者发送一条消息到一个topic,消息会被复制到所有消费者连接的channel上,从而实现多播分发,而channel上的每个消息被分发给它的订阅者,从而实现负载均衡。实际上,在消费者第一次订阅时就会创建channel。Channel会将消息进行排列,如果没有消费者读取消息,消息首先会在内存中排队,当量太大时就会被保存到磁盘中。
NSQ组件
- nsqd 是接收、队列和传送消息到客户端的守护进程。
- nsqlookupd 是管理的拓扑信息,并提供了最终一致发现服务的守护进程。
- nsqadmin 是一个 Web UI 来实时监控集群(和执行各种管理任务)。
NSQ架构
nsqlookupd,nsqd与客户端中消费者和生产者如何进行交互、沟通的呢?
- 生产者:必须直连nsqd去投递message
- 消费者:
- 消费者直连nsqd,这是最简单的方式,缺点是nsqd服务无法实现动态伸缩了.
- 消费者通过http查询nsqlookupd获取该nsqlookupd上所有nsqd的连接地址,然后再分别和这些nsqd建立连接(官方推荐的做法).
NSQD
一个负责接收、排队、转发消息到客户端的守护进程。是NSQ的核心部分,它主要负责message的收发,队列的维护。nsqd会默认监听一个tcp端口(4150)和一个http端口(4151)以及一个可选的https端口。当一个nsqd节点启动时,它向一组nsqlookupd节点进行注册操作,并将保存在此节点上的topic和channel进行广播。
客户端可以发布消息到nsqd守护进程上,或者从nsqd守护进程上读取消息。通常,消息发布者会向一个单一的local nsqd发布消息,消费者从连接了的一组nsqd节点的topic上远程读取消息。
总的来说,nsqd 具有以下功能或特性:
- 对订阅了同一个topic,同一个channel的消费者使用负载均衡策略(不是轮询)
- 只要channel存在,即使没有该channel的消费者,也会将生产者的message缓存到队列中(注意消息的过期处理)
- 保证队列中的message至少会被消费一次,即使nsqd退出,也会将队列中的消息暂存磁盘上(结束进程等意外情况除外)限定内存占用,能够配置nsqd中每个channel队列在内存中缓存的message数量,一旦超出,message将被缓存到磁盘中,topic,channel一旦建立,将会一直存在,要及时在管理台或者用代码清除无效的topic和channel,避免资源的浪费
NSQLOOKUP
nsqlookupd是守护进程负责管理拓扑信息。客户端通过查询 nsqlookupd 来发现指定话题(topic)的生产者,并且 nsqd 节点广播话题(topic)和通道(channel)信息。
简单的说nsqlookupd就是中心管理服务,它使用tcp(默认端口4160)管理nsqd服务,使用http(默认端口4161)管理nsqadmin服务。同时为客户端提供查询功能
总的来说,nsqlookupd具有以下功能或特性:
- 唯一性,在一个Nsq服务中只有一个nsqlookupd服务。当然也可以在集群中部署多个 nsqlookupd,但它们之间是没有关联的
- 去中心化,即使nsqlookupd崩溃,也会不影响正在运行的nsqd服务
- 充当nsqd和naqadmin信息交互的中间件
- 提供一个http查询服务,给客户端定时更新nsqd的地址目录
NSQADMIN
是一套 WEB UI,用来汇集集群的实时统计,并执行不同的管理任务。nsqadmin具有以下功能或特性:
- 提供一个对topic和channel统一管理的操作界面以及各种实时监控数据的展示,界面设计的很简洁,操作也很简单
- 展示所有message的数量
- 能够在后台创建topic和channel
- nsqadmin的所有功能都必须依赖于nsqlookupd,nsqadmin只是向nsqlookupd传递用户操作并展示来自nsqlookupd的数据
Run起来
下载源码
# 下载代码
git clone git@github.com:nsqio/nsq.git
# 编译服务,编译出来的二进制会在对应代码路径下的 build 目录下
make
启动服务
# nsqlookupd
nohup /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqlookupd > /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqlookupd.log &
# nsqd
nohup /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqd -lookupd-tcp-address 127.0.0.1:4160 > /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqd.log &
# nsqadmin
nohup /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqadmin -lookupd-http-address 127.0.0.1:4161 > /Users/yichuan.deng/workspace/go/src/github.com/nsqio/nsq/build/nsqadmin.log &
- 我们在本地启动了三个服务:nsqlookupd,nsqd,nsqadmin。nsqlookupd监听了默认的 TCP:4160 ,HTTP:4161 端口。nsqd 监听了默认的TCP:4150 ,HTTP:4151 端口。nsqadmin 监听了默认的 4171端口。可以访问 http://127.0.0.1:4171/ 查看服务的一些信息。
- 此时 nsqd 会向 nsqlookupd 注册服务。通过 nsqlookupd 的 http 接口:http://127.0.0.1:4161/nodes 可以查询到注册的 nsqd。
数据发送和测试程序
package main
import (
"bufio"
"fmt"
"os"
"time"
"github.com/nsqio/go-nsq"
)
var producer *nsq.Producer
var nsqdServer = "127.0.0.1:4150"
var nsqLookupServer = "127.0.0.1:4161"
var topic = "test"
func main(){
go ConsumerMain()
ProducerMain()
}
func ProducerMain() {
InitProducer(nsqdServer)
running := true
reader := bufio.NewReader(os.Stdin)
for running {
data, _, _ := reader.ReadLine()
command := string(data)
if command == "stop" {
running = false
}
if err := Publish(topic, command); err != nil{
fmt.Println("err: ", err)
return
}
}
producer.Stop()
}
func InitProducer(str string) {
var err error
fmt.Println("address: ", str)
producer, err = nsq.NewProducer(str, nsq.NewConfig())
if err != nil {
panic(err)
}
}
func Publish(topic string, message string) error {
var err error
if producer != nil {
if message == "" {
return nil
}
err = producer.Publish(topic, []byte(message))
return err
}
return fmt.Errorf("producer is nil:%+v", err)
}
type Consumer struct{}
func ConsumerMain() {
InitConsumer(topic, "test-channel", nsqLookupServer)
for {
time.Sleep(time.Second * 10)
}
}
func (*Consumer) HandleMessage(msg *nsq.Message) error {
fmt.Println("receive", msg.NSQDAddress, "message:", string(msg.Body))
return nil
}
func InitConsumer(topic string, channel string, address string) {
cfg := nsq.NewConfig()
cfg.LookupdPollInterval = time.Second
c, err := nsq.NewConsumer(topic, channel, cfg)
if err != nil {
panic(err)
}
c.SetLogger(nil, 0)
c.AddHandler(&Consumer{})
// 建立NSQLookup连接
if err := c.ConnectToNSQLookupd(address); err != nil {
panic(err)
}
}
参考文档
- How we redesigned the NSQ - NSQ重塑之详细设计
- https://nsq.io/
- https://segment.com/blog/scaling-nsq/
- NSQ源码解析(1)–设计理念浅析