nsq详细教程5 客户端和服务端的消息通信机制


之前的内容我们学习测试了nsq各个组件的功能以及用户身份认证机制,这篇文章我们根据官方文档来继续研究学习nsq客户端的工作内容及原理,有助于我们深入理解nsqd的工作机制,并进行性能优化和问题排查。
nsq设计者将很多的功能放到客户端实现,这样保障了服务端的健壮和高效。同时发送消息到服务端是很容易和简单的,这里主要探讨消费者的功能实现。

必要的配置

一个消费者通过一个到snqd的tcp链接来订阅一个主题的某个通道。一个topic被多个消费者消费的情况要考虑。

  1. 客户端要通过配置支持直接链接nsqd服务,和链接nsqlookupd服务 从中读取nsqd服务信息进行链接。当链接nsqlookupd时,它需要定期从nsqlookupd中拉取配置信息,定期拉取的周期需要可配置。
  2. 由于nsq部署环境一般都是多生产者多消费者的分布式环境,因此客户端应该尽量均匀分布,避免惊群效应。
  3. 对于客户端来说,可以批量接收多少条消息(不需要给服务器响应),是一个重要的能调节指标。nsq中这个参数就是max_in_flight。这个参数影响rdy 这个状态的管理。rdy后续会介绍
  4. 客户端要实现消息处理失败的重试机制,并且重试次数可配置。对于发送失败的消息要自动进行重排序,nsq支持通过req命令一起发送延迟时间,客户要能确定第一次失败延迟多久,下一次/每增加一次 失败 延迟多久。
  5. 客户端处理snqd推送的消息需要提供一个回调方法,这个方法只有一个参数就是message对象。

服务发现

nsqlookupd服务是一个重要服务,可以让客户端在运行时定位topic所在的nsqd节点。这大大减少了维护和扩展nsqd节点的配置工作量。
客户端要定期轮训从所有的nsqlookupd实例中获取配置信息进行汇总,并管理好到各个nsqd实例的链接信息。(因为多个nsqlookupd之间不共享数据,因此客户端要遍历所有已经配置的nsqlookupd服务请求数据然后将数据汇总);调用一个nsqlookupd 服务的一个http接口(/lookup?topic=clicks),查询某个topic的位置;根据查询到的nsqd信息的broadcast_address 和tcp_port 组合, 建立到nsqd实例的链接
官方给出的 topic 查询返回结果示例

{
    "channels": ["archive", "science", "metrics"],
    "producers": [
        {
            "broadcast_address": "clicksapi01.routable.domain.net",
            "hostname": "clicksapi01.domain.net",
            "remote_address": "172.31.27.114:51996",
            "tcp_port": 4150,
            "http_port": 4151,
            "version": "1.0.0-compat"
        },
        {
            "broadcast_address": "clicksapi02.routable.domain.net",
            "hostname": "clicksapi02.domain.net",
            "remote_address": "172.31.34.29:14340",
            "tcp_port": 4150,
            "http_port": 4151,
            "version": "1.0.0-compat"
        }
    ]
}

连接

客户端必须为每个想订阅的topic建立一个单独的连接,初始建连接需要发送如下内容

  1. 唯一标识
  2. 命令标识符 和相关参数 和读取/校验响应
  3. 子命令(指定目标topic) 和读取/校验响应
  4. 一个初始化的 rdy 值 1

如果和nsqd链接中断,客户端要实现自动重新连接功能,

  1. 当客户端直接链接nsqd服务时,重连要使用指数回退的时延进行尝试(第一次 4s 后尝试 第二次 8s 后 第三次 16s后…只到超时)
  2. 如果客户端时通过nsqlookupd发现nsqd时,要自动根据特定的间隔进行链接尝试,一般是每次从nsqlookupd拉取完进行链接尝试。

参数/特性协商

建立tcp连接后,客户端通过发给服务端的命令标识符可以更改服务端的配置信息,比如:告知服务端客户端的心跳间隔、是否使用压缩算法、使用TLS 等 ,详情见spec
服务端会通过包含服务端配置信息的json格式数据响应,如此完成双方通信参数的协商。

客户端发送信息示例

{
    "client_id": "metrics_increment",      --发给服务端来区分不同的客户端
    "hostname": "app01.bitly.net",         --发给服务端来区分不同的客户端
    "heartbeat_interval": 30000,           -- 告知服务端客户端的心跳间隔
    "feature_negotiation": true
}

如果服务端不支持特性协商,那么直接返回ok ,如果支持那么返回如下结构体:

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

心跳和数据流

客户端要通过异步IO/线程,来异步接收服务发来的消息数据。而且要定时(默认30s)响应服务端发来的心跳,客户端可以使用任何内容回复服务端的心跳请求,但是一般只回复一个NOP.
当发生协议传输错误时(比如:客户端发送了一个服务端不认识的命令)大部分情况下都认为是致命错误,服务端关闭连接来保护自己。
以下几种错误不是致命的,这几个错误一般是时间问题导致,通常发生在消息发送超时,服务端重排序并发送给其他消费者时。
E_FIN_FAILED - a FIN command for an invalid message ID
E_REQ_FAILED - a REQ command for an invalid message ID
E_TOUCH_FAILED - a TOUCH command for an invalid message ID

消息处理

