WuKongIM send-ban 持续资格诊断:Pebble flush 与复制阶段栈定位存储 I/O 瓶颈

原创2026-10-13 18:18:571,693 阅读
文章标签:即时通讯后端

WuKongIM send-ban 持续资格诊断:Pebble flush 与复制阶段栈定位存储 I/O 瓶颈

WuKongIM 在 5000 频道、4500 SEND/s 的 Cloud Medium 持续压力资格测试中反复出现 sender_read EOF。本文基于仓库诊断报告 docs/reports/2026-09-27-send-ban-flush-diagnostic.md,完整还原两次完整规模尝试的失败证据链:节点 3 commit coordinator 等待 memtable 写停顿解除、flush 后台栈位于目录同步、复制阶段直方图高 P99——这是一次典型的“存储 I/O 优先排查”正向定位,同时也严格划定了结论边界。读完本文,你将掌握 WuKongIM 三节点高 QPS 资格场景的 E2E 诊断开关、goroutine 栈取证方法、复制阶段指标的正确读法,以及“什么证据能证明、什么证据不能断言”的排查纪律。

背景:持续资格(Soak)与两次失败尝试

运行边界:严格隔离的诊断环境

完整规模尝试使用 3 个真实 cmd/wukongim 进程、256 Hash Slots、10 个物理 Slots、3 副本,负载形状为 5000 频道、4500 SEND/s、30 分钟目标;gateway 128 workers、队列 131072、batch 32,append workers 128(与 test/e2e/message/medium_recipient_hotpath/AGENTS.md 中“128 Store append workers 匹配当前产品默认值”的约束一致)。运行期间无并行编译、测试或其他本任务压力,也关闭了 CPU/heap profile。

本轮唯一允许的变更集中在 E2E 诊断侧,无生产代码、持久化策略、默认参数或验收条件变更:

  • 保留 Pebble 独立后台栈抓取;
  • 显式记录每节点 HTTP 状态、输入大小及截断状态;
  • 保留已有前后边界抓取中的复制阶段直方图;
  • 单节点输入上限 2 MiB、过滤输出共 256 KiB、每个指标边界最多 4096 行。

两次尝试:一次冷启动失败、一次测量窗口失败

两次完整规模尝试均被保留为失败证据,未做吞掉失败或广泛重试:

  1. 第一次:cold prime 第 1081 条消息收到 HTTP 408,未进入测量窗口,总耗时 106.30 秒;
  2. 第二次:同一二进制与参数的新集群重跑,prime 约 172.578 秒,随后测量窗口在 213.145 秒、959,151 次调用后发生 sender_read EOF,总耗时 448.286 秒;配置核对通过、磁盘保护未触发。

第二次是本文分析的主体:持续资格仍未通过,但失败后的栈取证给出了存储 I/O 排查的正向证据。

断连前的信号:未确认序号停滞与 pending 堆积

断连前最后三轮,25 条连接的最早未确认消息序号均未前进,全部对应节点 3 的 active Leader,LEO/HW = 191/191。同时:

观察项 三轮变化
pending ACK 3593 → 4716 → 5845
最早未确认消息年龄 836ms → 1086ms → 1336ms

全窗口记录了 852 份快照、54 次节点查询、零查询失败;失败消息计数时点不同、口径不同(pending 为 6490)。关键旁证:

  • SENDACK P99 = 884ms,permission busy = 0——权限路径没有成为拒绝瓶颈;
  • 公开关闭计数:节点 2/3 各有 2/1 次 async_dispatch_queue_full,各节点另有一次 peer_closed;
  • 最后十秒 driver 每 100ms 最多 453 次调用、单连接最多 19 次,最大调度滞后 23.31ms、socket Write 11.10ms——负载生成器本身无明显异常。

这些信号共同指向:消息已进入复制管道,但复制提交被阻塞,ACK 无法推进。而阻塞点正是下一步栈取证揭示的存储层。

栈取证:commit coordinator 停在 memtable 写停顿

三个 profile 均 HTTP 200,输入分别为 113636、113175、123253 字节,无输入截断;保存的过滤输出约 129 KB,无输出截断标记。节点 3 的关键栈:

  • commit coordinator 停在 Pebble v2.1.4 db.go:2644 的 memtable 写停顿分支(write-stall);
  • pebble=flush, output-level=L0 的后台栈位于: compactAndWrite -> provider.Sync -> vfsSync -> diskHealthCheckingDir.Sync -> os.File.Sync -> internal/poll.FD.Fsync——即 flush 完成 SST 输出后同步对象目录;
  • L5、L6 compaction 栈均进入 syscall.Write。

对应依赖源码表明:flush 完成 SST 输出后需同步对象目录;Go 1.25.11 Darwin 的 File.Sync 尝试 F_FULLFSYNC,ENOTSUP 时可回退到 fsync——该栈本身无法区分这两个系统调用分支。仓库当前依赖确认了 github.com/cockroachdb/pebble/v2 v2.1.4(go.mod)。

为什么“目录同步”不能靠 BytesPerSync 掩盖

仓库在 pkg/db/internal/engine/db.go 中配置了 Pebble 引擎选项:

  • BytesPerSync 仅在 Darwin 上显式设为 16 MiB(darwinBytesPerSync = 16 << 20),其余平台传 0 保留上游默认(platformBytesPerSync());
  • MemTableStopWritesThreshold: 4、L0CompactionThreshold: 8、L0StopWritesThreshold: 24,并注册了 WriteStallBegin/WriteStallEnd 事件监听(stallRecorder)。

