NSQ解析(一):从文档开始

172 阅读4分钟

  直接从源码入手,会存在找不到入手点的方法。所以第一次源码阅读之旅,打算先从文档开始,跟着文档走。文档地址

1.NSQ feature(选择几个重要的)

  1. 基于推送的低延迟消息传递

  2. 结合负载均衡和组播的消息路由

  3. 提供服务发现(nslookupd)

  4. 擅长于流(高吞吐量)和面向工作(低吞吐量)的工作负载

  5. 数据主要在内存中(超过高水位标记的消息被透明地保存在磁盘上)

  6. 集成statsd,实现实时监控

  7. 健壮的集群管理接口(nsqadmin)

NSQ是分为三部分的(守护进程):

  1. nsq 接受、传递消息给消费端
  2. nsqlookupd 相当于注册中心
  3. nsqadmin 管理界面

2.NSQ 的选择

在文档中,有一句话是:在分布式系统之中,为了达到预期的目标,是需要作出平衡的。

NSQ做了哪些平衡? 

  1. 消息的持久性

  2. NSQ主要还是一个内存消息队列,虽然存在配置可以持久化消息(--mem-queue-size。 超过这个数值会存放在硬盘上,那么如果设置为0,则表示一定会存在硬盘)

  3. 消息是no replication,没有副本的。每个nsqd的内容并不相似

  4. 至少一次的消息传利(at least once)

  5. 消息的接受是无序的

  6. 是内存存储和磁盘存储的组合以及每个nsqd节点不共享任何内容导致的结果。

  7. 服务发现是最终一致的

3.主题和通道

NSQ topic/channel的核心就是go的channel(即:带缓冲区的go channel的指针。缓冲区大小为--mem-queue-size设置)

每个主题保持3个主要的gorroutine。

  1. router,负责从进入的go-chan读取新发布的消息,并将它们存储在队列(内存或磁盘)中。

  2. messagePump,负责将消息复制和推送到上面描述的通道。

  3. DiskQueue 将溢出的消息写入磁盘(超过mem-queue-size大小后的消息)

queue goroutine

此外,每个NSQ channel维护2个按时间顺序排列的优先级队列,负责延迟和发送中的消息(已经传递但没有被消费者进一步处理的消息)超时(以及2个伴生的goroutine用于监视这些超时)。

4.减少GC压力

通过监控与分析,NSQ的文档给了一些意见:

  1. 避免[]byte和string的转换

  2. 重用缓冲区或对象

  3. 预分配切片(指定容量) 

  4. 对各种配置做限制(例如消息大小)

  5. 避免装箱(使用interface{})或不必要的包装类型

  6. 避免在热点代码中使用 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

比如:

  1. 因为框架的组成部分的确切类型和大小是提前知道的,所以我们可以规避了使用方便的编码二进制包的Read()和Write()封装(及它们外部接口的查找和会话)反之我们使用直接调用 binary.BigEndian方法。

  2. 为了消除socket 输入输出的系统调用,客户端net.Conn被封装了bufio.Readerbufio.Writer。这个Reader通过暴露ReadSlice(),复用了它自己的缓冲区。这样几乎消除了读完socket时的分配,这极大的降低了垃圾回收的压力。这可能是因为与数据相关的大多数命令并没有逃逸(在边缘情况下这是假的,数据被强制复制)。

  3. 在更低层,MessageID 被定义为 [16]byte,这样可以将其作为 map 的 key(slice 无法用作 map 的 key(可变的))。然而,考虑到从 socket 读取的数据被保存为 []byte,胜于通过分配字符串类型的 key 来产生垃圾,并且为了避免从 slice 到 MessageID 的底层数组产生复制操作,unsafe 包被用来将 slice 直接转换为 MessageID

    id := *(*nsq.MessageID)(unsafe.Pointer(&msgID))