Files
dsh_shenxian/dsh-server-docs/交接单/覆盖网络-序25-实例逐步拉起.md
T
admin 09ce76f3af feat(overlay): 内容块级寻址 + 实例逐步拉起 + 骨干选路 + 组密钥加密(序24–㉛ 累积同步)
代码
- 内容分发块级寻址:新增 src/net/relay/content/{chunker,store,runtime,source,peer,crypto}.ts
- 组密钥(C 档)确定性加密:AES-256-GCM,块 id β′ = sha256(密文) 前 32 hex;双 epoch 过渡窗口
- 实例生命周期:三处 teardown() 不再杀实例(local/remote/leased-spawner);启动认领 + TCP 探活判孤儿
- 骨干选路:jitter 选路 + endpoint-target;relay client/server/wire/identity/directory/rendezvous/switcher 调整
- 工作台 src/web/server.ts、src/worker/relay-tunnel.ts 装配与候选链观测

脚本与测试
- scripts/overlay-{probe,keyring,jitter}.cjs 更新
- 探针新增 OBS-21(每连接候选数)/ OBS-22(teardown 静态守卫 + 认领面)/ OBS-23(组密钥加密)
- 新增 test/{orchestrator-teardown,orchestrator-rehydrate,overlay-content,overlay-jitter}.test.mjs;relay 两例更新

文档
- 新增交接单:覆盖网络-序24-内容分发块级寻址 / 序25-实例逐步拉起 / 序26-骨干稳定选路与加密
- INDEX.md、交接单/README.md、skills/dsh-auto-handoff-chain/SKILL.md 同步

验收(零回归,2026-09-18 08:0x 复核)
- npm test           201 tests / 200 pass / 0 fail / 1 skipped
- overlay-failover-drill --scene all --table   12 PASS / 0 SKIP / 0 FAIL
- overlay-probe --table                        23 PASS / 0 SKIP / 0 FAIL (rc=0)
2026-09-18 08:08:51 +08:00

10 KiB
Raw Blame History

交接单 · 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 回报格式(执行会话必须回填)

### 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 行)
本单状态 待执行 ⇒ 交序 ㉕ 执行棒