# 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,超过 500ms 门禁;同机投递 P95 落在 2000ms 桶,超过 1000ms 门禁。 ### 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` 区分压测端排队与服务端投递延迟。