轻量级消息队列中间件 -- NSQ介绍及深度源码分析

NSQ介绍及源码分析


1简介

NSQ is a realtime distributed messaging platform designed to operate at scale, handling billions of messages per day.
It promotes distributed and decentralized topologies without single points of failure, enabling fault tolerance and high availability coupled with a reliable message delivery guarantee. See features & guarantees.
Operationally, NSQ is easy to configure and deploy (all parameters are specified on the command line and compiled binaries have no runtime dependencies). For maximum flexibility, it is agnostic to data format (messages can be JSON, MsgPack, Protocol Buffers, or anything else).

NSQ 是一个用Go语言实现的实时分布式消息处理平台,用于大规模系统中的实时消息服务,每天能够处理数十亿的消息。
NSQ具有分布式、去中心化的拓扑结构,该结构具有无单点故障、故障容错、高可用性以及能够保证消息的可靠传递的特征。
NSQ非常容易配置和部署(所有参数都可以在命令行上指定,编译后的二进制文件不需要额外运行时依赖)。灵活性强,支持众多消息格式(e.g. JSON, Protocol Buffer)。

2NSQ架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Ie1ofy1-1597635750968)(/download/attachments/201834509/image.png?version=7&modificationDate=1593525881099&api=v2 'image.png')]

2.1组成

  • producer 通过HTTP API或者TCP发布指定topic的消息到指定nsqd。

  • consumer 通过 TCP与nsqd建立连接,订阅指定的topic下的channel,每个消费者只能指定订阅一个channel。

  • nsqd:(broker) 负责接收、排队和分发传送消息到消费者,可集群部署。

  • nsqlookupd:(namesvr) 负责管理nsqd集群的拓扑信息,每个nsqd与所有nsqlookupd建立tcp连接,实时上报nsqd信息。它同时提供了服务发现的功能。可集群部署,但nsqlookupd之间是没有任何信息交互的。

  • nsqadmin:是一个实时监控和管理nsqd集群的 Web UI服务,一般只部署一个。

  • topic 是NSQ消息发布和订阅的主题关键词,生产者发布消息时,每个消息必须指定一个topic。nsqd集群部署时,同一个topic可以放到不同的nsqd中,如上图中的topic2,生产者通过某种算法选择其中一个nsqd发布消息即可,实现分布式消息处理。

  • channel 相当于队列。消费者订阅消息时,必须同时指定topic和channel。当生产者每次发布指定topic的消息时,消息会采用多播的方式复制到每一个channel 中,如下图所示。当多个消费者同时订阅一个channel的时候,channel中的消息只会被一个消费者消费,实现集群消费。如果要实现多播消费,每个消费者需要分别订阅topic下不同的channel。

集群消费(发送到clicks topic的消息被消费者中的其中一个消费者消费)
在这里插入图片描述

多播消费(发送到clicks topic的消息被所有消费者的消费者消费)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgHc4vQZ-1597635750973)(/download/attachments/201834509/image.png?version=8&modificationDate=1593527489618&api=v2 'image.png')]

3源码分析

3.1生产者投递消息

生产者需要import nsq提供的库文件go-nsq。下面是一个简单的生产者例子。首先新建配置信息cfg,这里使用默认配置。定义nsqd的地址,再通过nsq.NewProducer()新建一个生产者实例。通过这个实例调用Publish函数即可向nsqd发送指定topic的数据,例如这里向topic1发送了x字符串。此外,生产者还可以通过http的方式发布数据,这部分读者可自行分析。

import ("github.com/nsqio/go-nsq")
...
cfg := nsq.NewConfig()
nsqdAddr := "127.0.0.1:4150"
producer, err := nsq.NewProducer(nsqdAddr, cfg);
err := producer.Publish("topic1", []byte("x")); //可同步或异步发送数据,也可以通过http api发送

NewProducer初始化了一个Producer结构体,该结构体内部的重要管道有:transactionChan(事务管道,传送待发送的数据)、responseChan (响应管道,传送nsqd的发来的响应信息)。

func NewProducer(addr string, config *Config) (*Producer, error) {
	...
	p := &Producer{
		...
		transactionChan: make(chan *ProducerTransaction), //事务管道,传送待发送的数据
		exitChan:        make(chan int),
		responseChan:    make(chan []byte), //响应管道,传送nsqd的响应信息
		errorChan:       make(chan []byte),
	}
	...
	return p, nil
}

通过Publish函数发送消息时,首先通过通过Publish将topic和body封装为Command结构。然后通过sendCommandAsync发送Command数据。这里是同步发送的方式,所以需要通过doneChan管道等待nsqd的ack。也可以通过异步的方式发送,异步发送时接收到ack时会通过管道通知生产者。

func (w *Producer) Publish(topic string, body []byte) error {
	return w.sendCommand(Publish(topic, body)) //Publish将topic和body封装为Command结构,sendCommandAsync发送Command数据
}

func Publish(topic string, body []byte) *Command {//通过Publish将topic和body封装为Command结构。
	var params = [][]byte{[]byte(topic)}
	return &Command{[]byte("PUB"), params, body}
}

type Command struct { //Command结构
	Name   []byte
	Params [][]byte
	Body   []byte
}

func (w *Producer) sendCommand(cmd *Command) error {
	doneChan := make(chan *ProducerTransaction)
	err := w.sendCommandAsync(cmd, doneChan, nil)
	......
	t := <-doneChan //接收到nsqd响应时激活此管道
	return t.Error
}

发送数据时,如果还没有建立到nsqd的连接,则通过 w.connect()先建立连接。然后将待发送的cmd包装成ProducerTransaction的数据结构,再将包装好的数据发送到transactionChan事务管道。

func (w *Producer) sendCommandAsync(cmd *Command, doneChan chan *ProducerTransaction,
	args []interface{}) error {
	...
	if atomic.LoadInt32(&w.state) != StateConnected { //如果还没建立连接,则先建立连接
		err := w.connect()
		...
	}

	t := &ProducerTransaction{ //封装cmd为ProducerTransaction数据结构
		cmd:      cmd,
		doneChan: doneChan,
		Args:     args,
	}

	select {
	case w.transactionChan <- t: //发送数据到事务管道
	case <-w.exitChan:
		return ErrStopped
	}
	...
	return nil
}

建立连接的过程如下。先传入nsqd的地址w.addr和配置w.config新建一个Conn对象,通过其Connect()函数最终调用net库来建立tcp连接。通过go routine执行w.router()函数。

