☰
WuKongIM 会话读取背压归因:受控对比实验与源码级验证指南
2026/10/17 6:42:29 网站建设 项目流程
  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

会话列表(/conversation/list)与会话同步(/conversation/sync)是 IM 高频核心接口。WuKongIM 在 2026-09-12 的一次本地诊断中,通过"新旧实现 + 同一插桩观测"的受控对比实验,将 HTTP 拒绝精确归因到 serving 节点的持久化读取准入阶段,而非笼统的"存储并发不足"。本文基于 docs/superpowers/reports/2026-09-12-conversation-backpressure-attribution.md 展开,结合仓库源码与测试,说明该诊断的观测设计、实验协议、数据结论与决策边界,读完可复现同类背压归因实验,并理解 WuKongIM 会话读取路径的准入模型。

一、实验背景:从迭代器复用优化到背压归因

1.1 为什么需要"归因"而非"再压测"

此前一次迭代器复用(iterator reuse)实验已确认:本地三分钟 400 QPS 同步负载通过,但 420/440 QPS 同步与 1200 QPS 列表确认被拒绝。单纯提高 QPS 无法回答"拒绝发生在哪一层"。本报告的核心价值在于:

  • 固定负载、固定观测:新旧两个实现运行在完全相同的三节点集群与数据夹具上;
  • 区分失败类别:HTTP 拒绝(服务端返回)与 driver drop(压测端有界队列放不下)分开统计,不再把"过载窗口"一律当成存储并发失败;
  • 归因到准入阶段:通过 serving 节点新增的有界观测,证明所有 HTTP 拒绝都有对应的准入拒绝计数。

1.2 新旧变体的唯一差异

对比双方是重新插桩构建的二进制,而非原二进制的直接重跑。唯一生产代码差异是pkg/db/message/non_business_index.go(迭代器复用前保存的旧源码 vs 新源码),通过 Go build overlay 替换。其余生产源码、编译器、夹具与 profile 完全共享。

对应 JSON(docs/superpowers/reports/2026-09-12-conversation-backpressure-attribution.json)记录了:

  • 旧/新原始二进制与插桩二进制 SHA-256;
  • 旧/新non_business_index.go源码 SHA-256;
  • 共享插桩文件清单:pkg/cluster/channels/persisted_read_observer.go、persisted_conversation.go、persisted_messages.go、pkg/metrics/conversation.go、conversation_persisted.go、internal/app/observability.go;
  • 源码基线13f9687...,标记source_dirty: true、release_qualification: false。

这份报告是本地脏工作树诊断,不是发布佐证。

二、观测设计:有界批准入的可观测化

2.1 核心指标族:wukongim_conversation_persisted_*

可选的 Channel observer 经应用层复合 observer 透传到以下指标族(定义见 pkg/metrics/conversation_persisted.go):

指标类型语义
wukongim_conversation_persisted_admission_totalCounter(kind × result)准入决定:accepted/rejected,记录实际准入决策而非推断的队列溢出
wukongim_conversation_persisted_inflightGauge(kind)准入后尚未完成的批次
wukongim_conversation_persisted_limitGauge准入时刻观察到的共享批限额(当前 16)
wukongim_conversation_persisted_occupancyHistogram(kind × result,桶 0/1/2/4/8/12/14/15/16)每次准入尝试时共享占用槽位的并发采样
wukongim_conversation_persisted_hold_secondsHistogram(kind × result)持有读取槽位的墙钟时间,结果分ok/error/byte_budget
wukongim_conversation_persisted_batch_itemsHistogram(kind)每个被准入批次的请求条目数

kind 被归一到heads/recents/other(见persistedReadKind)。无 Channel/UID 标签、无错误串标签,避免高基数。

2.2 观测边界(哪些被刻意排除)

源码注释与报告都强调:

  • 无 serving 节点等待队列:beginPersistedRead是非阻塞准入,槽位满立即拒绝(见下);
  • 持有时间排除准入前路由/RPC 与准入 observer 回调,完成 observer 开销也不计入;
  • byte-budget 失败与准入拒绝是两类:前者发生在成功准入之后、读取结果超预算时;
  • 未引入新请求缓存、未提升活跃读并发、未新增重试路径;运行激活、持久化可见性、整体请求失败与所有释放阈值均不变。

2.3 源码验证:准入即拒绝

服务层定义在 pkg/cluster/channels/persisted_read_observer.go:

func (s *Service) beginPersistedRead(kind string) (time.Time, bool) { accepted := false select { case s.persistedReads <- struct{}{}: accepted = true default: } if o, ok := s.observer.(PersistedReadObserver); ok { o.ObservePersistedReadAdmission(kind, accepted, len(s.persistedReads), cap(s.persistedReads)) } return time.Now(), accepted }

persistedReads在 pkg/cluster/channels/service.go 中以make(chan struct{}, persistedConversationReadBatches)创建,容量为 16。每个准入路径都必须恰好调用一次 completion 并释放槽位:

