Files
dsh_shenxian/dsh-server-docs/04-调整方案/30-编排器孤儿实例清理.md
T

63 lines
3.7 KiB
Markdown
Raw Normal View History

# 30 · 编排器孤儿实例清理(单实例保证从内存升到 OS 层)
- 日期:2026-09-11
- 触发:admin 对话窗口报「模型连接异常」
- 状态:**✅ 已实施并 live 验证(2026-09-11)**
## 一、现象与根因
**现象**:admin 的 dsh 对话窗口「模型连接异常」。排查发现 **admin 有 2 个实例同时运行**:
- `dsh-114801-bf8afe93`(port 35685)= 编排器认的 main;
- `dsh-114801-f7ea9f97`(port 36449)= **孤儿残留**(编排器不认识)。
两实例**共享同一 profile(`DSH_HOME`)→ session/settings 写冲突** → 模型配置读取异常。(KEY 注入、网络均正常。)
**根因(已读源码定位)**:编排器的「单实例保证」**只在内存层**:
| 位置 | 事实 |
|---|---|
| `launch()` L123 | `if (this.mains.has(userId)) throw AlreadyRunningError` —— **只查内存 map** |
| `constructor()` L60-72 | 只建 portGuard + reap timer,**不扫描 OS 层已有 scope** |
| `teardown()` L230 | 只 stop map 内实例,**不清理未纳管的 scope** |
| 全文 | **无任何 `systemctl list-units` 扫描**(对 OS 层不感知)|
**根因链**:
1. 实例真实生命周期在 **OS 层(systemd scope)**,但编排器**只靠内存 map 追踪**;
2. **portal 重启 → map 清空**,而 systemd scope **不随 portal 死**(继续运行)→ 旧实例变**孤儿**;
3. 重启后再 `enter`/`launch` 时**不检查 OS 层** → 直接 spawn 新实例 → **新旧并存**;
4. 两实例共享 profile → 写冲突 → 模型连接异常。
**触发条件**:**portal 每次重启都可能留孤儿**(本次由白名单部署重启 + 此前熔断测试/A1 重启叠加命中)。
## 二、方案(治本:单实例保证升到 OS 层)
**改动文件**:`src/supervisor/orchestrator.ts`
1. **spawn 前清孤儿**(核心):`spawnInstance` 前,用 `resolveUid(userId)` 拼 scope 前缀 `dsh-<uid>-`,`systemctl list-units --type=scope` 扫描 → 对**该 uid 名下、且不等于当前 map 实例 unit** 的 scope `systemctl stop`。
2. **portal 启动时清一次**(防重启留孤儿):`LocalSpawner` 启动(构造后)扫描所有 `dsh-<数字>-*.scope` 并清理(编排器重启后无法接管它们,安全清)。
3. **kill 后校验**(可选):`killInstance` 后确认 scope 已消失,否则再 stop 一次。
**安全边界**:
- 只清理 **`dsh-<uid>-*`**(严格前缀),不误伤 portal 自身(`dshs.service` 名字不同)与其它服务;
- 清理是"停止后交给 systemd 回收",不改任何数据。
## 三、验证结果(live,2026-09-11)
- **场景 A(启动时清)**:造假孤儿 `dsh-114801-deadbeef.scope` → restart portal → **scope 数 0**(cleanAllStaleScopes 生效)→ enter 后只剩 1 个 ✅;
- **场景 B(spawn 前清)**:造孤儿 `deadbeef` → 停真实 main 触发退避重启 → spawn 前清理生效,**deadbeef 残留 0**、只剩 1 个新实例 ✅;
- `npm run build` 通过。
## 三之二、原验证计划(留档)
- 单测:模拟 `systemctl list-units` 输出含孤儿 → 断言 `stop` 被调用且前缀匹配;
- live:手工造孤儿(`systemd-run --scope` 起一个假的)→ 触发 `launch` → 断言孤儿被清、只剩 1 个实例;
- 回归:portal 重启后 `enter`,断言实例数 = 1(此前会 2)。
## 四、红线遵守
1. 不碰官方 dsh 主程序(R2 ✅);2. 不升级 dsh(R1 ✅);3. 只改本地编排器(`dshs`)。
## 五、风险
- `systemctl list-units` 在每次 spawn 前调用 → 轻微开销(~几十 ms),可接受;
- 若 uid 解析失败(用户无 uid)→ 跳过清理(不误伤),仅记日志。