func (w *Producer) connect() error {
	...
	w.conn = NewConn(w.addr, &w.config, &producerConnDelegate{w})
	...
	_, err := w.conn.Connect() // 该函数最终调用net库来建立连接
	...
	atomic.StoreInt32(&w.state, StateConnected)
	...
	go w.router()

	return nil
}

Connect()函数还会通过两个go routine分别执行readLoop()和writeLoop(),一个负责读取nsqd发来的数据,一个负责写数据到nsqd。readLoop通过ReadUnpackedResponse©读取TCP数据,该函数读到FrameTypeResponse类型的响应数据时,会通过OnResponse函数最终将读到的数据发送到w.responseChan channel。

func (c *Conn) Connect() (*IdentifyResponse, error) {
	...
	c.conn = conn.(*net.TCPConn)
	c.r = conn
	c.w = conn
 	...
	go c.readLoop()
	go c.writeLoop()
	return resp, nil
}
func (c *Conn) writeLoop() {
	for {
		select {
		...
		case cmd := <-c.cmdChan:
			err := c.WriteCommand(cmd)  //最终写入到TCP缓冲
		case resp := <-c.msgResponseChan:
			err := c.WriteCommand(resp.cmd)
		...
}
func (c *Conn) readLoop() {
	for {
		...
		frameType, data, err := ReadUnpackedResponse(c) //最终调用net库阻塞读取TCP数据
		...
		switch frameType {
		case FrameTypeResponse:
			c.delegate.OnResponse(c, data)
		...
		}
	}
}
func (d *producerConnDelegate) OnResponse(c *Conn, data []byte)  { d.w.onConnResponse(c, data) }
func (w *Producer) onConnResponse(c *Conn, data []byte) { w.responseChan <- data } //将读到的数据发送到w.responseChan channel。

connect()函数启动的router() go routine中,负责读取transactionChan管道中的ProducerTransaction类型数据,添加到transactions数组并发送到nsqd。同时也会在responseChan有消息时,说明此时已接收到nsqd的ack,调用popTransaction将ProducerTransaction类型的数据从transactions数组中删除。通过t.finish()往doneChan填充数据,结束前面的sendCommand函数同步发送函数等待doneChan的状态。

func (w *Producer) router() {
	for {
		select {
		case t := <-w.transactionChan://读取transactionChan管道中的ProducerTransaction类型数据
			w.transactions = append(w.transactions, t) //加到transactions数组
			err := w.conn.WriteCommand(t.cmd) //最终写入到TCP缓冲
		case data := <-w.responseChan:
			w.popTransaction(FrameTypeResponse, data) //将ProducerTransaction类型的数据从transactions数组中删除
		...
	}
	...
}

func (w *Producer) popTransaction(frameType int32, data []byte) {
	t := w.transactions[0]
	w.transactions = w.transactions[1:]
	...
	t.finish()
}

func (t *ProducerTransaction) finish() {
	if t.doneChan != nil {
		t.doneChan <- t
	}
}

3.2消费者接收消息

消费者需要import nsq提供的库文件go-nsq。以下是一个简单的消费者例子。同样也是先新建配置信息cfg,这里使用默认配置。通过nsq.NewConsumer()传入订阅的指定topic和channel创建一个消费者实例。通过consumer.AddHandler添加接收到nsqd数据时的回调函数。最后通过consumer.ConnectToNSQDs(NSQDsAddrs)连接到指定nsqd地址。也可以使用consumer.ConnectToNSQLookupd(NSQLookupdsAddrs)连接到NSQLookupd的方式,这样consumers将会通过NSQLookupd来获取nsqd的信息,再连接nsqd。这里只分析第一种方式。

import ("github.com/nsqio/go-nsq")

cfg := nsq.NewConfig() //新建配置信息cfg
NSQDsAddrs := []string{"127.0.0.1:4150", "127.0.0.1:4152"}
consumer, err := nsq.NewConsumer("topic1", "channel1", cfg) //创建一个消费者实例
consumer.AddHandler(nsq.HandlerFunc(  //添加接收到nsqd数据时的回调函数
func(message *nsq.Message) error {
	log.Println(string(message.Body) + " C1")
	return nil
}))
err := consumer.ConnectToNSQDs(NSQDsAddrs)  //consumer.ConnectToNSQLookupd(NSQLookupdsAddrs)

NewConsumer初始化了一个Consumer结构体,该结构体内部的重要管道有:incomingMessages(接收到数据的管道)。新建go routine定时更新消费者的ready状态

func NewConsumer(topic string, channel string, config *Config) (*Consumer, error) {
	...
	r := &Consumer{ 
		topic:   topic,
		channel: channel,
		...
		incomingMessages: make(chan *Message), //接收到nsqd消息的channel
		
		...
	}
 
	go r.rdyLoop() //负责更新消费者的ready状态
	return r, nil
}

func (r *Consumer) rdyLoop() {
	redistributeTicker := time.NewTicker(r.config.RDYRedistributeInterval)

	for {
		select {
		case <-redistributeTicker.C:
			r.redistributeRDY() //定时更新ready分布状态
		case <-r.exitChan:
			goto exit
		}
	}
	...
}

AddHandler中通过一个go routine来执行传入的函数handlerLoop,该函数监听r.incomingMessages管道,管道有数据时调用回调函数HandleMessage来处理接收到的数据。也就是前面介绍的consumer.AddHandler(nsq.HandlerFunc(func(message *nsq.Message) error {}))传入的回调函数。

func (r *Consumer) AddHandler(handler Handler) {
	r.AddConcurrentHandlers(handler, 1)
}
func (r *Consumer) AddConcurrentHandlers(handler Handler, concurrency int) {
	...
	for i := 0; i < concurrency; i++ {
		go r.handlerLoop(handler)
	}
}
func (r *Consumer) handlerLoop(handler Handler) {
	for {
		message, ok := <-r.incomingMessages
		...
		err := handler.HandleMessage(message)
	}
	...
}

建立与nsdq的连接时,对每个nsqd地址分别调用ConnectToNSQD函数。ConnectToNSQD中调用了我们前面介绍过的 func (c *Conn) Connect()函数与每个nsqd地址建立连接。Connect()函数还会通过两个go routine分别执行readLoop()和writeLoop(),一个负责读取nsqd发来的数据,一个负责写数据到nsqd。其中,readLoop通过ReadUnpackedResponse©读取TCP数据,该函数最终调用onConnMessage函数将读到的数据发送到incomingMessages channel。而上面介绍的handlerLoop()就可以处理接收到的消息,完成消费处理。

func (r *Consumer) ConnectToNSQDs(addresses []string) error {
	for _, addr := range addresses {
		err := r.ConnectToNSQD(addr)//每个nsqd地址分别调用ConnectToNSQD函数
		...
	}
	return nil
}

func (r *Consumer) ConnectToNSQD(addr string) error {
	conn := NewConn(addr, &r.config, &consumerConnDelegate{r})
	...
	resp, err := conn.Connect()
	...
	cmd := Subscribe(r.topic, r.channel)
	err = conn.WriteCommand(cmd)

	return nil
}

func (c *Conn) Connect() (*IdentifyResponse, error) {
    ...
    c.conn = conn.(*net.TCPConn)
    c.r = conn
    c.w = conn
     ...
    go c.readLoop()
    go c.writeLoop()
    return resp, nil
}
func (c *Conn) readLoop() {
	for {
		...
		frameType, data, err := ReadUnpackedResponse(c) //最终调用net库阻塞读取TCP数据
		...
		switch frameType {
		case FrameTypeMessage:
			msg, err := DecodeMessage(data)
			c.delegate.OnMessage(c, msg)
		...
		}
	}
}
func (d *consumerConnDelegate) OnMessage(c *Conn, m *Message)         { d.r.onConnMessage(c, m) }
func (r *Consumer) onConnMessage(c *Conn, msg *Message) {...; r.incomingMessages <- msg}//将读到的数据发送到incomingMessages channel

3.3 nsqd处理消息

每个topic都对应有一个或多个channel,对于每个topic都会创建一个go routine,将该topic的数据复制分发到它的channel。
topic和Channel结构体的主要内容如下:

type Topic struct {
	name              string
	channelMap        map[string]*Channel //保存该topic下的所有管道,key为channel名,值为Channel实例
	memoryMsgChan     chan *Message //接收到该topic的发布数据时,存储到此管道(内存中)
	backend           BackendQueue //当memoryMsgChan已满时,存储发布的数据到此BackendQueue。实际实现上是将数据传入一个channel,后台go routine将会将此channel中的数据写入磁盘。后台go routine也会不断从磁盘中取出消息到BackendQueue结构体内的channel。
	...
}

type Channel struct { 
	...
	name      string
	backend BackendQueue  //用于从磁盘读取数据的BackendQueue
	memoryMsgChan chan *Message //用于从内存读取发布的数据的管道
	
	deferredMessages map[MessageID]*pqueue.Item 
	deferredPQ       pqueue.PriorityQueue //保存延迟发送的数据优先队列(以发送时间为优先级实现的最小堆)
	
	inFlightMessages map[MessageID]*Message
	inFlightPQ       inFlightPqueue //保存正在发送数据的优先队列(以发送时间为优先级实现的最小堆)
	
}

nsqd使用svc开源框架来启动进程以及控制进程的退出。启动时,将会调用func (n *NSQD) Main()。该函数中主要开启了四个服务:
1.TCPServer负责处理消费者和生产者的连接请求,接收生产者发布的消息和发送消息给消费者。
2.httpServer负责处理生产者通过http api的方式发布的数据,nsqlookupd发来的集群管理数据。
3.queueScanLoop负责扫描正在发送的数据和需要延迟发送的数据,进行超时重传。
4.lookupLoop负责定时同步nsqd的数据(topic,channel等)到nsqlookupd。
Wrap函数里面通过一个go routine执行传入的函数,因此每个服务都运行在一个go routine中。下面将分别介绍这些服务。

func (p *program) Start() error {
	opts := nsqd.NewOptions()
	...
	nsqd, err := nsqd.New(opts)
	p.nsqd = nsqd
	...
	go func() {
		err := p.nsqd.Main()
		...
	}()
	...
}

func (n *NSQD) Main() error {
	...
	n.waitGroup.Wrap(func() {
		exitFunc(protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf)) //通过一个go routine启动 TCPServer
	})

	httpServer := newHTTPServer(ctx, false, n.getOpts().TLSRequired == TLSRequired)
	n.waitGroup.Wrap(func() {
		exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))//通过一个go routine启动 httpServer
	})
	...
	n.waitGroup.Wrap(n.queueScanLoop) //通过一个go routine启动 queueScanLoop
	n.waitGroup.Wrap(n.lookupLoop) //通过一个go routine启动 lookupLoop
	...
	err := <-exitCh
	return err
}

