- NSQ源码刨析 go-nsq的Producer处理细节
- NSQ源码刨析 go-nsq的Consumer处理细节
- NSQ源码刨析 nsqd 中消息的处理(topic, channel, clients ...
- NSQ源码刨析 nsqd-lookupd之间的状态同步 & RDY 命令
基于
nsqd v1.2.0
go-nsq v1.0.8
这是一个系列文章(后续会慢慢补全
概览(图
通过图示我们可以看到,消息发布过程的编码主要由如下三个文件控制
- producer.go - 这个文件主要处理用户的消息发布请求
- conn.go - 这个文件主要用于处理 TCP 流相关的逻辑
- command.go - 这个文件主要用于 TCP 的封包逻辑
在这三个文件中,又有两个 goroutine
w.router
,c.readLoop
是处理过程中最核心的函数,接下来我们主要就是分析这两个个循环
Router
func (w *Producer) router() {
for {
select {
case t := <-w.transactionChan:
w.transactions = append(w.transactions, t)
err := w.conn.WriteCommand(t.cmd)
if err != nil {
w.close()
}
case data := <-w.responseChan:
w.popTransaction(FrameTypeResponse, data)
case data := <-w.errorChan:
w.popTransaction(FrameTypeError, data)
case <-w.closeChan:
goto exit
case <-w.exitChan:
goto exit
}
}
exit:
w.transactionCleanup()
w.wg.Done()
}
- transactionChan (消息管道,用于汇集用户发布的消息
- 所有用户的发布消息都会写入到这个 channel (可能是 N 个goroutine,最终在这一个 go router 中,排列处理。
- 将消息写入到 TCP 流中
- 这里另外还维护了一个 transactions 结构,因为 TCP 是顺序的,因此在这里用 FIFO 队列维护过程中的消息体
- responseChan & errorChan (消息管道从 TCP 接收端 收取到正常 & 错误的回复
- 这两个 channel 接收到消息后,都会对回复进行处理,从 transactions 队列中取出头消息,通过消息发送出
done
信号 (主要是返回成功错误
- 这两个 channel 接收到消息后,都会对回复进行处理,从 transactions 队列中取出头消息,通过消息发送出
- closeChan & exitChan 用于接收关闭&退出
信号
- exit (退出点
- 首先退出逻辑处理循环;
- 当接收到 关闭|退出 信号时,在这里处理清理流程。
- 首先处理
发送中
的消息 transactions(发布 done 信号, 主要是提示错误 失去连接 - 等待其他消息发布过程全部完结 concurrentProducers
- 处理还在
内部处理过程中
消息 transactionChan (还是发布错误信号 失去连接 - 当全部处理完成后, wg.Done() 退出阻塞,完成退出逻辑。
我们可以看到在这个循环体中 go-nsq 是如何使用
select
&channel
设计出简洁且高效的多线程处理模型
- 基于 select 中的 transactionChan,closeChan,exitChan 管道,进行消息的发布和接收
- 利用类似 FIFO 的队列,管理发布出去的消息
- 利用 closeChan,exitChan 接收外部的中断指令
- concurrentProducers 用于统计同时处理的异步指令
- wg 用于阻塞等待清理完毕
readLoop
这里会和源代码稍微有些出入,减少了日志打印代码,和一些无关的逻辑代码。
func (c *Conn) readLoop() {
delegate := &connMessageDelegate{c}
for {
if atomic.LoadInt32(&c.closeFlag) == 1 {
goto exit
}
frameType, data, err := ReadUnpackedResponse(c)
if err != nil {
goto exit
}
if frameType == FrameTypeResponse && bytes.Equal(data, []byte("_heartbeat_")) {
err := c.WriteCommand(Nop())
if err != nil {
c.delegate.OnIOError(c, err)
goto exit
}
continue
}
switch frameType {
case FrameTypeResponse:
c.delegate.OnResponse(c, data)
case FrameTypeError:
c.delegate.OnError(c, data)
default:
c.delegate.OnIOError(c, fmt.Errorf("unknown frame type %d", frameType))
}
}
exit:
atomic.StoreInt32(&c.readLoopRunning, 0)
messagesInFlight := atomic.LoadInt64(&c.messagesInFlight)
if messagesInFlight == 0 {
c.close()
}
c.wg.Done()
c.log(LogLevelInfo, "readLoop exiting")
}
这里和上面的循环使用的逻辑处理模型不同,(这里是一个普通的for循环。 这里吧解释分三个部分
- unpack 从 TCP 流中拿到 bytes,并进行解析。获得 frame_type & data 两个数据
- frame 通过不同的 frame 类型,将数据通过 delegate 传递到 producer 中(就是前面提到的
responseChan
&exitChan
- exit 退出点 (这里源码中给出非常详细的退出处理过程,这里直接贴了
- CLOSE cmd sent to nsqd
- CLOSE_WAIT response received from nsqd
- set c.closeFlag
- readLoop() exit
- c.exitChan close
- launch cleanup() goroutine & waitForCleanup() goroutine
Producer 总结
- 发布是采用的 TCP 协议(通过command进行消息的组装
- 发布端需要在构建的时候指定对应的 nsqd,Producer 和 Nsqd 是 1:1 的关系
- 发布端可以同时向多个Nsqd,写入消息(多个Producer同时写,目的是实现HA
- 发布空的消息(message ,会导致panic
- 关键文件介绍
- producer.go 实现了消息的发布逻辑
- conn.go 实现了网络相关的逻辑
- command.go 实现了tcp的封包逻辑
最后推荐个自己的微服务框架 barid-go 一个基于模块组合,简单易用的微服务框架 ;)