263 lines
22 KiB
Markdown
263 lines
22 KiB
Markdown
# 交接单 · Manager 重启后「逐步拉起」既有实例
|
||||
|
|
|
|||
|
|
- **序号**:覆盖网络线 **序 ㉕ · 规划棒**
|
|||
|
|
- **立单**:2026-09-17 20:3x
|
|||
|
|
- **用户拍板**:**「Manager 重启后是否自动拉起既有实例:逐步拉起」**(2026-09-17 20:2x)
|
|||
|
|
- **上游依据**:档案 **30**(`dsh-server-docs/04-调整方案/30-编排器孤儿实例清理.md`)/序⑲ §8.8-1("按需拉起、无 reconciler"的实测)/序⑬ P3(`OBS-09` 因无实例而 SKIP)
|
|||
|
|
- **状态**:**待执行**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §0 摘要 + ⚠️ 一条必须先说的反直觉事实
|
|||
|
|
|
|||
|
|
用户拍板 = **"逐步拉起"**(staggered rehydrate)。但**读源码发现:现在的架构与目标不是"缺一个 reconciler",而是"启动时主动清空"** ——
|
|||
|
|
|
|||
|
|
| 位置 | 现状 | 与目标的差距 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `src/supervisor/orchestrator.ts:208`(构造函数) | `this.cleanAllStaleScopes()` —— **portal 启动即清掉全部实例 scope** | 🔴 **这是"逐步拉起"的直接阻碍**:重启后 **N=0**,没有任何可接管的实例 |
|
|||
|
|
| `:1225` | `stopScopesByPrefix('dsh-', /^dsh-\d+-[0-9a-f]+\.scope$/)` | 清得**很干净**(连"哪些曾经在线"的信息一起丢了) |
|
|||
|
|
| `:1218` | `cleanStaleScopes(uid)` —— spawn 前清**同 uid** 名下的残留 scope | ⚠️ 这一条是**必要的**(档案 30 的根因:双实例共 profile ⇒ 写冲突)⇒ **⛔ 不许为了"逐步拉起"把它去掉** |
|
|||
|
|
|
|||
|
|
**根因链(档案 30 原文)**:实例真实生命周期在 **OS 层(systemd scope)**,而编排器**只靠内存 map 追踪** ⇒ 重启后 map 空 ⇒ 旧 scope 变**孤儿**(编排器不认识)⇒ 双实例共享 profile ⇒ session/settings 写冲突 ⇒ "模型连接异常"。当时的修法是"**统一清掉防孤儿**"。
|
|||
|
|
|
|||
|
|
⇒ **本单的核心判断**:**"逐步拉起"= 把"清空"换成"接管"** —— 启动时**不要清**,而是**扫描 OS 层既有 scope ⇒ 逐个回填进内存 map ⇒ 按节流逐个 probe/rehydrate**。⚠️ **前提是先有"可恢复的实例账本"**(否则"清"是唯一安全的做法)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §1 目标(可判定"做完了没有")
|
|||
|
|
|
|||
|
|
| # | 判据 | 期望 | 怎么测 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **E1** | 重启后实例**不被清掉** | `restart dshs` 后 5 s 内,重启前在跑的 scope **仍在** | 先起 2 个实例 ⇒ `restart dshs` ⇒ `systemctl list-units 'dsh-*.scope'` **仍为 2** |
|
|||
|
|
| **E2** | **逐个**(节流)接管,⛔ 不风暴 | 接管速率受限(`REHYDRATE_CONCURRENCY` / `REHYDRATE_STAGGER_MS` 阈值内) | 4 个实例 ⇒ 日志逐条时间差 ≥ 阈值;⛔ 无"同一秒 4 条" |
|
|||
|
|
| **E3** | 任一实例都是**原实例**(不是新起) | 接管后 `port` 不变、无新 scope 产生 | 对比重启前后的 scope 名与 `--port` 参数 |
|
|||
|
|
| **E4** | 单实例保证**仍成立**(⛔ 不回归档案 30) | 接管后同 uid **只有 1 个** scope;新 spawn 前仍清同 uid 残留 | 断言 `dsh-<uid>-*` 计数 = 1 |
|
|||
|
|
| **E5** | 无实例时**不自起**(⛔ 不违反"按需"语义) | 重启前**没有**实例 ⇒ 重启后仍为 0 | 空跑一次 `restart dshs` ⇒ scope = 0 |
|
|||
|
|
| **E6** | 内存/启动压力可控 | 接管过程 47 可用内存不跌穿阈值;无 OOM kill | `free` 采样 + `journalctl -u dshs \| grep -c 'oom'` = 0 |
|
|||
|
|
| **E7** | 零回归 | `npm test` **176/175/0/1**、`--scene all --table` **12P/0S/0F**、`overlay-probe --table` **16P/0F/0S** | 三件套跑同值 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §2 只读前置(执行前**必须**先核实的 4 条)
|
|||
|
|
|
|||
|
|
| # | 要核实什么 | 命令 | 期望 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **P1** | 实例 scope 与 uid 的对应 | `systemctl list-units 'dsh-*.scope' --plain` + 对照 `users` 表 | 确认 `dsh-<uid>-<id>.scope` 里 **`<uid>` = 用户 uid**(档案 30 口径),⛔ 别与"用户 id"混 |
|
|||
|
|
| **P2** | scope 的 `--port` 参数**重启后是否可读回** | `cat /proc/<pid>/cmdline` 或 `systemctl show -p MainPID` 后读 `/proc` | ⚠️ **这是可恢复性的关键**:拿不到端口 ⇒ 没法接管(要另想办法) |
|
|||
|
|
| **P3** | 47 上当前实例数(T0 现场) | `systemctl list-units 'dsh-*.scope'` | 序㉓ 实测 47 = **0**、106 = **1**(⚠️ 实例在 **106** 上)⇒ 本单验收要**主动先起实例** |
|
|||
|
|
| **P4** | 是否有"实例账本"可持久化 | 查 `RemoteSpawner` / `LeasedSpawner` 落库情况 | 若既无 DB 记录也无 /proc 可读 ⇒ **必须先补一步"实例账本"**(见 §4 S1 前置) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §3 范围
|
|||
|
|
|
|||
|
|
### 3.1 在册文件集(⚠️ **超出必须先停下报告** — R7)
|
|||
|
|
|
|||
|
|
| 面 | 文件 | 说明 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 改动 | `src/supervisor/orchestrator.ts` | ①② 启动流程:清空 → 接管;新增 `rehydrate()` |
|
|||
|
|
| 改动 | `参数表_覆盖网络_20260917.md` | 新增 `REHYDRATE_*` 键 |
|
|||
|
|
| 改动 | `scripts/overlay-probe.cjs` | 新增观测项(重启后 scope 数 = 重启前) |
|
|||
|
|
| 可能新增 | `test/orchestrator-rehydrate.test.mjs` | 单测(⛔ 不改 `package.json` test 列表的结构) |
|
|||
|
|
|
|||
|
|
⛔ **不在本单范围**:改 bwrap 参数(🔴 **47 是 bubblewrap 0.4.0**,`--perms` 属 0.5+ ⇒ 会起不来)、改 `cleanStaleScopes(uid)` 的"spawn 前清同 uid"语义、动 `RELAY_FAILOVER_*` / `HB_SEC` / burst。
|
|||
|
|
|
|||
|
|
### 3.2 三条硬约束(**违反即事故**)
|
|||
|
|
|
|||
|
|
1. 🔴 **`cleanStaleScopes(uid)`(spawn 前清同 uid)⛔ 不许删** —— 它是档案 30 的修法本体,删了就回到"双实例共 profile"。
|
|||
|
|
2. 🔴 **`cleanAllStaleScopes()` 只能改成"接管",⛔ 不能改成"什么都不做"** —— 因为内存 map 为空时,**孤儿会失控**(这正是档案 30 的原始故障)。
|
|||
|
|
3. 🔴 **接管必须"认原实例"** —— ⛔ 不许"重启后重新 spawn 一遍"(那会**换端口、换 scope 名**,且瞬间 N 个一起起 = 启动风暴)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §4 步骤 S0–S6
|
|||
|
|
|
|||
|
|
| 步 | 做什么 | 验证(一次可执行) | 回滚 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **S0** | 只读前置 `P1–P4`(**零改动**) | 拿到"scope 名 ↔ uid ↔ 端口"三者的可读性结论 | 无(只读) |
|
|||
|
|
| **S1** | **(可能需要)实例账本**:把"在线实例"持久化(⚠️ 归属判定:跟着用户走 ⇒ **放实例 home**;本机运维用 ⇒ **Worker 本地库**;⛔ 只有 Manager 能写归属/租约) | 重启后能列出"重启前在跑的实例"清单(含 uid / 端口) | 删账本文件 |
|
|||
|
|
| **S2** | **接管取代清空**:启动扫描 → 回填 map(⛔ 不 stop、⛔ 不 spawn) | `E1`:`restart dshs` 后 scope 数**不变** | 还原 `cleanAllStaleScopes()` 调用 |
|
|||
|
|
| **S3** | **节流**:`REHYDRATE_CONCURRENCY` / `REHYDRATE_STAGGER_MS` | `E2`:4 实例 ⇒ 日志时间差 ≥ 阈值、⛔ 无同秒多条 | 阈值调回 0 ⇒ 回 S2 行为 |
|
|||
|
|
| **S4** | **单实例保证复验**(⛔ 不回归档案 30) | `E4`:同 uid 恒 1 个 scope;`E5`:无实例时不自起 | 同 S2 |
|
|||
|
|
| **S5** | 观测 + 探针新项 | 探针出一项 PASS/FAIL(⛔ 阈值不许是脚本魔数) | 还原探针 |
|
|||
|
|
| **S6** | 真机验收(`E1`–`E6`)+ 零回归(`E7`)+ 收口 | `E6`:内存不跌穿、无 OOM;三件套同值 | 产物回滚点 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §5 真机验收怎么造条件的(⚠️ 关键)
|
|||
|
|
|
|||
|
|
⚠️ **不能只靠"等用户访问"**(本线已因"无 reconciler ⇒ 不自回"卡过一次)。验收必须:
|
|||
|
|
|
|||
|
|
1. **先主动起 2–4 个实例**(用 R4 临时 session 走真实 `enter`,或用本机多实例)⇒ 记录 scope 名 / uid / 端口;
|
|||
|
|
2. `systemctl restart dshs`;
|
|||
|
|
3. **立刻**(≤ 5 s)与 **+60 s** 两个时刻各取一次 `list-units 'dsh-*.scope'`;
|
|||
|
|
4. 断言 `E1`(数不变)/`E2`(逐条时间差)/`E3`(端口不变、⛔ 无新 scope)/`E4`(同 uid = 1)。
|
|||
|
|
|
|||
|
|
⚠️ **已知副作用**:`restart dshs` **会收掉用户实例 scope**(本线既有实测)⇒ 本单要**故意利用**这个副作用做 E1/E5 的对照(重启前有实例 → 应被接管;重启前无实例 → 应保持 0)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §6 回滚(两层)
|
|||
|
|
|
|||
|
|
1. **代码级**:还原 `orchestrator.ts` 的启动流程为原 `cleanAllStaleScopes()` 调用 → `tsc` → scp `lib/` → `restart dshs` ⇒ 回到"统一清掉防孤儿"(档案 30 行为)。
|
|||
|
|
2. **产物级**:scp 回滚点 `/opt/dsh/backups/seq25-<ts>/lib/supervisor/orchestrator.js`。
|
|||
|
|
🔴 ⛔ **回滚路径里不许出现 `RELAY_FAILOVER_COOLDOWN_MS=0`**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §7 回报格式(执行会话**必须**回填)
|
|||
|
|
|
|||
|
|
```markdown
|
|||
|
|
### 8.x 序 ㉕ 执行棒回报(YYYY-MM-DD HH:MM–HH:MM)
|
|||
|
|
|
|||
|
|
#### ① S0–S6 逐条(命令 + 原文输出 + 判定)
|
|||
|
|
|
|||
|
|
#### ② 主判据 E1:重启后实例数不变
|
|||
|
|
- 重启前 scope = ? | +5 s = ? | +60 s = ? | 端口是否全同 = ?
|
|||
|
|
|
|||
|
|
#### ③ 先红后绿(原文级)
|
|||
|
|
- (先跑"仍清空"的旧行为 ⇒ 哪条断言红;改成接管 ⇒ 绿)
|
|||
|
|
|
|||
|
|
#### ④ 零回归三件套(均带 `--table`)
|
|||
|
|
|
|||
|
|
#### ⑤ 边界自证
|
|||
|
|
⛔ 未改生产值 / ⛔ 未改 bwrap 参数 / ⛔ 未删 `cleanStaleScopes(uid)` / 🔴 `COOLDOWN_MS=0` = ? / ⛔ 未 commit·未 push
|
|||
|
|
|
|||
|
|
#### ⑥ 指纹
|
|||
|
|
参数表 = ?(前值 `42238175d84319ada99afa56d583f9db`)|orchestrator.ts md5 = ?
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §8 回头条件(**一出现必须回头**)
|
|||
|
|
|
|||
|
|
1. `P2` 拿不到 scope 的端口 ⇒ **停下报告**("接管"不可行,需先出"实例账本"方案)。
|
|||
|
|
2. 接管导致**双实例共 profile**(档案 30 故障复现)⇒ 立刻回滚 + 报告。
|
|||
|
|
3. 需要**超出 §3.1 文件集** ⇒ 停下报告(R7)。
|
|||
|
|
4. 要**改 bwrap 参数 / 改配额 / 动生产值** ⇒ 停下报告。
|
|||
|
|
5. `E2` 跑不出节流(同秒多条)⇒ 停下报告,⛔ 不许放宽判据。
|
|||
|
|
6. 零回归三件套任一退化 ⇒ 停下报告。
|
|||
|
|
7. 🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §9 未验证项
|
|||
|
|
|
|||
|
|
| # | 项 | 状态 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 1 | 重启后能否从 OS 层读回实例端口 | ⚠️ 待 `P2` 取证 |
|
|||
|
|
| 2 | 是否需要独立"实例账本" | ⚠️ 待 `P4` 取证 |
|
|||
|
|
| 3 | 接管 4 个实例的内存与启动耗时 | ⚠️ 待 S6 实测 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §10 指纹与状态
|
|||
|
|
|
|||
|
|
| 项 | 值 |
|
|||
|
|
|---|---|
|
|||
|
|
| 本单 §7 之前正文前缀指纹 | **`d9d48121e68faa00e1da17ad8fea36ad`**(口径 = `sed '/^## §7 /,$d' 交接单_实例逐步拉起_20260917.md \| md5sum`) |
|
|||
|
|
| 参数表指纹(立单时) | **`42238175d84319ada99afa56d583f9db`** |
|
|||
|
|
| 代码仓 HEAD(立单时) | `04776af` |
|
|||
|
|
| 本单全文件 md5(立单时) | `2c53a018c2d9bfc2c422d0758413a485`(161 行) |
|
|||
|
|
| 本单状态 | **已执行(2026-09-17 22:05–22:2x)** —— ✅ **认领逻辑已落地并在真机夹具上实测工作**;🔴 **但主判据 E1 在"真实实例"上未达成**,根因 = 单子未识别的**第二条杀实例路径(退出时 `teardown()`)**。详见 §11。⏸ 本序**未登记续棒**(需你拍板是否扩大文件集)。 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## §11 序 ㉕ 执行棒回报(2026-09-17 22:05–22:2x)
|
|||
|
|
|
|||
|
|
### 8.1 六段回报
|
|||
|
|
|
|||
|
|
#### ① S0–S6 逐条
|
|||
|
|
|
|||
|
|
| 步 | 结果 | 命令 / 证据 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **S0** 只读前置 P1–P4 | ✅ | **P1** `dsh-100002-ef8d1d12.scope` ⇒ scope 名第 2 段 = **uid**(`100002`)✅。**P2** ⚠️ `systemctl show -p MainPID --value` **为空**,但 `-p Description` **携带完整 argv**(末尾 `--port 21000`、`--reuid 100002`、`--chdir …/users/4092b965-…/ws`)⇒ **端口可读回,通道是 `Description` 不是 `/proc`**;且解析可用 **三道自洽校验**(`--profile` 必有、`--chdir` 含 `/users/<uuid>/`、`--reuid` == scope 名 uid)⇒ **§8 回头条件 1 未命中**。**P3** T0 现场:47 `dsh-*.scope` = **0**、106 = **1**(实例在 106)✅ 与序㉓ 同。**P4** 🔴 `dsh_instances` 表**全仓零调用方**(`src/db/pg.ts` 定义了 `upsertInstance/getInstanceByUser/…`,但 `grep -rn` 在 `src/db/` 外 **0 命中**)= **k8s 形态预留的死代码** ⇒ **无现成账本** ⇒ 采纳「**以 OS 层 scope 为唯一权威**」,⛔ 不新建账本(新建 = 引入双写与陈旧风险,R11)。 |
|
|||
|
|
| **S1** 实例账本 | ✅ **判为不需要** | 由 P4 得出:所需四要素(uid / userId / port / folder)**全部可从 scope 名 + Description 解出** ⇒ 加账本是净负担。 |
|
|||
|
|
| **S2** 接管取代清空 | ✅ 代码已落 / 🔴 真机**部分**达成 | 构造函数由 `cleanAllStaleScopes()` 改为 `rehydrateAdoptedScopes()`;新增 `scanExistingScopes`/`scopeDescription`/`adoptOne`/`probeAdopted`/`stopUnit`;`cleanAllStaleScopes()` **保留**(非 account 回退 + 回滚路径)。 |
|
|||
|
|
| **S3** 节流 | ✅ | 逐条经 `setTimeout` 排队,间隔 `DSHS_REHYDRATE_STAGGER_MS`(缺省 500 ms);真机日志逐条时间差 ≥ 阈值,⛔ 无同秒多条。 |
|
|||
|
|
| **S4** 单实例保证复验 | ✅ | 同 uid 多 scope ⇒ `decideScopeAction` 判 `dup-uid` ⇒ **全部停**;`cleanStaleScopes(uid)`(spawn 前清同 uid)**一字未动** ⇒ 档案 30 未回归。 |
|
|||
|
|
| **S5** 观测 + 探针新项 | ✅ | 参数表新增 `DSHS_REHYDRATE_STAGGER_MS` / `DSHS_REHYDRATE_PROBE_MS` + §6 `OBS-18`;探针新增 `OBS-18` + `--rehydrate-fixture`(三态夹具实证见 ③)。 |
|
|||
|
|
| **S6** 真机验收 + 零回归 | ⚠️ **部分** | 零回归 ✅(见 ④);`E1` **夹具 ✅ / 真实实例 ❌**(见 ②)。 |
|
|||
|
|
|
|||
|
|
#### ② 主判据 E1:重启后实例数不变
|
|||
|
|
|
|||
|
|
- **夹具 scope(真实 systemd scope + 真实 argv 形态 + 真监听 21500)**:重启前 `dsh-100002-abcdef01.scope` = 1 ⇒ `restart dshs-worker` ⇒ **+6 s 仍 `is-active=active`、21500 仍在听、count = 1** ⇒ 🎉 **E1 ✅**(**证明 systemd 不会带走 scope、认领逻辑确实工作**)。
|
|||
|
|
- **真实实例(`dsh-100002-ef8d1d12.scope`)**:重启前 count = **1** ⇒ `restart dshs-worker` ⇒ **+5 s count = 0** ⇒ 🔴 **E1 ❌**。
|
|||
|
|
- 🔑 **根因(本次最重要的发现)**:`journalctl -u dshs-worker` 同一秒内 `Stopping … → Stopped …` + 新进程打印 `[rehydrate] 无既有实例 scope ⇒ 不动作` ⇒ **scope 在新进程扫描之前就没了**。真凶 = **`src/supervisor/orchestrator.ts:604` `teardown()`**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
async teardown(): Promise<void> {
|
|||
|
|
if (this.reapTimer !== undefined) clearInterval(this.reapTimer)
|
|||
|
|
for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
由 **`src/worker/agent.ts:569`**(SIGTERM 关闭链)调用 ⇒ **退出路径主动停掉所有在册实例**。「原有实例」**不在** `mains` ⇒ `teardown` 碰不到它 ⇒ 它能活;**真实实例在** `mains` ⇒ 被杀。
|
|||
|
|
- 🔴 **结论:本序只改"启动时清空"不足** —— 杀实例的**实际生效路径**是**退出时的 `teardown()`**(**同机** `LocalSpawner.teardown` + **跨机** `RemoteSpawner.teardown`,后者正是序⑲ 观测到"`restart dshs` 会收掉 `dsh-*.scope`"的原因)。
|
|||
|
|
- ⛔ **未动 `teardown()`** —— 它需要改 `src/supervisor/remote-spawner.ts` / `leased-spawner.ts`(**不在 §3.1 在册文件集**)⇒ 按 **§8 回头条件 3**(R7)**停下报告**,见文末待拍板项。
|
|||
|
|
|
|||
|
|
#### ③ 先红后绿(原文级)
|
|||
|
|
|
|||
|
|
- **单测面**(`test/orchestrator-rehydrate.test.mjs`,**刻意不进 `npm test`** 以保基线可比):批量破坏三处 ⇒ **破坏后 3 红(正是预期的 R7 uid 交叉校验 / R13 同 uid 多 scope / R16 构造函数接线)**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
not ok 5 - R7 ⛔ uid 交叉校验不符 ⇒ 拒
|
|||
|
|
not ok 11 - R13 ⛔ 同 uid 多 scope ⇒ 全部停
|
|||
|
|
not ok 14 - R16 接线:构造函数必须调 rehydrateAdoptedScopes
|
|||
|
|
# pass 15 / # fail 3
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
还原后 **`# pass 18 / # fail 0`**,且 `orchestrator.ts` md5 **与破坏前逐字一致**(`86f1f05f878d706cdc656e3d769800e5`)⇒ 断言链**有效且可自证**。
|
|||
|
|
- **探针面(`OBS-18` 三态夹具,离线零副作用)**:旧行为原文(无 `[rehydrate]` 行)⇒ **FAIL**;合法 summary ⇒ **PASS**;`adopted+stopped ≠ scanned` ⇒ **FAIL 并点名**。
|
|||
|
|
|
|||
|
|
#### ④ 零回归三件套(均带 `--table`)
|
|||
|
|
|
|||
|
|
| 件 | 实测 | 基线 | 判定 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| `npm test`(Node 22) | **176 / 175 pass / 0 fail / 1 skip** + `verify-inject` rc=0 | 176/175/0/1 | ✅ **逐字一致** |
|
|||
|
|
| `--scene all --table` | **12 PASS / 0 SKIP / 0 FAIL** | 12P/0S/0F | ✅ 一致 |
|
|||
|
|
| `overlay-probe --table` | **17 PASS / 0 FAIL / 1 SKIP**(18 项 = 基线 17 + 本序新增 `OBS-18`;`OBS-09` SKIP = "无实例在册"的**已知环境态**,⛔ 非回归) | 17P/0F/0S | ✅ 等价(`OBS-18` 由"不存在"→ **PASS**)|
|
|||
|
|
|
|||
|
|
⚠️ 中途一次 `overlay-probe` 出现 `OBS-08 FAIL`(端点表 2 条 / 离线 1 条)⇒ **重跑即回绿**(1 条 / 离线 0 条)⇒ 判为 `--scene all` **演练后的暂态**,⛔ 非本序引入(其数据源是 relay `/status`,与 `orchestrator` 无耦合)。✅ **如实留档**。
|
|||
|
|
|
|||
|
|
#### ⑤ 边界自证
|
|||
|
|
|
|||
|
|
⛔ 未改任何生产值(`DSHS_REHYDRATE_*` **只读进程环境取代码缺省**,47/106 的 `dshs.env` / `dshs-worker.env` **一字未写**)· ⛔ 未改 bwrap 参数 · ⛔ **未删** `cleanStaleScopes(uid)` · ⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst · 🔴 `COOLDOWN_MS=0` 计数 = **0** · ⛔ 未新增公网口 / 未改 nft·nginx · ⛔ **未 commit / 未 push** · ⛔ 认领路径**零凭据落盘**(单测 `R19` 钉住)· ⛔ 未改 `04-调整方案/**`。
|
|||
|
|
|
|||
|
|
**部署**:`lib/supervisor/orchestrator.js` scp 到 47 `/opt/dshs/lib` + 106 `/opt/dshs-cluster/lib`,两处 md5 全同 = **`5bfb15e00b592c293548ae17b639399b`**;回滚点 = **两机 `/opt/dsh/backups/seq25-20260917-220556/lib/supervisor/orchestrator.js`**(旧值 `fad7926494578258f86a91eac613a204`);已 `restart` 47 `dshs` + 106 `dshs-worker`(门户 200)。
|
|||
|
|
|
|||
|
|
#### ⑥ 指纹
|
|||
|
|
|
|||
|
|
- **本单 §7 之前前缀指纹** = `d9d48121e68faa00e1da17ad8fea36ad` —— ✅ **开工时校验通过**(依据未被改动)。⚠️ 回填 §11 之后**仍为同值**(回填全在 §7 之后,⛔ 不触碰 §7 之前正文)。
|
|||
|
|
- **参数表指纹** = `54c854a5c09b561ea4920c44d8545cfe` → **`93e38d1a57fbcd3126d2122150a2e838`**(新增 2 键 + §6 `OBS-18`;口径 = `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum`)。
|
|||
|
|
- **`src/supervisor/orchestrator.ts` md5** = `86f1f05f878d706cdc656e3d769800e5`(先红后绿还原校验后值)。
|
|||
|
|
- **本单全文件 md5(回填 §11 后)** = `b73f2ec18fcf52055c4a24c3fcbdeb5b`(**242 行**)。
|
|||
|
|
- **代码仓 HEAD** = `04776af`(⛔ 未 commit / 未 push)。
|
|||
|
|
- **本序新增/改动文件**:`src/supervisor/orchestrator.ts`(✏️ 在册)|`scripts/overlay-probe.cjs`(✏️ 在册)|`参数表_覆盖网络_20260917.md`(✏️ 在册)|`test/orchestrator-rehydrate.test.mjs`(➕ 在册可新增)|⚠️ **无越界文件**。
|
|||
|
|
|
|||
|
|
### 8.2 本序沉淀的四条可复用事实
|
|||
|
|
|
|||
|
|
1. 🔴 **"杀实例"有两条独立路径,只改一条等于没改** —— 启动路径 `cleanAllStaleScopes()` 与**退出路径 `teardown()`**。判据:**用一个"不在 `mains` 里"的夹具 scope 做对照** —— 它活着就证明 systemd 无辜、真凶在应用层。(本次一步定位,省下大量猜测。)
|
|||
|
|
2. 🔴 **`--scope` 的端口不在 `/proc`,在 `Description`** —— `systemctl show -p Description` 携带 `systemd-run` 记录的完整 argv。⛔ 别再按"读 `/proc/<MainPID>/cmdline`"设计。
|
|||
|
|
3. 🔴 **"认领"不能把实例写回 `mains`** —— 那会让 `enter` 的复用分支去等一个**永不会出现**的 `launchToken`(`right dsh web` 只在启动时打印一次、不落盘、运行期取不出)⇒ 用户被 **503** 挡住 = **R11 净变差**。⇒ 正确的认领语义 = **只登记 + 探活**,实例由用户访问时的 `cleanStaleScopes(uid)` **自然替换**。⛔ 也**不许**把 token 落盘换"无感接管"(安全维度净变差,R11)。
|
|||
|
|
4. ⚠️ **探针里 `ssh()` 的形参作用域是个坑** —— `sshPort` 定义在外层块,`OBS-18` 在另一个块里取不到 ⇒ `sshPort is not defined` 被 `catch` 吞成 `text=''` ⇒ 表现成"读不到日志"。⇒ 失败判别器**必须能区分"读不到"与"确无该行"**(本序已落:0 行时一并打出 `journalctl` 原文字节数)。
|
|||
|
|
|
|||
|
|
### 8.3 待拍板(**唯一一项**):E1 的缺口要不要打通
|
|||
|
|
|
|||
|
|
**问题**:`teardown()`(退出时主动停掉所有在册实例)是"重启后实例不被清掉"的**实际阻碍**,而它跨了三个文件 —— 其中 **两个不在本单 §3.1 在册文件集**(`src/supervisor/remote-spawner.ts`、`src/supervisor/leased-spawner.ts`)。按 §8 回头条件 3(R7)我**已停下、未动手**。
|
|||
|
|
|
|||
|
|
**候选 A —— 只改 `LocalSpawner.teardown()`(同机那一处)**
|
|||
|
|
· 优点:文件集最小,1 个在册文件即可;同机形态(纯本机部署场景)能达成 E1。
|
|||
|
|
· 缺点:**Manager 重启仍会跨机收掉 Worker 实例**(Manager 侧 `LeasedSpawner → RemoteSpawner.teardown()` 会向 Worker 下发停止)⇒ **你的诉求「Manager 重启后逐步拉起」依然不成立** ⇒ 基本等于白改。
|
|||
|
|
|
|||
|
|
**候选 B —— 三处一起改:`LocalSpawner.teardown()` + `RemoteSpawner.teardown()` + `LeasedSpawner.teardown()`**
|
|||
|
|
· 优点:**唯一能真正达成用户诉求**的路径;语义自洽 —— 退出进程把存活实例留给下一个进程,回收责任完全交给「启动认领 + TCP 探活判孤儿」这一套(本序已实测工作)。
|
|||
|
|
· 缺点:**超出本单 §3.1 文件集,需要你授权扩范围**;影响面 = **所有实例的生命周期语义**(实例不再随进程退出而回收)⇒ 必须接受"进程长期不重启时,实例由 idle-reap / 用户访问替换来收"这个前提。
|
|||
|
|
|
|||
|
|
**候选 C —— 用 env 开关把"退出不停实例"做成可灰度**
|
|||
|
|
· 优点:能先在一台机打开、观察后再推开。
|
|||
|
|
· 缺点:跨机那一处**照样要改**(开关落地点就在 `teardown` 里)⇒ 文件集不比 B 小;而"默认关"等于不生效、"默认开"等于 B ⇒ **没有增量价值,只多一个开关**,且多一个"忘开/忘关"的失效面。
|
|||
|
|
|
|||
|
|
**我的倾向 = B**(A 不达成目标,C 无增量价值)。⏸ 在你拍板前,本序**不登记续棒**,也不动那三个文件。
|
|||
|
|
|
|||
|
|
|