func (w *WaitGroupWrapper) Wrap(cb func()) {
	w.Add(1)
	go func() {
		cb()
		w.Done()
	}()
}

3.3.1 TCPServer

TCPServer通过listener.Accept()阻塞等待新连接。当有新的消费者或生产者建立tcp连接时。通过go routine处理新连接。对每一个client的连接调用IOLoop(conn net.Conn)函数。该函数通过go routine执行messagePump函数,负责发送订阅的数据到client。IOLoop接着进入for循环,处理client发来的消息,读取到新消息时,通过Exec函数进行处理,根据处理结构进行响应。

func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) error {
	logf(lg.INFO, "TCP: listening on %s", listener.Addr())

	var wg sync.WaitGroup

	for {
		clientConn, err := listener.Accept()
		...
		wg.Add(1)
		go func() {
			handler.Handle(clientConn)
			wg.Done()
		}()
	}
	wg.Wait() //等待所有go routine结束
	return nil
}

func (p *tcpServer) Handle(clientConn net.Conn) {
	...
	err = prot.IOLoop(clientConn)
	...
}

func (p *protocolV2) IOLoop(conn net.Conn) error { 
	client := newClientV2(clientID, conn, p.ctx)
	p.ctx.nsqd.AddClient(client.ID, client) 
	...
	go p.messagePump(client, messagePumpStartedChan) //新go routine负责发送订阅的数据到client
	...

	for {  // for循环处理client发来的消息
		line, err = client.Reader.ReadSlice('\n')
		...
		// trim the '\n'
		line = line[:len(line)-1]
		// optionally trim the '\r'
		if len(line) > 0 && line[len(line)-1] == '\r' {
			line = line[:len(line)-1]
		}
		params := bytes.Split(line, separatorBytes)
		...
		var response []byte
		response, err = p.Exec(client, params) //处理client发来的消息
		...

		if response != nil {
			err = p.Send(client, frameTypeResponse, response)//根据接收的消息进行回复
			...
		}
	}

	conn.Close() 
	...
	return err
}

Exec函数处理client端发来的消息,其类型有十多种,其中比较典型的有以下三种,分别是生产者发布的消息、消费者进行订阅的消息和消费者接收到消息的ack。

func (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) {
	...
	switch {
	case bytes.Equal(params[0], []byte("PUB")): //生成者发布消息
		return p.PUB(client, params)
	case bytes.Equal(params[0], []byte("SUB")): //消费者订阅消息
		return p.SUB(client, params)
	case bytes.Equal(params[0], []byte("FIN")): //消费者ack
		return p.FIN(client, params)
	...
	}
	...
}
3.3.1.1 处理发布消息

