Files
dsh_shenxian/dsh-server-docs/04-调整方案/58-实例内存优化与配额下调.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

118 lines
8.6 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.
# 58 · 实例内存优化:V8 堆上限、编译缓存与配额 512→384
- 日期:2026-09-12
- 状态:✅ **已实施并验证**(服务器已生效;本地待提交)
- 触发:用户问「2 个实例占 212 / 329 MiB —— 为什么一个用户要占用这么多内存」,随后要求「按照你建议优化;512 MiB 是否可以降低」
- 关联:档案 16/38(隔离与配额来源)、33(权限档位,同属「平台注入 env」一族)、44(运行时冻结,同属 baseEnv 注入口)、20(崩溃退避 —— OOM kill 的兜底)、57(同批:实例侧可用性改造)
---
## 一、先纠正口径:那 179 MiB 里有一半不是它的
| | guest | admin | 空 node 基线 |
|---|---|---|---|
| `ps` 看到的 RSS | 179 MiB | 168 MiB | 54 MiB |
| ├ 共享代码页 | **62** | **62** | 48 |
| └ **私有(真独占)** | **117** | **106** | **6** |
那 62 MiB 是 **node 运行时本身**(`/usr/local/bin/node` 的 50 MiB = V8 启动快照 + 代码页,加系统库/原生模块 12)。逐段拆 Shared/Private 后确认 **node 二进制部分是 `共享 50 / 私有 0`** —— 所有 node 进程(2 实例 + 编排器 + 探针)映射同一份物理页,OS 只算一次。
**私有那 100 多 MiB 才是每实例独占**,也正是 cgroup 的计费口径(实测 `MemoryCurrent` 93~98 MiB 与之同量级)。
**私有的构成**(按 smaps 逐段聚合):
| 归属 | 共享 | 私有 |
|---|---|---|
| `[anon]`(V8 code range = JIT 机器码 + 新生代 + Buffer)| 0 | **88 MiB** |
| `[heap]`(V8 老生代)| 0 | 27 MiB |
| `/usr/local/bin/node` | **50** | **0** |
| dsh 原生模块(sharp / koffi / node-pty)| 7 | 1 |
**因果**:空 node 私有仅 6 MiB → dsh 使其 **+110 MiB**。因为 dsh 是**大单体**:**223 个 `@deepseek-ai` 包 / 717 个 JS 文件 / 21.2 MB 源码 / 12 个原生模块**,启动即全部加载并被 V8 编译成机器码 —— `[anon]` 那 88 MiB 主要就是这些 code range。
**两个完全不同的用户冷启动几乎同量(117 / 106 MiB)** ⇒ 这是 **dsh 基座成本**,与用户内容无关。
**对照证据**:guest 比 admin 少启用了 4 个官方插件,私有内存反而**更高** ⇒ **禁用少量插件省不了内存**,瓶颈在基座。
**宿主全貌**(804 MiB 在用):**编排器自身 160 MiB**(比单实例私有还大)+ 2 实例私有 206 + 系统与其它 438。
---
## 二、三项改动
### ① `NODE_OPTIONS=--max-old-space-size=256`(baseEnv)
**为什么必须设**:V8 的默认堆上限是**按宿主物理内存推算**的 —— 本机 1870 MiB → 实测 `heap_size_limit = 960 MiB`,而 cgroup 只给 384/512 MiB。**V8 感知不到 cgroup**,所以内存失控时先撞的是**内核 SIGKILL**(无优雅退出、无 stderr、日志里查不到死因),而不是 V8 自己触发 GC 后抛 JS 异常。
设 256 的依据:dsh 实测老生代仅 **27~30 MiB**,三倍余量足够。作用是把「失控上限」从 960 压到 256,**让 V8 先 GC、实在不够就抛可诊断的 JS 错**。
> ⚠️ **它不降低稳态内存**(实测优化前后 cgroup 都是 ~98 MiB)—— dsh 本来就只用 30 MiB 堆。本项买的是**可诊断性**,不是省内存。
> 该 env 会被实例内的子进程继承(用户自己跑 node 也受限);用户可 `NODE_OPTIONS= node …` 覆盖;运维可用 `DSH_INSTANCE_NODE_OPTIONS` 整体覆盖。
### ② `NODE_COMPILE_CACHE=<userRoot>/home/.node-compile-cache`(baseEnv)
Node 22+ 官方特性(本机 v22.23.2 实测 `module.enableCompileCache` 为函数、可用)。dsh 的 717 个 JS 每次冷启动都要重新编译,**A/B 实测:无缓存 5.0 s → 有缓存 4.0 s(省 20%)**;首轮运行后缓存长到 **8.2 MB / 1615 个文件**。
目录选 `<userRoot>/home/`:不放 `ws`(会被 `ws-cleanup` 当平台产物清理)、不放 `/tmp`(实例的 `/tmp` 是私有 tmpfs,重启即丢)。
### ③ `MemoryMax` 512M → **384M**(`spawnAsUser` 的 `systemd-run` scope)
- 依据:dsh 私有内存稳态 106~117 MiB、峰值约 145 MiB(= VmHWM 206 减共享 62)→ **384 − 145 = 239 MiB 留给用户任务**(python / ffmpeg / pip 编译原生模块),覆盖绝大多数场景;而同样的重任务在 512 下也大概率超限,**不是本次新引入的风险**。
- **作用范围要看清**:`MemoryMax` 是**上限而非预留**(systemd 不预扣),所以它**不决定并发数**(并发取决于宿主 2 核 / 1870 MiB 与实时 `available`)。降它的真实收益是**收窄单实例失控时的破坏半径** —— 给同宿主其他实例留活路。
- 被 OOM kill 时:scope 非 0 退出 → 编排器按崩溃退避重启(档案 20)并写结构化日志;**只影响该租户**(uid + cgroup 隔离)。
- **不建议继续下调到 256**:dsh 启动峰值私有已达 ~160 MiB,256 会把用户任务的余量压到 ~96 MiB。
---
## 三、验证记录(服务器实测)
| 项 | 结果 |
|---|---|
| 构建 | `npm run build`(tsc)退出码 0;`lib/supervisor/orchestrator.js` 含 `MemoryMax=384M`、`NODE_COMPILE_CACHE`、`max-old-space-size=256` |
| 重启 | drain 实例(残留 scope 0)→ `systemctl restart dshs` → **active**、门户 **200** |
| 配额生效 | `dsh-114801-*.scope MemoryMax=384 MiB` ✓ |
| env 注入 | 实例进程 `/proc/<pid>/environ` 实测含 `NODE_OPTIONS=--max-old-space-size=256` 与 `NODE_COMPILE_CACHE=…/home/.node-compile-cache` ✓ |
| 编译缓存 | 目录 `<home>/.node-compile-cache/v22.23.2-x64-8339e56a-114801/` 生成,运行后 **8.2 MB / 1615 文件** ✓ |
| 冷启动 A/B | 清空缓存 **5.0 s** → 保留缓存 **4.0 s**(`Running scope as unit` → `dsh web: http` 时间戳差)✓ |
| 实例可用性 | 启动正常打印 launch token,实例页 **200 / 24.8 KB** ✓ |
| 内存稳态 | cgroup `MemoryCurrent` 98 MiB(与优化前 ~93~98 同级 —— 见 §二.① 说明) |
**未做的验证**:`--max-old-space-size=256` 在**长会话 / 重任务**下的表现(需真实使用积累)。若出现 `JavaScript heap out of memory`,说明 256 偏小 → 调大或改用 `DSH_INSTANCE_NODE_OPTIONS` 覆盖。
---
## 四、配套:内存峰值采样(为「能否再降」积累数据)
**问题**:本机内核 **5.10** 的 cgroup v2 **没有 `memory.peak`**(5.19+ 才有)→ systemd 的 `MemoryPeak` 恒为 `[not set]` → **拿不到历史峰值,配额只能猜**。
**新增 `scripts/instance-mem-sample.cjs`**(只读 + 追加式写一个 json):每小时把全部 `dsh-*.scope` 的 `memory.current` 记一次,按 **uid** 聚合维护峰值(scope 每次重启换名字,按 uid 才连续)→ `/opt/dsh/state/instance-mem-peak.json`。
cron `/etc/cron.d/dsh-maintenance` 每小时 **35 分**,日志 `/var/log/dsh-instance-mem.log`。
**用法**:几天后看该 json 的 `peakMiB` 与 `limitMiB` 之比 —— 若峰值长期 ≤ 上限的 50%,即可安全再降。
> ⚠️ **踩坑**:`systemd` 的 `MemoryCurrent` / `MemoryMax` 单位是**字节**,不是 KB。首版按 KB 除 1024,把 98 MiB 显示成 **100336 MiB**(放大 1024 倍)。已修,并与 `systemctl show` 原始值对照验证。
---
## 五、同时发现:本机镜像缺少 42 个已跟踪文件
改动前跑 `git diff --stat` 发现本机 `D:\github\dsh_shenxian` 有 **42 个已跟踪文件处于「已删除」** 状态(`src/cli.ts`、`src/config.ts`、`src/db/**`、`web/**` 等),而**服务器侧工作树是干净的**。这是**长期状态**(本机 `src/` 只剩 `db/ fs/ supervisor/ web/` 四个目录,顶层 `.ts` 全缺)—— 与档案记录的 2026-09-11「57 项未提交删除」是**同一类现象,已复发**。
**处置**:`git status --porcelain | awk '$1=="D"' | … git checkout --` **只恢复被删的**(保住我当轮对 `orchestrator.ts` 的改动),恢复后状态 = `M orchestrator.ts` + 3 个未跟踪新脚本,干净。
**未查清**:根因(怀疑与杀软/同步工具/误执行的 `git` 操作有关)。**建议**:每次在本机镜像上做改动前先跑 `git status --short` 确认基线干净(本次即靠它拦下"把 42 个删除一起提交"的事故)。
---
## 六、回滚
```bash
# ① 配额与 env(回退到 512 / 不注入)
cp /opt/dsh/backups/orchestrator.ts.bak-20260912-mem /opt/dshs/src/supervisor/orchestrator.ts
cd /opt/dshs && npm run build
# drain + 重启
systemctl restart dshs
# ② 采样 cron:注释 /etc/cron.d/dsh-maintenance 里 35 分那行
# ③ 编译缓存可随时删(下次启动重建):rm -rf <userRoot>/home/.node-compile-cache
```
改配额/注入 env **必须重启平台**才生效(env 在 spawn 时构造、scope 在 spawn 时创建)。