test: harden group load verification
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# 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. 结果文件
|
||||
|
||||
本轮原始结果位于:
|
||||
|
||||
```text
|
||||
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. 历史回查默认调整为 1 个 worker、每页 50 条、全局每 100ms 最多发起一次查询,避免核验程序自身制造 Topic/Proxy 洪峰。
|
||||
5. 新增 `backend-debug-metrics-rbac.yaml`,为测试环境的 `argocd/backend-debug` ServiceAccount 提供 `metrics.k8s.io/pods` 的 namespace 只读权限。
|
||||
|
||||
上述改造只解决统计口径、历史查询压力和资源指标权限。机器 A 的公网连接异常仍需通过下一轮主机与网络观测定位。
|
||||
Reference in New Issue
Block a user