Files
im_server/docs/test/IM-单机60%容量压力测试结果-20260929.md
T

58 KiB
Raw Blame History

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,limit 2 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. 压测工具修复

  1. 新增单机 60% 配置和执行方案,A/B 各 600 连接。
  2. 群聊和混合发送者按 users_per_machine 比例缩放,1000 用户的原参数保持不变。
  3. 投递观测按 client_mid 分为 64 片,每个接收账号独立维护 seq,延迟直方图改为原子计数,移除 WS 读循环中的全局锁争用。
  4. summary/report 新增 disconnect_reasons 和最多 100 条 disconnect_samples。
  5. 群创建后立即 checkpoint manifest;批量邀请失败后重读实际成员,仅邀请缺失用户。
  6. P2P 历史查询 limit 按 seq 区间扩展,上限 200,修复双方消息交错造成的假缺失。

7. 已实施改进

  1. 好友申请接受和自动接受路径增加 store 空结果保护。底层异常返回 nil, nil 时记录租户、申请、操作人和状态并返回 HTTP 500,不再触发进程 panic。
  2. Hub 向已满的 Topic client queue 投递失败时,立即向原发送连接返回 503 service unavailable,并记录 topic、tenant、队列长度和容量。客户端可据此重试,压测也不再把这类请求归为等待超时的未知结果。
  3. 群消息权限检查改为复用群设置缓存,并为个人禁言增加 Redis 正向和负向缓存;新增禁言和解除禁言会同步更新缓存,到期禁言会自动淘汰。此改动减少群 Topic 串行处理路径上的 MySQL 查询。
  4. message_publish_stage_duration_seconds 新增 group/permission_check 和 */realtime_fanout 阶段,下轮可区分权限查询、持久化和实时扩散耗时。
  5. A/B 两个逻辑 agent 按账号顺序交错关闭连接。close_ramp=60s 时每个 agent 的本地关闭过程约 120 秒,A/B 相差半个时隙,整体约每 100ms 关闭一个连接,避免两侧同时下线形成双倍尖峰。
  6. 已增加空结果防 panic、Topic queue 满返回 503、双 agent 关闭时隙的自动化测试。

8. 复测顺序

  1. 发版后先跑单机群聊 60%,验收实时发送阶段和关闭阶段 topic_client enqueue failed 均为 0;若仍过载,发送端必须收到 503,不能再出现对应的纯 ACK timeout。
  2. 对比 permission_check、save、realtime_fanout 三阶段的 P50/P95/P99,确认群权限缓存是否消除了数据库检查瓶颈。
  3. 群聊通过后再跑单机混合 60%,要求 ACK、历史和独立 SQL 三方账本一致,连接断开数为 0。
  4. 补齐 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 风暴,已完成以下代码改造:

  1. pushForData 在 Topic goroutine 内只复制成员推送快照,不再逐成员同步查询 Redis,也不再执行跨 Pod 收件人拆分和 user updater 投递。
  2. 新增总容量 4096 的有界异步推送队列和 4 个固定分片 worker。同一 tenant + topic 固定进入同一分片,保证同一群的推送和未读更新顺序;不同群可并行处理。
  3. 离线成员 presence 使用一次 Redis pipeline 批量读取,单条消息的 Redis 网络往返由成员数 N 次降为 1 次。
  4. 群离线 presence 遇到 Topic server queue 满时不再直接丢弃。服务端按 tenant + topic + source user 合并为最新状态,并在 Topic 队列恢复后重投;待处理集合上限 16384。
  5. 新增 Topic client queue wait、push prepare、push dispatch queue、push dispatch duration、push recipient count、deferred presence 当前深度等指标。

新增重点指标:

  • im_topic_queue_wait_duration_seconds
  • im_message_publish_stage_duration_seconds{stage="push_prepare"}
  • im_push_dispatch_queue_enqueue_total
  • im_push_dispatch_queue_depth
  • im_push_dispatch_queue_wait_duration_seconds
  • im_push_dispatch_duration_seconds
  • im_push_recipient_count
  • im_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 人基线。

已完成两项修复:

  1. 附件关联仅对 usr... 用户引用执行用户 Topic 转换,cvp... 等业务保留键直接按普通关联键落库,消除确定性 panic。
  2. 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.json
  • loadtest/im-pressure/results/s600m-pushfix-r1-0930/db-verification.json
  • loadtest/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 人测试前,应先满足以下任一条件:

  1. 使用与测试集群同地域、低丢包的压测机,并保留当前 500ms/1s 延迟门禁。
  2. 取得 Ingress/LB 的 upstream、出站带宽、连接 reset 和重传指标,将公网延迟与服务端处理延迟分开验收。
  3. 双机正式压测时分别记录本机到 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.json
  • loadtest/im-pressure/results/s900m-pushfix-r2-0930/db-verification.json
  • loadtest/im-pressure/results/s900m-pushfix-r2-0930/server-metrics/

17. 900 连接心跳容错与压测端解耦(待发版复测)

