# 84-实例内存配额口径统一(消灭「388 MiB」误报) > 2026-09-13 落地 | 触发:用户问「**实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多**」 > 定性:**同一事实被复制成两份、只改了一份** —— 属于本项目反复出现的「两处副本必然漂」类缺陷。 ## 一、现象与实测(先取证,再动手) 「dsh 设置 → 功能管理」顶部那条内存条显示 **勾选后 388 MiB** 并报 **⚠ 将超出上限**。 实测真实配额(`systemctl show -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,需要平台侧新增采样接口(当前未做,文案不声称是实时值)。