首先来看生产者发布的消息,接收到该类型的消息时,将会调用PUB函数。该函数中1.解析接收到的消息的topicName、messageBody等数据。2.通过GetTopic函数传入topicName得到topic实例,如果该topic不存在,则需要新建一个topic实例并返回。3.将生成的MessageID和messageBody一起封装为Message结构。4.通过topic实例执行topic.PutMessage(msg)将消息传递到指定topic。

func (p *protocolV2) PUB(client *clientV2, params [][]byte) ([]byte, error) {
	topicName := string(params[1])

	messageBody := make([]byte, bodyLen)
	_, err = io.ReadFull(client.Reader, messageBody) //读取messageBody数据
	...

	topic := p.ctx.nsqd.GetTopic(topicName) //传入topicName得到topic实例
	msg := NewMessage(topic.GenerateID(), messageBody)//封装为Message结构
	err = topic.PutMessage(msg)
	...
}

GetTopic时,通过topicMap字典获取topic实例,如果该topic不存在,则通过NewTopic要新建一个topic实例并保存到字典中。

func (n *NSQD) GetTopic(topicName string) *Topic {
	// most likely, we already have this topic, so try read lock first.
	n.RLock()
	t, ok := n.topicMap[topicName]//通过topicMap字典获取topic实例
	n.RUnlock()
	if ok {
		return t
	}

	n.Lock()
	t = NewTopic(topicName, &context{n}, deleteCallback)
	n.topicMap[topicName] = t
	n.Unlock()

	t.Start()
	return t
}

新建的topic时,channelMap保存该topic下的所有管道,memoryMsgChan用于存储的发布到该topic的数据(内存中)。当memoryMsgChan已满时,存储发布的数据到backend BackendQueue(磁盘中)。实际实现上是将数据传入一个channel,后台go routine将会将此channel中的数据写入磁盘,每一个BackendQueue有一个go routine处理读写磁盘事件。另外对于每一个topic,新建时还会通过一个go routine执行messagePump函数。messagePump负责处理发布到此topic的数据,复制分发到它所有的channel。读取时优先读取内存channel memoryMsgChan,也可通过backendChan从磁盘中读出数据。当磁盘中有保存数据时,管理磁盘的后台go routine会自动将数据读出到backendChan。最后在一个for循环中通过channel.PutMessage(chanMsg)复制消息到所有channel。

// Topic constructor
func NewTopic(topicName string, ctx *context, deleteCallback func(*Topic)) *Topic {
	t := &Topic{
		...
	}
	t.backend = newDummyBackendQueue()
	t.waitGroup.Wrap(t.messagePump) //通过go routine启动messagePump函数
	...
	return t
}

type Topic struct {
    name              string
    channelMap        map[string]*Channel //保存该topic下的所有管道,key为channel名,值为Channel实例
    memoryMsgChan     chan *Message //接收到该topic的发布数据时,存储到此管道(内存中)
    backend           BackendQueue //当memoryMsgChan已满时,存储发布的数据到此BackendQueue。
    ...
}

func (t *Topic) messagePump() {
	...
	var backendChan <-chan []byte
	backendChan = t.backend.ReadChan()
	for _, c := range t.channelMap {//读取该topic下的所有管道并保存到chans
		chans = append(chans, c)
	}
	// main message loop
	for { //复制消息到所有channel
		select {
		case msg = <-memoryMsgChan: //优先读取内存channel
		case buf = <-backendChan: //读取磁盘channel,当磁盘中有保存数据时,管理磁盘的后台go routine会自动将数据读出到backendChan
			msg, err = decodeMessage(buf)//解码磁盘数据
			...
		for i, channel := range chans {
			chanMsg := msg
			chanMsg = NewMessage(msg.ID, msg.Body)
			chanMsg.Timestamp = msg.Timestamp
			...
			err := channel.PutMessage(chanMsg)
		}
	} 
}

func (t *Topic) put(m *Message) error {
	select {
	case t.memoryMsgChan <- m:
	default:
		b := bufferPoolGet()
		err := writeMessageToBackend(b, m, t.backend)
		bufferPoolPut(b)
		t.ctx.nsqd.SetHealth(err)
		if err != nil {
			t.ctx.nsqd.logf(LOG_ERROR,
				"TOPIC(%s) ERROR: failed to write message to backend - %s",
				t.name, err)
			return err
		}
	}
	return nil
}

Channel的主要结构如下,memoryMsgChan用于存取发布的数据(内存),backend用于存取发布的数据(磁盘),inFlightPQ是保存正在发送数据的优先队列(以发送时间为优先级实现的最小堆)。
对于topic里面的每个channel,通过PutMessage函数发送消息。最终通过put函数将消息发送到的memoryMsgChan管道或者是通过backend磁盘。写入磁盘时和topic中的实现一样,当memoryMsgChan已满时,才存储发布的消息到backend BackendQueue。
到此,我们分析完了发布数据部分。下面来看订阅部分。

type Channel struct { 
    ...
    name      string
    backend BackendQueue  //用于存取发布的数据(磁盘)
    memoryMsgChan chan *Message //用于存取发布的数据(内存)
    deferredMessages map[MessageID]*pqueue.Item
    deferredPQ       pqueue.PriorityQueue //保存延迟发送的数据优先队列(以发送时间为优先级实现的最小堆)
    inFlightMessages map[MessageID]*Message
    inFlightPQ       inFlightPqueue //保存正在发送数据的优先队列(以发送时间为优先级实现的最小堆)
}

func (c *Channel) PutMessage(m *Message) error { 
	err := c.put(m)
	...
}

func (c *Channel) put(m *Message) error {
	select {
	case c.memoryMsgChan <- m://
	default:
		b := bufferPoolGet()
		err := writeMessageToBackend(b, m, c.backend)
		bufferPoolPut(b)
		...
	}
	return nil
}
3.3.1.2消费者订阅消息

接收到订阅消息时,将会调用SUB函数。解析得到订阅的topicName和channelName后,通过名字得到topic和channel实例(当这些实例不存在时,会自动创建,读者可自行分析)。接着将得到的channel实例传递给订阅客户端client.SubEventChan。

func (p *protocolV2) SUB(client *clientV2, params [][]byte) ([]byte, error) {
	...
	topicName := string(params[1])
	channelName := string(params[2])

	var channel *Channel
	for {
		topic := p.ctx.nsqd.GetTopic(topicName)
		channel = topic.GetChannel(channelName)
		...
		break
	}
	client.Channel = channel
	// update message pump
	client.SubEventChan <- channel

	return okBytes, nil
}

消费者是通过前面的tcpServer与nsqd建立连接的。从前面分析的可知,当tcpServer接收到新的连接时,会调用IOLoop函数处理新连接并通过go routine来执行messagePump函数。在该函数中,当client.SubEventChan有数据可读时,将读到的channel保存到subChannel,在下一轮循环时将subChannel的内存数据subChannel.memoryMsgChan和磁盘数据管道分别保存到memoryMsgChan和backendMsgChan,实现对订阅channel的监听。当有数据可读时,通过p.SendMessage(client, msg)最终将数据写入tcp缓存,发送到消费者。
同时,在发送数据之前会调用StartInFlightTimeout函数将数据写入发送缓存。

func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
	var err error
	var memoryMsgChan chan *Message
	var backendMsgChan <-chan []byte
	var subChannel *Channel

	subEventChan := client.SubEventChan
	...

	for {
		...
		memoryMsgChan = subChannel.memoryMsgChan //下一轮循环时将subChannel的内存数据subChannel.memoryMsgChan和磁盘数据管道分别保存到memoryMsgChan和backendMsgChan
		backendMsgChan = subChannel.backend.ReadChan()
		select {
		...
		case subChannel = <-subEventChan://将读到的channel保存到subChannel
			subEventChan = nil
		case b := <-backendMsgChan:
			msg, err := decodeMessage(b)
			...
			subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) //将数据写入发送缓存队列
			err = p.SendMessage(client, msg) //最终将数据写入tcp缓存

		case msg := <-memoryMsgChan:
			...
			subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) //将数据写入发送缓存队列
			err = p.SendMessage(client, msg)  //最终将数据写入tcp缓存
		...
	}
}

