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

160 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`