Files
im_server/docs/test/IM-group2k-r10-双机群聊压测分析与改进-20261004.md
T

11 KiB
Raw Blame History

GROUP-2000 R10 双机群聊压测分析与改进

1. 测试结论

group2k-r10-20261004-01 的 A/B 同步有效,可以作为 2000 人群聊的真实双机容量结论。本轮持久化链路基本正常,但实时扇出已明显超过当前服务容量,整体判定为 失败。

  • A/B 各建立 1000 个 WS 连接,共 2000/2000 就绪;两机实际开始时间相差约 84ms。
  • 2000 条发送尝试中,1984 条收到 202 ACK,16 条因 socket 中断而结果未知,无显式 403/502/503 拒绝。
  • 数据库最终找到 1999 条,无重复、无字段不一致;仅 g-a-0098-02 未落库。
  • 按 1985 条已在服务端确认的消息计算,期望实时投递 3,970,000 次,实际收到 1,979,444 次,到达率 49.86%,实时缺失率 50.14%。
  • 1,990,556 次实时缺失全部可从历史消息查回,说明主要问题是实时投递容量,不是 Kafka/MySQL 丢数据。
  • 负载期间共 152 次 WS 断连:147 次 connection reset、4 次 timeout、1 次 closed connection。
  • ACK P95 为 71.18s,本地实时投递 P95 为 329.68s,已不是普通抖动,而是持续性排队。

JPush 空设备查询放大、MySQL 连接频繁回收和 A/B 起跑错位等 R9 问题在本轮已解决。当前的主要容量瓶颈是大群每条消息的单 Topic 有序处理、跨 Pod multiplex 转发和各 Pod WS 逐连接序列化/写出组合成的实时扇出链路。

2. 四层对账

层级 结果 结论
发送尝试 2000 A/B 各 1000
即时收到 202 1733 251 条在超时后迟到
最终收到 202 1984 发送成功率 99.2%
发送结果未知 16 全部为 socket 中断,无业务拒绝
历史回查 1985/1985 服务端可确认消息全部可查
数据库实际落库 1999/2000 14 条“结果未知”也已落库
真正未落库 1 A 机 g-a-0098-02
实时投递 1,979,444/3,970,000 到达率 49.86%
重复 / 乱序 0 / 0 已收到数据的正确性正常

16 条结果未知中,14 条 B 机消息已在数据库找到;A 机 g-a-0102-02 被实时观测到且已落库;只有 g-a-0098-02 在连接断开前未被服务端处理。因此不能把“客户端未收到 ACK”直接等同于“消息未落库”。

3. A/B 表现

指标 机器 A 机器 B
就绪连接 1000 1000
发送尝试 1000 1000
最终观测 ACK 998 986
结果未知 2 14
收到群消息 996,563 982,881
断连 7 145

B 机的断连明显更多,但 reset 分布在多个公网源 IP,没有单一坏 IP 的证据。机器差异会影响结果,但 A 机同样出现大量投递缺失和长尾,不能将本轮失败只归因于 B 机网络。

4. 指标与时间线

  • 负载窗口约为 09:21:43–09:26:32。
  • A 机断连集中在 09:23:20–09:25:24;B 机采样到的 reset 集中在 09:24:20–09:25:23。
  • 服务端 /metrics 在约 09:23:01–09:25:37 多次超过 10s 采集超时;233 次 im-server 采样中 14 次失败,17 次超过 5s。
  • Kafka consumer lag 和 oldest age 在收敛阶段均为 0。
  • im_kafka_message_persist_total{kind="group",result="batch_ok"} 合计 1999,与数据库实际条数一致。
  • im_push_dispatch_queue_enqueue_total{result="ok"} 为 1999,未观测到 push queue full/error;推送平均处理约 22ms。
  • Topic group queue wait 未超过 1s,Cluster P2M/route 计数未观测到 full/error。
  • 三个 Pod 的 ACK enqueue/write 指标都在 1s 内。注意该计时从服务端 WS 读循环完成解析后开始,因此客户端几十秒至数分钟的 ACK 延迟,主要发生在服务端 dispatch 之前的读取/调度阶段,或写入 socket 后的网络/客户端接收阶段。