前面分析中我们知道每个Channel中有一个以时间为优先级的最小堆inFlightPQ保存正在发送中的消息。StartInFlightTimeout修改发送消息的优先级msg.pri为当前时刻后,通过c.inFlightPQ.Push(msg)保存到FlightPQ数据结构中。

func (c *Channel) StartInFlightTimeout(msg *Message, clientID int64, timeout time.Duration) error {
	now := time.Now()
	msg.clientID = clientID
	msg.deliveryTS = now
	msg.pri = now.Add(timeout).UnixNano()
	err := c.pushInFlightMessage(msg)
	if err != nil {
		return err
	}
	c.addToInFlightPQ(msg)
	return nil
}

func (c *Channel) addToInFlightPQ(msg *Message) {
	c.inFlightMutex.Lock()
	c.inFlightPQ.Push(msg) //放入以时间为优先级的最小堆
	c.inFlightMutex.Unlock()
}

当nsqd接收到消费者ack的时候,调用FIN函数,在此函数中调用FinishMessage移除FlightPQ中对应的消息,因为该消息已被消费者成功消费。
到此,完成了发布消息推送到消费者的过程。

func (p *protocolV2) FIN(client *clientV2, params [][]byte) ([]byte, error) {
	...
	id, err := getMessageID(params[1])
	...
	err = client.Channel.FinishMessage(client.ID, *id)
	...
}

func (c *Channel) FinishMessage(clientID int64, id MessageID) error {
	msg, err := c.popInFlightMessage(clientID, id)
	...
	c.removeFromInFlightPQ(msg)
	...
}

func (c *Channel) removeFromInFlightPQ(msg *Message) {
	c.inFlightMutex.Lock()
	...
	c.inFlightPQ.Remove(msg.index)//移除FlightPQ中对应的消息
	c.inFlightMutex.Unlock()
}

3.3.2 HTTPServer

HTTPServer主要负责处理生产者通过http api的方式发布的数据和接收nsqlookupd发来的topic和channel管理信息。该server是通过go开源框架httprouter实现的,该框架通过"基数树"实现了高效的路由查找。首先需要新建一个httpserver实例,如下图所示,在该实例中注册了很多路由处理函数。其中,doPUB和doMPUB就是用于处理生成者发布的数据。当接收到/pub路径下的http数据时,就会调用http_api.V1和s.doPUB函数。http_api.Decorate是用传入参数右边的函数来包装左边函数的函数。

func newHTTPServer(ctx *context, tlsEnabled bool, tlsRequired bool) *httpServer {
	router := httprouter.New()
	......
	s := &httpServer{
		ctx:         ctx,
		tlsEnabled:  tlsEnabled,
		tlsRequired: tlsRequired,
		router:      router,
	}

	router.Handle("GET", "/ping", http_api.Decorate(s.pingHandler, log, http_api.PlainText)) //处理心跳数据
	router.Handle("GET", "/info", http_api.Decorate(s.doInfo, log, http_api.V1))

	// v1 negotiate
	router.Handle("POST", "/pub", http_api.Decorate(s.doPUB, http_api.V1)) //处理生成者发布的数据
	router.Handle("POST", "/mpub", http_api.Decorate(s.doMPUB, http_api.V1)) //处理生成者发布的数据
	router.Handle("GET", "/stats", http_api.Decorate(s.doStats, log, http_api.V1))

	// only v1
	router.Handle("POST", "/topic/create", http_api.Decorate(s.doCreateTopic, log, http_api.V1))
	router.Handle("POST", "/topic/delete", http_api.Decorate(s.doDeleteTopic, log, http_api.V1))
	router.Handle("POST", "/topic/empty", http_api.Decorate(s.doEmptyTopic, log, http_api.V1))
	router.Handle("POST", "/topic/pause", http_api.Decorate(s.doPauseTopic, log, http_api.V1))
	router.Handle("POST", "/topic/unpause", http_api.Decorate(s.doPauseTopic, log, http_api.V1))
	router.Handle("POST", "/channel/create", http_api.Decorate(s.doCreateChannel, log, http_api.V1))
	router.Handle("POST", "/channel/delete", http_api.Decorate(s.doDeleteChannel, log, http_api.V1))
	router.Handle("POST", "/channel/empty", http_api.Decorate(s.doEmptyChannel, log, http_api.V1))
	router.Handle("POST", "/channel/pause", http_api.Decorate(s.doPauseChannel, log, http_api.V1))
	router.Handle("POST", "/channel/unpause", http_api.Decorate(s.doPauseChannel, log, http_api.V1))
	router.Handle("GET", "/config/:opt", http_api.Decorate(s.doConfig, log, http_api.V1))
	router.Handle("PUT", "/config/:opt", http_api.Decorate(s.doConfig, log, http_api.V1))

	// debug
	router.HandlerFunc("GET", "/debug/pprof/", pprof.Index)
	router.HandlerFunc("GET", "/debug/pprof/cmdline", pprof.Cmdline)
	router.HandlerFunc("GET", "/debug/pprof/symbol", pprof.Symbol)
	router.HandlerFunc("POST", "/debug/pprof/symbol", pprof.Symbol)
	router.HandlerFunc("GET", "/debug/pprof/profile", pprof.Profile)
	router.Handler("GET", "/debug/pprof/heap", pprof.Handler("heap"))
	router.Handler("GET", "/debug/pprof/goroutine", pprof.Handler("goroutine"))
	router.Handler("GET", "/debug/pprof/block", pprof.Handler("block"))
	router.Handle("PUT", "/debug/setblockrate", http_api.Decorate(setBlockRateHandler, log, http_api.PlainText))
	router.Handler("GET", "/debug/pprof/threadcreate", pprof.Handler("threadcreate"))

	return s
}

