NSQ的设计思想

2016-11-06

NSQ的设计思想

NSQ继承于simplequeue,是一个实时分布式消息平台,设计目标有:

  • 支持拓扑结构,实现高可用和消除单点故障

  • 满足更强的消息投递的可靠性

  • 限制单个进程的内存上限(超过的写入硬盘)

  • 极大简化生产者和消费者的配置要求

  • 提供简单直接的升级方式

  • 提高效率

简化配置和管理

单个nsqd实例一次可以处理多个消息流。消息流被称为“topics”,一个topic有一个或者多个“channels”。每个channel会接收来自topic的所有消息。实际上,一个channel对应下游个服务消费的一个topic

topics和channels并不是预先配置的。topics是在首次发布或者订阅的时候创建的,channels是在首次订阅的时候创建的。

topics和channels的所有缓存数据都相互独立,目的是为了防止一个“慢”消费者造成消息积压而影响其他topic或channel。

一个channel通常有多个消费者连接,假如所有消费者都是在准备接收消息状态,每个消息会被随机投递到消费者中。

image

综述,消息是从topic广播到channel,然后从channel投递到消费者中。

NSQ还有一个辅助工具nsqlookupd,它提供了服务发现功能,使消费者能够订阅感兴趣的topic所在的所有nsqd的地址。同时在配置方面,使消费者和生产者解耦,他们不需要知道彼此,只需要通过nsqlookupd来建立联系,降低复杂性。

在底层实现中,每个nsqd和nsqlookupd都建立了长连接,定期推送自己的状态信息。nsqlookupd通过这些信息来判断哪个nsqd地址应该返回给消费者。

当添加一个新的消费者时,只要给nsqd客户端初始化nsqlookupd的实例地址。所以在新增消费者或者生产者时都无需修改任何配置,大大减少了复杂度。

需要重点强调的是,nsqd和nsqlookupd的守护进程是相互独立的,在兄弟节点之间没有通信和协作。

我们认为通过某种方式去观察、管理这些节点是非常重要的。我们构建了nsqadmin来处理这件事情,它提供了web界面来浏览topics/channels/consumers的层级、检查深度,以及其他的信息。而且还提供了一些管理员命令,比如移除和清空channel。

简单直接的升级方式

这是我们的重中之重,我们的正式系统处理大量的数据,并依赖于现有的消息工具,所以需要一个有条不紊的方式来升级特定的基础设施,并且没有任何的影响。

首先,在消息生产者方面,构建nsqd来对应simplqueue。比如,nsqd提供了http的/put接口,如同simplequeue,去post二进制的数据。想把消息发布到nsqd的服务只需要修改一小部分代码。

其次,还构建了连接新旧组件的工具。包含以下几个:

  • nsq_pubsub -在nsq的集群中提供一个类似http的发布订阅接口

  • nsq_to_file -把所有的信息写入文件

  • nsq_to_http -提供http请求把所有信息发送到多个目的地址

消除单点故障(SPOF)

NSQ采用的是分布式的设计方式。客户端通过长连接,连接所有指定topic的所有nsqd实例,这里没有中间人,没有单点故障。

只要有足够的客户端消费者连接到所有的生产者以保障大量的消息处理,就能保证所有的信息最终都可以被处理。

对于nsqlookupd,可以通过多个实例来实现高可用。他们之间不需要之间通信,数据是会最终一致的。消费者通过配置的nsqlookupd实例来获取所有信息并将其汇总。

消息投递的可靠性

NSQ保证消息投递至少一次,有可能重复投递。消费者应该自行保证消息处理的幂等性。

这个可靠性也作为消息协议的一部分,处理流程如下:

1.客户端声明准备好接收消息

2.NSQ发送一个消息并临时保存消息到本地

3.客户端发送FIN或者REQ表明处理成功或者失败。如果客户端未在规定的时间内作出响应,NSQ会根据设置的超时时间来自动把消息重新入队列。

这就保证了消息只有在NSQ非正常关闭时会发生丢失。这种情况下,所有内存的信息都会丢失。

如果防止消息丢失是非常重要的,即使是非正常退出,也是可以减少影响度的。一种方式是增加nsqd节点(部署在不同host),来备份消息。因为客户端处理消息是幂等的所以多次消息投递不影响下游系统,以保证任何单个节点故障不至于引起消息丢失。

关键是NSQ要提供构建的模块去支持多种生产用例和可配置程度的耐久性。

内存限制

NSQ通过提供–mem-queue-size的配置选项来设置内存中的消息队列的大小。如果超过消息队列的大小,消息将写入硬盘。细心的读者会发现,通过设置内存消息队列的大小低到某个值时(如1或0)可以提高消息投递的可靠性。后端的硬盘队列用于非正常退出时恢复消息投递。

对于消息投递的可靠性,正常退出时消息会安全的被持久化到硬盘中,包括内存队列、投递中队列、延迟队列以及各种内部缓存。

注意,每个topic和channel的名称后面如果是以#ephemeral结尾,那么缓存中的消息不被持久化到硬盘中而且超过内存消息队列大小时,消息将被丢弃。这使消费者在订阅一个channel时可以无需消息投递可靠性。这种临时的channel在无客户端连接时会自动消失。对于一个临时的topic,意味着至少有一个channel被创建、消费和删除(通常也是临时的channel)。

效率

NSQ的通信协议是一种带有简单报文大小前缀的类似“memcached”的命令协议。所有的消息数据都包含尝试次数、时间戳等元数据。这就减少了数据在服务器到客户端的来回复制,这也简化了客户端对信息状态的维护。

同时通过简化配置的复杂度,开发时间被大大减少。对于数据协议,通过推送数据给客户端来最大化性能和吞吐量,而不是等待客户端来轮询。这个我们称为RDY状态,本质上也是客户端的流控制。

当客户端连接到nsqd并订阅了一个channel,它会被设置为RDY为0的状态。这意味着不会有消息投递给这个客户端。当客户端准备接收消息时,它发送一个命令更新它的RDY值到某个准备处理的值,如100。无需其他命令,100个消息将投递给客户端。

Go

参考资料

https://speakerdeck.com/snakes/nsq-nyc-data-engineering-meetup