/metrics 与 WS 同时出现长时间停顿,说明不只是某个业务队列满,而是 Pod 的 CPU/调度/网络写路径在大规模扇出下被长时间占用。本轮无法通过集群 API 读取 Pod Event 和完整日志,因此不将 CPU throttling、Ingress 和节点网卡中的任何一项单独定为唯一根因。

5. 代码链路分析

5.1 Topic 主节点

Topic.broadcastToSessions 遍历主 Topic 上的本地 session 和远程 Pod multiplex session,为每个目标复制、整理消息并入队。一个 2000 人群的 2000 条消息会生成约 400 万次终端投递。

当 multiplex 队列暂时满时,Topic goroutine 会等待最多 250ms;一个主 Topic 又需要保持消息顺序,所以无法通过对单条消息无界并发来解决。

5.2 跨 Pod multiplex

每个远程 Pod 使用一个 multiplex session。它的 drain 为保证 Topic 顺序,对每条 Cluster.TopicProxy RPC 同步等待远程返回。这保证了顺序,但对单条 RPC 往返、gob 编解码和远程 proxy Topic 处理的抖动很敏感。

5.3 Pod 内 WS 扇出

proxy Topic 收到一条群消息后,仍然遍历本 Pod 上的所有订阅 session,为每个 WS 连接复制消息并入队。每个 WS 只有单个 write loop 串行做序列化和 socket write。当扇出速度超过写出速度,session 队列积压,随后出现连接 reset、客户端重连以及更大的订阅恢复压力。

6. 已排除和未排除项

已排除为本轮主因

  • Kafka 积压或持久化丢失:最终 lag=0,1999 条 batch_ok 与 DB 一致。
  • MySQL 批量落库能力:无重复、无字段不一致,只有 1 条在服务端未接收。
  • JPush 设备查询放大:1999 次异步入队成功,无队列满,平均耗时低。
  • A/B 起跑错位:本轮最终屏障通过,两机实际开始差距小于 100ms。
  • 显式限流或内部队列拒绝:没有 429/502/503,未观测到 Cluster、Topic、push 队列 full/error。

仍需补齐证据

  • 每个 Topic 单条消息的完整处理耗时和扇出目标数。
  • multiplex 队列等待、RPC 耗时、远程 proxy 接收到入队完成的分段耗时。
  • WS send queue 深度、队列等待 P95/P99、full/purge 和按 Pod 的发生时间。
  • Pod CPU usage/throttling、Go scheduler latency、GC pause、goroutine 数、网卡吞吐/丢包/retransmit。
  • Ingress/LB 上游 reset、upstream response time、连接数和每 Pod 连接分布。

7. 改进方案

P0:先定位每段长尾

  1. 增加 Topic 处理总耗时、单条扇出目标数和 broadcastToSessions 耗时直方图,区分 master/proxy。
  2. 增加 multiplex 入队等待、队列深度、Cluster.TopicProxy RPC 耗时和远程处理耗时。
  3. 补齐 WS send queue P95/P99/max、full、purged,并区分 data/ctrl。ACK 必须能从大量 data 帧中被单独观测。
  4. 增加 WS 读取到 dispatch 前的时间戳,避免现有 ACK 指标忽略服务端读循环前的停顿。
  5. 恢复压测期间的 Pod/Event/CPU 可观测性,指标采集超时不应导致整段监控空白。