func (s *Service) endPersistedRead(kind, result string, items int, started time.Time) { if o, ok := s.observer.(PersistedReadObserver); ok { o.ObservePersistedReadCompletion(kind, result, items, time.Since(started)) } <-s.persistedReads }

准入拒绝路径返回ch.ErrBackpressured(persisted_conversation.go 的 heads 路径、persisted_messages.go 的 recents 路径),HTTP 层将其映射为可识别的拒绝信封。应用层复合 observer(internal/app/observability.go 的multiChannelObserver)把信号广播给每个支持PersistedReadObserver的子 observer。

2.4 指标测试验证行为

pkg/metrics/conversation_persisted_test.go 验证:准入后 inflight 增加、拒绝计数、未知 kind/result 归一到有界标签、完成后 inflight 归零、以及禁用 observer(nil receiver)时的安全性。服务层测试 persisted_read_observer_test.go 与 persisted_conversation_test.go 验证槽位满时的拒绝与释放。

报告还提到回归测试的演进:实现前先跑回归测试,因 serving 节点拒绝产生零 observer 回调而失败;实现后通过,并证明"溢出时无存储调用""读失败时准入/完成平衡"。完整单元包(pkg/metrics、pkg/cluster/channels、internal/app)通过,另有针对性的 race、准入策略单元、E2E 编译/opt-out 与 vet。

三、测量协议:可复现的固定负载实验

3.1 环境与夹具

  • 三节点集群 + page-100 夹具:600 组、24 用户、每用户 200 组、每组 3 条 256 字节消息、256 个 hash slot、12 个初始物理 slot;
  • 读前通过 bench API 安全驱逐所有夹具 runtime(见 conversation_qps_test.go 的evictFixtureRuntimes,只有 busy=0 且 active=0 才继续);
  • Linux ARM64 Docker:10 vCPU、6 GiB 容器限额、无 CPU quota、每服务GOMAXPROCS=2、driverGOMAXPROCS=10;
  • 48 个 driver worker + 有界到达队列(queuedArrivalLimit= max(workers, QPS×P99预算/1000))。

3.2 固定负载与窗口

固定负载为 list 1200 QPS、sync 400 QPS、sync 420 QPS。每种负载记录三个连续的 60 秒窗口;有界指标抓取/排空分隔窗口,因此不是一次不间断的 180 秒负载。没有折半、没有"重试直到通过",任何拒绝/丢弃都计为失败窗口。意外错误、运行激活或成员关系写入会中止采集——完整采集不代表全部窗口通过。

driver 额外记录调度/排队等待与 HTTP 请求时间,二者 P99不可相加;请求时间包含连接等待、网络、服务端处理与响应校验。

3.3 运行命令

WK_E2E_CONVERSATION_BACKPRESSURE=1 \ WK_E2E_CONVERSATION_BACKPRESSURE_REPORT=/out/VARIANT/report.json \ WK_E2E_BINARY=/out/VARIANT-instrumented GOWORK=off \ go test -tags=e2e ./test/e2e/message/conversation_qps \ -run '^TestConversationQPSBackpressure$' -count=1 -timeout=18m -p=1 -v

VARIANT取old或new;/out映射到/tmp/conversation-backpressure;源码与依赖挂载只读;需 Linux 以获取进程 CPU 指标。测试入口见 backpressure_test.go,报告 schema 为wukongim/conversation-backpressure/v1,并记录 git 基线、脏标记、profile 与二进制 SHA-256。

四、结果:归因到准入,而非笼统过载

4.1 核心数据表

每行聚合三个 60 秒无 profile 窗口;P99 是三个窗口 P99 的范围而非合并分位;"Drops"指 driver 有界到达队列无法提交已排程请求;HTTP 拒绝与 drop 都算失败:

Endpoint / offered QPSVariantHTTP refusalsDriver dropsWindow P99 rangePassing windows
list / 1200old012755.2–567.6 ms2/3
list / 1200new4034.7–293.3 ms2/3
sync / 400old923115.9–628.0 ms0/3
sync / 400new0068.2–96.5 ms3/3
sync / 420old240132.7–187.7 ms0/3
sync / 420new4085.9–166.3 ms2/3

18 个窗口中的全部 41 次 HTTP 拒绝都有对应的 serving 节点准入拒绝:old sync-400 9、old sync-420 24、new list-1200 4、new sync-420 4。新 list 拒绝是 node 3 的 4 个 heads 批次;新 sync-420 拒绝分布在 node 1 heads(2)、node 1 recents(1)、node 2 heads(1)。byte-budget 失败、意外错误、运行负载、活跃 runtime 与成员写入在无 profile 对比阶段全为零;每个节点/kind 的 accepted/completed 平衡、每窗口后 inflight 为零、观测共享限额保持 16。

4.2 占用率与低均值:突发而非持续饱和

时间积分处理占用率(60 秒均值,来自 hold 求和,不含 observer 开销):old list 0.90–1.22 槽位、new list 0.65–0.89;old sync-400 0.94–1.28、new sync-400 0.66–0.82。立即拒绝证明共享 16 槽池在特定准入时刻是满的;低均值支持瞬时突发而非持续饱和。采样无法确定每个突发由哪个调度器、RPC fanout 或 I/O 事件产生。

