NOTE: 有关配套的视觉说明,请参阅此 slide deck。
NSQ 是 simplequeue(simplehttp 的一部分)的继任者,因此 其设计目标(顺序不分先后)是:
单个 nsqd 实例设计为同时处理多个数据流。流称为
“topics”(主题),一个主题有 1 个或多个 “channels”(通道)。每个通道接收主题
所有消息的副本。在实践中,一个通道映射到消费主题的下游服务。
主题和通道不需要预先配置。主题在首次 使用时创建,即向命名主题发布消息或向命名主题上的通道订阅。通道在首次使用时通过向命名通道订阅来创建。
主题和通道都独立缓冲数据,防止慢消费者导致 其他通道的积压(主题级别也同样适用)。
一个通道可以,并且通常有多个客户端连接。假设所有连接的客户端 都处于准备好接收消息的状态,每个消息将交付给一个随机 客户端(当启用拓扑感知消费时,消息将随机交付,但优先基于地理位置最近的客户端(s),请参阅 Topology Aware Consumption)。例如:

总之,消息从主题 -> 通道进行多播(每个通道接收该主题所有消息的副本),但从通道 -> 消费者进行均匀分发(每个消费者接收该通道消息的一部分)。
NSQ 还包括一个辅助应用程序 nsqlookupd,它提供目录服务,消费者可以查找提供它们感兴趣的主题的 nsqd 实例地址。就配置而言,这将消费者与生产者解耦(它们各自只需知道联系共同的 nsqlookupd 实例,从不直接联系对方),从而降低复杂性和维护成本。
在更低层面,每个 nsqd 与 nsqlookupd 保持一个长连接(通过 TCP),通过它定期推送其状态。此数据用于告知 nsqlookupd 将向消费者提供哪些 nsqd 地址。对于消费者,暴露了一个 HTTP /lookup 端点用于轮询。
要引入一个新的主题消费者,只需启动一个配置了您的 nsqlookupd 实例地址的 NSQ 客户端。添加新消费者或新发布者无需任何配置更改,从而大大降低开销和复杂性。
NOTE: 在未来版本中,nsqlookupd 用于返回地址的启发式方法可能基于
深度、连接客户端数量或其他“智能”策略。当前实现
仅为全部。最终目标是确保所有生产者都被读取,从而
保持深度接近零。
重要的是要注意,nsqd 和 nsqlookupd 守护进程设计为独立运行,
兄弟进程之间无需通信或协调。
我们还认为,提供一种查看、内省和管理
集群整体的方法非常重要。我们构建了 nsqadmin 来实现这一点。它提供了一个 Web UI,用于浏览主题/通道/消费者的层次结构,并检查每个层的深度和其他关键统计信息。此外
它支持一些管理命令,例如删除和清空通道(当通道中的消息可以安全丢弃以将深度恢复为 0 时,这是一个有用的
工具)。

这是我们的最高优先级之一。我们的生产系统处理大量流量, 所有这些都基于我们现有的消息工具,因此我们需要一种缓慢且有条不紊地升级 基础设施特定部分的方法,而几乎没有或没有影响。
首先,在消息生产者端,我们构建 nsqd 以匹配 simplequeue。
具体来说,nsqd 暴露了一个 HTTP /pub 端点,就像 simplequeue 一样,用于 POST 二进制数据
(唯一的注意事项是该端点接受一个额外的查询参数指定“topic”)。
想要切换到开始向 nsqd 发布的服务的只需进行少量代码更改。
其次,我们在 Python 和 Go 中构建了库,这些库匹配了我们现有库中习惯的功能和习惯用法。这缓和了消息消费者端的过渡,只需将代码更改限制在引导部分。所有业务逻辑保持不变。
最后,我们构建了实用工具来将旧组件和新组件粘合在一起。这些工具均可在仓库的
examples 目录中找到:
nsq_to_file - 将给定主题的所有消息持久化写入文件nsq_to_http - 为主题中的所有消息执行 HTTP 请求到(多个)端点NSQ 设计为分布式使用。nsqd 客户端(通过 TCP)连接到
所有提供指定主题的实例。没有中间人、没有消息代理,也没有
SPOF:

这种拓扑消除了链式单一聚合馈送的需求。相反,您直接 从所有生产者消费。技术上,哪个客户端连接到哪个 NSQ 并不重要,只要有足够的客户端连接到所有生产者以满足消息量, 您就可以保证所有消息最终都会被处理。
对于 nsqlookupd,高可用性通过运行多个实例实现。它们
不直接相互通信,数据被视为最终一致。消费者轮询
所有配置的 nsqlookupd 实例并合并响应。过时、不可访问或
其他故障节点不会使系统停滞。
NSQ 保证消息将至少交付一次,尽管可能出现重复消息。 消费者应预期这种情况,并去重或执行 幂等 操作。
此保证作为协议的一部分强制执行,并按以下方式工作(假设客户端已 成功连接并订阅了主题):
这确保了唯一可能导致消息丢失的边缘情况是 nsqd
进程的不干净关闭。在那种情况下,任何在内存中的消息(或任何未刷新
到磁盘的缓冲写入)都会丢失。
如果防止消息丢失至关重要,即使是此边缘情况也可以缓解。一个
解决方案是搭建冗余 nsqd 对(在单独的主机上),它们接收相同
部分消息的副本。因为您已将消费者编写为幂等的,对
这些消息进行双倍处理不会对下游产生影响,并允许系统承受任何单节点故障
而不会丢失消息。
要点是 NSQ 提供了构建块来支持各种生产用 例和可配置的持久性程度。
nsqd 提供了一个配置选项 --mem-queue-size,它将确定给定队列中保留
在内存中的消息数量。如果队列的深度超过此阈值,消息
将透明地写入磁盘。这将给定 nsqd 进程的内存占用限制为
mem-queue-size * #_of_channels_and_topics:

此外,一个敏锐的观察者可能已经识别出,这是一种方便的方式,通过将此值设置为较低值(例如 1 或甚至 0)来获得更高的交付保证。磁盘支持的 队列设计为在不干净重启后存活(尽管消息可能被交付两次)。
此外,与消息交付保证相关,干净关闭(通过向 nsqd 进程发送
TERM 信号)会安全地将当前在内存中、飞行中、延迟和各种
内部缓冲区中的消息持久化。
注意,以字符串 #ephemeral 结尾的主题/通道名称将不会缓冲到磁盘,
而是在超过 mem-queue-size 后丢弃消息。这使得不需要
消息保证的消费者可以订阅通道。这些临时通道将在其最后一个客户端断开连接后消失。对于临时主题,这意味着至少有一个通道
已被创建、消费和删除(通常是一个临时通道)。
NSQ 设计为通过“memcached-like”命令协议进行通信,并使用简单的 大小前缀响应。所有消息数据(包括尝试次数、时间戳等元数据)都保留在核心中。这消除了服务器与客户端之间数据的来回复制, 这是先前工具链在重新排队消息时的固有属性。这还简化了 客户端,因为它们不再需要负责维护消息状态。
此外,通过降低配置复杂性,设置和开发时间大大减少 (特别是在主题有 >1 个消费者的情况下)。
对于数据协议,我们做出了一个关键设计决策,通过
将数据推送到客户端而不是等待其拉取,从而最大化性能和吞吐量。这个概念,我们
称之为 RDY 状态,本质上是客户端侧的流控制形式。
当客户端连接到 nsqd 并订阅通道时,它被置于 RDY 状态 0。
这意味着不会向客户端发送任何消息。当客户端准备好接收消息
时,它发送一个命令,将其 RDY 状态更新为它准备处理的某个数字,例如 100。没有
任何额外命令,100 个消息将随着可用而推送到客户端(每次
递减该客户端的服务器侧 RDY 计数)。
客户端库设计为在 RDY 计数达到可配置的 max-in-flight 设置的 ~25% 时发送命令更新 RDY 计数(并正确考虑连接到多个 nsqd
实例,适当划分)。

这是一个重要的性能旋钮,因为某些下游系统能够更容易地批量
处理消息,并从更高的 max-in-flight 中大大受益。
值得注意的是,因为它既是缓冲的并且基于推送的,并具有满足独立流副本需求(通道)的能力,我们产生了一个像 simplequeue
和 pubsub 结合 一样的守护进程。这在简化我们系统拓扑方面非常强大,在传统情况下我们会维护上面讨论的旧工具链。
nsqd 提供了一个配置选项 --enable-experiments,可用于启用 topology-aware-consumption 功能。如果启用此实验并为 --topology-region 和 --topology-zone 提供了配置,nsqd 将基于拓扑感知行为模式向消费者推送消息。
消费者可以通过在向 nsqd 主机发送的 IDENTIFY 消息中设置的选项提供自己的拓扑区域和区域。如果未提供此信息,则消费者将被视为位于 nsqd 的区域和区域之外。
配置后,nsqd 将基于地理位置最近的消费者优先推送消息。它将优先向同一区域和区域内的消费者推送消息(如果可能),然后向同一区域内的消费者推送消息,最后将回退到随机向任何可能的消费者推送(常规行为)。
此行为模式允许在多区域应用架构中运行 nsqd,并最小化 nsqd 与不同地理位置的消费者交互的网络出口成本。
我们早期做出了一个战略决策,使用 Go 构建 NSQ 核心。我们最近在博客中 介绍了我们在 bitly 的 Go 使用情况,并提到了这个项目——浏览该帖子可能有助于理解我们对该语言的思考。
关于 NSQ,Go 通道(不要与 NSQ 通道混淆)和语言的内置
并发功能非常适合 nsqd 的内部工作。我们利用缓冲
通道来管理内存中的消息队列,并无缝地将溢出写入磁盘。
标准库使编写网络层和客户端代码变得容易。内置的 内存和 CPU 分析钩子突出了优化机会,并需要很少 努力来集成。我们还发现测试组件隔离、模拟类型使用 接口以及迭代构建功能非常容易。