NSQ’s design pushes a lot of responsibility onto client libraries in order to maintain overall cluster robustness and performance.


This guide attempts to outline the various responsibilities well-behaved client libraries need to fulfill. Because publishing to nsqd is trivial (just an HTTP POST to the /put endpoint), this document focuses on consumers.

本指南试图概述行为良好的客户端库需要履行的各种职责。因为发布到“nsqd”非常简单(只是到“/put”端点的HTTP POST),所以本文主要关注消费者。

By setting these expectations we hope to provide a foundation for achieving consistency across languages for NSQ users.


Overview 概述

  1. Configuration
  2. Discovery (optional)
  3. Connection Handling
  4. Feature Negotiation
  5. Data Flow / Heartbeats
  6. Message Handling
  7. RDY State
  8. Backoff
  9. Encryption/Compression

Configuration 配置

At a high level, our philosophy with respect to configuration is to design the system to have the flexibility to support different workloads, use sane defaults that run well “out of the box”, and minimize the number of dials.


A consumer subscribes to a topic on a channel over a TCP connection to nsqd instance(s). You can only subscribe to one topic per connection so multiple topic consumption needs to be structured accordingly.


Using nsqlookupd for discovery is optional so client libraries should support a configuration where a consumer connects directly to one or more nsqd instances or where it is configured to poll one or more nsqlookupdinstances. When a consumer is configured to poll nsqlookupd the polling interval should be configurable. Additionally, because typical deployments of NSQ are in distributed environments with many producers and consumers, the client library should automatically add jitter based on a random % of the configured value. This will help avoid a thundering herd of connections. For more detail see Discovery.


An important performance knob for consumers is the number of messages it can receive before nsqd expects a response. This pipelining facilitates buffered, batched, and asynchronous message handling. By convention this value is called max_in_flight and it effects how RDY state is managed. For more detail see RDY State.

对于消费者来说,一个重要的性能旋钮是它在“nsqd”预期响应之前可以接收到的消息数量。这种管道简化了缓冲、批处理和异步消息处理。按照惯例,这个值称为' max_in_flight ',它影响' RDY '状态的管理方式。有关详细信息,请参见RDY状态。

Being a system that is designed to gracefully handle failure, client libraries are expected to implement retry handling for failed messages and provide options for bounding that behavior in terms of number of attempts per message. For more detail see Message Handling.


Relatedly, when message processing fails, the client library is expected to automatically handle re-queueing the message. NSQ supports sending a delay along with the REQ command. Client libraries are expected to provide options for what this delay should be set to initially (for the first failure) and how it should change for subsequent failures. For more detail see Backoff.

与此相关,当消息处理失败时,客户端库将自动处理重新排队的消息。NSQ支持发送延迟和REQ 命令。客户端库应该提供选项,以确定最初应该将延迟设置为什么(对于第一次故障),以及在后续故障时应该如何更改延迟。有关详细信息,请参阅Backoff

Most importantly, the client library should support some method of configuring callback handlers for message processing. The signature of these callbacks should be simple, typically accepting a single parameter (an instance of a “message object”).


Discovery 发现

An important component of NSQ is nsqlookupd, which provides a discovery service for consumers to locate the nsqd that provide a given topic at runtime.

NSQ的一个重要组件是nsqlookupd ,它为使用者提供了一个发现服务,以定位在运行时提供给定主题的 nsqd

Although optional, using nsqlookupd greatly reduces the amount of configuration required to maintain and scale a large distributed NSQ cluster.


When a consumer uses nsqlookupd for discovery, the client library should manage the process of polling all nsqlookupd instances for an up-to-date set of nsqd providing the topic in question, and should manage the connections to those nsqd.


Querying an nsqlookupd instance is straightforward. Perform an HTTP request to the lookup endpoint with a query parameter of the topic the consumer is attempting to discover (i.e. /lookup?topic=clicks).


