Files
dsh_shenxian/dsh-server-docs/04-调整方案/84-实例内存配额口径统一-消灭388误报.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

98 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 84-实例内存配额口径统一(消灭「388 MiB」误报)
> 2026-09-13 落地 | 触发:用户问「**实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多**」
> 定性:**同一事实被复制成两份、只改了一份** —— 属于本项目反复出现的「两处副本必然漂」类缺陷。
## 一、现象与实测(先取证,再动手)
「dsh 设置 → 功能管理」顶部那条内存条显示 **勾选后 388 MiB** 并报 **⚠ 将超出上限**。
实测真实配额(`systemctl show <scope> -p MemoryMax`,字节换算):
| 实例 | 装载的重型插件 | 真实 `MemoryMax` | 验算 |
|---|---|---|---|
| guest(uid 100002) | `dsh-univer-office` | `704643072` = **672 MiB** | 160 + 512 ✓ |
| admin(uid 114801) | `dsh-plugin-mcn-suite` | `402653184` = **384 MiB** | clamp(160+128)=384(踩下限)✓ |
V8 堆两边都 = `min(256, 配额−96)` = **256 MB**。⇒ **配额确实早就改过了,且是真生效的。**
## 二、根因:两套模型从未对齐
| 项 | 平台侧(决定真实 `MemoryMax`) | 插件前端(决定界面显示) |
|---|---|---|
| 位置 | `src/supervisor/orchestrator.ts` | `poc/business-plugins/lib/client.js` |
| 基线 | `BASE_MEM_MB = 160` | `MEM_BASE_MIB = **285**` |
| `dsh-univer-office` | **512** | **64** |
| `dsh-plugin-mcn-suite` | **128** | **39** |
| 合成规则 | `clamp(160+Σ, **384**, 1024)` | `285 + Σ`,并把 **384 当硬上限**判超限 |
| 「上限」 | 384 只是**下限**,硬顶是 1024 | `MEM_LIMIT_MIB = 384`(档案 58 时代的**写死值副本**) |
- **388 的来源**:候选池恰好只有这 2 个插件(实调 `/api/plugins/business` 确认),前端表对两者都有值 ⇒ 全勾 = `285+64+39 = 388`。而平台侧同一情形 = `160+512+128 = 800`。
- **误报机制**:`388 > 384` 触发「将超出上限」红色警告;但 384 在平台侧**早已只是下限** ⇒ 纯误报。
- **为什么插件只能自估**:`/api/dsh/status` **不返回任何内存字段**(只有 running / instance / watchdog / breaker / url)⇒ 插件无从得知真值。
- **漂移时点**:档案 81 R1 把「写死 384」改成「按插件集合推导」时,**只改了编排器**;插件那份副本原地未动,此后所有改动(univer 384→512、mcn-suite 128)都没同步。
## 三、改法(两步,一次做掉)
### ① 平台侧:把已算好的真值透出来(零新增计算)
| 文件 | 改动 |
|---|---|
| `src/supervisor/spawner.ts` | `Spawner` 接口新增可选方法 `quotaInfo?(userId): { memMb; heapMb } \| null`(照 `breakerInfo?` 的模式) |
| `src/supervisor/orchestrator.ts` | 实现 `quotaInfo()` —— 调**同一个** `instanceMemMb()` / `heapMbFor()`,读不到返回 `null` |
| `src/web/routes/dsh.ts` | `/api/dsh/status` 增加 `quota: app.supervisor.quotaInfo?.(request.user!.id) ?? null` |
⇒ 因为走的是 `spawn` 时用的同一函数,**不可能再与实际 `MemoryMax` 对不上**。
### ② 插件侧:改读真值 + 对齐规则 + 修正超限判据
- 常量与规则**逐项对齐**编排器(`MEM_BASE_MIB 160` / `MEM_MIN_MIB 384` / `MEM_MAX_MIB 1024` / `MEM_HEAP_HEADROOM_MIB 96` / `MEM_TABLE{univer:512, mcn:128}`),并新增 `quotaOf()` / `rawMiB()` / `heapMiBFor()` 与编排器同构。
- `load()` 并行取 `/api/dsh/status`(失败静默,只影响内存条),把 `quota.memMb/heapMb` 作为**事实行**直接显示:`实际配额 384 MiB · V8 堆 256 MiB`;读不到才退回估算并如实标注。
- **超限判据改为「原始需求 > 硬顶 1024」** —— 旧版拿 clamp 后的值判,既误报、又在语义上不可能触发(clamp 后永远 ≤ MAX)。
- 未进成本表的插件不再显示 `≈ 0 MiB` 徽章(平台口径「未识别按 0 计」,显示 0 只会制造噪音)。
- 文案:`上限`→`硬顶`、`将超出上限`→`顶到硬顶`,`overWarn` 明确写出「1024 MiB 硬顶」。
- 版本 **0.3.6**。
### ③ 钉死(关键):新增 `scripts/verify-mem-model.mjs`
**根因是「同一事实两处副本」,所以修法必须包含「让它不可能再漂」。**
脚本解析编排器与插件的常量/成本表/clamp 算式/接线点,**逐项交叉断言**,并接进 `npm run verify`:
常量不一致、成本表任一键不一致、clamp 算式不同构、`quotaInfo` 接口/实现/路由任一环缺失、插件不再读 status —— **任一命中即构建失败**。
**反向验证(已做)**:把插件的 `MEM_BASE_MIB` 改回旧值 `285` ⇒ 脚本报
`✗ 常量一致 · 基线 BASE -> 编排器 160 / 客户端 285`、退出码 1;还原后全绿。
(说明这道闸门真的会拦,而不是摆设。)
## 四、验证记录
| 层 | 证据 |
|---|---|
| 编译 | `npm run build`(tsc)零报错 |
| 单测 | `npm run verify` 全绿 —— 含新增的 15 项模型交叉断言 |
| 平台实装 | 服务器 `/opt/dshs/lib/supervisor/orchestrator.js`、`lib/web/routes/dsh.js` md5 与本机编译产物一致(差异已核:orchestrator 正好 19 行 = 我的改动;dsh.js 3 行 + 1 行他人旧的注释) |
| 接口 | `GET /api/dsh/status` → `quota: {"memMb":384,"heapMb":256}`,与实测 `MemoryMax=402653184` **吻合** |
| 插件实装 | admin/guest 两 profile 均 **0.3.6**;`lib/client.js` md5 `86bc8a46…` **与本地产物一致**;旧符号 `MEM_LIMIT_MIB` **0 处** |
| 端到端 | 冷启动取下发 bundle(`rev=a8eb3ecf1edb`,4.30 MB):`MEM_BASE_MIB=160`、`MEM_MAX_MIB=1024`、univer 成本 512、读 status **6 处**、旧符号 **0 处** |
**修复后界面应有的样子**(admin 为例):`当前 384 MiB → 勾选后 800 MiB` + `实际配额 384 MiB · V8 堆 256 MiB`,
状态 **余量充足**(800 < 1024)—— **388 与红色误报都不再出现**。
## 五、事故/踩坑记录
1. **并发撞车(本次最该记住的一条)**:`poc/business-plugins/lib/client.js` 在我作业期间**被并行会话连续改了两轮**(0.3.4「系统管理改为门户 6 功能页」、0.3.5「插件管理列表高度」,**0.3.5 已部署**)。因为每次 `Edit` 前都**重读了文件**,我的改动是**叠在它们之上**、没有覆盖;但版本号我以为是 0.3.3、实际已是 0.3.5 ⇒ 我第一把 `npm pack` 打出了「内容是别人的 0.3.5 + 我的修改、版本号却写 0.3.5」的**同名假产物**(已删除),改判为 **0.3.6**。
⇒ **纪律:动手前先 `git status` + 查最新 tgz 版本号;版本号取「当前值 +1」,不要用记忆里的值。**
2. **未提交区已被多方污染**:该仓 `git status` 同时有他人未提交的 0.3.4/0.3.5(已上线但**未 commit**)。
⇒ 本次**只提交我自己的文件**,`poc/business-plugins/lib/client.js` 与其 `package.json` 保持未提交、留给他人的批次一起提交(避免把别人在途改动「顺手上锁」)。
3. `server 侧 dsh.js` 比仓库**旧一行注释**(`/desktop.html`→`/portal.html`)—— 说明服务器编译产物整体略滞后于仓库;本次顺手带上,无行为影响。
## 六、回滚
- 平台:`/opt/dshs/.bak-20260913-memmodel/{orchestrator.js,dsh.js}` 拷回原位 → `systemctl restart dshs.service`
- 插件:`/opt/dsh/artifacts/` 保留历代 tgz(0.3.5 仍可取)→ `node /opt/dshs/scripts/ensure-biz-plugins.cjs --all --restart` 指定旧版
- 校验脚本:删掉 `package.json` verify 链末尾的 `&& node scripts/verify-mem-model.mjs` 即可(脚本本身独立,不影响运行)
## 七、后续(未做,非阻塞)
- 若成本表再加插件:**只改编排器 + 跑 verify 会失败** ⇒ 逼你同步插件侧(这就是设计意图)。若将来希望彻底「只维护一处」,可考虑由 `/api/dsh/status` 或新增只读接口**下发成本表本身**,插件不再内置副本。
- `business-plugins` 前端那条内存条现在展示的是**配额**(cgroup 上限)而非**实时占用**;若要显示真实 RSS,需要平台侧新增采样接口(当前未做,文案不声称是实时值)。