BytesPerSync 控制的是写入过程中的周期性同步,而本案例阻塞发生在 flush 完成后对对象目录的同步——这是两类不同的同步路径。诊断报告明确指出:目录同步不能由 BytesPerSync 参数替代,也不能为通过测试而删除。这正是“栈取证 + 参数语义”结合后得到的结论:不能靠调参数绕开,只能从存储 I/O 本身入手。

证据边界:什么还没被证明

报告严谨地划定了限制:

  • profile 在失败后采集,通用数据库标签不能把每个后台 goroutine 精确关联到同一物理 DB;
  • 单次栈不能给出同步阻塞时长,不能据此断言磁盘带宽耗尽,也不能排除调度或其他进程影响;
  • 源文件摘要保存在 flush-findings.json。

也就是说,这是“存储 I/O 排查的正向证据,尚不是目录同步耗时测量或整条故障链的逐请求证明”。

复制阶段指标:直方图的正确读法

前后各 528 行指标完整、无溢出;离线分析验证三个节点计数未回退、桶单调、+Inf 与 count 一致。节点 3 的关键分位数:

阶段 P99 P99.9
本机 quorum 存储 90.83ms 402ms
远端 foreground exchange 92.68ms —
本机 queue 9.77ms —

直方图是“已完成操作”,不是“在途请求”

该指标定义在 pkg/metrics/channel_runtime.go:wukongim_channelv2_replication_stage_duration_seconds,标签 {stage, result},Help 明确为“Channel runtime follower replication stage latency”。诊断代码 test/e2e/message/medium_recipient_hotpath/permission_soak_replication_test.go 只保留 replication_stage_duration_seconds_* 前缀的桶/count/sum 行,每边界最多 4096 行,溢出或抓取失败显式标记。

由此得出三条不可忽略的读法纪律:

  1. 运行时每 32 次采样一次已完成操作——尚未完成的停顿不会进入最终耗时,不能用这些分位数否定写阻塞;
  2. 边界抓取不是各节点同时发生——不能把阶段分位数相加成请求分位数;
  3. 直方图 delta 无法识别在途请求,也不能包含一个仍被阻塞操作的最终耗时。

P99.9 高达 402ms 的插值数据只是“已有证据”,写阻塞的真实时长需要下一步的有界磁盘 I/O 时间关系记录才能量化。

诊断开关与运行方式:如何复现这条证据链

本轮新增指标输出由环境变量 WK_E2E_MEDIUM_RECIPIENT_REPLICATION_DIAGNOSTICS=1 开启,它只复用已有前后边界抓取、不增加 SEND 窗口中的 HTTP 请求。在 test/e2e/message/medium_recipient_hotpath/AGENTS.md 中可找到完整的运行方式:

WK_E2E_MEDIUM_RECIPIENT_PERMISSION_SOAK=1 \
WK_E2E_MEDIUM_RECIPIENT_SOAK_DURATION=30m \
WK_E2E_MEDIUM_RECIPIENT_GROUP_CHANNELS=5000 \
WK_E2E_MEDIUM_RECIPIENT_QPS=4500 \
GOWORK=off go test -tags=e2e ./test/e2e/message/medium_recipient_hotpath \
  -run TestCloudMediumPermissionSoak -count=1 -timeout 40m -p=1 -v

配套的故障取证开关包括:WK_E2E_MEDIUM_RECIPIENT_DRIVER_DIAGNOSTICS=1(负载生成器调度滞后、SendFrame 队列准入、socket Write 时长,固定 100-bin/100ms 环,只保留最后十秒)、WK_E2E_MEDIUM_RECIPIENT_STALL_DIAGNOSTICS=1(250ms 观测器跟踪每发送方最早未确认序号)、WK_E2E_MEDIUM_RECIPIENT_STORAGE_HISTORY=1(保留最多 180 个每秒节点快照的固定 Pebble 指标)。同目录下已有关联报告 2026-09-27-send-ban-io-timeline.md 记录了三个节点出现重叠 flush 积压、节点 1 断连前未确认消息集中、节点 2 失败后有 memtable 写停顿的第二种失败形态,可与本报告互为对照。

诊断纪律同样适用于基线对照:真实三节点正向对照(25 频道、500 SEND/s、10 秒)完成 5000 条消息、零 pending,前后指标 528/576 行,无采集失败或溢出——小规模通过不能报告为持续验收通过,正如报告所强调“不能将上述小规模通过报告为持续验收通过”。

下一步与验收门槛

报告给出的下一步是:在相同负载下有界记录磁盘 I/O 与存储停顿的时间关系,或在既有专用测试环境做相同二进制和持久化条件的对照。先区分“单机三进程共享存储的限制”与“产品内写放大”,再决定生产修复。本轮没有提高队列/memtable 阈值,没有降低同步或副本保证。

验收状态明确:R2 的 30 分钟资格与 R6 的“三次完整配对/5% 门槛”仍未完成。完整失败尝试、正向对照、源码/二进制摘要、冻结上下文、配置、原始指标与可重复分析脚本均归档在产物清单(manifest.json,属运行产物,未随仓库分发)。

小结

从 884ms 的 SENDACK P99、停滞不前的 LEO/HW,到 commit coordinator 停在 Pebble 写停顿分支、flush 栈指向目录同步,再到复制阶段直方图 402ms 的 P99.9——WuKongIM 的这次诊断完整展示了如何用 E2E 黑盒取证把“发送失败”收敛到“存储 I/O”:既有源码级参数语义(BytesPerSync 管写入、目录同步是另一条路径)、又有测试侧严谨的采集约束(256 KiB 输出上限、4096 行边界、无窗口内额外请求)。更重要的是它示范了证据边界:直方图只能说明“已完成操作”的耗时,单次栈测不出阻塞时长——排查的下一步必须是磁盘 I/O 时间关系的有界记录,而非直接调参或降级同步保证。

登录后查看全文
WuKongIM