func Decorate(f APIHandler, ds ...Decorator) httprouter.Handle {
	decorated := f
	for _, decorate := range ds {
		decorated = decorate(decorated)
	}
	return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
		decorated(w, req, ps)
	}
}

先看doPUB函数,它先通过req读取http消息体,解析req得到发布的topic。然后根据消息体包装为Message结构,最后通过topic.PutMessage(msg)将数组发送到指定topic。这个函数我们在分析tcp服务的时候已经分析过了,数据最终会推送到消费者。另外,这个函数没有回复部分的逻辑,它被封装在http_api.V1里面了。

func (s *httpServer) doPUB(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
	// TODO: one day I'd really like to just error on chunked requests
	// to be able to fail "too big" requests before we even read

	if req.ContentLength > s.ctx.nsqd.getOpts().MaxMsgSize {
		return nil, http_api.Err{413, "MSG_TOO_BIG"}
	}

	// add 1 so that it's greater than our max when we test for it
	// (LimitReader returns a "fake" EOF)
	readMax := s.ctx.nsqd.getOpts().MaxMsgSize + 1
	body, err := ioutil.ReadAll(io.LimitReader(req.Body, readMax)) //读取http消息体
	if err != nil {
		return nil, http_api.Err{500, "INTERNAL_ERROR"}
	}
	if int64(len(body)) == readMax {
		return nil, http_api.Err{413, "MSG_TOO_BIG"}
	}
	if len(body) == 0 {
		return nil, http_api.Err{400, "MSG_EMPTY"}
	}

	reqParams, topic, err := s.getTopicFromQuery(req) //读取http消息体
	if err != nil {
		return nil, err
	}

	var deferred time.Duration
	if ds, ok := reqParams["defer"]; ok {
		var di int64
		di, err = strconv.ParseInt(ds[0], 10, 64)
		if err != nil {
			return nil, http_api.Err{400, "INVALID_DEFER"}
		}
		deferred = time.Duration(di) * time.Millisecond
		if deferred < 0 || deferred > s.ctx.nsqd.getOpts().MaxReqTimeout {
			return nil, http_api.Err{400, "INVALID_DEFER"}
		}
	}

	msg := NewMessage(topic.GenerateID(), body)
	msg.deferred = deferred
	err = topic.PutMessage(msg)
	if err != nil {
		return nil, http_api.Err{503, "EXITING"}
	}

	return "OK", nil
}

V1函数首先调用了它包装的函数f(也就是上面的doSub),然后调用RespondV1函数。在RespondV1中将处理结果通过w.Write(response)发送到客户端。

func V1(f APIHandler) APIHandler {
	return func(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
		data, err := f(w, req, ps)
		if err != nil {
			RespondV1(w, err.(Err).Code, err)
			return nil, nil
		}
		RespondV1(w, 200, data)
		return nil, nil
	}
}

func RespondV1(w http.ResponseWriter, code int, data interface{}) {
	var response []byte
	var err error
	var isJSON bool

	if code == 200 {
		switch data.(type) {
		case string:
			response = []byte(data.(string))
		case []byte:
			response = data.([]byte)
		case nil:
			response = []byte{}
		default:
			isJSON = true
			response, err = json.Marshal(data)
			if err != nil {
				code = 500
				data = err
			}
		}
	}

	if code != 200 {
		isJSON = true
		response, _ = json.Marshal(struct {
			Message string `json:"message"`
		}{fmt.Sprintf("%s", data)})
	}

	if isJSON {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
	}
	w.Header().Set("X-NSQ-Content-Type", "nsq; version=1.0")
	w.WriteHeader(code)
	w.Write(response)
}

3.3.3 queueScanLoop服务

nsqd通过go routine启动了queueScanLoop服务,它负责扫描正在发送的消息FlightPQ和需要延迟发送的消息,进行超时重传。该函数中首先通过channels := n.channels()获取当前nsqd中所有topic下的所有channel。然后通过resizePool来调整扫描的go routine数量(默认为4),这些go routine用于并发处理所有channel的FlightPQ中的数据。queueScanLoop服务最后进入一个for循环,循环中默认100ms会唤醒一次,唤醒后通过range util.UniqRands函数从nsqd的所有channel中随机取出num条channels,放到workCh中。

func (n *NSQD) queueScanLoop() {
	workCh := make(chan *Channel, n.getOpts().QueueScanSelectionCount)
	responseCh := make(chan bool, n.getOpts().QueueScanSelectionCount)
	closeCh := make(chan int)

	workTicker := time.NewTicker(n.getOpts().QueueScanInterval)
	refreshTicker := time.NewTicker(n.getOpts().QueueScanRefreshInterval)

	channels := n.channels()
	n.resizePool(len(channels), workCh, responseCh, closeCh)

	for {
		select {
		case <-workTicker.C: //默认100ms
			if len(channels) == 0 {
				continue
			}
		case <-refreshTicker.C:
			channels = n.channels()
			n.resizePool(len(channels), workCh, responseCh, closeCh)
			continue
		case <-n.exitChan:
			goto exit
		}

		num := n.getOpts().QueueScanSelectionCount
		if num > len(channels) {
			num = len(channels)
		}

		for _, i := range util.UniqRands(num, len(channels)) { //随机取出num条channels,放到workCh
			workCh <- channels[i]
		}


	}

exit:
	...
}


func (n *NSQD) channels() []*Channel {
	var channels []*Channel
	n.RLock()
	for _, t := range n.topicMap {//所有topic
		t.RLock()
		for _, c := range t.channelMap {//所有channel
			channels = append(channels, c)
		}
		t.RUnlock()
	}
	n.RUnlock()
	return channels
}