针对 R2 在约一个心跳超时窗口后出现的 410 次断连,已完成三项改造:

  1. WebSocket Ping 周期和 Pong 等待时间从硬编码中拆出,支持 ws_ping_period_seconds 和 ws_pong_wait_seconds 配置。默认为 15 秒 Ping、90 秒 Pong 容错,不再复用 45 秒业务 idle timeout。
  2. 新增 im_ws_heartbeat_seconds 和 im_ws_heartbeat_total{event="pong_received"},用于核对每个 Pod 的有效心跳配置和 Pong 接收数;测试环境 ArgoCD patch 显式设置 15/90 秒。
  3. 压测客户端为每条连接新增有序数据回调队列。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.json
  • loadtest/im-pressure/results/s900m-heartbeat-r1-0930/db-verification.json
  • loadtest/im-pressure/results/s900m-heartbeat-r1-0930/server-metrics/

19. 900 人复测后第二轮修复

针对第 18 节剩余的 11 次 timeout、1 条 MySQL invalid connection 和压测端回调排队不可观测问题,已完成以下修复:

  1. 服务端收到有效客户端 Ping 时立即续期读截止时间并回 Pong;成功读取业务帧时也续期读截止时间。连接活跃判定不再只依赖服务端 Ping 对应的 Pong,新增 im_ws_heartbeat_total{event="ping_received"} 观测客户端心跳。
  2. MySQL 只读查询遇到 ErrInvalidConn 时向 database/sql 返回 ErrBadConn,使连接池丢弃坏连接并在新连接上重试。个人消息仅在存在 client_msg_id 唯一键保护时重试一次;群消息由 message_id 和 client_msg_id 唯一键保护,整个保存操作可安全重试一次。新增 im_db_invalid_connection_retry_total{operation,outcome}。
  3. recorder 的群消息接收位图改为原子 CAS。同一 client_mid 只在首次创建 observation 时使用 shard 锁,后续数百个收件人不再串行争用同一把写锁。
  4. 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.json
  • loadtest/im-pressure/results/s900m-activity-r1-0930/db-verification.json
  • loadtest/im-pressure/results/s900m-activity-r1-0930/metrics.jsonl
  • loadtest/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.json
  • loadtest/im-pressure/results/s900m-mixed-r3-1001/db-verification.json
  • loadtest/im-pressure/results/s900m-mixed-r3-1001/metrics.jsonl
  • loadtest/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 处理建议

  1. 压测端增加 late ctrl 台账:ctrl 超时后保留 ID 和发送时间 10 分钟,记录晚到 ACK 数、最终延迟和完全未到数。
  2. 服务端将 WS 出站指标按 ctrl/data/pres/info 分类,新增从客户端请求时间到 ctrl ACK 入队、实际 socket write 完成的直方图;跨 Pod 时再记录 master-to-proxy ctrl 延迟。
  3. 下一轮保持完全相同的 900 人混合负载,将 command_timeout 改为 90 秒,但单独统计超过 20 秒的 ACK。若 47 类消息都在 90 秒内到达,且服务端 ctrl write 延迟仍在毫秒级,则队头阻塞结论得到直接验证。
  4. 当前业务不对端到端延迟设阻断门禁,因此不建议现在改造独立控制连接或破坏 WebSocket 有序性。应先补齐上述观测,并将“消息已实时观测且历史/MySQL 命中”与“20 秒内收到 ctrl ACK”分开报告。
  5. 另有一项与 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.json
  • loadtest/im-pressure/results/s900m-p2p-r1-1001/db-verification.json
  • loadtest/im-pressure/results/s900m-p2p-r1-1001/metrics.jsonl
  • loadtest/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.json
  • loadtest/im-pressure/results/s900m-mixed-r4-1001/db-verification.json
  • loadtest/im-pressure/results/s900m-mixed-r4-1001/metrics.jsonl
  • loadtest/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。该竞态与本轮现象完全一致。

修复内容:

  1. multiplex session 增加原子调度所有权,同一 session 同时只允许一个 Cluster 写任务排空;释放所有权时再次检查队列,覆盖入队与退出之间的竞态。
  2. Master 到 Proxy 的同一 Topic 响应改为逐条等待 RPC 完成,保证远端处理完 seq=N 后才发送 seq=N+1;不同 Topic 仍由 worker pool 并行处理。
  3. 新增本地 RPC 回归测试,故意延迟 seq=1 的处理并重复调度同一 session,验证接收顺序始终为 1、2;该测试在 go test -race 下通过。
  4. 压测工具新增晚到 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.json
  • loadtest/im-pressure/results/s900m-mixed-r5-1001/db-verification.json
  • loadtest/im-pressure/results/s900m-mixed-r5-1001/metrics.jsonl
  • loadtest/im-pressure/results/s900m-mixed-r5-1001/server-metrics/
  • loadtest/im-pressure/results/s900m-mixed-r5-1001/im-server-{0,1,2}.log