当nsqd io循环收到消息,就会将消息传递给配置好的消费者/处理器。nsqd 希望希望能够在超时时间内(默认60s)收到答复。有几种可能的场景:

  1. 消息处理成功
  2. 消息处理未成功
  3. 接收端处理超时
  4. 超过nsqd的发送超时时间
    前三种情况 客户端应该发送适当的命令(FIN REQ TOUCH)
    FIN:表示处理成功或者无需处理,可以丢弃消息了
    REQ:表示处理失败,服务端应该重新发送该消息,同时可能会携带延迟时间参数。如果消费者没有穿这个参数,客户端程序需要根据尝试次数自己计算这个延迟时间。如果重发超过一定次数 客户端要丢弃这条消息,同时调用回调方法记录通知并进行特殊处理(写入日志或磁盘)。
    TOUCH:该命令可以重置nsqd端的等待时间(比如 一条消息的处理时间可能要超过nsqd的发送超时时间,但是又不希望超时重发,这个时候消费者就可以通过touch重置nsqd的等待时间),这个命令可以重复发送直到 客户端处理成功或失败。或者直到nsqd端max_msg_timeout参数 配置的时间。客户端库不应该代表消费者自动调用touch.
    如果nsqd 没有收到任何命令那么消息超时自动重新排队,重新发送给可用的消费者。

RDY 状态

消息从nsqd 发送到客户端,通过RDY状态来控制数据流。
客户端会配置一个max_in_flight 参数,这是一个并发和性能控制器。比如 如果下游消费者 消费很快的 这个值会自动变大。反之 变小。
当一个客户端链接到一个nsqd实例时,会传递一个初始的RDY值 0. 表示nsqd不会推送任何消息过来。
客户端必须做好以下处理:

  1. 启动后将配置的max_in_flight 值平均分配给所有的连接
  2. 不允许所有连接的max_in_flight 总和超过配置的max_in_flight
  3. 每个连接的max_in_flight不要超过nsqd配置的max_rdy_count。
  4. 提供一个api接口来显示信息流不足

启动和分配
启动并为连接分配max_in_flight 时需要考虑如下因素:

  1. 连接数是冬天的,并不总能提前直到
  2. max_in_flight 值可能小于连接数

考虑上上述两个因素,一般启动时最初只为每个连接分配rdy的值为1
之后每次消息处理完成,客户端库都要评估是否需要更新rdy的值,如果nsqd端当前的rdy为0 或者为低于上次设定值得25%,则重新设定。
为了公平分配一般客户端为每个连接 分配 max_in_flight /num_conns 值作为rdy的值。但是当 max_in_flight <num_conns 时 ,客户端要通过上次收到消息到现在的时间间隔,对nsqd的活跃度进行动态评估。保证将max_in_flight 额度尽量分配给活跃的nsqd连接。

管理max_in_flight
客户端要管理所有连接对应的rdy值不超过配置的max_in_flight 参数。如下为python客户端中的控制逻辑
在这里插入图片描述
nsqd 的max_rdy_count参数
每个nsqd实例都会配置一个max_rdy_count参数,握手过程中会将该值传至客户端。如果设置nsqd端rdy的值时超过了这个限制那么连接将会被关闭。如果nsqd不支持协商那么就坚定这个值时2500

Message Flow Starvation

客户端应该提供一个api接口来显示信息流不足,
在消息处理过程中,消费者仅仅比较正在传输的消息数量和max_in_flight的大小是不够的的。以下两种情况下这样会有问题

  1. 当消费者配置的max_in_flight>1 ,max_in_flight 可能无法被 num_conns 除尽,这个时候必须向下取整,最终导致 rdy值总和小于max_in_flight
  2. 嘉定只有一部分nsqd实例在工作,但是由于平均分配的原因,哪些活跃的nsqd服务只分得了一部分max_in_flight 的值。
    这两种情况下客户端无法收到max_in_flight 数量的并发消息。所以应该提供一个is_starved 方法 用来评估是否有连接是饥饿的。消费者/消息处理者调用这个方法来进行评估。
    示例: 说实在的不是十分理解,慢慢理解吧!
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

Backoff/补偿

消息处理失败的情况不太好处理。如上所述可以要求服务器延时发送,其实同时还可以要求降低流量,这种方式可以通过发送RDY 0 给nsqd 来实现。

加密和压缩

nsq支持压缩和加密,客户端可以通过 IDENTIFY 命令来和服务端协商。TLS用来实现加密, Snappy 和 DEFLATE用来实现压缩。
当请求了 携带了tls_v1 标识,那么会得到如下响应:

{
    "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,   表示 nsqd支持TLS,客户端可以发起TLS握手。
    "version": "0.2.28"
}

“tls_v1”: true, 表示 nsqd支持TLS,发送/接收数据前客户端可以发起TLS握手。握手完成后必须先接收一个加密的ok报文。
压缩也类似,snappy / deflate =true 表示支持压缩,

总结

分布式系统很有意思,各个组件分工协作提供一个健壮的消息服务。实现客户端的时候可以参考pynsq 和go-nsq 。大概分为三个核心部分
Message: 一个高级消息对象,包含了所有对nsqd的请求和响应消息(FIN REQ TOUCH)和元数据信息。
connection: 一个高级的tcp连接包装类,包含了协商信息、rdy状态等
Consumer:用户交互的前端API,包括处理创建连接、管理rdy状态、解析数据、创建消息并分发给处理方法 等功能
Producer:处理消息发布的前端API,供用户调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

catch that elf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值