Files
dsh_ai1net_server/交付物/扇出背压修复-20260926.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

4.7 KiB
Raw Permalink Blame History

交付物 · 扇出背压修复(2026-09-25/26 · 第 15 棒)

工作区:E:/ProgramData/AIProject/aliyun-dsh-server|基线:D:/github/dsh_shenxian 用户口径:「A 方案」= 现在就修背压 上游:交付物/端口与容量瓶颈评估-20260925.md(本棒即其实施)


§0 一句话

修掉一处单点即可打垮服务器的隐患:消息扇出的投递点原先丢弃 socket.write() 的返回值、 也不看水位 ⇒ 慢客户端(弱网 / 故意不读)让待写缓冲无界增长。现在:水位满记账、 超上限具名断开、且正常路径语义一字未变。

§1 交付(1 新 3 改 · 改动形态极干净)

文件 改动 形态
src/im/ws.ts 新增 IM_SEND_BUFFER_LIMIT_BYTES(4 MiB)+ 可注入阈值;send() 加背压三道判定 +84 / −1 —— 🔴 被删改的既有行只有 1 行,正是 socket.write(textFrame(text))(可机械断言)
src/im/hub.ts HubStats 加 3 个背压字段;新增 noteBackpressure() / noteBackpressureClose()(只记账、不抛、不做 I/O) +36 / −0(纯插入)
test/im-backpressure.test.mjs 8 例(含假 socket 精确控水位) 新增
package.json test / verify 各登记 1 处 2/2

§2 三条行为与判据

行为 判据 为什么这么定
write() 返回 false ⇒ 记一次背压,仍算已投递 delivered === onlineIn 不变 false 的含义是"水位满",数据已排队、会送达 ⇒ 若记成丢帧,"送达数"会随客户端网速变小、污染观测
待写缓冲 > 上限 ⇒ 具名断开(1013 send-buffer-overflow)+ warn 日志 连接真被销毁;该帧计 skipped 这是防内存无界增长的唯一硬闸
恰好 等于 上限 ⇒ 不断开 判据是 >,⛔ 不是 >= 边界明确、可断言

§3 判据读数

  • npm run build rc=0
  • node --test test/im-backpressure.test.mjs = 8 / 8 过 / 0 败
  • npm test = 626 / 616 过 / 5 败 / 5 跳过(基线 618/608 ⇒ +8 全为本棒;5 败仍是投放线 tar EBUSY,未增一败)
  • npm run check:layering rc=0 ✅ 无新增违规
  • src/im/store.ts 零改动

§4 阈值:当前是保守起点,待标定

  • 缺省 4 MiB,依据 = 单帧上限(1 MiB)的 4 倍 ⇒「允许积压约 4 个满帧」。
  • ⚠️ 未做真实水位标定 ⇒ 阈值可注入(运行时选项)⇒ 标定后改参数即可,⛔ 不必改代码。
  • 观测面(GET /api/im/stats,admin):backpressureEvents · peakPendingBytes(最有价值,它决定阈值该定多大) · closedByBackpressure。
  • 建议:真实流量跑一段后取 peakPendingBytes 的分位值校准。

§5 判据演进(已写进 09 §8)

「修 bug」与「新增能力」的判据不同:

  • 新增能力 ⇒ 一律纯插入(未接线时行为逐字不变)。
  • 修 bug ⇒ 允许改既有逻辑(否则没法修),但必须同时满足三条: ① 既有语义有回归用例 ② 新行为可观测 ③ 阈值可注入。

内核改动现为四档:store.ts 零改动 | hub.ts 纯插入 | ws.ts 允许改但被删的行只限投递点 | routes/im.ts 纯插入。 142 §五-6 的意图不变:⛔ 不为场景改内核,只许"接扩展点"+"修内核自身缺陷"。

§6 ⚠️ 一条实测教训(值得复用)

测试跑的是 lib/ 编译产物 ⇒ 我改完 ws.ts 后忘记重新 build,导致 hub.ts(新)与 ws.ts(旧)混跑 ⇒ 测试连挂两次,症状是"断言该断开却投递了",而诊断输出显示注入值完全正确 ⇒ 一度误判为 Node 属性遮蔽问题。

✅ 定位法:打印两个独立观测点(注入值 ✅ 但 backpressureEvents = 0)⇒ 一眼看出"值对、但走的是旧代码"。 ⇒ 🔴 改完必 build;⛔ 别被"注入看起来没生效"骗到去改实现。

(附带一条真知识:Node 的 writableLength 是原型 getter、非实例自有属性;实例级 Object.defineProperty 可覆盖。)

§7 未做 / 边界 / 连锁影响

  • ⛔ 未部署、未动 47/106、未 commit/push、未动云安全组、未改 A–E 五单结论。
  • ⚠️ 生产标定未做(阈值仍保守)。
  • 🔴 一条连锁影响需你知悉:设计上慢客户端不虚减 delivered(理由见 §2)⇒ 若你希望"慢客户端单独计入 skipped",那是语义变更、需重新拍板 —— 我按"不污染观测"选了当前做法。
  • ⛔ 未登记下一棒。