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 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,117 @@
# 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 时创建)。