在调用resizePool调整扫描的go routine数量数量时,会创建idealPoolSize(一般为4)个go routine执行queueScanWorker函数。queueScanWorker函数中等待需要处理的channel(workCh),读取到需要处理的workCh时,通过processInFlightQueue处理管道发送中的消息。通过processDeferredQueue处理需延迟发送的消息。这里我们只分析processInFlightQueue。


func (n *NSQD) resizePool(num int, workCh chan *Channel, responseCh chan bool, closeCh chan int) {
	idealPoolSize := int(float64(num) * 0.25)
	if idealPoolSize < 1 {
		idealPoolSize = 1
	} else if idealPoolSize > n.getOpts().QueueScanWorkerPoolMax { //默认最大为4
		idealPoolSize = n.getOpts().QueueScanWorkerPoolMax
	}
	for {
		if idealPoolSize == n.poolSize {//不必调整
			break
		} else if idealPoolSize < n.poolSize {//现有协程数量过多,通过closeCh关闭一些
			// contract
			closeCh <- 1
			n.poolSize--
		} else { //现有协程数量不够,开启新go routine
			// expand
			n.waitGroup.Wrap(func() {
				n.queueScanWorker(workCh, responseCh, closeCh) //通过go routine执行queueScanWorker函数
			})
			n.poolSize++
		}
	}
}

func (n *NSQD) queueScanWorker(workCh chan *Channel, responseCh chan bool, closeCh chan int) {
	for {
		select {
		case c := <-workCh:
			now := time.Now().UnixNano()
			c.processInFlightQueue(now)
			c.processDeferredQueue(now)
			...
		case <-closeCh:
			return
		}
	}
}

我们只看处理channel发送中的消息的processInFlightQueue函数。该函数通过c.inFlightPQ.PeekAndShift(t)来读取并删除堆顶中超时(超过传入时间t)的数据。如果没有,则直接goto exit。当堆顶有数据超时,将会通过c.put(msg)来重新发送数据。这个函数重新将msg放入Channel的memoryMsgChan或者存入磁盘。后续的处理就和正常的消息处理一样了。

func (c *Channel) processInFlightQueue(t int64) bool {
	...
	for {
		c.inFlightMutex.Lock()
		msg, _ := c.inFlightPQ.PeekAndShift(t) //读取并删除堆顶中超时的数据
		if msg == nil {
			goto exit
		}
		c.inFlightMutex.Unlock()
		_, err := c.popInFlightMessage(msg.clientID, msg.ID) //删除检索map中的数据
		c.put(msg)
	}

exit:
	...
}

func (c *Channel) put(m *Message) error {
	select {
	case c.memoryMsgChan <- m:
	default:
		b := bufferPoolGet()
		err := writeMessageToBackend(b, m, c.backend)
		bufferPoolPut(b)
		c.ctx.nsqd.SetHealth(err)
		if err != nil {
			c.ctx.nsqd.logf(LOG_ERROR, "CHANNEL(%s): failed to write message to backend - %s",
				c.name, err)
			return err
		}
	}
	return nil
}

3.3.4 lookupLoop服务

最后的lookupLoop服务比较简单,主要负责定时同步nsqd的数据(topic,channel等)到所有nsqlookupd。首先在没有建立连接时会遍历配置的NSQLookupdTCPAddresses地址,新建lookupPeer实例并与对应地址的NSQLookupd建立tcp连接。
每隔15秒会通过nsq.Ping()生成心跳命令,并通过lookupPeer.Command(cmd)向所有nsqlookup发送心跳信息。另外,在nsqd的管道或者topic发生改动时,也会通知所有nsqlookup。通过nsq.UnRegister或nsq.Register函数来生成注册或者注销channel或topic的命令cmd。最后for循环中遍历lookupPeer,通过lookupPeer.Command(cmd)发送命令到各个nsqlookup。

func (n *NSQD) lookupLoop() {
	var lookupPeers []*lookupPeer
	var lookupAddrs []string
	connect := true
	
	ticker := time.Tick(15 * time.Second)
	for {
		if connect {//没有建立连接,遍历配置的NSQLookupdTCPAddresses地址,与所有NSQLookupd建立tcp连接。
			for _, host := range n.getOpts().NSQLookupdTCPAddresses {
				lookupPeer := newLookupPeer(host, n.getOpts().MaxBodySize, n.logf,
					connectCallback(n, hostname))//根据地址建立lookupPeer实例
				lookupPeer.Command(nil) // 建立tcp连接
			}
			connect = false
		}

		select {
		case <-ticker: //15s
			for _, lookupPeer := range lookupPeers {
				cmd := nsq.Ping()//生成心跳命令
				_, err := lookupPeer.Command(cmd)
			}
		case val := <-n.notifyChan:
			var cmd *nsq.Command
			switch val.(type) {
			case *Channel: //channel发生改动
				// notify all nsqlookupds that a new channel exists, or that it's removed
				branch = "channel"
				channel := val.(*Channel)
				if channel.Exiting() == true {
					cmd = nsq.UnRegister(channel.topicName, channel.name)
				} else {
					cmd = nsq.Register(channel.topicName, channel.name)
				}
			case *Topic: //topic发生改动
				// notify all nsqlookupds that a new topic exists, or that it's removed
				branch = "topic"
				topic := val.(*Topic)
				if topic.Exiting() == true {
					cmd = nsq.UnRegister(topic.name, "")
				} else {
					cmd = nsq.Register(topic.name, "")
				}
			}

			for _, lookupPeer := range lookupPeers {
				...
				_, err := lookupPeer.Command(cmd)
				...
			}
		...
		}
	}

exit:
	...
}

3.4 nsqlookupd源码分析

nsqlookupd同样使用svc开源框架来启动进程以及控制进程的退出。启动时将会新建配置选项实例,再根据配置创建nsqlookupd实例,最终通过go routine执行nsqlookupd的mian函数。

func (p *program) Start() error {
	opts := nsqlookupd.NewOptions()
	...
	nsqlookupd, err := nsqlookupd.New(opts)
	p.nsqlookupd = nsqlookupd

	go func() {
		err := p.nsqlookupd.Main()
		...
	}()
	return nil
}

main函数中主要是通过两个go routine启动了两个服务,tcpServer和HTTPServer服务。其中,tcpServer服务负责处理nsqd上报信息,保存到底层数据结构中。HTTPServer用于向nsqadmin和消费者提供查询nsqd拓扑结构接口,本质上,就是一个web服务器,提供http查询接口。我们先分析tcpServer部分。

