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 一律写「远程服务器」。
8.6 KiB
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(与优化前 |
未做的验证:--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 个删除一起提交"的事故)。
六、回滚
# ① 配额与 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 时创建)。