Files
im_server/docs/test/IM-group2k-r4-群聊复测分析-20260928.md
T

8.2 KiB
Raw Blame History

IM 2000 人群聊 r4 复测分析

日期:2026-09-28

Run ID:group2k-r4-20260928-01

场景:机器 A/B 各 1000 个 WebSocket、各发送 1000 条,计划共发送 2000 条群消息。

1. 结论

本轮不能按合并报告中的 14.39% 实时丢失率直接判定服务端群消息丢失。两台压测机结果存在明显单边故障:

  • 机器 B 的 1000 个连接全部稳定,1000 次发送全部收到 ACK,1497000 次 ACK 台账内投递全部到达,断线和实时缺失均为 0。
  • 机器 A 出现 654 次断线、314 次 socket 错误和 189 次 ACK 超时。A 的 TCP 源地址错误中包含 broken pipe、websocket closed,是本轮 430832 次实时缺失的主要来源。
  • 服务端实际接受并持久化 1601 条消息,其中客户端收到 ACK 1497 条,另外 104 条属于“客户端未收到 ACK,但服务端已接受”。B 恰好多收到 104000 次消息,即 104 条乘以 B 的 1000 个连接,账目完全吻合。
  • ACK 台账内 1497 条消息历史回查全部找到,persistence_lost=0;Kafka 群消息消费 lag 在采样点均为 0。
  • 上一轮的 O(群成员数) Redis fanout 放大已经消除:1601 条服务端消息只产生 1601 次 group_unread/users publish,每个远端 Pod 各 receive 1601 次,不再产生约 320 万次逐用户 Redis publish。
  • 实时发送与 60 秒收尾阶段没有新增 topic_server 或 cluster_topic_proxy 满队列。满队列发生在约 5 分钟后的并发历史回查阶段,属于另一项需要优化的问题,不能作为本轮实时丢失的原因。

因此,本轮结论为:批量群未读 fanout 修复通过;B 侧实时投递通过;数据库持久化通过;A 侧网络稳定性失败;历史回查并发会打满 Topic/Proxy 队列。整体尚不能完成 2000 人群聊验收。

2. 客户端结果

指标 A B 合计
READY 连接 1000 1000 2000
发送尝试 1000 1000 2000
收到 2xx ACK 497 1000 1497
明确拒绝 0 0 0
ACK 超时 189 0 189
socket 结果未知 314 0 314
断线 654 0 654
ACK 台账内应收 1497000 1497000 2994000
ACK 台账内实收 1066168 1497000 2563168
ACK 台账内实时缺失 430832 0 430832
ACK 台账外额外投递 68668 104000 172668
重复 / 乱序 0 / 0 0 / 0 0 / 0
历史检查 / 找到 1497 / 1497 0 / 0 1497 / 1497

合并报告按 ACK 台账计算出的到达率为 85.61%,但这只是 A/B 网络条件混合后的观测值,不代表服务端总体投递率。B 单机的 ACK 台账内到达率是 100%,A 单机是 71.22%。

A 端记录了 160074 个跨机器负延迟样本,说明两机时钟没有达到单向延迟统计要求。本轮不使用跨机器 P95/P99 判断性能,只使用 ACK、到达计数、断线、历史回查和服务端阶段指标判断正确性。

3. 未收到 ACK 的 503 条不是服务端拒绝

本轮没有任何明确的 HTTP/Tinode 503 响应。报告字段 publish_rejected=503 实际由以下结果未知项组成:

  • ctrl_timeout=189
  • socket=314

服务端持久化计数增加 1601,而 ACK 只有 1497,说明至少 104 条结果未知消息已被服务端成功接受。B 的 unexpected_messages=104000 与这 104 条消息对 1000 个 B 连接的广播数量完全一致。

后续报告应把字段继续显示为 publish_unknown,不能把它展示为 rejected,否则容易误判服务端主动拒绝。

4. 服务端与中间件

4.1 Kafka 与数据库落库

  • im_kafka_message_persist_total{kind="group",result="batch_ok"} 增加 1601。
  • im.group.message.persist consumer lag 在采样点均为 0。
  • 1497 条 ACK 台账消息通过历史接口全部找到,持久化缺失为 0。
  • 服务端群消息 save 总耗时增加约 26.27s,平均约 16.4ms/条。
  • Kafka produce 总耗时增加约 19.15s,平均约 12.0ms/条。

这证明已接受消息能够及时进入 Kafka 并完成落库。104 条结果未知消息也计入 1601 条 batch success,但当前历史校验只以 ACK 台账为准,下一版应对“已观测但未 ACK”的消息也做 message ID 回查。