The response format is JSON:


    "channels": ["archive", "science", "metrics"],
    "producers": [
            "broadcast_address": "",
            "hostname": "",
            "remote_address": "",
            "tcp_port": 4150,
            "http_port": 4151,
            "version": "1.0.0-compat"
            "broadcast_address": "",
            "hostname": "",
            "remote_address": "",
            "tcp_port": 4150,
            "http_port": 4151,
            "version": "1.0.0-compat"

The broadcast_address and tcp_port should be used to connect to an nsqd. Because, by design, nsqlookupdinstances don’t share or coordinate their data, the client library should union the lists it received from all nsqlookupd queries to build the final list of nsqd to connect to. The broadcast_address:tcp_port combination should be used as the unique key for this union.

应该使用broadcast_address tcp_port 连接到 nsqd 。因为按照设计,nsqlookupd 实例不共享或协调它们的数据,客户端库应该将它从所有 nsqlookupd 查询中接收到的列表联合起来,以构建要连接到的nsqd 的最终列表。

A periodic timer should be used to repeatedly poll the configured nsqlookupd so that consumers will automatically discover new nsqd. The client library should automatically initiate connections to all newly discovered instances.


When client library execution begins it should bootstrap this polling process by kicking off an initial set of requests to the configured nsqlookupd instances.


Connection Handling 链接处理

Once a consumer has an nsqd to connect to (via discovery or manual configuration), it should open a TCP connection to broadcast_address:port. A separate TCP connection should be made to each nsqd for each topic the consumer wants to subscribe to.


When connecting to an nsqd instance, the client library should send the following data, in order:


  1. the magic identifier


  1. an IDENTIFY command (and payload) and read/verify response (see Feature Negotiation)

一个' IDENTIFY '命令(和有效负载)和read/verify响应(参见Feature Negotiation)

  1. a SUB command (specifying desired topic) and read/verify response

一个 SUB 命令(指定所需的主题)并读取/验证响应

  1. an initial RDY count of 1 (see RDY State).

初始' RDY '计数为1(参见RDY State)。

(low-level details on the protocol are available in the spec)


Reconnection 重新连接

Client libraries should automatically handle reconnection as follows:


  • If the consumer is configured with a specific list of nsqd instances, reconnection should be handled by delaying the retry attempt in an exponential backoff manner (i.e. try to reconnect in 8s, 16s, 32s, etc., up to a max).


  • If the consumer is configured to discover instances via nsqlookupd, reconnection should be handled automatically based on the polling interval (i.e. if a consumer disconnects from an nsqd, the client library should only attempt to reconnect if that instance is discovered by a subsequent nsqlookupd polling round). This ensures that consumers can learn about nsqd that are introduced to the topology and ones that are removed (or failed).

如果使用者被配置为通过' nsqlookupd '发现实例,则应根据轮询间隔自动处理重连接(即,如果使用者断开与' nsqd '的连接,则只有在随后的' nsqlookupd '轮询发现该实例时,客户端库才应尝试重新连接)。这确保用户可以了解引入到拓扑中的“nsqd”和删除(或失败)的“nsqd”。Feature Negotiation 会话参数协商

The IDENTIFY command can be used to set nsqd side metadata, modify client settings, and negotiate features. It satisfies two needs:


  1. In certain cases a client would like to modify how nsqd interacts with it (such as modifying a client’s heartbeat interval and enabling compression, TLS, output buffering, etc. - for a complete list see the spec)


  1. nsqd responds to the IDENTIFY command with a JSON payload that includes important server side configuration values that the client should respect while interacting with the instance.

nsqd 使用JSON有效负载响应 IDENTIFY 命令,该有效负载包含客户端在与实例交互时应该尊重的重要服务器端配置值。

After connecting, based on the user’s configuration, a client library should send an IDENTIFY command, the body of which is a JSON payload:


    "client_id": "metrics_increment",
    "hostname": "",
    "heartbeat_interval": 30000,
    "feature_negotiation": true

The feature_negotiation field indicates that the client can accept a JSON payload in return. The client_id and hostname are arbitrary text fields that are used by nsqd (and nsqadmin) to identify clients. heartbeat_intervalconfigures the interval between heartbeats on a per-client basis.

feature_negotiation 字段表示客户端可以接受JSON有效负载作为回报。client_idhostname是由nsqd(和nsqadmin)用来标识客户机的任意文本字段。 heartbeat_interval 根据每个客户端配置心跳之间的间隔。

The nsqd will respond OK if it does not support feature negotiation (introduced in nsqd v0.2.20+), otherwise:

如果nsqd不支持特性协商(在nsqd v0.2.20+中引入),则nsqd将响应OK,否则:

    "max_rdy_count": 2500,
    "version": "0.2.20-alpha"

More detail on the use of the max_rdy_count field is in the RDY State section.

有关 max_rdy_count字段的使用的更多细节,请参见RDY State部分

Data Flow and Heartbeats 数据流和心跳

Once a consumer is in a subscribed state, data flow in the NSQ protocol is asynchronous. For consumers, this means that in order to build truly robust and performant client libraries they should be structured using asynchronous network IO loops and/or “threads” (the scare quotes are used to represent both OS-level threads and userland threads, like coroutines).


Additionally clients are expected to respond to periodic heartbeats from the nsqd instances they’re connected to. By default this happens at 30 second intervals. The client can respond with any command but, by convention, it’s easiest to simply respond with a NOP whenever a heartbeat is received. See the protocol spec for specifics on how to identify heartbeats.


A “thread” should be dedicated to reading data off the TCP socket, unpacking the data from the frame, and performing the multiplexing logic to route the data as appropriate. This is also conveniently the best spot to handle heartbeats. At the lowest level, reading the protocol involves the following sequential steps:


  1. read 4 byte big endian uint32 size

读取四个字节 大端存储的 无符号32整形 大小

  1. read size bytes data


  1. unpack data



  1. profit


  1. goto 1

跳转到 第一步

A Brief Interlude on Errors 错误的一个插曲

Due to their asynchronous nature, it would take a bit of extra state tracking in order to correlate protocol errors with the commands that generated them. Instead, we took the “fail fast” approach so the overwhelming majority of protocol-level error handling is fatal. This means that if the client sends an invalid command (or gets itself into an invalid state) the nsqd instance it’s connected to will protect itself (and the system) by forcibly closing the connection (and, if possible, sending an error to the client). This, coupled with the connection handling mentioned above, makes for a more robust and stable system.

由于它们是异步的,因此需要进行一些额外的状态跟踪,以便将协议错误与生成错误的命令关联起来。相反,我们采用了“快速失败”方法,因此绝大多数协议级错误处理都是致命的。这意味着,如果客户机发送了一个无效命令(或使自己进入无效状态),那么它所连接的' nsqd '实例将通过强制关闭连接(如果可能的话,还会向客户机发送错误)来保护自己(和系统)。这一点,加上上面提到的连接处理,使得系统更加健壮和稳定。

The only errors that are not fatal are:


  • E_FIN_FAILED - a FIN command for an invalid message ID

E_FIN_FAILED - FIN 命令获取无效的消息ID

  • E_REQ_FAILED - a REQ command for an invalid message ID

E_REQ_FAILED - REQ 命令获取无效的消息ID

  • E_TOUCH_FAILED - a TOUCH command for an invalid message ID


Because these errors are most often timing issues, they are not considered fatal. These situations typically occur when a message times out on the nsqd side and is re-queued and delivered to another consumer. The original recipient is no longer allowed to respond on behalf of that message.


Message Handling 消息处理

When the IO loop unpacks a data frame containing a message, it should route that message to the configured handler for processing.


The sending nsqd expects to receive a reply within its configured message timeout (default: 60 seconds). There are a few possible scenarios:


  1. The handler indicates that the message was processed successfully.


  1. The handler indicates that the message processing was unsuccessful.


  1. The handler decides that it needs more time to process the message.


  1. The in-flight timeout expires and nsqd automatically re-queues the message.


In the first 3 cases, the client library should send the appropriate command on the consumer’s behalf (FIN, REQ, and TOUCH respectively).

在前3种情况下,客户端库应该代表消费者发送适当的命令(分别是' FIN '、' REQ '和' TOUCH ')。

The FIN command is the simplest of the bunch. It tells nsqd that it can safely discard the message. FIN can also be used to discard a message that you do not want to process or retry.

“FIN”命令是其中最简单的一个。它告诉' nsqd '它可以安全地丢弃该消息。“FIN”还可以用来丢弃不想处理或重试的消息。

The REQ command tells nsqd that the message should be re-queued (with an optional parameter specifying the amount of time to defer additional attempts). If the optional parameter is not specified by the consumer, the client library should automatically calculate the duration in relation to the number of attempts to process the message (a multiple is typically sufficient). The client library should discard messages that exceed the configured max attempts. When this occurs, a user-supplied callback should be executed to notify and enable special handling.


If the message handler requires more time than the configured message timeout, the TOUCH command can be used to reset the timer on the nsqd side. This can be done repeatedly until the message is either FIN or REQ, up to the sending nsqd’s configured max_msg_timeout. Client libraries should never automatically TOUCH on behalf of the consumer.

如果消息处理程序需要的时间超过配置的消息超时时间,可以使用TOUCH命令重置nsqd端上的计时器。这可以重复执行,直到消息是 FIN REQ,直到发送' nsqd '配置的max_msg_timeout 为止。客户端库永远不应该代表消费者自动TOUCH

If the sending nsqd instance receives no response, the message will time out and be automatically re-queued for delivery to an available consumer.


Finally, a property of each message is the number of attempts. Client libraries should compare this value against the configured max and discard messages that have exceeded it. When a message is discarded there should be a callback fired. Typical default implementations of this callback might include writing to a directory on disk, logging, etc. The user should be able to override the default handling.


RDY State RDY状态

Because messages are pushed from nsqd to consumers we needed a way to manage the flow of data in user-land rather than relying on low-level TCP semantics. A consumer’s RDY state is NSQ’s flow control mechanism.


As outlined in the configuration section, a consumer is configured with a max_in_flight. This is a concurrency and performance knob, e.g. some downstream systems are able to more-easily batch process messages and benefit greatly from a higher max-in-flight.


When a consumer connects to nsqd (and subscribes) it is placed in an initial RDY state of 0. No messages will be delivered.


Client libraries have a few responsibilities:


  1. bootstrap and evenly distribute the configured max_in_flight to all connections.


  1. never allow the aggregate sum of RDY counts for all connections (total_rdy_count) to exceed the configured max_in_flight.


  1. never exceed the per connection nsqd configured max_rdy_count.


  1. expose an API method to reliably indicate message flow starvation


1. Bootstrap and Distribution 引导和分布

There are a few considerations when choosing an appropriate RDY count for a connection (in order to evenly distribute max_in_flight):


  • the # of connections is dynamic, often times not even known in advance (ie. when discovering nsqd via nsqlookupd).

连接的#是动态的,通常甚至事先不知道(即。当通过' nsqlookupd '发现' nsqd '时)。

  • max_in_flight may be lower than your number of connections

' max_in_flight '可能低于您的连接数

To kickstart message flow a client library needs to send an initial RDY count. Because the eventual number of connections is often not known ahead of time it should start with a value of 1 so that the client library does not unfairly favor the first connection(s).

要启动消息流,客户机库需要发送一个初始RDY 计数。因为连接的最终数量通常不提前知道,所以它应该以值“1”开始,这样客户端库就不会不公平地偏爱第一个连接。

Additionally, after each message is processed, the client library should evaluate whether or not it’s time to update RDY state. An update should be triggered if the current value is 0 or if it is below ~25% of the last value sent.

此外,在处理每条消息之后,客户机库应该评估是否应该更新RDY状态。如果当前值为0 或小于上次发送值的25%,则应触发更新。

The client library should always attempt to evenly distribute RDY count across all connections. Typically, this is implemented as max_in_flight / num_conns.

客户端库应该始终尝试在所有连接之间平均分配RDY计数。通常,这被实现为max_in_flight / num_conns

However, when max_in_flight < num_conns this simple formula isn’t sufficient. In this state, client libraries should perform a dynamic runtime evaluation of connected nsqd “liveness” by measuring the duration of time since it last received a message over a given connection. After a configurable expiration, it should re-distribute whatever RDY count is available to a new (random) set of nsqd. By doing this, you guarantee that you’ll (eventually) find nsqd with messages. Clearly this has a latency impact.

然而,当max_in_flight < num_conns 这个简单的公式是不够的。在这种状态下,客户端库应该执行一个动态运行时评估连接的nsqd活动,方法是测量自它上次通过给定连接接收消息以来的持续时间。在一个可配置的过期之后,它应该重新分配一个新的(随机的) nsqd集合可以使用的任何RDY计数。通过这样做,您可以保证(最终)找到带有消息的nsqd。显然,这有一个延迟影响。

2. Maintaining max_in_flight 维护 max_in_flight

The client library should maintain a ceiling for the maximum number of messages in flight for a given consumer. Specifically, the aggregate sum of each connection’s RDY count should never exceed the configured max_in_flight.


Below is example code in Python to determine whether or not the proposed RDY count is valid for a given connection:


def send_ready(reader, conn, count):
    if (reader.total_ready_count + count) > reader.max_in_flight:

    conn.rdy_count = count
    reader.total_ready_count += count

3. nsqd Max RDY Count nsqd 最大RDY 计数

Each nsqd is configurable with a --max-rdy-count (see feature negotiation for more information on the handshake a consumer can perform to ascertain this value). If the consumer sends a RDY count that is outside of the acceptable range its connection will be forcefully closed. For backwards compatibility, this value should be assumed to be 2500 if the nsqd instance does not support feature negotiation.

每个“nsqd”都可以配置一个“—max-rdy-count”(参见feature negotiation,以获得关于消费者可以执行的握手的更多信息,从而确定这个值)。如果使用者发送一个超出可接受范围的“RDY”计数,则其连接将被强制关闭。为了向后兼容,如果“nsqd”实例不支持feature negotiation,那么这个值应该假定为“2500”。

4. Message Flow Starvation

Finally, the client library should provide an API method to indicate message flow starvation. It is insufficient for consumers (in their message handlers) to simply compare the number of messages they have in-flight vs. their configured max_in_flight in order to decide to “process a batch”. There are two cases when this is problematic:

最后,客户端库应该提供一个API方法来表示消息流不足。对于消费者(在他们的消息处理程序中)来说,简单的比较他们在飞行中的信息数量和。为了决定“process a batch”,他们配置了`max_in_flight。有两种情况这是有问题的。

  1. When consumers configure max_in_flight > 1, due to variable num_conns, there are cases where max_in_flight is not evenly divisible by num_conns. Because the contract states that you should never exceed max_in_flight, you must round down, and you end up with cases where the sum of all RDY counts is less than max_in_flight.

当用户配置max_in_flight > 1时,由于变量num_conns ,在某些情况下max_in_flight 不能被 num_conns 均匀地整除。因为合同规定永远不要超过max_in_flight ,所以必须四舍五入,这样就会出现所有RDY 计数之和小于 max_in_flight 的情况。

  1. Consider the case where only a subset of nsqd have messages. Because of the expected even distribution of RDY count, those active nsqd only have a fraction of the configured max_in_flight.

考虑这样一种情况,即只有 nsqd 的一个子集有消息。由于预期的RDY计数even distribution,那些活动的nsqd只有配置的max_in_flight的一小部分。

In both cases, a consumer will never actually receive max_in_flight # of messages. Therefore, the client library should expose a method is_starved that will evaluate whether any of the connections are starved, as follows:

在这两种情况下,使用者实际上永远不会收到' max_in_flight ' #消息。因此,客户端库应该公开一个 is_hungry 方法,该方法将评估是否有任何连接是不足的,如下所示:

def is_starved(conns):
    for c in conns:
        # the constant 0.85 is designed to *anticipate* starvation rather than wait for it
        if c.in_flight > 0 and c.in_flight >= (c.last_ready * 0.85):
            return True
    return False

The is_starved method should be used by message handlers to reliably identify when to process a batch of messages.

is_starved 消息处理程序应该使用方法来可靠地确定何时处理一批消息。

Backoff 回退

The question of what to do when message processing fails is a complicated one to answer. The message handlingsection detailed client library behavior that would defer the processing of failed messages for some (increasing) duration of time. The other piece of the puzzle is whether or not to reduce throughput. The interplay between these two pieces of functionality is crucial for overall system stability.


By slowing down the rate of processing, or “backing off”, the consumer allows the downstream system to recover from transient failure. However, this behavior should be configurable as it isn’t always desirable, such as situations where latency is prioritized.


Backoff should be implemented by sending RDY 0 to the appropriate nsqd, stopping message flow. The duration of time to remain in this state should be calculated based on the number of repeated failures (exponential). Similarly, successful processing should reduce this duration until the reader is no longer in a backoff state.

Backoff应该通过发送“RDY 0”到适当的“nsqd”来实现。停止消息流。保持这种状态的持续时间应该根据重复故障的数量(指数级)计算。同样,成功的处理应该减少这段时间,直到读者不再处于后退状态。

While a reader is in a backoff state, after the timeout expires, the client library should only ever send RDY 1regardless of max_in_flight. This effectively “tests the waters” before returning to full throttle. Additionally, during a backoff timeout, the client library should ignore any success or failure results with respect to calculating backoff duration (i.e. it should only take into account one result per backoff timeout).

当阅读器处于回退状态时,超时过期后,客户机库应该只发送“RDY 1”,而不管“max_in_flight”是什么。这有效地“试水”之前,返回全速。此外,在回退超时期间,客户端库应该忽略与计算回退时间有关的任何成功或失败结果(即,它应该只考虑每个回退超时一个*结果)。


Encryption/Compression 加密/压缩

NSQ supports encryption and/or compression feature negotiation via the IDENTIFY command. TLS is used for encryption. Both Snappy and DEFLATE are supported for compression. Snappy is available as a third-party library, but most languages have some native support for DEFLATE.

NSQ支持通过IDENTIFY命令进行加密和/或压缩特性协商 .TLS用于加密。压缩支持Snappy和DEFLATE.Snappy是一个第三方库,但是大多数语言都支持DEFLATE。

When the IDENTIFY response is received and you’ve requested TLS via the tls_v1 flag you’ll get something similar to the following JSON:


    "deflate": false,
    "deflate_level": 0,
    "max_deflate_level": 6,
    "max_msg_timeout": 900000,
    "max_rdy_count": 2500,
    "msg_timeout": 60000,
    "sample_rate": 0,
    "snappy": true,
    "tls_v1": true,
    "version": "0.2.28"

After confirming that tls_v1 is set to true (indicating that the server supports TLS), you initiate the TLS handshake (done, for example, in Python using the ssl.wrap_socket call) before anything else is sent or received on the wire. Immediately following a successful TLS handshake you must read an encrypted NSQ OK response.

确认tls_v1被设置为true 之后(表明服务器支持TLS),您可以启动TLS握手(例如,在Python中使用ssl.wrap_socket调用)在任何其他东西通过网络发送或接收之前,在一次成功的TLS握手之后,您必须立即读取一个加密的NSQ OK 响应。

In a similar fashion, if you’ve enabled compression you’ll look for snappy or deflate being true and then wrap the socket’s read and write calls with the appropriate (de)compressor. Again, immediately read a compressed NSQ OK response.

类似于,如果您启用了压缩,您将查找snappydeflate是否为true,然后使用适当的压缩器包装套接字的读和写调用。同样,立即读取压缩的NSQ OK 响应。

These compression features are mutually-exclusive.


It’s very important that you either prevent buffering until you’ve finished negotiating encryption/compression, or make sure to take care to read-to-empty as you negotiate features


Bringing It All Together 全体总结

Distributed systems are fun.


The interactions between the various components of an NSQ cluster work in concert to provide a platform on which to build robust, performant, and stable infrastructure. We hope this guide shed some light as to how important the client’s role is.


In terms of actually implementing all of this, we treat pynsq and go-nsq as our reference codebases. The structure of pynsq can be broken down into three core components:


  • Message - a high-level message object, which exposes stateful methods for responding to nsqd (FIN, REQ, TOUCH, etc.) as well as metadata such as attempts and timestamp.

消息 - 高级消息对象,它公开了响应nsqd(FINREQTOUCH等)的有状态方法,以及尝试和时间戳等元数据。

  • Connection - a high-level wrapper around a TCP connection to a specific nsqd, which has knowledge of in flight messages, its RDY state, negotiated features, and various timings.

连接 - 一个高级的TCP连接包装器,连接到一个特定的“nsqd”,它了解飞行消息、RDY状态、协商特性和各种时间。

  • Consumer - the front-facing API a user interacts with, which handles discovery, creates connections (and subscribes), bootstraps and manages RDY state, parses raw incoming data, creates Message objects, and dispatches messages to handlers.

消费者 - 用户交互的前端API,处理发现、创建连接(和订阅)、引导和管理RDY状态、解析原始传入数据、创建“Message”对象,并将消息分派给处理程序。

  • Producer - the front-facing API a user interacts with, which handles publishing.


We’re happy to help support anyone interested in building client libraries for NSQ. We’re looking for contributors to continue to expand our language support as well as flesh out functionality in existing libraries. The community has already open sourced many client libraries.


最后修改:2021 年 02 月 24 日 08 : 21 AM