8.2 KiB
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/userspublish,每个远端 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=189socket=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.persistconsumer 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 closedwrite: broken pipectrl timeout- 到多个公网入口 IP 的 TCP 读写超时
这种强烈单边性不符合服务端全局过载的表现。当前最可能的故障域依次是 A 本机网络/NAT/安全软件、A 到公网 LB 的链路、A 的文件描述符或本地调度压力。由于 Kubernetes Metrics API 被 RBAC 拒绝,本轮没有取得 Pod CPU/内存数据,不能用资源指标进一步排除 Pod 饱和。
6. 下一轮执行顺序
- 先做 A/B 角色互换:B 仍发 1000 条,A 暂时只保持 1000 个连接不发送。若 A 仍大量断线,可确认与服务端发送负载无关。
- 在 A 机同步采集
ping、TCP retransmit、连接跟踪、文件描述符、CPU、内存和进程调度数据;压测前检查ulimit -n。 - 将历史回查改为分页和限并发,或先关闭自动历史回查,仅验证实时阶段;实时阶段结束后再启动独立历史校验。
- 修正报告字段,把超时/socket 明确归类为
publish_unknown,并把 104 条 unacked-observed 消息纳入历史校验。 - 补充
backend-debug对metrics.k8s.io/pods的只读权限,否则下一轮仍无法采集 Pod CPU/内存。 - 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. 已实施改造
本分析完成后,压测工具已实施以下改造:
- agent 分开输出
publish_rejected和publish_outcome_unknown;前者只包含服务端明确拒绝,后者包含 ACK 超时和 socket 失败。 - A/B 在 drain 后向 coordinator 提交“实时已观测但未 ACK”的消息台账,coordinator 去重合并后将其加入历史落库核验。
- 报告新增
history_unacked_observed_checked和history_unacked_observed_found,可直接确认结果未知消息是否最终落库。 - r5 复测后,历史回查进一步调整为 1 个 worker、每页 20 条、全局每 500ms 最多发起一次查询,避免核验程序自身制造 Topic/Proxy 洪峰。
- 新增
backend-debug-metrics-rbac.yaml,为测试环境的argocd/backend-debugServiceAccount 提供metrics.k8s.io/pods的 namespace 只读权限。
上述改造只解决统计口径、历史查询压力和资源指标权限。机器 A 的公网连接异常仍需通过下一轮主机与网络观测定位。