4.2 Redis fanout 修复验证

1601 条服务端接受消息对应:

  • master Pod group_unread/users publish ok 增加 1601。
  • 两个远端 Pod 的 group_unread/users receive ok 各增加 1601。
  • 普通 message/user 在原生 Cluster 开启时继续使用 skip_native_cluster,没有恢复逐成员 Redis 发布。

这符合“一条群消息一次 Redis publish、每个 Pod 一次本地扫描”的设计,上一轮每条消息约 2000 次 Redis publish 的放大已经消失。

4.3 队列满发生在历史回查阶段

按时间序列拆分:

  • 16:25:05Z 开始发送。
  • 16:28:36Z 最后一台压测机结束发送,之后进入收尾和历史等待。
  • 截至 16:34:01Z,topic_server/full 仍停留在测试前基线 1349,cluster_topic_proxy/full 仍为 0。
  • 16:34:25Z 起,历史回查并发开始使 topic_server/full 增加。
  • 最终历史阶段新增 topic_server/full=1355、cluster_topic_proxy/full=55。

因此这些队列丢弃不是实时发送阶段 430832 次缺失的原因。历史回查一次并发拉取大量消息,经 Topic/Proxy 回送时会形成独立洪峰,应限制每机历史校验并发或分页节流,并单独验收历史吞吐。

5. A 机器故障判断

A 与 B 使用相同代码、相同公共入口和相同发送模型,但只有 A 发生 654 次断线。A 的错误包含:

  • websocket closed
  • write: broken pipe
  • ctrl timeout
  • 到多个公网入口 IP 的 TCP 读写超时

这种强烈单边性不符合服务端全局过载的表现。当前最可能的故障域依次是 A 本机网络/NAT/安全软件、A 到公网 LB 的链路、A 的文件描述符或本地调度压力。由于 Kubernetes Metrics API 被 RBAC 拒绝,本轮没有取得 Pod CPU/内存数据,不能用资源指标进一步排除 Pod 饱和。

6. 下一轮执行顺序

  1. 先做 A/B 角色互换:B 仍发 1000 条,A 暂时只保持 1000 个连接不发送。若 A 仍大量断线,可确认与服务端发送负载无关。
  2. 在 A 机同步采集 ping、TCP retransmit、连接跟踪、文件描述符、CPU、内存和进程调度数据;压测前检查 ulimit -n。
  3. 将历史回查改为分页和限并发,或先关闭自动历史回查,仅验证实时阶段;实时阶段结束后再启动独立历史校验。
  4. 修正报告字段,把超时/socket 明确归类为 publish_unknown,并把 104 条 unacked-observed 消息纳入历史校验。
  5. 补充 backend-debug 对 metrics.k8s.io/pods 的只读权限,否则下一轮仍无法采集 Pod CPU/内存。
  6. A 链路稳定后,使用相同 2000 连接、2000 条消息模型重新验收。门禁要求两侧断线为 0、明确拒绝为 0、实时到达率 100%、持久化丢失为 0,并分别报告实时阶段和历史阶段的队列指标。

7. 结果文件

本轮原始结果位于:

loadtest/im-pressure/results/group2k-r4-20260928-01/
  agent-a-summary.json
  agent-b-summary.json
  coordinator.json
  report.json
  pre-metrics.log
  pod-metrics.log
  post-metrics.log

8. 已实施改造

本分析完成后,压测工具已实施以下改造:

  1. agent 分开输出 publish_rejected 和 publish_outcome_unknown;前者只包含服务端明确拒绝,后者包含 ACK 超时和 socket 失败。
  2. A/B 在 drain 后向 coordinator 提交“实时已观测但未 ACK”的消息台账,coordinator 去重合并后将其加入历史落库核验。
  3. 报告新增 history_unacked_observed_checked 和 history_unacked_observed_found,可直接确认结果未知消息是否最终落库。
  4. r5 复测后,历史回查进一步调整为 1 个 worker、每页 20 条、全局每 500ms 最多发起一次查询,避免核验程序自身制造 Topic/Proxy 洪峰。
  5. 新增 backend-debug-metrics-rbac.yaml,为测试环境的 argocd/backend-debug ServiceAccount 提供 metrics.k8s.io/pods 的 namespace 只读权限。

上述改造只解决统计口径、历史查询压力和资源指标权限。机器 A 的公网连接异常仍需通过下一轮主机与网络观测定位。