4.3 失败类别必须分开解读

  • old list 窗口 3:无 serving 节点拒绝却 drop 了 127 个请求,端到端 P99 567.6 ms,其中 driver 调度/排队 P99 521.3 ms、请求时间 P99 61.8 ms——瓶颈在压测端排队;
  • new list 窗口 2:既有 4 次准入拒绝又有升高的 driver 等待(251.4 ms P99)——两类压力叠加。

结论:后端变慢也会产生 driver 队列,这不能证明 driver CPU 单独构成瓶颈。

4.4 CPU 与 trace 证据

每次成功请求的服务端进程 CPU(整个测量区间含失败请求与后台工作,除以成功响应数):list-1200 从 3.930 → 3.643 ms(-7.3%)、sync-400 从 11.622 → 10.759 ms(-7.4%)、sync-420 从 11.402 → 10.733 ms(-5.9%)。sync-400 的 node/window heads hold 均值从 1.58–2.13 ms 降到 1.08–1.35 ms——支持保留迭代器复用,但残余拒绝使 list-1200 与 sync-420 尚不能定性为稳定容量。

单独 sync-400 node-1 CPU 样本中,CountOrdinaryMessages累计时间从 1.26 s/12.21 s(10.32%)降到 0.93 s/11.58 s(8.03%)。聚焦的 old-sync 存储 trace 在五秒捕获中报告 89.29 ms 聚合同步延迟(77.47 ms 互斥锁等待 + 11.82 ms GC 启动延迟)——不支持主导性的持续存储锁 stall,也不排除捕获窗口外的尖峰。容器计数器在采样检查点无 CPU 限流与 OOM。

4.5 发布门禁回归(对照)

正常 8 worker、阈值不变的发布门禁在新插桩二进制上通过全部 12 个用例:26,400 个已排程且成功的请求、零错误/丢弃/运行负载/成员写入、零活跃 runtime、最大窗口 P99 59.18 ms;覆盖单节点与三节点集群的 page 25/100/200,并检查分配上限。Profile SHA-256 为b29f053bd0f8dfb7437a48edba020c0bd2ff6dc788ac303e91a07abe4576cba3。诊断用的 48 worker 速率不会被复制进发布阈值。

WK_E2E_CONVERSATION_QPS=1 \ WK_E2E_CONVERSATION_QPS_REPORT=/out/release-gate.json \ WK_E2E_BINARY=/out/new-instrumented GOWORK=off \ go test -tags=e2e ./test/e2e/message/conversation_qps \ -run '^TestConversationQPSReleaseGate$' -count=1 -timeout=12m -p=1 -v

门禁对应的固定 profile 见 test/e2e/message/conversation_qps/profile.json(6 个用例、每请求分配上限、stress 配置),判定逻辑见 policy.go 的evaluate:完整/失败请求、QPS 下限、P99 预算、运行激活与成员写入、分配回归上限;拒绝信封识别见 response_status.go。

五、决策与后续:证据够做什么、不够做什么

  • 保留迭代器优化与有界指标;
  • 不引入准入提升、等待队列、透明重试或新调度策略——单次成对运行不足以支撑这些;
  • 剩余性能问题未被宣布修复;
  • 后续应把已捕获的 runnable/network 等待与请求 RPC fanout 关联后再选择定向改动;
  • 用交替变体顺序重复匹配窗口,以区分稳定收益与本地机器波动;
  • 当前证据足以把实际拒绝定位在准入,但不足以把每次瞬时满池事件归因到单一根因。

六、复现与运维要点

  • 背压语义:sending 节点共享 16 批准入、无等待队列、溢出即backpressured(HTTP 400/503 信封,见 response_status.go 的capacityRefusal);
  • 观测依赖进程 CPU 指标,必须运行在 Linux;
  • 报告是脏树本地诊断(release_qualification: false),引用时需区分"定位证据"与"发布证据";
  • 原始产物(报告 JSON、二进制、profile、trace)保留在/tmp/conversation-backpressure,companion JSON 内含窗口结果、归因、完整门禁回执与工件哈希;
  • 仓库只读视角:本文所有命令仅用于本地诊断复现,不涉及修改仓库内容。

读完本文,你可以:用TestConversationQPSBackpressure复现同类归因实验,用wukongim_conversation_persisted_*判断准入是否满池,以及避免把 driver 排队误判为存储并发失败。

  • 即时通讯
  • 后端

【免费下载链接】WuKongIM

More than just IM 不只是即时通讯(IM)

项目地址:https://gitcode.com/gh_mirrors/wu/WuKongIM
点击查看免费下载

相关推荐

上一篇:rea 安装 5 分钟跑通:终端命令与 AI 智能体一次接好
下一篇:用Context做主题系统:yoinks的ThemeProvider架构深度解析(不足70行)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询