NSQ 设计理念

原文是对 NSQ 设计理念的阐述,要理解 NSQ 的源代码,对设计理念的理解是至关重要的,因此翻译了 NSQ 团队的设计理念,以方便同行阅读,如有错漏,烦请指正。

术语

英文 中文
topic 主题
channel 频道
producer 生产者
consumer 消费者
push 发布
subscribe 订阅
SPOF 单点故障
depth 深度

======================================================

注意:文中的视觉插图,请参见此幻灯片

NSQ是simplequeue的继任者(simplehttp是 simplequeue 的一部分),因此被设计为(没有特定的顺序):

简化配置和管理

单个 nsqd 实例旨在一次处理多个数据流。流称为”主题”(Topic),一个主题具有1个或多个”频道”(Channel)。每个频道都会收到一个主题的所有消息的*副本*。实际上,频道映射到消耗主题的下游服务。

主题和频道*不是*事先配置的。通过发布到指定主题或订阅指定主题上的频道,可以在首次使用时创建主题。同理,消费者首次订阅指定的频道时创建频道。

主题和频道都相互独立地缓冲数据,从而防止性能不佳的消费者造成其他频道的积压(在主题级别也是如此)。

通常来说,一个频道通常可以连接多个客户端。假设所有连接的客户端都处于准备接收消息的状态,则每条消息都将传递给随机的客户端。例如:

nsqd客户

总而言之,消息是从主题->频道多播的(每个频道都接收该主题的所有消息的副本),但从频道->消费者均匀分发(每个消费者都接收该频道的一部分消息)。

NSQ 还包括一个 nsqlookupd 应用程序,该应用程序提供了目录服务,消费者可以在其中查找提供特定的主题的nsqd 实例的地址。从配置方面来看,这使消费者与生产者分离(他们两个都只需要知道在哪里联系的nsqlookupd实例,而不再需要从配置文件从获取到各自的地址),从而降低了系统的复杂性和维护成本。

在较低级别,每个nsqd服务器与nsqlookupd都有一个长期存在的 TCP 连接,nsqd周期性地将其状态推送到 nsqlookupd。此数据用于告知给消费者nsqd的地址。对于消费者,向 nsqlookupd 发送 HTTP /lookup 请求获取 nsqd 的信息。

对于一个主题,要引入新的消费者,只需启动配置有nsqlookupd实例地址的 NSQ 客户端即可。无需更改配置即可添加新的消费者或新的发布者,从而大大降低了开销和复杂性。

注意:当前的实现仅仅返回*全部*的实例地址,在将来的版本中,nsqlookupd返回地址的可能包括基于深度、连接的客户端数量或其他”智能”策略。最终,目标是确保所有生产者都会被消费,以使深度保持接近零。

重要的是要注意,nsqdnsqlookupd被设计为独立运行,同种类型的实例之间无需通信或协调。

我们还认为,拥有一种查看和管理集群的方法非常重要。nsqadmin就是为此目的而创建的。它提供了一个 Web 界面,以浏览主题/频道/消费者的层次结构,并检查每一层的深度和其他关键统计数据。此外,它还支持一些管理命令,例如删除和清空通道。

nsqadmin

简单的升级路径

这是我们的最高优先事项之一。我们的生产系统全部基于我们现有的消息传递工具来处理大量流量,因此我们需要一种缓慢而有条不紊、逐步地升级基础架构的方法。

首先,在消息*生产者*方面,我们构建了nsqd以匹配simplequeue。具体地说,nsqd 与simplequeue 一样通过post /put 发布二进制数据。切换为nsqd服务只需更改较小的代码即可。

其次,我们在 Python 和 Go 中都构建了与我们现有库中惯用的功能和习惯相匹配的库。通过将代码更改限制为在初始化阶段,这简化了消息*消费者*的过渡。所有业务逻辑都保持不变。

最后,我们构建了将新旧组件粘合在一起的实用程序。这些都可以在examples仓库中找到:

消除单点故障

NSQ按照*分布式*的理念设计和开发。nsqd客户端(通过TCP)连接到提供指定主题的所有实例。没有中间人,没有消息代理,也没有单点故障:

nsq客户

这种拓扑结构消除了链接单个聚合订阅源的需要。相反,您直接从所有生产者那里消费。*从技术上讲*,哪个客户端连接到哪个NSQ都无关紧要,只要有足够的客户端连接到所有生产者来满足消息量,就可以确保最终将处理所有消息。

对于nsqlookupd,可以通过运行多个实例来实现高可用性。它们不会直接相互通信,并且数据最终被认为是一致的。消费者轮询*所有*已配置的nsqlookupd实例,并合并响应。过时,无法访问或出现其他故障的节点不会使系统陷入瘫痪。

