72 KiB
IM 单机 60% 容量压力测试结果
日期:2026-09-29
1. 结论
本轮使用一台物理压测机同时运行逻辑 agent A/B,每侧 600 个 WebSocket,合计 1200 连接。60% 容量的正确性和性能总验收未通过。
- 单聊实时和持久化正确性通过;ACK P95 和投递 P95 仅记录,不再作为当前公网单机压测的阻断条件。
- 群聊在 1200 条群消息、144 万设计投递量下出现 ACK 超时、连接重置和分钟级投递延迟。
- 混合场景明显越过容量拐点,737 个连接断开,仅 44.83% 发送得到 ACK。
- 独立 MySQL 核验确认:三个场景的所有 ACK 消息均精确落库,0 错配、0 重复;所有“无 ACK 但 WS 已观测”消息也均已落库。
- 压测工具本轮峰值 RSS 不超过 127 MB,每个 agent 峰值 FD 614,未显示本机内存或 FD 先饱和。
- 混合实时阶段的群 Topic 所在
im-server-0出现 broadcast queue full,服务端 Topic 处理能力是已证实的瓶颈之一。
2. 环境与口径
- 租户:
allrich - HTTP/WS 入口:
api.118im.com - 物理压测机:1 台
- 逻辑 agent:A/B 各2个中的一个,每侧 600 连接
- 账号:1200,好友对:600,群成员:1200
- API 域名当前 3 个 IP 均经
en1和局域网网关直连,未走 VPN/TUN - IM Server:3 Pod,无重启;每 Pod request
1 CPU/512Mi,limit2 CPU/1Gi - 独立群与 manifest:
loadtest/im-pressure/manifest.single-machine.json
准备阶段最终验证为 accounts=1200 pairs=600 group_members=1200。群批量邀请曾出现入口 502 和幂等任务长时间处理中,工具通过按实际成员补齐缺失用户完成恢复。
3. 结果汇总
| 场景 | Run ID | ACK | 实时投递 | 断连 | ACK P95 | 投递 P95 | 独立 DB 核验 | 结果 |
|---|---|---|---|---|---|---|---|---|
| 单聊 | s60-p2p-r2-0929 |
12000/12000 | 12000/12000 | 0 | 1341.555ms | 2000ms | 12000 ACK 全部精确命中 | 正确性通过 |
| 群聊 | s60-group-r1-0929 |
1145/1200 | 1350508/1374000 ACK 台账 | 47 | 11207.859ms | 94621.882ms | 1145 ACK + 39 无 ACK 落库 | 未通过 |
| 混合 | s60-mixed-r1-0929 |
1076/2400 | 234829/263657 ACK 台账 | 737 | 17491.634ms | 101226.392ms | 1076 ACK + 1017 无 ACK 落库 | 未通过 |
表中群聊和混合的“期望投递”只按有 ACK 的 ledger 计算。发送结果未知的消息不能在没有 seq 时直接加入期望投递分母,因此该分母小于设计投递量。
4. 分场景记录
4.1 单聊
R1 的历史查询将 limit=10 错误地按目标消息数计算,没有计入 P2P Topic 中双方交错的 seq,造成 4619 条假缺失。修复后 R2 结果:
- 12000 条全部 ACK,无结果未知。
- 12000 次对端投递全部到达,无重复、乱序、断连。
- 12000 条历史回查全部命中,持久化缺失 0。
- ACK P95 1341.555ms,同机投递 P95 落在 2000ms 桶;两项作为公网链路观测数据,不影响正确性验收。
4.2 群聊
- 发送 1200 条,1145 条 ACK,43 条 ACK 超时,12 条 socket 结果未知,无明确拒绝。
- 47 个连接断开:22 个 abnormal close,25 个 connection reset by peer。
- ACK 台账下实时缺失 23492 次,这些消息最终均可从历史查到。
- 55 条结果未知中,39 条被接收端实时观测,且全部被独立 SQL 确认落库;另 16 条既无 ACK、无 WS 观测,也无数据库记录。
- 断连发生时间为
03:40:10Z至03:41:15Z,与群实时发送阶段重合。
4.3 混合
- 设计发送为 1800 条单聊和 600 条群聊,合计 2400 条。
- 1076 条 ACK,1324 条结果未知,其中 1036 条 ACK 超时、288 条 socket 失败,无明确拒绝。
- 737 个连接断开:69 个 abnormal close,668 个 connection reset by peer。
- ACK 台账下实时缺失 28828 次,其中 28633 次对应消息最终已查到持久化记录。
- 历史验证 ledger 含 2002 条,命中 1331 条,缺失 671 条全部为 P2P。独立 SQL 确认所有 1076 条 ACK 消息和全部 926 条 WS 已观测的无 ACK 消息均已落库,因此这 671 条是 verifier 连接掉线和 3 分钟总预算造成的验证不完整,不是数据库丢失。
- 1324 条结果未知中,1017 条实际落库:926 条被 WS 观测,另有 91 条未被 WS 观测但已落库。剩余 307 条无 ACK、无 WS 观测、无数据库记录。
4.4 独立 MySQL 核验
核验租户 ID 为 8,消息表为 im_008_messages 和 im_008_group_messages_202609。
| 场景 | ACK expected | 精确命中 | ACK 缺失 | 错配 | 重复 | 无 ACK 但落库 |
|---|---|---|---|---|---|---|
| 单聊 | 12000 | 12000 | 0 | 0 | 0 | 0 |
| 群聊 | 1145 | 1145 | 0 | 0 | 0 | 39 |
| 混合 | 1076 | 1076 | 0 | 0 | 0 | 1017 |
verify-db 对无 ACK 但落库的行按 unexpected 报告,所以群聊和混合命令会以非零状态结束;这不代表数据错误,而是发送结果未知但服务端已持久化的重要诊断证据。
5. 服务端观测
im-server-0/1/2全部 Running,本轮重启数均为 0。- 混合实时阶段
03:53:00Z..03:53:02Z,im-server-0记录 55 次topic's broadcast queue is full。 - 混合渐进关闭末尾
04:03:45Z,im-server-0再记录 375 次同类队列满。当前 60 秒 close ramp 仍未完全避免关闭风暴。 - 同一观测窗口的
03:45:13Z,im-server-2在serveIMFriendRequestAction(server/im_http.go:6256) 出现一次 nil pointer panic。该端点不属于群实时发送路径,不将其归因为本轮断连,但需单独修复。 - 当前 kubeconfig 无权读取 metrics-server,未能取得负载窗口的 Pod CPU/内存历史值。
6. 压测工具修复
- 新增单机 60% 配置和执行方案,A/B 各 600 连接。
- 群聊和混合发送者按
users_per_machine比例缩放,1000 用户的原参数保持不变。 - 投递观测按
client_mid分为 64 片,每个接收账号独立维护 seq,延迟直方图改为原子计数,移除 WS 读循环中的全局锁争用。 - summary/report 新增
disconnect_reasons和最多 100 条disconnect_samples。 - 群创建后立即 checkpoint manifest;批量邀请失败后重读实际成员,仅邀请缺失用户。
- P2P 历史查询 limit 按 seq 区间扩展,上限 200,修复双方消息交错造成的假缺失。
7. 已实施改进
- 好友申请接受和自动接受路径增加 store 空结果保护。底层异常返回
nil, nil时记录租户、申请、操作人和状态并返回 HTTP 500,不再触发进程 panic。 - Hub 向已满的 Topic client queue 投递失败时,立即向原发送连接返回
503 service unavailable,并记录 topic、tenant、队列长度和容量。客户端可据此重试,压测也不再把这类请求归为等待超时的未知结果。 - 群消息权限检查改为复用群设置缓存,并为个人禁言增加 Redis 正向和负向缓存;新增禁言和解除禁言会同步更新缓存,到期禁言会自动淘汰。此改动减少群 Topic 串行处理路径上的 MySQL 查询。
message_publish_stage_duration_seconds新增group/permission_check和*/realtime_fanout阶段,下轮可区分权限查询、持久化和实时扩散耗时。- A/B 两个逻辑 agent 按账号顺序交错关闭连接。
close_ramp=60s时每个 agent 的本地关闭过程约 120 秒,A/B 相差半个时隙,整体约每 100ms 关闭一个连接,避免两侧同时下线形成双倍尖峰。 - 已增加空结果防 panic、Topic queue 满返回 503、双 agent 关闭时隙的自动化测试。
8. 复测顺序
- 发版后先跑单机群聊 60%,验收实时发送阶段和关闭阶段
topic_clientenqueue failed 均为 0;若仍过载,发送端必须收到 503,不能再出现对应的纯 ACK timeout。 - 对比
permission_check、save、realtime_fanout三阶段的 P50/P95/P99,确认群权限缓存是否消除了数据库检查瓶颈。 - 群聊通过后再跑单机混合 60%,要求 ACK、历史和独立 SQL 三方账本一致,连接断开数为 0。
- 补齐 Prometheus/metrics-server 权限并同步保存 Pod CPU、内存、网络、Kafka lag、Redis、MySQL 指标;在此之前不进行双机 100% 最终验收。
9. 发版后群聊 R2
Run ID:s60-group-r2-0929。
本轮开始前 3 个 Pod 均为 Running、0 重启,账号和群成员核验为 accounts=1200 pairs=600 group_members=1200。测试完整运行到 A/B 历史验证完成,并执行了错峰关闭和独立 MySQL 核验。
| 指标 | 结果 |
|---|---|
| 发送尝试 / ACK | 1200 / 871 |
| 结果未知 | 329(222 timeout、107 socket) |
| 断连 | 360(315 reset、44 abnormal close、1 timeout) |
| ACK P95 | 16630.441ms |
| 投递 P95 | 101823.156ms |
| ACK 台账期望 / 实时收到 | 1045200 / 889777 |
| 实时缺失 | 155423,最终全部查到持久化 |
| 历史回查 | 1080 / 1080 |
| DB:ACK 精确命中 | 871 / 871 |
| DB:无 ACK 但落库 | 209 |
| DB:缺失 / 错配 / 重复 | 0 / 0 / 0 |
329 条结果未知中,209 条已被 WS 观测且独立 SQL 确认落库,剩余 120 条数据库中不存在。断连样本主要是公网对端 connection reset by peer,且样本连接均指向 64.118.158.131:443;本机到域名全部当前 IP 均使用 en1 静态直连,没有经过 VPN/TUN。
服务端指标显示群 Topic 所在 im-server-0 的 1080 条 save 全部低于 100ms,Kafka consumer lag 为 0;但 topic_server 队列累计出现 393 次 full。压测客户端 CPU、RSS 不构成瓶颈。
本轮不能作为提交 75bb448 的有效回归:Pod 中运行的二进制不包含该提交新增的 permission_check、realtime_fanout 阶段字符串和好友申请空结果保护日志,Prometheus 也没有相应时序,说明实际部署镜像不是该提交构建结果。必须正确部署 75bb448 或后续提交后重跑。
此外,本轮发现 agent 只在正式开始前维护连接,统计窗口内断线后不会恢复,导致历史核验依赖剩余连接。后续已改为持续维护到 A/B 历史验证均完成,并将重连并发限制为每 agent 16,避免恢复风暴;每次计划发送前重新取得当前连接。
10. 产物
- 方案:
docs/test/IM-单机60%容量压力测试方案.md - 单聊 R2:
loadtest/im-pressure/results/s60-p2p-r2-0929/ - 群聊 R1:
loadtest/im-pressure/results/s60-group-r1-0929/ - 混合 R1:
loadtest/im-pressure/results/s60-mixed-r1-0929/ - 群聊 R2:
loadtest/im-pressure/results/s60-group-r2-0929/
结果目录包含 coordinator、A/B summary、合并 report 和 agent 指标 JSONL。
12. 精确成员规模诊断复测
为区分“在线连接数”和“群成员总数”的影响,新增 300、600 人独立群复测。每个测试群在起压前均按用户 ID 集合核验,要求实际成员与目标账号完全相等。
| 场景 | Run ID | 在线/群成员 | ACK | 实时投递 | 历史 | DB 精确命中 | ACK P95 | 投递 P95 |
|---|---|---|---|---|---|---|---|---|
| 原 1200 人群、仅 300 人在线 | s300-group-r1b-0929 |
300/1200 | 45/300 | 13500/13500(按 ACK 台账) | 206/206 | 45 ACK + 210 结果未知全部落库 | 18994ms | 89489ms |
| 精确 300 人群 | s300m-group-r1-0930 |
300/300 | 300/300 | 90000/90000 | 300/300 | 300/300 | 235.177ms | 500ms 桶内 |
| 精确 600 人群 | s600m-group-r1-0930 |
600/600 | 600/600 | 360000/360000 | 600/600 | 600/600 | 235.177ms | 500ms 桶内 |
| 精确 900 人群 | s900m-group-r1-0930 |
900/900 | 890/900 | 800555/801000(按 ACK 台账) | 899/899 | 890 ACK + 9 结果未知落库 | >10s,最大19.272s | >10s,最大55.279s |
三轮均未出现 WS 发送队列满、WS 写失败或 Kafka lag。精确 300、600 人群在实时阶段也没有新增 session_topic_publish、topic_client、topic_server 满队列,说明 WS 写链路在 600 在线规模内不是瓶颈。
原 1200 人群仅 300 人在线时有约 900 个离线成员。群消息处理会在 Topic 串行路径中为每个离线成员逐个调用 Redis presence 查询;上一条消息 ACK 后的推送准备持续占用 Topic goroutine,使后续消息长时间等待,最终表现为 ACK 超时和分钟级投递延迟。精确成员群消除这部分离线成员后,同等 300 在线规模恢复正常,验证了该路径是主要根因。
600 人实时发送通过后,240 秒错峰下线仍使群主 Pod 的 topic_server full 从 36 增至 128,净增 92;其他两类内部队列无增量。该问题来自最后会话离线时的 presence 扩散,与消息保存、Kafka 消费和 WS 实时投递无关,仍需在服务端合并或限流离线状态广播。
精确 900 人群出现容量拐点:900 次发送中 890 次 ACK,9 次 ACK 超时、1 次 socket 失败;7 条连接在实时阶段超时断开并自动恢复。按 ACK 台账计算的 445 次实时缺口全部可从历史补回,890 条 ACK 全部精确落库;9 条结果未知实际落库,1 条 socket 失败未落库。因此本轮没有持久化丢失,但实时正确性和延迟门禁未通过。
900 人实时阶段三个内部队列的 full 计数均无增量,Kafka lag 为 0。成功处理的 899 条消息中,permission_check P95 不超过 10ms、save P95 不超过 50ms、kafka_produce P95 不超过 25ms、realtime_fanout P95 不超过 5ms;抽样覆盖后半段 575468 次 WS 投递,发送队列等待 P95 不超过 10ms、socket write P95 不超过 0.5ms,且没有发送队列满或 text write 失败。
现有 realtime_fanout 指标在 pushForData 之前结束。pushForData 位于 Topic 串行处理路径中,每条群消息遍历全部成员、构造推送收件人并调用 sendPush;即使成员全部在线,900 人规模下这段未计时工作仍可让后续发布在 topic_client 中排队。队列未达到容量上限并不代表没有排队,这与 ACK 延迟逐步增长到 19 秒、投递延迟增长到 55 秒的现象一致。下一步应先补充 push_prepare、push_dispatch 和 Topic client queue wait 指标,再将群推送准备移出 Topic 串行热路径或改为异步批处理。
900 人在 240 秒错峰下线后,群主 Pod 的 topic_server full 从 0 增至 379,说明 presence 离线扩散问题随群规模明显放大。压测机两个 agent 的峰值 CPU 分别约 39.5% 和 38.5% 单核,峰值 RSS 各约 76MB、FD 各 460,不是本轮瓶颈。
新增产物:
- 300 人精确群:
loadtest/im-pressure/results/s300-group-members-setup-0929/ - 300 人复测:
loadtest/im-pressure/results/s300m-group-r1-0930/ - 600 人精确群:
loadtest/im-pressure/results/s600-group-members-setup-0930/ - 600 人复测:
loadtest/im-pressure/results/s600m-group-r1-0930/ - 900 人精确群:
loadtest/im-pressure/results/s900-group-members-setup-0930/ - 900 人复测:
loadtest/im-pressure/results/s900m-group-r1-0930/
13. 容量拐点修复(待发版复测)
针对精确 900 人群暴露的 Topic 串行积压和批量下线 presence 风暴,已完成以下代码改造:
pushForData在 Topic goroutine 内只复制成员推送快照,不再逐成员同步查询 Redis,也不再执行跨 Pod 收件人拆分和 user updater 投递。- 新增总容量 4096 的有界异步推送队列和 4 个固定分片 worker。同一
tenant + topic固定进入同一分片,保证同一群的推送和未读更新顺序;不同群可并行处理。 - 离线成员 presence 使用一次 Redis pipeline 批量读取,单条消息的 Redis 网络往返由成员数 N 次降为 1 次。
- 群离线 presence 遇到 Topic server queue 满时不再直接丢弃。服务端按
tenant + topic + source user合并为最新状态,并在 Topic 队列恢复后重投;待处理集合上限 16384。 - 新增 Topic client queue wait、push prepare、push dispatch queue、push dispatch duration、push recipient count、deferred presence 当前深度等指标。
新增重点指标:
im_topic_queue_wait_duration_secondsim_message_publish_stage_duration_seconds{stage="push_prepare"}im_push_dispatch_queue_enqueue_totalim_push_dispatch_queue_depthim_push_dispatch_queue_wait_duration_secondsim_push_dispatch_duration_secondsim_push_recipient_countim_internal_queue_current{queue="topic_presence_pending"}im_internal_queue_enqueue_total{queue="topic_presence_deferred"}
发版后先复测精确 600 人群,确认基线无回归;再复测精确 900 人群。900 人验收要求:900/900 ACK、810000/810000 实时投递、0 断连、0 持久化缺失,topic_client/topic_server/push_dispatch 无 full,Topic queue wait P95 不超过 500ms,ACK P95 不超过 500ms,投递 P95 不超过 1s。渐进下线结束后 deferred presence 必须回落到 0,且不得出现 topic_server full。
14. 异步推送发版后精确 600 人回归
Run ID:s600m-pushfix-r1-0930。测试使用精确 600 人群 grpJZjWbOek928,A/B 各建立 300 条连接;两端均完整运行至 5 分钟落库沉淀、历史回查和 240 秒交错关闭结束,没有提前终止连接。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 600 / 600 |
| 发送尝试 / 收到 ACK | 600 / 523 |
| 发送结果未知 | 77(71 timeout、6 socket) |
| 服务端实际接收并落库 | 593 / 600 |
| ACK P95 | 17522.536ms |
| ACK 台账实时投递 | 308745 / 313800 |
| 实时到达率 | 98.3891% |
| 实时缺口 | 5055,全部可从历史补回 |
| 历史回查 | 593 / 593 |
| 无 ACK 但历史可查 | 70 / 70 |
| MySQL 精确命中 | 593 / 593 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| 断连 | 34(21 reset、13 abnormal close) |
| 重启 | 0 |
异步推送改造本身没有形成服务端队列瓶颈。群主 Pod 处理的 600 次发布 Topic queue wait 全部不超过 50ms;593 次异步推送均成功入队,push_dispatch 没有 full,队列等待 P95 不超过 0.5ms,dispatch P95 不超过 10ms、最大不超过 25ms,deferred presence 最终为 0。Kafka lag 仍为 0。
本轮 77 条 ACK 未知中,70 条已被对端 WS 观察并精确落库,剩余 7 条 socket 失败未进入服务端。由此可确认本轮失败发生在客户端连接和 ACK 返回链路,不是消息持久化、Kafka 消费、Topic 排队或异步推送丢弃。
测试期间 3 个 IM Pod 持续收到培养服务文件确认请求,并在 store.Files.LinkAttachments 中对 cvp... 业务保留键调用 types.GetTopicCat,约每 5 秒产生一轮 HTTP panic 和完整堆栈。该接口允许业务保留键,不能按 Tinode Topic 强制分类;持续 panic 日志和并发异常请求污染了本轮环境,因此本轮不能作为异步推送改造的有效 600 人基线。
已完成两项修复:
- 附件关联仅对
usr...用户引用执行用户 Topic 转换,cvp...等业务保留键直接按普通关联键落库,消除确定性 panic。 verify-db将双机共同观察到的无 ACK 消息与 ACK ledger 合并去重。本轮修正后的独立 SQL 结果为expected=593, found_exact=593,不再把 70 条已落库的 ACK 超时消息误报为 unexpected。
下一轮必须在上述修复发版后重新执行相同的精确 600 人场景。验收前先确认 3 个 Pod 不再出现 invalid topic type for name 'cvp...';随后要求 600/600 ACK、360000/360000 实时投递、MySQL 600/600 精确命中、0 断连,并保持 Topic、push dispatch、Kafka 和数据库指标无积压。
新增产物:
loadtest/im-pressure/results/s600m-pushfix-r1-0930/report.jsonloadtest/im-pressure/results/s600m-pushfix-r1-0930/db-verification.jsonloadtest/im-pressure/results/s600m-pushfix-r1-0930/server-metrics/
15. Panic 修复后精确 600 人连续复测
部署提交 06e08a6 后,3 个 Pod 使用同一镜像摘要,均为 Ready、零重启。连续观测和三轮压测期间不再出现 invalid topic type for name 'cvp...' 或对应 HTTP panic,确认培养文件关联修复生效。
为区分代码回归与公网瞬态,使用同一配置和精确 600 人群连续执行 R2、R3、R4。每轮 A/B 各 300 连接,保持到 5 分钟沉淀、历史验证和 240 秒交错关闭全部完成。
| 指标 | R2 | R3 | R4 |
|---|---|---|---|
| ACK | 600 / 600 | 600 / 600 | 600 / 600 |
| 实时投递 | 359400 / 360000 | 360000 / 360000 | 360000 / 360000 |
| 断连 | 1 次 timeout | 0 | 0 |
| 历史回查 | 600 / 600 | 600 / 600 | 600 / 600 |
| MySQL 精确命中 | 600 / 600 | 600 / 600 | 600 / 600 |
| MySQL 缺失 / 错配 / 重复 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
| ACK P95 | 314.530ms | 1555.109ms | 6228.311ms |
| 投递 P95 | 500ms 桶内 | 2000ms 桶内 | 10000ms 桶内 |
R2 唯一异常是 lta0259 到 216.236.31.189:443 的一次客户端读超时。该连接在发送阶段开始后自动恢复,因此该账号错过 600 条实时消息;600 条均可从历史补回,服务端和 MySQL 没有丢失。R3、R4 均达到消息正确性全通过:零断连、零实时缺失、零重复、零乱序、零持久化缺失。
R3、R4 的延迟门禁没有通过,但服务端分段指标未出现同步恶化:
- Topic queue wait P95 不超过 0.5ms。
- 消息保存 P95 不超过 25ms。
- push dispatch P95 不超过 10ms,队列无 full,完成后深度为 0。
- WS send queue wait P95 约 3ms,socket write 超过 99% 不超过 0.5ms。
- deferred presence 当前深度为 0。
- 600 条历史和数据库记录全部精确命中,Kafka 未出现消费积压。
ACK 延迟按 3 个拨号 IP 分组后同步上升,不集中于单一入口。R2 三个 IP 的 ACK P95 为 302–372ms,R3 为 1536–1643ms,R4 为 5999–6559ms,说明延迟发生在服务端 socket write 之后的共享公网、负载均衡或压测机接收链路。
压测结束后的网络基线同样异常:
| 入口 IP | TCP connect 平均 | HTTPS TTFB 平均 | ICMP 丢包 | ICMP RTT |
|---|---|---|---|---|
216.236.31.189 |
179ms | 623ms | 10% | 平均 256ms,最大 511ms |
216.236.31.191 |
178ms | 543ms | 10% | 平均 154ms |
64.118.158.131 |
178ms | 588ms | 0% | 平均 168ms |
三条静态路由均经 en1 和网关 192.168.1.1 直连,没有经过 VPN/TUN。当前公网 RTT、丢包和 HTTPS TTFB 已高于或接近 500ms ACK 门禁,TCP 重传会进一步放大 WebSocket 队头等待。因此本组结果可证明精确 600 人下服务端消息正确性和持久化能力通过,但不能证明公网端到端延迟 SLA 通过。
后续在进入精确 900 人测试前,应先满足以下任一条件:
- 使用与测试集群同地域、低丢包的压测机,并保留当前 500ms/1s 延迟门禁。
- 取得 Ingress/LB 的 upstream、出站带宽、连接 reset 和重传指标,将公网延迟与服务端处理延迟分开验收。
- 双机正式压测时分别记录本机到 3 个入口的 RTT、丢包和 HTTPS TTFB;网络基线不合格时只验收正确性,不输出容量延迟结论。
新增产物:
loadtest/im-pressure/results/s600m-pushfix-r2-0930/loadtest/im-pressure/results/s600m-pushfix-r3-0930/loadtest/im-pressure/results/s600m-pushfix-r4-0930/
16. 异步推送发版后精确 900 人复测
Run ID:s900m-pushfix-r2-0930,精确群成员 900 人,A/B 各 450 条 WebSocket 连接。两端均完整运行至 5 分钟沉淀、历史回查和 240 秒渐进关闭结束,没有提前终止。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / 收到 ACK | 900 / 590 |
| 发送结果未知 | 310(186 条已被对端 WS 观察,124 条未入服) |
| 服务端实际接收并落库 | 776 / 900 |
| ACK P95 | 17075.685ms |
| ACK 台账实时投递 | 381424 / 531000 |
| ACK 台账实时到达率 | 71.8313% |
| 实时缺口 | 149576,全部可从历史补回 |
| 重复 / 乱序 | 0 / 0 |
| 历史回查 | 776 / 776 |
| MySQL 精确命中 | 776 / 776 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| 断连 | 410(322 reset、88 abnormal close) |
| Pod 重启 | 0 |
本轮消息正确性结论很明确:590 条有 ACK,186 条虽然 ACK 超时或 socket 失败,但已被其他 WS 客户端观察到;这 776 条全部能从历史查到,且 MySQL 精确落库 776 条。服务端已接收消息的持久化丢失率为 0,无重复、无字段错配。余下 124 条在连接断开期间未进入服务端,不属于服务端落库后丢失。
服务端在 776 条实际入服消息上没有出现容量积压:
- Topic queue wait P95 不超过 10ms,95.7% 不超过 10ms。
- push dispatch 776/776 成功入队,无 full;队列等待 P95 不超过 5ms,dispatch P95 不超过 25ms。
- WS send queue 等待 P95 不超过 5ms,socket text write P95 不超过 0.5ms;1177345 次成功写入,16 次写入失败均是对端已关闭连接。
- 3 个 Pod 的 Kafka consumer lag 和 oldest backlog age 始终为 0,无 Topic、push dispatch 或 WS send queue full。
- 渐进关闭后 push dispatch 和 WS send queue 深度均为 0,
topic_presence_pending回落为 0。
压测期间客户端记录 410 次非预期断连,服务端指标同期新增 412 次 WS timeout,三个 Pod 分别为 135、133、144 次,并非单一 Pod 故障。客户端 reset 样本同样均匀分布在三个公网 IP。压测 agent A/B 峰值 CPU 约为 20.8%/22.0% 单核,峰值 RSS 约为 108/101MiB,FD 均为 460,本机进程没有出现 CPU、内存或 FD 容量耗尽。结合本章前已观测到的高 RTT、ICMP 丢包和 HTTPS TTFB,本轮只能判定“当前端到端公网路径下 900 长连接门禁失败”,具体故障点可能在压测机网络、NAT/ISP、Ingress/LB 或其间 TCP 链路,不能据此判定 IM 服务端在 900 人处理容量上失败。
下一次 900 人容量验收必须改为集群同地域压测机,或先取得 Ingress/LB 的 reset、TCP 重传、出站带宽和 upstream 时延指标。网络基线不合格时,继续从当前机器反复施压只会重复得到链路上限,无法给出服务端容量上限。
新增产物:
loadtest/im-pressure/results/s900m-pushfix-r2-0930/report.jsonloadtest/im-pressure/results/s900m-pushfix-r2-0930/db-verification.jsonloadtest/im-pressure/results/s900m-pushfix-r2-0930/server-metrics/
17. 900 连接心跳容错与压测端解耦(待发版复测)
针对 R2 在约一个心跳超时窗口后出现的 410 次断连,已完成三项改造:
- WebSocket Ping 周期和 Pong 等待时间从硬编码中拆出,支持
ws_ping_period_seconds和ws_pong_wait_seconds配置。默认为 15 秒 Ping、90 秒 Pong 容错,不再复用 45 秒业务 idle timeout。 - 新增
im_ws_heartbeat_seconds和im_ws_heartbeat_total{event="pong_received"},用于核对每个 Pod 的有效心跳配置和 Pong 接收数;测试环境 ArgoCD patch 显式设置 15/90 秒。 - 压测客户端为每条连接新增有序数据回调队列。Socket 读循环不再同步执行消息去重、顺序和延迟统计,避免统计锁竞争阻塞 ACK、Ping 和 Pong 读取。
发版后仍使用同一精确 900 人群复测。起压前先确认三个 Pod 的 heartbeat gauge 均为 15/90;验收时对比 Pong 增量、timeout 增量、agent 断连和入服/落库数。若仍然同时批量 timeout,下一步必须取得 Ingress/LB TCP 指标或改用同地域压测机,不再继续放大应用心跳窗口。
18. 90 秒心跳发版后精确 900 人复测
Run ID:s900m-heart-r1-0930。三个 Pod 发版后均为 Ready、零重启,指标确认有效心跳配置均为 ping_period=15、pong_wait=90。使用与上一轮相同的精确 900 人群,A/B 各 450 条连接,完整运行至历史回查和渐进关闭结束。
| 指标 | 本轮 | 改造前 R2 |
|---|---|---|
| 发送尝试 | 900 | 900 |
| 收到 ACK | 854 | 590 |
| ACK 超时 / socket 错误 | 45 / 0 | 310 条结果未知,其中 124 条未入服 |
| 明确失败 | 1 条 500 internal error |
0 |
| 服务端实际接收并落库 | 899 / 900 | 776 / 900 |
| 断连 | 11 | 410 |
| ACK P95 | 11475.912ms | 17075.685ms |
| ACK 台账实时投递 | 765336 / 768600 | 381424 / 531000 |
| ACK 台账实时到达率 | 99.5753% | 71.8313% |
| 重复 / 乱序 | 0 / 0 | 0 / 0 |
| 历史回查 | 899 / 899 | 776 / 776 |
| MySQL 精确命中 | 899 / 899 | 776 / 776 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 |
心跳容错和压测端读循环解耦有明显效果:断连从 410 次降到 11 次,降幅约 97.3%;未入服数从 124 降到 1。负载期三 Pod 累计成功发送约 14554 次 Ping,已观察到约 14472 次 Pong。11 次断连仍在 90 秒窗口附近发生,客户端全部自动重连。当前心跳仅在 Pong 回调时续期;下一步应将有效的客户端 Ping 和业务入站帧同样视为连接活动,而不是继续扩大 Pong 窗口。
45 条 ACK 超时消息全部被对端 WS 观察,且 899 条实际入服消息全部历史可查、MySQL 精确命中,证明已入服消息仍为零持久化丢失。唯一未入服消息已定位到 10:07:58 群主 Pod 的 MySQL invalid connection:同时出现群禁言策略查询失败和消息保存失败,客户端收到 500 internal error。需要对失效 MySQL 连接进行识别、丢弃并安全重试。
服务端分段指标中,899 条成功消息的 permission check P95 不超过 25ms、save P95 不超过 50ms、Kafka produce P95 不超过 25ms、realtime fanout P95 不超过 5ms、push prepare 和 push queue wait P95 不超过 10ms、push dispatch P95 不超过 25ms,无任何内部队列 full,Kafka lag 为 0。但 Topic queue wait P95 为 5s,最后 124 条在连接 timeout 与 presence 处理同期等待 0.1–10s,使 ACK P95 仍未过门禁。
压测 agent 峰值 CPU 约为 28.8%/22.8% 单核,RSS 约 121MiB,FD 各 460,进程资源未饱和。但异步数据回调队列尚未暴露当前深度和等待时间,因此本轮 98 秒投递 P95 包含网络、socket 读取和压测统计队列三段时间,不能单独作为服务端投递 SLA。下一轮前需增加压测回调队列深度/等待指标,并减少 recorder 在同一 client_mid 上的锁竞争。
新增产物:
loadtest/im-pressure/results/s900m-heartbeat-r1-0930/report.jsonloadtest/im-pressure/results/s900m-heartbeat-r1-0930/db-verification.jsonloadtest/im-pressure/results/s900m-heartbeat-r1-0930/server-metrics/
19. 900 人复测后第二轮修复
针对第 18 节剩余的 11 次 timeout、1 条 MySQL invalid connection 和压测端回调排队不可观测问题,已完成以下修复:
- 服务端收到有效客户端 Ping 时立即续期读截止时间并回 Pong;成功读取业务帧时也续期读截止时间。连接活跃判定不再只依赖服务端 Ping 对应的 Pong,新增
im_ws_heartbeat_total{event="ping_received"}观测客户端心跳。 - MySQL 只读查询遇到
ErrInvalidConn时向database/sql返回ErrBadConn,使连接池丢弃坏连接并在新连接上重试。个人消息仅在存在client_msg_id唯一键保护时重试一次;群消息由message_id和client_msg_id唯一键保护,整个保存操作可安全重试一次。新增im_db_invalid_connection_retry_total{operation,outcome}。 - recorder 的群消息接收位图改为原子 CAS。同一
client_mid只在首次创建 observation 时使用 shard 锁,后续数百个收件人不再串行争用同一把写锁。 - agent 新增回调队列当前深度、处理数、峰值、入队/完成数、累计等待和最大等待指标,并将快照写入
summary.json的callback_queue。生成实时到达率前会等待排队及正在处理的回调全部完成,避免统计尚未落入 recorder 时产生假丢失。
下一轮仍使用精确 900 人群复测。除原有正确性门禁外,重点检查:
im_ws_heartbeat_total{event="ping_received"}有持续增量,服务端 timeout 和客户端断连进一步下降。im_db_invalid_connection_retry_total若出现,结果应为discard或recovered,客户端不再收到对应 500。callback_queue.current和callback_queue.processing在结算时均为 0;结合maximum、wait_seconds_total / completed和wait_seconds_max区分压测端排队与服务端投递延迟。
20. 第二轮修复后精确 900 人复测
Run ID:s900m-activity-r1-0930,部署提交 36129e3。复用精确 900 人群 grpQ5_hPdAz3cE,A/B 各 450 条 WebSocket 连接。两个 agent 均通过 30 秒满连接屏障,并一直运行至实时投递、5 分钟沉淀、历史回查、MySQL 校验和 120 秒渐进关闭全部完成。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / 收到 ACK | 900 / 899 |
| 结果未知 | 1 条 ACK 超时,socket 错误 0 |
| 服务端实际接收并落库 | 900 / 900 |
| 实际实时投递 | 810000 / 810000 |
| 实时到达率 | 100% |
| 重复 / 乱序 | 0 / 0 |
| 压测期非预期断连 | 0 |
| 服务端 WS timeout | 0 |
| 历史回查 | 900 / 900 |
| MySQL 精确命中 | 900 / 900 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| ACK P95 | 4748.423ms |
| 投递 P95 | 10000ms 桶内 |
唯一 ACK 超时为 s900m-activity-r1-0930-g-b-0121-01。该消息虽然 20 秒内没有收到 ctrl ACK,但 A/B 均实时观察到,服务端分配 seq 3714,历史可查且 MySQL 精确命中;因此是 ACK 返回链路超时,不是消息未入服或丢失。报告中的 unexpected_messages=900 正是该条没有进入 ACK ledger、但被 900 个连接实际收到的消息。
本轮连接活跃改造生效。三 Pod 均持续记录 ping_received 和 pong_received,负载阶段没有 WS timeout;两个 agent 全程 450/450 在线且断连为 0。与上一轮 11 次断连相比,客户端 Ping 和业务帧续期后已消除本轮心跳误断。
压测端回调队列指标证明 recorder 不再构成秒级延迟:
| agent | 入队 / 完成 | 峰值等待数 | 最大等待 | 平均等待 | 结算时等待 / 处理中 |
|---|---|---|---|---|---|
| A | 405000 / 405000 | 106 | 11.353ms | 约 0.022ms | 0 / 0 |
| B | 405000 / 405000 | 98 | 5.075ms | 约 0.020ms | 0 / 0 |
服务端内部处理也没有出现容量积压:
- Topic queue wait P95 不超过 50ms,全部 900 条不超过 250ms。
- permission check P95 不超过 25ms,消息保存 P95 不超过 50ms,realtime fanout P95 不超过 5ms,push prepare P95 不超过 10ms。
- WS send queue wait P95 不超过 25ms,socket text write P95 不超过 0.5ms。
- push dispatch queue 无 full,Kafka consumer lag 和 oldest backlog age 均为 0。
- 没有
invalid connection、消息保存失败、内部队列 full、panic 或 Pod 重启;关闭后 push 队列和topic_presence_pending均回落为 0。
因此本轮消息正确性和持久化门禁已经通过,但端到端延迟门禁仍未通过。测试结束后的三入口公网 RTT 为 153–189ms,HTTPS TTFB 为 0.50–1.04s;三条路由均经 en1 和网关 192.168.1.1 直连。服务端从 Topic 排队到 socket write 的 P95 远低于端到端 ACK/投递 P95,压测回调队列最大等待也只有 11ms,剩余延迟主要位于服务端 socket 写入之后的 Ingress/LB、公网 TCP、NAT 或压测机网络路径。后续服务端容量验收应在集群同地域压测机执行,并同时采集 Ingress/LB 出站带宽、TCP 重传、reset 和 upstream 指标。
新增产物:
loadtest/im-pressure/results/s900m-activity-r1-0930/report.jsonloadtest/im-pressure/results/s900m-activity-r1-0930/db-verification.jsonloadtest/im-pressure/results/s900m-activity-r1-0930/metrics.jsonlloadtest/im-pressure/results/s900m-activity-r1-0930/server-metrics/
21. 精确 900 人混合场景测试
混合场景使用 900 条 WebSocket、135 对单聊用户和 224 个群发送者。单聊双方各发 5 条,共 1350 条;群发送者各发 2 条,共 448 条;总发送尝试 1798 条。每个 agent 理论应观察 201600 次群投递和 1350 次单聊发送方回显/接收方投递,共 202950 次唯一回调。
21.1 R1:发现结算窗口过早
Run ID:s900m-mixed-r1-1001。
- 1798 条消息全部进入服务端,历史 1798/1798,MySQL 1798/1798,缺失、错配、重复和额外记录均为 0。
- 收到 ACK 1411 条,387 条 ctrl ACK 超时;ACK P95 为 15756.928ms。
- A/B 负载期均为 450 条连接且断连为 0。
- 固定 60 秒 drain 结束时,A 的 summary 记录 202867/202950,误报 83 次实时缺口;summary 生成后指标继续增长至 202950/202950,说明这些帧是公网链路中的晚到帧,并未丢失。
- B 为 202950/202950。回调队列最大等待分别约 98ms/92ms,结算时等待和处理中均为 0。
- 发现 1 次单聊顺序回退:接收方 A-27 在同一 P2P Topic 上先收到 seq 245、后收到 seq 244。
本轮证明只等待“回调队列瞬时为空”不够:帧可能仍位于服务端 socket、Ingress/LB、TCP 或客户端 socket 缓冲区。压测程序已增加 delivery_quiet_period 和 delivery_quiet_timeout。固定 drain 后,接收计数和回调队列必须连续静默 10 秒才允许生成实时 summary;任意晚到帧都会重置静默窗口,最长等待 3 分钟。
同时修正实时统计口径:无 ACK 但已被任一 WebSocket 观察到的消息会与 ACK ledger 合并后参与全部收件人的实时到达率和“实时缺失但已落库”计算,不再被记为 unexpected_messages。该口径从后续轮次开始生效;R1/R2 原始报告仍保留当时输出,以上结论已按全量唯一回调和历史/MySQL 结果校正。
21.2 R2:静默结算修复验证
Run ID:s900m-mixed-r2-1001。本轮使用修复后的压测程序,静默等待在 A/B 均正常执行并完成后才生成 summary。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / ACK | 1798 / 1760 |
| ACK 超时 / socket 错误 | 37 / 1 |
| 压测期断连 | 14,全部为 timeout,A/B 各 7 |
| A 唯一实时回调 | 202535 / 202950 |
| B 唯一实时回调 | 202787 / 202950 |
| 全量唯一实时回调 | 405322 / 405900,约 99.8576% |
| ACK 台账实时缺口 | 566,全部已落库 |
| 重复 / 乱序 | 0 / 0 |
| 历史回查 | 1798 / 1798 |
| MySQL 精确命中 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| ACK P95 | 14317.159ms |
| 投递 P95 | 46622.373ms |
R2 的 A/B 各 7 条连接在负载期超时断开,随后全部自动恢复至 450/450。实时缺口与断连窗口一致,静默窗口结束后接收计数不再增长,因此不是 R1 那种过早结算。所有实时缺口均可由历史补回,MySQL 独立校验证明服务端持久化零丢失。
两个 agent 的回调队列最大等待约 10ms,结算时 current/processing 均为 0,压测 recorder 不是本轮瓶颈。服务端 1350 条 P2P 消息全部处理完成,P2P Topic queue P95 位于 250–500ms,relationship check P95 位于 100–250ms;Kafka lag 为 0,未出现 MySQL invalid connection、内部队列 full 或 Pod 重启。关闭后 push 队列和 deferred presence 均回落为 0。
混合场景在当前公网压测机上未通过实时和延迟门禁,但持久化门禁通过。下一步不应直接扩大到 1200 人,应先在集群同地域压测机重复 900 人混合场景;若同地域仍出现乱序或实时缺口,再分别处理跨 Pod P2P 顺序和 WS 投递路径。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r1-1001/loadtest/im-pressure/results/s900m-mixed-r2-1001/
21.3 R3:零断连、零实时丢失,但仍有 ctrl ACK 超时
Run ID:s900m-mixed-r3-1001。本轮沿用 900 条 WebSocket、135 对单聊用户和 224 个群发送者,共发送 1350 条单聊和 448 条群消息。端到端 P95 延迟按最新口径仅作观测,不参与通过判定。
| 指标 | R3 | R2 |
|---|---|---|
| 就绪连接 | 900 / 900 | 900 / 900 |
| 发送尝试 / ACK | 1798 / 1751 | 1798 / 1760 |
| ctrl ACK 超时 / socket 错误 | 47 / 0 | 37 / 1 |
| 压测期非预期断连 | 0 | 14 |
| 两端唯一 WS 回调 | 405900 / 405900 | 405322 / 405900 |
| 实时缺口 | 0 | 578 次底层回调,ACK 台账折算缺口 566 |
| 重复 / 乱序 | 0 / 0 | 0 / 0 |
| 历史回查 | 1798 / 1798 | 1798 / 1798 |
| 无 ACK 但已观察消息的历史命中 | 47 / 47 | 38 / 38 |
| MySQL 精确命中 | 1798 / 1798 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 | 0 / 0 / 0 / 0 |
| ACK P95(仅观测) | 14020.142ms | 14317.159ms |
| 投递 P95(仅观测) | 52784.346ms | 46622.373ms |
R3 的消息正确性明显好于 R2:A/B 各收到 202950/202950 个唯一回调,包括每端 201600 次群投递、675 次单聊对端投递和 675 次发送方回显。固定 drain 后又经过连续 10 秒静默窗口,计数未再变化,本轮没有过早结算。
47 条 ctrl ACK 超时中,单聊 26 条、群聊 21 条,A/B 分别为 22/25 条。它们全部被 WS 实时观察,47/47 历史可查,并被 MySQL 独立校验纳入 1798/1798 精确命中,所以不是消息未入服或持久化丢失。但客户端在 20 秒内未收到 ctrl ACK,仍是独立的发送协议问题,不属于已取消的 500ms 延迟门禁。因此本轮应判定为:
- 实时到达、重复、顺序、断连、历史和落库正确性通过。
- 混合场景总验收仍未通过,唯一阻断为 47 条
command_timeout=20s的 ctrl ACK 超时。 - ACK P95 和投递 P95 保留在报告中,不影响上述判定。
服务端负载期没有 Topic/WS 发送队列 full,WS send queue 结算时均为 0,所有 Kafka consumer lag 为 0,三个 Pod 均为 Ready 且零重启。连接渐进关闭后,三 Pod 连接数从 518/373/550 回落至 7/2/7,与起压前 8/3/6 基本一致。Pod 2 增加了 1 次 Cluster.Route 和 1 次 Cluster.UserCacheUpdate 瞬时 error,均不足 1ms,最近日志无对应错误,且未影响本轮投递与落库。
压测端回调队列在结算时均为 0;A/B 队列峰值为 355/174,最大等待仅为 37.054ms/25.036ms,不是 20 秒 ACK 超时或 52 秒投递 P95 的原因。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r3-1001/report.jsonloadtest/im-pressure/results/s900m-mixed-r3-1001/db-verification.jsonloadtest/im-pressure/results/s900m-mixed-r3-1001/metrics.jsonlloadtest/im-pressure/results/s900m-mixed-r3-1001/server-metrics/
21.4 R3 ctrl ACK 超时原因分析
21.4.1 代码路径
服务端的消息发布顺序为:Topic 队列出队、权限/关系校验、持久化、生成 NoErrAccepted ctrl ACK 并调用 queueOut,然后才做当前消息的 Topic 广播、跨 Pod 实时 fanout 和离线推送准备。因此,当前消息的大规模 fanout 不会阻塞同一条消息的 ACK 生成。
但所有 ctrl 和 data 最终共用用户的同一个 WebSocket/TCP 有序字节流。ACK 可以在当前消息的 data 之前入队,但无法超过已经写入 socket 或正在 Ingress/LB/公网 TCP 中传输的早先 data 帧。混合场景中发送者同时也是 900 人群的收件人,因此每条发送连接在等待自己 ACK 时,前面可能已有数百条群 data 帧。
21.4.2 服务端分段证据
按起压前与负载结束 Prometheus 快照计算本轮增量:
| 阶段 | 增量样本 | P95 |
|---|---|---|
| P2P Topic 队列等待 | 1350 | 100ms 桶内 |
| P2P 关系校验 | 1350 | 100ms 桶内 |
| P2P 保存 | 1350 | 50ms 桶内 |
| 群 Topic 队列等待 | 448 | 25ms 桶内 |
| 群权限校验 | 448 | 25ms 桶内 |
| 群保存 | 448 | 50ms 桶内 |
| Cluster TopicMaster RPC | 2495 | 3至5ms 桶内 |
| Cluster proxy 队列深度 | 7988 | P95=1 |
| WS 发送队列深度 | 817924 | P95=0 |
| WS 发送队列等待 | 817924 | 10ms 桶内 |
| WS text frame 写入 | 817946 | 0.5ms 桶内 |
本轮三 Pod 共成功写出约 81.8 万个文本帧、193.6MB。服务端应用层队列、保存、Cluster RPC 和 socket write 均远低于 20 秒;压测时间窗内无 queue full、write timeout、数据库错误、Pod 重启或 panic。因此没有证据支持“业务处理或服务端应用队列阻塞 20 秒”。
21.4.3 超时分布证据
- 47 条超时全部发生在起压后 20至40 秒,正好是每条连接已经接收约 200至400 条群广播帧的区间。
- 40/47 是同一用户的第 2 条消息,5 条是第 3 条,只有 2 条是第 1 条;超时不是随机散布在整个测试周期。
- 三个入口 IP 分别有 16/16/15 条超时,排除单一 Ingress IP 热点。
- 压测端回调队列最大等待只有 37ms,数据回调已与 socket 读循环解耦;超时不是 recorder 锁竞争或回调队列堵塞。
- 测试后三个入口的当前 ICMP RTT 约为 167至197ms,HTTPS TTFB 约为 0.49至0.61 秒;路由均经
en1直连,未经 VPN/TUN。
综合上述证据,当前根因判断为:**高置信度的 WebSocket/TCP 单流队头阻塞,延迟主要发生在服务端完成 socket write 之后的 Ingress/LB、公网 TCP、NAT 和压测机接收路径。**这个结论可以解释“服务端已快速保存且客户端最终收到全部 data,但部分 ctrl 在 20 秒窗口内未被读到”。
尚未完全证明的环节是:当前压测程序在 ctrl 超时后删除 wait entry,晚到 ctrl 会被静默忽略;服务端 WS 写指标也没有区分 ctrl 和 data。因此现有数据无法逐条证明 47 个 ACK 全部在 20 秒后到达,只能证明对应消息全部已被服务端接受、投递和持久化。
21.4.4 处理建议
- 压测端增加 late ctrl 台账:ctrl 超时后保留 ID 和发送时间 10 分钟,记录晚到 ACK 数、最终延迟和完全未到数。
- 服务端将 WS 出站指标按
ctrl/data/pres/info分类,新增从客户端请求时间到 ctrl ACK 入队、实际 socket write 完成的直方图;跨 Pod 时再记录 master-to-proxy ctrl 延迟。 - 下一轮保持完全相同的 900 人混合负载,将
command_timeout改为 90 秒,但单独统计超过 20 秒的 ACK。若 47 类消息都在 90 秒内到达,且服务端 ctrl write 延迟仍在毫秒级,则队头阻塞结论得到直接验证。 - 当前业务不对端到端延迟设阻断门禁,因此不建议现在改造独立控制连接或破坏 WebSocket 有序性。应先补齐上述观测,并将“消息已实时观测且历史/MySQL 命中”与“20 秒内收到 ctrl ACK”分开报告。
- 另有一项与 ACK 超时无关的独立缺陷:负载结束后 Pod 2 记录
gob: type not registered for interface: []map[string]interface {},影响另一个群 Topic 的 Cluster Route。当前 cluster gob 已注册[]any和map[string]any,但未注册[]map[string]any,应单独补注册和编解码回归测试。
21.4.5 观测改造完成情况与 R4 口径
上述观测缺口已补齐:
- 压测端会统计
publish_ctrl_observed、publish_ctrl_over_20s、publish_ctrl_late、publish_ctrl_unresolved和publish_ctrl_p95_ms,并保留最多 1000 条超过 20 秒或晚到的样本。 - agent Prometheus 新增
im_load_agent_publish_ctrl_observed_total、im_load_agent_publish_ctrl_over_20s_total、im_load_agent_publish_ctrl_late_total和im_load_agent_publish_ctrl_unresolved。 - 服务端 Prometheus 新增
im_ctrl_ack_delivery_duration_seconds{stage="enqueue|write"},按 Pod 记录从请求进入到 ACK 进入 WS 发送队列、以及完成 socket write 的时间。 - Cluster gob 已注册
[]map[string]any,并用真实系统消息结构补了编解码回归测试。
R4 保持 R3 的 900 连接混合负载,仅将 command_timeout 从 20s 改为 90s。验证时必须同时满足:publish_ctrl_unresolved=0、消息实时与历史/MySQL 零丢失,并对比服务端 enqueue/write P95 与客户端 ctrl P95。如服务端两阶段仍为毫秒级,但 publish_ctrl_over_20s>0,则可将超过 20 秒的部分定位在 socket write 之后的网络/TCP 接收路径。
22. 精确 900 人全量单聊测试
Run ID:s900m-p2p-r1-1001。900 条 WebSocket 连接组成 450 对好友,A/B 各承担 450 个用户。每个用户以 1 秒间隔发送 10 条消息,共尝试发送 9000 条。两个 agent 均完整运行至实时静默判定、5 分钟沉淀、历史回查、MySQL 校验和 120 秒渐进关闭结束,没有提前终止。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / ACK | 9000 / 9000 |
| 明确拒绝 / 结果未知 | 0 / 0 |
| 对端实时投递 | 9000 / 9000 |
| 发送方回显 | 9000 / 9000 |
| WS 唯一回调总数 | 18000 / 18000 |
| 实时到达率 / 丢失率 | 100% / 0% |
| 重复 / 乱序 / 非预期消息 | 0 / 0 / 0 |
| 压测期非预期断连 | 0 |
| 历史回查 | 9000 / 9000 |
| MySQL 精确命中 | 9000 / 9000 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| ACK P95 / 最大值 | 854.385ms / 1324.17ms |
| 实时投递 P95 / 最大值 | 1000ms 桶内 / 1324.255ms |
| 服务端 Pod 重启 | 0 |
报告的 expected_deliveries=9000 只统计对端投递,不包含发送方自身回显。A/B 各收到 4500 次对端投递和 4500 次本地回显,因此底层唯一 WS 数据回调合计为 18000 次,不是重复消息。
本轮单聊正确性和持久化门禁完整通过:9000 条发送全部收到 ACK,所有收发双方均实时收到应有消息,历史接口与 im_008_messages 都精确命中 9000 条。没有发生 ACK 超时、socket 错误、重复、乱序、实时丢失或持久化丢失。
压测端的回调处理没有构成瓶颈:
| agent | 回调入队 / 完成 | 队列峰值 | 最大等待 | 结算时等待 / 处理中 |
|---|---|---|---|---|
| A | 9000 / 9000 | 7 | 16.317ms | 0 / 0 |
| B | 9000 / 9000 | 9 | 13.004ms | 0 / 0 |
服务端运行状态稳定:
- 三个 Pod 均为 Ready、0 重启,单 Pod 资源配置为
requests=1 CPU/512Mi、limits=2 CPU/1Gi。 - 负载结束时 WS 连接分布为 527/373/544;压测连接渐进关闭后回落至 1/0/2,剩余为环境常驻连接,未再出现压测 agent FD 不释放。
- 三个 Pod 的 WS send queue 结算时均为 0;Kafka 的消息持久化、群消息持久化、管理命令、配置事件、缓存失效、死信和工作台 consumer lag 均为 0。
- 关闭过程中的少量
ping closed累计指标来自客户端主动渐退,本轮 agent 记录的负载期非预期断连为 0。
按最新验收口径,ACK P95 和投递 P95 仅用于趋势对比,不作为当前公网单机容量测试的阻断条件。重新合并后本轮 passed=true:单聊消息正确性、实时到达、顺序性、连接稳定性和落库均通过。ACK P95 854.385ms 和投递 P95 1000ms 仍保留在报告中,但不影响本轮通过结论。
全量单聊已通过,无需为 500ms 延迟门禁单独重复 R2。下一步可进入更高风险的群聊或混合场景复测,仍以零丢失、零重复、零乱序、零断连和全量落库为通过条件。
新增产物:
loadtest/im-pressure/results/s900m-p2p-r1-1001/report.jsonloadtest/im-pressure/results/s900m-p2p-r1-1001/db-verification.jsonloadtest/im-pressure/results/s900m-p2p-r1-1001/metrics.jsonlloadtest/im-pressure/results/s900m-p2p-r1-1001/server-metrics/
23. 900 人混合 R4 长 ACK 观测复测
Run ID:s900m-mixed-r4-1001。负载与 R3 相同:900 条 WebSocket、135 对单聊用户和 224 个群发送者,共发送 1350 条单聊和 448 条群消息。本轮仅将 command_timeout 改为 90 秒,并启用 ctrl ACK 晚到台账和服务端 enqueue/write 分段指标。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / ACK | 1798 / 1798 |
| 明确拒绝 / 结果未知 | 0 / 0 |
| 超过 20 秒的 ACK | 118 |
| 90 秒超时 / 晚到 / 最终未到 | 0 / 0 / 0 |
| ACK P95 / 最大值 | 23425.942ms / 68730.192ms |
| 单聊对端实时投递 | 1350 / 1350 |
| 群聊实时投递 | 403200 / 403200 |
| 实时到达率 / 丢失率 | 100% / 0% |
| 重复 / 乱序 / 断连 | 0 / 0 / 0 |
| 历史回查 | 1798 / 1798 |
| MySQL 精确命中 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| Pod Ready / 重启 | 3 / 3,0 重启 |
118 条慢 ACK 的精确延迟范围为 20.317 至 68.731 秒,中位数约 27.062 秒,慢样本 P95 约 39.465 秒。两个 agent 分别有 56 和 62 条超过 20 秒的 ACK,但全部在 90 秒内返回。R4 直接证明同负载下存在“20 秒后才到、但未丢失”的 ACK,强力支持 R3 的 47 条超时属于同类晚 ACK,而非服务端拒绝或消息丢失。
服务端三个 Pod 的 ctrl ACK 指标共有 1801 个自 Pod 启动后的样本,与本轮 1798 条发送只相差 3 条环境流量:
| Pod | enqueue 平均 | write 平均 | enqueue/write P95 |
|---|---|---|---|
| im-server-0 | 49.672ms | 50.059ms | 均在 250ms 桶内 |
| im-server-1 | 51.612ms | 52.176ms | 均在 250ms 桶内 |
| im-server-2 | 48.051ms | 48.358ms | 均在 250ms 桶内 |
ACK 从 WS 队列入队到 socket write 完成只增加约 0.3至0.6ms,而客户端有 118 条超过 20 秒。因此可确认长延迟发生在服务端 socket write 完成之后的 Ingress/LB、公网 TCP/NAT 或压测机接收路径,不在 Topic、持久化、Cluster RPC 或 WS 应用发送队列内。
本轮底层共处理 405900 个匹配的 WS data 回调,A/B 的回调队列峰值为 120/149,最大等待仅 9.603/9.199ms,结算时全部排空。Kafka 各 consumer lag 均为 0,未出现 Topic、Cluster proxy 或 WS send queue full;渐进关闭后只有 4 次预期的 ping closed 写错误。
报告中 ack_p95_ms=23425.942 是基于 1798 条 ACK 原始时间戳的精确值。本轮二进制的 ctrl 直方图上界仅到 10 秒,使 publish_ctrl_p95_ms 在 +Inf 桶中退化为最大值 68731.141ms,该字段本轮不作为 P95 使用。直方图已增加 20、30、45、60、90 和 120 秒桶及回归测试,下轮会输出正确的长延迟分位桶。
服务端日志还发现两次与本轮 Topic 无关的 Cluster Route 失败:gob: type not registered for interface: types.KVMap。types.KVMap 是命名 map 类型,不会被 map[string]any 的 gob 注册覆盖。已补充 types.KVMap 和 []types.KVMap 注册,并扩展 Cluster gob 回归测试;该修复需要再发版。
本轮 report.json 最终 passed=true。混合场景的消息正确性、实时到达、顺序、连接稳定性、历史回查和 MySQL 落库均通过;长 ACK 已被定位为 socket write 之后的端到端队头阻塞,不再阻断当前容量验收。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r4-1001/report.jsonloadtest/im-pressure/results/s900m-mixed-r4-1001/db-verification.jsonloadtest/im-pressure/results/s900m-mixed-r4-1001/metrics.jsonlloadtest/im-pressure/results/s900m-mixed-r4-1001/server-metrics/loadtest/im-pressure/results/s900m-mixed-r4-1001/im-server-{0,1,2}.log
24. 900 人混合 R5 顺序复测
Run ID:s900m-mixed-r5-1001。本轮在 R4 相同的 900 条 WebSocket、135 对单聊用户和 224 个群发送者负载下,验证长延迟直方图、Cluster gob 类型修复及消息顺序。
| 指标 | 结果 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / 90 秒命令窗口内 ACK | 1798 / 1797 |
| 晚到 2xx / 最终接受 | 1 / 1798 |
| 明确拒绝 / 最终未知 | 0 / 0 |
| 超过 20 秒的 ACK | 375 |
| ctrl ACK P95 桶 / 精确 ACK P95 | 45s / 41746.854ms |
| 单聊对端实时投递 | 1350 / 1350 |
| 群聊实时投递 | 403200 / 403200 |
| 实时总投递 / 丢失 | 404550 / 404550,0 丢失 |
| 重复 / 乱序 / 断连 | 0 / 1 / 0 |
| 历史回查 | 1798 / 1798 |
| 无及时 ACK 消息历史命中 | 1 / 1 |
| MySQL 精确命中 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| Pod Ready / 重启 | 3 / 3,0 重启 |
唯一超时消息 s900m-mixed-r5-1001-g-a-0136-02 在 95.028 秒收到 202,因此属于晚到成功,不是拒绝或丢失。压测报告已区分 publish_accepted(命令等待窗口内)与 publish_final_accepted(包含晚到 2xx);用新口径重算后发送成功率为 100%,报告只因真实乱序而失败。长延迟直方图的 P95 正确落在 45 秒桶,不再退化为最大值。
本轮唯一乱序发生在 A 机 97 号连接的同一单聊 Topic:客户端先收到 seq=322,后收到 seq=321。两条消息由会话两端在几乎同一时刻发送,服务端均实时投递并精确落库,所以问题不在 MySQL、Kafka 或历史回查,而在 Topic master 到 proxy Pod 的实时转发顺序。
代码检查确认,同一个 multiplex session 每次入队都会调度 clusterWriteLoop;此前没有排他状态,多个 worker 可以同时消费同一个 send channel。随后使用 fire-and-forget RPC 时,先取出的 seq=321 与后取出的 seq=322 会在远端并发执行,导致 seq=322 先进入 proxy Topic。该竞态与本轮现象完全一致。
修复内容:
- multiplex session 增加原子调度所有权,同一 session 同时只允许一个 Cluster 写任务排空;释放所有权时再次检查队列,覆盖入队与退出之间的竞态。
- Master 到 Proxy 的同一 Topic 响应改为逐条等待 RPC 完成,保证远端处理完 seq=N 后才发送 seq=N+1;不同 Topic 仍由 worker pool 并行处理。
- 新增本地 RPC 回归测试,故意延迟 seq=1 的处理并重复调度同一 session,验证接收顺序始终为 1、2;该测试在
go test -race下通过。 - 压测工具新增晚到 2xx 最终接受计数,已解决“最终已 202 但报告仍算发送失败”的重复判错。
服务端三个 Pod 共记录 1798 条 ctrl ACK,分布为 574/611/613;enqueue/write 平均约 60至66ms,P95 均不超过 250ms。未出现 Topic、Cluster proxy、WS send queue full、Kafka 错误、WS text write 错误或 Pod 重启。A/B 回调队列峰值为 253/290,最大等待 10.703/5.929ms,结算时均已排空。完整 Pod 日志中未再出现 types.KVMap 或其他 gob 注册错误,证明 R4 后的 Cluster gob 修复已生效。
本轮 MySQL 校验覆盖 im_008_group_messages_202610 与 im_008_messages,1798 条业务消息全部精确命中。结论是持久化、实时到达、长 ACK 统计和 gob 修复均通过,当前唯一阻断项为已完成代码修复但尚待发版复验的跨 Pod 顺序竞态。
下一步发版后执行完全相同的 900 人混合 R6。R6 通过条件为:最终接受 1798/1798、实时投递 404550/404550、重复/乱序/断连均为 0、历史和 MySQL 均为 1798/1798;ACK 延迟继续观测但不作为阻断门禁。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r5-1001/report.jsonloadtest/im-pressure/results/s900m-mixed-r5-1001/db-verification.jsonloadtest/im-pressure/results/s900m-mixed-r5-1001/metrics.jsonlloadtest/im-pressure/results/s900m-mixed-r5-1001/server-metrics/loadtest/im-pressure/results/s900m-mixed-r5-1001/im-server-{0,1,2}.log
25. 900 人混合 R6/R7 顺序修复发版复验
25.1 R6:顺序修复通过,公网瞬时断连导致整轮失败
Run ID:s900m-mixed-r6-1001。本轮首次验证同步 Master-to-Proxy 顺序投递版本,跨 Pod 乱序已从 R5 的 1 次降为 0,但压测机在 15:42:13 至 15:42:18 UTC 同时对两个入口 IP 发生 TCP 写超时。
| 指标 | R6 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / ACK | 1798 / 1785 |
| socket 结果未知 | 13 |
| 压测期断连 | 11,均为 write timeout |
| 实时投递 | 401361 / 404542 |
| 实时缺口 | 3181,全部历史可查 |
| 重复 / 乱序 | 0 / 0 |
| 历史回查 | 1790 / 1790 |
| MySQL 精确命中 | 1790 / 1790 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
11 次断连分布在 216.236.31.189 和 216.236.31.191 两个入口,并集中于约 5 秒窗口;三个服务 Pod 均 Ready、零重启,负载期无 Topic/Cluster/WS 队列满、Kafka 错误或 gob 错误。13 条 socket 结果未知中有 5 条被对端观察并精确落库,另外 8 条未落库且未形成额外数据,符合请求在进入服务端前失败的特征。
R6 的顺序修复目标已通过,但整轮因断连、发送未知和实时缺口判定失败。三条目标 IP 路由复查均为 en1 直连,未经过 VPN/TUN。为排除一次性公网抖动,随即执行相同参数的 R7。
25.2 R7:消息业务门禁全部通过
Run ID:s900m-mixed-r7-1001。运行前对三个入口 IP 的 HTTPS 预检均返回 200,TLS 总耗时约 1.0 至 1.1 秒。
| 指标 | R7 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / 最终接受 | 1798 / 1798 |
| 明确拒绝 / 结果未知 | 0 / 0 |
| 超过 20 秒 / 晚到 / 最终未到 ACK | 74 / 0 / 0 |
| 精确 ACK P95 | 16795.372ms |
| 单聊对端实时投递 | 1350 / 1350 |
| 群聊实时投递 | 403200 / 403200 |
| 实时总投递 | 404550 / 404550 |
| WS 匹配回调总数(含单聊回显) | 405900 / 405900 |
| 重复 / 乱序 / 断连 | 0 / 0 / 0 |
| 历史回查 | 1798 / 1798 |
| MySQL 精确命中 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| Pod Ready / 重启 | 3 / 3,0 重启 |
R7 的 report.json 为 passed=true。R5 暴露的同 Topic 跨 Pod 乱序在 R6 和 R7 连续两轮均为 0,证明单 session 排他 drain 与同步 RPC 顺序修复有效。压测端 A/B 回调队列峰值为 156/150,最大等待 13.281/25.038ms,结算时均为 0。
按 R7 起压前和负载结束快照计算增量:服务端新增 1798 条 ctrl ACK,enqueue/write 平均约 82.43/82.89ms,P95 均在 500ms 桶内;Cluster.TopicProxy 新增 7943 次调用,平均约 2.27ms、P95 在 10ms 桶内。负载期无新增 RPC error、内部队列失败、Kafka lag、queue full、gob 错误或 Pod 重启,说明同步 RPC 没有形成负载期瓶颈。关闭后服务端 active WS 回落为 0,压测进程和 FD 均完全释放。
25.3 关闭阶段 multiplex 队列竞态
R6 和 R7 均在最后一批连接关闭后记录 1 次 s.queueOut: session's send queue full 8:grpQ5_hPdAz3cE-im-server-0。该事件发生在业务发送、实时静默、历史回查和 summary 生成之后,没有影响上述消息结果,但违反“渐进关闭阶段也不得队列满”的严格运维门禁。
根因是 proxy stop/detach 后 multiplex worker 已退出并从 SessionStore 删除,而 Topic 的异步 unsubscribe 尚未完成,短时间内仍可能通过旧指针调用 queueOut。旧 session 未标记 terminating,消息进入无人消费的 send channel 并最终填满。修复已在本地完成:Cluster session 清理开始时先原子标记 terminating,并排空残留 channel;后续对旧指针的写入会被直接丢弃,不再进入失去 drain owner 的队列。新增关闭竞态测试和顺序测试均在普通模式连续 20 次、race 模式连续 10 次通过。
因此当前结论分为两层:
- 900 人混合消息业务门禁已经通过,跨 Pod 顺序修复有效。
- 完整运维门禁还需发布关闭竞态补丁后做一次 R8,重点验证业务指标继续全绿且渐进关闭阶段
queue full=0。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r6-1001/loadtest/im-pressure/results/s900m-mixed-r7-1001/
26. 900 人混合 R8 关闭竞态补丁复验
Run ID:s900m-mixed-r8-1002。本轮使用与 R7 完全相同的 900 条 WebSocket、135 对单聊用户和 224 个群发送者负载,重点验证关闭竞态补丁发布后,业务负载期和 120 秒渐进关闭全过程不再出现 send queue full。
| 指标 | R8 |
|---|---|
| 就绪连接 | 900 / 900 |
| 计划发送 / 实际尝试 | 1798 / 1798 |
| 最终接受 / socket 结果未知 | 1796 / 2 |
| 明确拒绝 / ACK 超过 20 秒 | 0 / 0 |
| 精确 ACK P95 / 最大值 | 1041.578ms / 2055.915ms |
| 单聊对端实时投递 | 1350 / 1350 |
| 已接受群消息应投 / 实投 | 401400 / 400954 |
| 已接受消息实时总投递 | 402750 / 402304 |
| 实时缺口 | 446,全部可持久化回查 |
| 重复 / 乱序 | 0 / 0 |
| 压测期非预期断连 | 1,公网读超时后自动重连 |
| 历史回查 | 1796 / 1796 |
| MySQL 精确命中 | 1796 / 1796 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| 负载期 / 关闭期 queue full | 0 / 0 |
| Pod Ready / 重启 | 3 / 3,0 重启 |
26.1 严格业务门禁结论
本轮 report.json 为 passed=false。失败由 A 机第 231 个连接的一次公网读超时引起:
read tcp 192.168.1.135:57717->216.236.31.191:443: read: operation timed out
断连发生于 2026-10-02T06:31:53.391465Z,客户端约 1.5 秒后报告连接恢复。该用户的两条群发送 s900m-mixed-r8-1002-g-a-0231-01/02 分别得到 websocket closed 和 websocket connection unavailable,未进入服务端,MySQL 中也没有对应额外行。因此实际被服务端接受的业务消息为 1796 条,不能把这两条计为服务端持久化丢失。
同一连接在群消息阶段漏收 446 条已接受群消息,形成 446 次实时投递缺口;这些消息全部可通过历史回查获得。其余连接的群投递、全部 1350 次单聊对端投递、重复和顺序检查均正确。按严格验收标准,本轮仍因 disconnects=1、publish_outcome_unknown=2 和实时缺口判定失败,不能替代 R7 的业务全绿结论。
服务端三个 Pod 在超时窗口均保持 Ready、零重启、Kafka lag 为 0,也没有 Topic、Cluster、WS 队列满、RPC error、gob 或持久化错误。证据更符合压测机到单个公网入口的瞬时网络读超时,不支持服务端容量拐点或队列阻塞判断。
26.2 关闭竞态补丁结论
关闭竞态补丁的专项验收通过。两个 agent 均完成实时静默、5 分钟沉淀、历史回查、summary 生成和 120 秒渐进关闭,并以退出码 0 结束;没有提前停止进程。从负载开始到全部压测连接关闭后的完整 Pod 日志中:
s.queueOut: session's send queue full为 0;- Cluster P2M
queue_full增量为 0; - WS send queue
queue_full增量为 0,三个 Pod 结算队列深度均为 0; - 未出现旧 multiplex session 无消费者却继续积压消息的现象;
- 三个 Pod 均保持 Ready、0 重启,Cluster peer 均为 2/2。
这证明“清理 Cluster session 前先标记 terminating 并 purge channel”的修复已经消除 R6/R7 关闭末尾出现的孤儿发送队列积压。关闭期日志仅有 1 次客户端渐退附近的 close 1006 unexpected EOF,没有引发队列堆积或 Pod 状态异常。
26.3 服务端与压测端观测
按起压前和负载结束快照计算增量:
| 指标 | 结果 |
|---|---|
Cluster.TopicProxy |
8227 次,平均 2.285ms,P95 在 10ms 桶内 |
| ctrl ACK enqueue | 1798 次,平均 62.11ms,P95 在 250ms 桶内 |
| ctrl ACK write | 1798 次,平均 62.43ms,P95 在 250ms 桶内 |
| Cluster P2M queue full | 0 |
| WS send queue full | 0 |
| Kafka consumer lag | 全部为 0 |
A/B 回调队列峰值为 63/47,最大等待为 9.865/10.031ms,结算时等待和处理中均为 0。客户端回调处理没有形成瓶颈。MySQL 校验覆盖 im_008_group_messages_202610 与 im_008_messages,1796 条服务端已接受消息全部精确命中。
本轮结论应分开使用:**关闭竞态修复已通过专项复验;R8 整体业务门禁因一次公网断连失败。**如需获得“修复版本下业务与关闭运维门禁同时全绿”的单轮证据,应保持参数不变再执行 R9;无需因本轮结果继续修改服务端队列代码。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r8-1002/report.jsonloadtest/im-pressure/results/s900m-mixed-r8-1002/db-verification.jsonloadtest/im-pressure/results/s900m-mixed-r8-1002/metrics.jsonlloadtest/im-pressure/results/s900m-mixed-r8-1002/server-metrics/loadtest/im-pressure/results/s900m-mixed-r8-1002/im-server-{0,1,2}.log
27. 900 人混合 R9 全量复验
Run ID:s900m-mixed-r9-1002。本轮保持 R8 的配置、账号、消息量和验收口径不变。起压前三个公网入口均返回 200,HTTPS 总耗时约 0.48 至 0.56 秒;三个 Pod 使用相同镜像,均为 Ready、0 重启。
| 指标 | R9 |
|---|---|
| 就绪连接 | 900 / 900 |
| 发送尝试 / 最终接受 | 1798 / 1798 |
| 明确拒绝 / 结果未知 | 0 / 0 |
| 超过 20 秒的 ACK | 10 |
| 精确 ACK P95 | 11417.014ms |
| 单聊对端实时投递 | 1350 / 1350 |
| 群聊实时投递 | 403200 / 403200 |
| 实时总投递 | 404550 / 404550 |
| WS 匹配回调总数(含单聊回显) | 405900 / 405900 |
| 实时到达率 / 丢失率 | 100% / 0% |
| 重复 / 乱序 / 断连 | 0 / 0 / 0 |
| 历史回查 | 1798 / 1798 |
| MySQL 精确命中 | 1798 / 1798 |
| MySQL 缺失 / 错配 / 重复 / 额外 | 0 / 0 / 0 / 0 |
| 负载期 queue full | 0 |
| 渐进关闭期 queue full | 1 |
| Pod Ready / 重启 | 3 / 3,0 重启 |
27.1 业务门禁通过
本轮 report.json 为 passed=true。1798 条发送全部得到最终 ACK,404550 次对端投递全部实时到达;没有发送未知、实时丢失、重复、乱序或非预期断连。A/B 各收到 202950 次匹配回调,其中包含 675 次单聊发送方回显,因此底层回调总数为 405900。
历史接口和 MySQL 均精确命中 1798/1798。数据库校验覆盖 im_008_group_messages_202610 与 im_008_messages,缺失、错配、重复和额外行均为 0。Kafka consumer lag 全部为 0,三个 Pod 全程 Ready、0 重启。
A/B 回调队列峰值为 218/210,结算时均完全排空。按起压前和负载结束快照计算,服务端 Cluster.TopicProxy 新增 7857 次调用,平均 1.752ms、P95 在 5ms 桶内;ctrl ACK enqueue/write 各 1798 次,平均 72.513/72.812ms,P95 均在 500ms 桶内。负载期没有 Topic、Cluster、WS 队列满、RPC 失败、gob 或持久化错误。
因此 R9 已取得修复版本下完整的消息业务全绿证据,并再次验证跨 Pod 顺序修复有效。10 条超过 20 秒的 ACK 均在 90 秒命令窗口内返回,继续按既定口径只做趋势观测。
27.2 关闭期仍有一个低概率队列满
在最后一批连接退出时,Pod im-server-1 于 2026-10-02T07:23:16Z 记录:
s.queueOut: session's send queue full 8:grpQ5_hPdAz3cE-im-server-2
topic[grpQ5_hPdAz3cE]: connection stuck, detaching - 8:grpQ5_hPdAz3cE-im-server-2
cluster: session proxy closed 8:grpQ5_hPdAz3cE-im-server-2
该事件发生在 summary 已生成、1798 条历史核验完成之后,没有影响消息正确性和持久化。结算时三个 Pod 的 WS send queue 与 Cluster P2M queue 深度均为 0,Kafka lag 为 0。
R9 证据修正了 R6/R7 时的部分根因判断。此次队列满发生时,grp...-im-server-2 multiplex session 尚未终止;日志顺序是先队列满、Topic 判定连接卡住并 detach,最后 session 才关闭。因此 R8 发布的 terminating + purgeChannels 补丁只能防止“已进入终止流程的旧 session 被继续写入”,不能处理仍存活 multiplex session 在大量连接关闭时的瞬时响应突发。
当前 multiplex send channel 容量为 sendQueueLimit + 32 = 160。Cluster 写任务采用排他调度和同步 Master-to-Proxy RPC;正常负载期吞吐稳定,但关闭阶段多个 unsubscribe/presence 响应可在 drain worker 获得调度前快速填满 160 个槽位。R8 未复现、R9 复现 1 次,符合低概率调度竞态,而不是持续消费能力不足:R9 关闭阶段 Cluster.TopicProxy 仍完成 3195 次调用,平均约 0.9ms、P95 在 3ms 桶内。
后续修复不应只继续增加 terminating 判断。建议为 MULTIPLEX 出站队列增加一个很短的有界背压重试:首次非阻塞入队失败时确保 drain 已调度,并等待最多约 250ms 获取队列槽位;超时后才维持现有的 queue full -> detach 保护。这样可以吸收调度抖动和关闭突发,同时避免永久阻塞 Topic goroutine。需要增加“队列已满但 worker 很快释放槽位”与“worker 永久不消费时按时失败”两类回归测试,发版后再执行 R10 专项复验。
上述修复已在 R9 后完成:MULTIPLEX 出站队列首次满时会确保 Cluster drain 已调度,并在 250ms 有界窗口内等待一个槽位;只有持续无消费超过窗口才沿用原来的失败和 detach 保护。新增的 clusterQueueMu 将 terminating 检查与入队、以及终止标记与 channel 清理串行化,防止等待期间 session 恰好终止后消息重新进入已清空队列。普通 WebSocket、gRPC 和长轮询队列仍保持原有非阻塞语义,不受此次改动影响。
回归测试覆盖短暂队列满后恢复、持续不消费按时失败、等待期间 session 终止不残留消息,以及原有跨 Pod 顺序与终止拒绝写入场景。针对性测试普通模式连续 20 次、race 模式连续 10 次通过,go test ./server ./loadtest/im-pressure 全部通过。该修复尚待发版并执行 R10 复验。
本轮最终结论:消息业务门禁通过;完整运维门禁仍因关闭期 1 次 multiplex queue full 未通过。
新增产物:
loadtest/im-pressure/results/s900m-mixed-r9-1002/report.jsonloadtest/im-pressure/results/s900m-mixed-r9-1002/db-verification.jsonloadtest/im-pressure/results/s900m-mixed-r9-1002/metrics.jsonlloadtest/im-pressure/results/s900m-mixed-r9-1002/server-metrics/loadtest/im-pressure/results/s900m-mixed-r9-1002/im-server-{0,1,2}.log