50万客户端订阅同一主题,QoS 2 广播时尾部 ACK 延迟高达 60+ 秒

环境信息

  • EMQX 版本 :5.7.1 (Open Source)
  • 集群规模 :17 个节点(replicant 角色)
  • 客户端数量 :约 50-56 万
  • 订阅模式 :所有客户端订阅同一个主题(如 topic/warning )
  • 消息 QoS :QoS 2
  • 消息频率 :每小时1条消息

问题描述

当向 50 万订阅者广播一条 QoS 2 消息时,大部分客户端能在 1-5 秒内完成 ACK,但每次都会有一批客户端(约 0.1-1%)的 ACK 延迟非常高,最高达到 63 秒 。

具体数据

三次广播的延迟数据:

发送批次 发送时间 最快 ACK 最慢 ACK 平均延迟 客户端总数 第 1 批 14:08:52 14:08:52 14:09:27 ~25 秒 567,559 第 2 批 14:21:00 14:21:00 14:21:49 ~49 秒 567,792 第 3 批 14:24:15 14:24:15 14:25:18 ~63 秒 567,792

延迟分布特征:

  • P50 :< 1 秒
  • P90 :< 5 秒
  • P99 :> 30 秒
  • 最大延迟 :63 秒

已尝试的优化

  1. TCP 参数调整 :

    • send_timeout :15s → 60s → 120s
    • send_timeout_close :true → false
    • sndbuf/recbuf :4KB → 64KB
    • active_n :100 → 500
    • nodelay :true
  2. 集群配置 :

    • 检查各节点连接数分布(基本均匀,每节点约 3 万连接)
    • 检查 GenRPC 指标(发送队列正常)
    • 检查节点间网络延迟(< 1ms)
  3. 应用层观察 :

    • 通过插件记录了 sendTime 、 deliver_begin_at 、 endTime 三个时间戳
    • 发现 deliver_begin_at - sendTime 很大(部分客户端超过 40 秒)
    • 说明延迟主要发生在 EMQX 内部消息扇出阶段

问题分析

根据观察,问题的核心特征是:

  1. 每次广播都有一批固定比例的慢客户端
  2. 慢客户端的 deliver_begin_at 明显滞后于 sendTime
  3. 慢客户端在各节点上均匀分布 ,不是节点热点问题

插件功能

通过注册message.acked 事件,
image


我是通过ack消息头中的deliver_begin_at时间与钩子触发的时候的时间差计算的延迟时间。求老板帮忙分析一下

1.


我这么设置的时候慢订阅是空的。

2.


当我设立设置为whole和response的时候慢订阅的时间和我统计的延时时间基本一致的如下图。

慢不在 EMQX 内部扇出,主要在客户端响应段。
慢订阅 internal 为空,而 whole/response 和你插件统计基本一致。按慢订阅的定义:internal 是消息到达 EMQX 到开始投递;response 是开始投递到完成传输。QoS 2 的“完成传输”是 EMQX 收到客户端的 PUBCOMP。所以 20s/60s 主要是在 PUBLISH 发出后到 PUBCOMP 回来这段,不是路由表或集群内部转发慢。

下一步别再调 send_timeout,它只控制 socket send timeout,不会把 50w 个 QoS 2 ACK 尾延迟变没。
抓慢订阅列表里的前 10 个 client,逐个看:

emqx_ctl clients show <clientid>

重点看 inflightawaiting_relenqueued_msgs / mqueue_lendropped_msgs。如果慢客户端广播时 inflightawaiting_rel 抬高,说明客户端处理 QoS 2 握手慢,或者 Receive Maximum / 飞行窗口把它限住了。

同时做一个 A/B:

  1. 同样 50w 订阅,发布 QoS 1。若尾延迟明显下降,瓶颈就是 QoS 2 四步握手加客户端回包。
  2. 保持 QoS 2,但把广播分批或分主题,例如按区域/hash 分片,不要 50w 同主题一次扇出。
  3. 临时关掉自定义 message.acked hook,或者只采样 1%。50w ACK 一次广播会触发 50w 次 hook;单次再轻,也会放大尾延迟。
  4. 对慢订阅 client 抓客户端侧日志/线程池/网络 RTT,看收到 PUBLISH 到发 PUBREC/PUBREL/PUBCOMP 的时间。服务端 slow_subs 已经说明 response 段慢,必须看客户端侧。
    你现在插件里取的 deliver_begin_at 先别当“EMQX 内部耗时”的唯一依据。用内置慢订阅的 internal/response 分段更准;需要二次验证时,用 $events/message/delivered$events/message/acked 分别落库,对齐 delivery timestamp 和 ack timestamp。