P1:降低实时扇出成本

  1. 将 master 到每个 proxy Pod 的多条消息改为有界批量 RPC,批内保持 SeqId 顺序,减少 2000 次同步 RPC 往返、gob 编码和调度开销。
  2. 为每个 Topic-Pod 建立单写者有界队列,允许不同 Pod 并行处理,同一 Pod 内严格按 SeqId 投递;不对单 Topic 做无界 goroutine 并发。
  3. 研究一次序列化、多 session 复用字节的可行性,避免同一条群消息在每个连接重复做等价 JSON 序列化。用户相关 Topic/权限字段必须先完成安全性分类,不可盲目全局复用。
  4. ctrl/ACK 与普通 data 帧分级调度,保证发送者在大群扇出积压时仍能快速获得明确结果。需保持单连接协议语义,不得导致客户端对 data/ctrl 的依赖关系错乱。
  5. 对慢消费 WS 定义明确策略:队列达阈值后记录可追踪原因、主动断开并依靠历史补拉,避免一批慢连接长时间拖住整个 Topic。

8. 实施和复测顺序

  1. 先完成 P0 指标,用相同 2000 人、2000 条参数复现一轮,定量确认 Topic、multiplex RPC、WS queue 和 socket write 各占多少。
  2. 实施 multiplex 批量化和有界单写者,先做单元测试、顺序测试、队列满/节点断开/重连测试。
  3. 用 500、1000、1500、2000 人阶梯复测,每档至少两轮,不直接只跑最高档。
  4. 最后重复 R10 同参数双机测试,必须同时通过实时、历史、DB 和基础设施门禁。

9. 下一轮通过门禁

  • 2000/2000 WS 就绪,A/B 实际开始差小于 1s。
  • 2000 条发送全部获得明确 ACK,无 outcome unknown,无 5xx。
  • 数据库精确找到 2000 条,缺失、重复、字段不一致均为 0,Kafka lag 最终归零。
  • 实时投递到达率不低于 99.99%,重复和乱序为 0。
  • ACK P95 小于 2s,投递 P95 小于 5s;本轮已明确不以 500ms 作为阻断门禁。
  • 断连、Topic/multiplex/WS queue full、Pod restart、OOM、liveness failure 均为 0。
  • 负载窗口内 /metrics 可持续采集,无连续 10s 超时;CPU throttling、GC、网络 retransmit 和 Ingress reset 有完整记录。

10. 产物

  • 结果目录:loadtest/im-pressure/results/group2k-r10-20261004-01/
  • 汇总报告:report.json
  • 数据库核验:db-verification.json
  • A/B 明细:agent-a-summary.json、agent-b-summary.json
  • coordinator 台账:coordinator.json
  • 服务指标:metrics.jsonl
  • 代码版本:c46209f

11. 本轮已实施改造

R10 分析后已完成以下代码改造,待发布后进行 R11 复测:

  • 普通非频道群消息在每个 Pod 仅执行一次 JSON 序列化,同 Pod 的所有 WS 连接复用同一份不可变字节。权限过滤、SkipSid、频道匿名、P2P 别名和非 WS 协议仍保持原路径。
  • 增加 Topic 完整处理耗时、广播耗时和按 websocket/multiplex/other 分类的扇出目标数。
  • 增加 master-to-proxy multiplex 入队深度和排队时间,以及远程 proxy Topic 接收后的队列等待时间。
  • 增加 WS 完成读帧后的 JSON 解析和本地 dispatch 耗时。
  • 新快路仍在 WS write loop 记录原有 OutgoingMessageSize 统计,不会因预序列化丢失原有监控口径。

本次没有直接引入新的批量 net/rpc 方法。StatefulSet 滚动发布时会短暂出现新旧 Pod 混合,新 RPC 端点与旧 Pod 不兼容,会产生发布窗口风险。R11 先用新增指标确认 multiplex RPC 在优化后是否仍为主瓶颈;若需要批量化,应采用“先发布双协议接收端,再开启批量发送端”的两阶段发布。

新增指标:

  • im_topic_process_duration_seconds
  • im_topic_broadcast_duration_seconds
  • im_topic_broadcast_targets
  • im_topic_shared_serialization_total
  • im_cluster_m2p_queue_wait_duration_seconds
  • im_cluster_m2p_queue_depth
  • im_cluster_proxy_queue_wait_duration_seconds
  • im_ws_read_dispatch_duration_seconds