Files
dsh_ai1net_server/交付物/扇出背压修复-20260926.md
T

73 lines
4.7 KiB
Markdown
Raw Normal View 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`",那是**语义变更**、需重新拍板 —— 我按"不污染观测"选了当前做法。
- ⛔ **未登记下一棒**。