直接从源码入手,会存在找不到入手点的方法。所以第一次源码阅读之旅,打算先从文档开始,跟着文档走。文档地址
1.NSQ feature(选择几个重要的)
-
基于推送的低延迟消息传递
-
结合负载均衡和组播的消息路由
-
提供服务发现(nslookupd)
-
擅长于流(高吞吐量)和面向工作(低吞吐量)的工作负载
-
数据主要在内存中(超过高水位标记的消息被透明地保存在磁盘上)
-
集成statsd,实现实时监控
-
健壮的集群管理接口(nsqadmin)
NSQ是分为三部分的(守护进程):
- nsq 接受、传递消息给消费端
- nsqlookupd 相当于注册中心
- nsqadmin 管理界面
2.NSQ 的选择
在文档中,有一句话是:在分布式系统之中,为了达到预期的目标,是需要作出平衡的。
NSQ做了哪些平衡?
-
消息的持久性
-
NSQ主要还是一个内存消息队列,虽然存在配置可以持久化消息(--mem-queue-size。 超过这个数值会存放在硬盘上,那么如果设置为0,则表示一定会存在硬盘)
-
消息是no replication,没有副本的。每个nsqd的内容并不相似
-
至少一次的消息传利(at least once)
-
消息的接受是无序的
-
是内存存储和磁盘存储的组合以及每个nsqd节点不共享任何内容导致的结果。
-
服务发现是最终一致的
3.主题和通道
NSQ topic/channel的核心就是go的channel(即:带缓冲区的go channel的指针。缓冲区大小为--mem-queue-size设置)
每个主题保持3个主要的gorroutine。
-
router,负责从进入的go-chan读取新发布的消息,并将它们存储在队列(内存或磁盘)中。
-
messagePump,负责将消息复制和推送到上面描述的通道。
-
DiskQueue 将溢出的消息写入磁盘(超过mem-queue-size大小后的消息)

此外,每个NSQ channel维护2个按时间顺序排列的优先级队列,负责延迟和发送中的消息(已经传递但没有被消费者进一步处理的消息)超时(以及2个伴生的goroutine用于监视这些超时)。
4.减少GC压力
通过监控与分析,NSQ的文档给了一些意见:
-
避免[]byte和string的转换
-
重用缓冲区或对象
-
预分配切片(指定容量)
-
对各种配置做限制(例如消息大小)
-
避免装箱(使用interface{})或不必要的包装类型
-
避免在热点代码中使用 defer。
5.协议
NSQ的传输协议是基于TCP的
该协议的结构带有长度前缀的帧,使其编码和解码简单而高效。
[x][x][x][x][x][x][x][x][x][x][x][x]...
| (int32) || (int32) || (binary)
| 4-byte || 4-byte || N-byte
------------------------------------...
size frame ID data
比如:
-
因为框架的组成部分的确切类型和大小是提前知道的,所以我们可以规避了使用方便的编码二进制包的Read()和Write()封装(及它们外部接口的查找和会话)反之我们使用直接调用 binary.BigEndian方法。
-
为了消除socket 输入输出的系统调用,客户端net.Conn被封装了bufio.Reader和bufio.Writer。这个Reader通过暴露ReadSlice(),复用了它自己的缓冲区。这样几乎消除了读完socket时的分配,这极大的降低了垃圾回收的压力。这可能是因为与数据相关的大多数命令并没有逃逸(在边缘情况下这是假的,数据被强制复制)。
-
在更低层,MessageID 被定义为 [16]byte,这样可以将其作为 map 的 key(slice 无法用作 map 的 key(可变的))。然而,考虑到从 socket 读取的数据被保存为 []byte,胜于通过分配字符串类型的 key 来产生垃圾,并且为了避免从 slice 到 MessageID 的底层数组产生复制操作,unsafe 包被用来将 slice 直接转换为 MessageID
id := *(*nsq.MessageID)(unsafe.Pointer(&msgID))