func (l *NSQLookupd) Main() error {
	ctx := &Context{l}

	exitCh := make(chan error)
	var once sync.Once
	...

	l.tcpServer = &tcpServer{ctx: ctx}
	l.waitGroup.Wrap(func() {
		exitFunc(protocol.TCPServer(l.tcpListener, l.tcpServer, l.logf))
	})
	httpServer := newHTTPServer(ctx)
	l.waitGroup.Wrap(func() {
		exitFunc(http_api.Serve(l.httpListener, httpServer, "HTTP", l.logf))
	})

	err := <-exitCh
	return err
}

TCPServer部分和前面分析的nsqd的tcpServer结构非常类似,当它监听到新的tcp连接时,会通过一个go routine调用Handle函数。在Handle函数中调用IOLoop函数处理新连接。

func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) error {
	logf(lg.INFO, "TCP: listening on %s", listener.Addr())

	var wg sync.WaitGroup

	for {
		clientConn, err := listener.Accept()

		wg.Add(1)
		go func() {
			handler.Handle(clientConn)
			wg.Done()
		}()
	}
	...
}

func (p *tcpServer) Handle(clientConn net.Conn) {
	buf := make([]byte, 4)
	_, err := io.ReadFull(clientConn, buf)
	protocolMagic := string(buf)

	var prot protocol.Protocol
	switch protocolMagic {
	case "  V1":
		prot = &LookupProtocolV1{ctx: p.ctx}
	...
	}
	...
	err = prot.IOLoop(clientConn)
	...
}

IOLoop函数不断读取client发来的数据,简单解析后调用Exec函数进行处理,并将处理后的响应发回给client。

func (p *LookupProtocolV1) IOLoop(conn net.Conn) error {
	client := NewClientV1(conn)
	reader := bufio.NewReader(client)
	for {
		line, err = reader.ReadString('\n')
		...
		var response []byte
		response, err = p.Exec(client, reader, params)//调用Exec函数进行处理
		...
		if response != nil {
			_, err = protocol.SendResponse(client, response)//将处理后的响应发回给client。
			...
		}
	}
	...
}

Exec函数中处理的nsqd发来的消息,主要有以下几种:
1.PING nsqd每隔一段时间都会向nsqlookupd发送心跳;
2.IDENTITY 当nsqd第一次连接nsqlookupd时,发送IDENTITY,验证自己身份;
3.REGISTER 当nsqd创建一个topic或者channel时,向nsqlookupd发送REGISTER请求,在nsqlookupd上更新当前nsqd的topic或者channel信息;
4.UNREGISTER 当nsqd删除一个topic或者channel时,向nsqlookupd发送UNREGISTER请求,在nsqlookupd上更新当前nsqd的topic或者channel信息;
我们前面分析nsqd的nsqlookup时也见过了这几种类型的消息,收到这些消息时会执行对应的消息处理函数去更新nsqlookupd中保存的nsqd信息(例如上一次心跳时间,topic及其channel数据等),内容比较简单,这里只简单看一下REGISTER部分。需要提一点是,nsqd的信息是保存在registration.db这样的实例里面的;

func (p *LookupProtocolV1) Exec(client *ClientV1, reader *bufio.Reader, params []string) ([]byte, error) {
	switch params[0] {
	case "PING":
		return p.PING(client, params)
	case "IDENTIFY":
		return p.IDENTIFY(client, reader, params[1:])
	case "REGISTER":
		return p.REGISTER(client, reader, params[1:])
	case "UNREGISTER":
		return p.UNREGISTER(client, reader, params[1:])
	}
	return nil, protocol.NewFatalClientErr(nil, "E_INVALID", fmt.Sprintf("invalid command %s", params[0]))
}

接收到REGISTER消息时,调用REGISTER函数。简单解析注册的topic、 channel信息后,通过nsqlookupd.DB.AddProducer函数将新的注册信息保存到nsqlookupd.DB结构中。nsqadmin或消费者需要查询nsqd信息时可直接nsqlookupd.DB查询,读者可自行分析。

func (p *LookupProtocolV1) REGISTER(client *ClientV1, reader *bufio.Reader, params []string) ([]byte, error) {
	topic, channel, err := getTopicChan("REGISTER", params)
	...

	if channel != "" {
		key := Registration{"channel", topic, channel}
		if p.ctx.nsqlookupd.DB.AddProducer(key, &Producer{peerInfo: client.peerInfo}) {
			...
		}
	}
	key := Registration{"topic", topic, ""}
	if p.ctx.nsqlookupd.DB.AddProducer(key, &Producer{peerInfo: client.peerInfo}) {
		...
	}

	return []byte("OK"), nil
}
func (r *RegistrationDB) AddProducer(k Registration, p *Producer) bool {
	r.Lock()
	defer r.Unlock()
	_, ok := r.registrationMap[k]
	if !ok {
		r.registrationMap[k] = make(map[string]*Producer)
	}
	producers := r.registrationMap[k]
	_, found := producers[p.peerInfo.id]
	if found == false {
		producers[p.peerInfo.id] = p
	}
	return !found
}

3.5 nsqadmin

nsqadmin主要用于通过 Web UI 来实时监控和管理nsqd集群,管理页面如下。通过HTTP获取或修改nsqd的topic和channel信息。这里不进行分析。
在这里插入图片描述

4总结

nsq通过go语言原生的go routine和channel优雅地实现了消息队列的功能。整个架构和源码理解起来还是比较简单的,对go和消息队列比较感兴趣的话建议可以精读一遍,非常适合用于入门go语言或学习go语言的编程模式。源码里面对go语言面向对象编程、go routine和channel的运用有很多值得学习的地方。据网上点评,NSQ是将Golang的语言特性应用的最为恰到好处的一套架构,学习Golang,首选NSQ源码。
nsq是比较轻量级的消息队列,优势是简单易用,具有优雅的代码结构,功能齐全,非常容易进行定制化修改。下面是一张来自golang2017开发者大会的Nsq与其他mq的对比图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lzV3eKde-1597635750979)(/download/attachments/201831015/image.png?version=1&modificationDate=1592378510843&api=v2 'image.png')]

缺陷是nsq没有类似kafka和rocket MQ这样通过partition提高吞吐量的方式。它的集群消费模式本质上只是多个客户端goroutine对一个channel管道的竞争读取,而kafka和rocket MQ是通过对topic进行分区,不同的分区给消费者集群中的不同消费者进行消费,这样来提高系统的吞吐量。它没有kafka和rocket MQ里面通过主从模式、消息持久化来保证broker down后自动恢复的高可用机制。它不支持消息持久化、优先队列、有序消息,事务消息等功能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值