消息投递保证

NSQ保证消息将至少投递一次,尽管可能会出现重复投递消息的情况。消费者应该对此有所期待,并进行数据去重或执行幂等操作。

此保证作为协议的一部分而强制执行,并且按以下方式工作(假定客户端已成功连接并订阅了主题):

  1. 客户端表明他们已准备好接收消息
  2. NSQ发送消息并在本地临时存储数据(重新排队或超时)
  3. 客户端分别答复 FIN(完成)或 REQ(重新排队),分别指示成功或失败。如果客户端未回复,则NSQ将在可配置的持续时间后超时并自动重新排队该消息。

这样可以确保导致消息丢失的唯一极端情况是nsqd进程的异常关闭 。在这种情况下,内存中的任何消息(或任何未刷新到磁盘的缓冲写入)都将丢失。

如果防止消息丢失至关重要,那么即使是这种极端情况也可以缓解。一种解决方案是启动冗余nsqd进程(在不同的主机上),以接收消息的相同部分的副本。因为您已将消费者编写成等幂的,所以对这些消息进行两次处理不会对下游产生影响,并且使系统能够承受任何单节点故障而不会丢失消息。

结论是,NSQ提供了构建模块来支持各种生产用例和可配置的耐久性。

有界内存占用

nsqd提供一个配置选项--mem-queue-size,该选项将确定给定队列*在内存*中保留的消息数。如果队列的深度超过此阈值,则将消息透明地写入磁盘。这将给定nsqd进程的内存占用空间限制为 mem-queue-size * num_of_channels_and_topics

消息溢出

同样,一个精明的用户可能已经发现,通过将此值设置为较低的值(例如1或甚至0),这是获得更高的交货保证的便捷方法。磁盘支持的队列旨在承受意外的重启(尽管消息可能会投递两次)。

同样,与消息投递保证有关,*有计划的*关闭(通过发送TERM信号给nsqd进程)可以安全地将内存中、进行中、延迟的以及各种内部缓冲区中的消息保存到磁盘中。

请注意,名称以字符串#ephemeral结尾的主题/频道不会被缓冲到磁盘,超过mem-queue-size的消息将被丢弃。这使不需要消息保证的用户可以订阅频道。在其最后一个客户端断开连接后,这些临时通道也将消失。对于临时主题,这意味着已创建,使用和删除了至少一个频道(通常是一个临时频道)。

效率

NSQ旨在通过具有简单大小前缀响应的”memcached-like”的命令协议进行通信。所有消息数据都保存在核心中,包括元数据,例如尝试次数,时间戳等。这消除了从服务器到客户端来回复制数据的情况,这是先前工具链在重新排队消息时的固有属性。这也简化了客户端,因为它们不再需要负责维护消息状态。

此外,通过降低配置复杂性,可以大大减少设置和开发时间(尤其是在某个主题的消费者超过1个的情况下)。

对于数据协议,我们做出了一项关键设计决策,该决策通过将数据推送到客户端而不是等待其拉取来最大化性能和吞吐量。这个概念(我们称为RDY状态)本质上是客户端流控制的一种形式。

当客户端连接nsqd并订阅频道时,其RDY状态为0。这意味着不会有任何消息发送到客户端。当客户端准备好接收消息时,它会发送一条命令,将其RDY状态更新为准备处理的序号,例如100。如果没有任何其他命令,则会在有可用消息时将100条消息推送到客户端(每次减少该客户端的服务器端RDY计数)。

客户端库会在RDY在其达到max-in-flight配置项的~25%左右时发送命令更新计数(并正确考虑到多个nsqd 实例的连接,并进行适当划分)。

nsq协议

这是一个重要的性能调节机制,因为某些下游系统能够更轻松地批量处理消息,并从更高的max-in-flight中受益匪浅。

值得注意的是,由于它既基于缓冲*又具有*推送功能,并且能够满足流(通道)对独立副本的需求,因此我们创建了一个行为类似于simplequeue 和pubsub *组合*的守护程序。就简化我们需要维护的系统拓扑。

Go

我们很早就做出了一项战略决策,即使用Go构建 NSQ 核心。我们最近在的博客中介绍了我们对Go的使用。浏览该帖子以了解我们对语言的看法可能会有所帮助。

关于NSQ,Go Channel(不要与NSQ通道相混淆)和该语言的内置并发功能非常适合nsqd的内部工作。我们利用缓冲的通道来管理内存消息队列,并将溢出无缝地写入磁盘。

标准库使编写网络层和客户端代码变得容易。内置的内存和cpu性能分析Hook帮助进行性能优化,并且几乎不需要进行任何集成。Go test 使得隔离测试各部分也能很容易实现。

评论

退出登录