起因:用户 2026-09-17 明确「所有文档都要同步,都放在开发仓库 docs 对应文件夹下」。
判据:工作区根 *.md 中在仓库(git ls-files --quotepath=false)搜不到的那些。
入仓 33 份(一律复制,工作区根原件保留不动,避免引用断链):
- 04-调整方案/113–128(16 份 · 原子占号后落盘):覆盖网络 传输方案取舍 / 应用场景与待完善清单 /
插件化vs改内核 / 问题逐条推演 / 参数表与观测口径;会合中继拆分取证与改造方案;
集群化改造方案 Manager-Worker;跨节点迁移与节点自举;项目代码分层范式与迭代风险评估;
搬运与共享重建方案 guest w47→w106;方案规划方法提炼;文档无效信息审计报告;
会话接续机制复盘与修复;会话接续规范;dsh 客户端化部署方案;dsh 桌面客户端开发方案
- 交接单/archive/交接单-已完成/T09–T21(13 份 · 覆盖网络线已完成单归档)
- ops/(2 份运行态指针:接续入口 / 接续包 · 覆盖网络线)
- archive/(2 份临时与内部简报)
已排除(无需重复入仓):覆盖网络线 10 份方案正文已入档案 103–112(文件名不同)。
登记:INDEX.md §二 新增 04-113–128 共 16 行 + §四 追加 T09–T21 说明 + 机器摘要行刷新
(⛔ 未跑 docs-index-stats.py --write:该脚本会按 \r\n 归一化全文件行尾,故改为字节级单行替换);
README.md 追加 1 条入仓指针;docs-manifest.json 复跑 scripts/docs-manifest.py 刷新。
验收:工作区根 47 份 .md —— 同名已入仓 8 / 本次内容一致 29 / 已知改名映射 10 / 未入仓 0;
git status 待提交清单只含本次新增与登记 3 件(未涉 src/ 与 relay 代码面)。
15 KiB
跨节点迁移 & 节点自举 —— 完整流程方案
立稿 2026-09-16 00:3x | 起因:09-15 的 guest 迁移走的是手工临时方案(AI 人肉做了 6 件事)。 用户要求:「该谁处理就让对应功能处理,不要做这种临时方案;迁移应该谁发起、迁移完成应该如何处理,整个完整的流程」。 本文回答三件事:谁处理(责任矩阵)→ 完整流程(目标态)→ 要开发什么(缺口清单)。
0. 一句话
把「人手搬 6 件事」收敛成 两个平台功能: ① 节点自举(新节点加入时执行一次,含旧角色清理)② 迁移编排(admin 发起 → 平台搬数据 → 收尾归档)。
1. 这次实际做了什么(对照:本该谁做)
| # | 我手工做的(临时方案) | 本该谁做 | 平台现状 |
|---|---|---|---|
| 1 | useradd -u 100002 … 建 OS 账号 |
worker 自己(dsh-provision.path 触发) |
⚠️ 单元只在 47;且 uid 取自 SQLite ⇒ 集群下失效 |
| 2 | npm i -g @deepseek-ai/[email protected] |
节点自举(一次) | ❌ 无此功能 |
| 3 | 传 /usr/local/dsh-runtime(python/jq/rg) |
节点自举(一次) | ❌ 无此功能 |
| 4 | chmod 711 /var/lib/dshs/users |
节点自举(一次) | ❌ 无此功能(且漏了就必崩) |
| 5 | 搬 203 MB 用户数据 + 依赖 | 平台迁移编排 | ❌ 迁移路由注释原文「数据不搬家」 |
| 6 | 源目录打包归档 → 删源 | 平台迁移收尾 | ❌ 无此功能 |
| 7 | ffmpeg 我从 47 搬(错) | 节点自举:直接 dnf install |
✅ 106 内网源 43 MB/s,几十秒装完(本轮已改) |
2. 责任矩阵(谁处理什么)
| 环节 | 责任方 | 触发时机 | 幂等要求 |
|---|---|---|---|
| 节点自举:dsh 主程序 / dsh-runtime / 目录权限 / provisioner 单元 / egress | 平台(运维脚本) | 节点加入集群时一次 | ✅ 必须可重跑 |
| 节点角色切换:清理旧角色服务(如 106 上的旧控制面) | 平台(运维脚本) | 角色变更时 | ✅ |
用户目录出现 ⇒ 建 OS 账号 + chown |
worker 自己(systemd path 单元) | 目录出现时 | ✅ 已有(待铺到各 worker) |
| 迁移发起 | admin(人) | 人为决策 | — |
| 迁移执行(drain → 数据同步 → 目标机拉起 → 归属) | 平台 | 发起后 | ✅ |
| 迁移收尾(源归档 / 清理 / 审计 / 通知) | 平台 | 迁移成功后 | ✅ |
原则:凡是"节点级别、一次性的"归自举;凡是"跟着用户走、每次迁移都发生"的归迁移编排。 ⛔ 两者都不该由 AI 或人手临时执行。
3. 完整流程(目标态)
A. 节点加入(自举,一次)
1. 运维执行 bootstrap-worker(幂等)
├─ 装 dsh 主程序(同版本,npm 或离线包)
├─ 装 dsh-runtime(python3.12 / jq / rg / ffmpeg —— 优先 dnf/内网源,而非从别处搬)
├─ 目录与权限(/var/lib/dshs 711、users 711、secret.key)
├─ 铺 provisioner 单元(dsh-provision.path + .service,脚本参数化到本机路径)
└─ 清理旧角色残留(如该机曾跑控制面 ⇒ 停用并禁用)
2. 自检 doctor:四件套 + provisioner + 端口 + 数据根,全绿才允许被调度
3. 注册进 dsh_hosts(capacityMb / endpoint)
B. 用户迁移(admin 发起 → 平台执行 → 收尾)
1. 【发起】admin 在「用户管理」选用户 → 选目标节点 → 点"迁移"
(入口已有 API:POST /api/admin/users/:id/dsh/migrate;门户缺 UI 入口)
2. 【前置校验】平台检查目标节点:status=up、capacity 足够、doctor 全绿
不就绪 ⇒ 拒绝并列出缺项(不让人手工补)
3. 【执行】
a. drain:停源机实例(优雅停机,会话落盘)
b. 同步数据:源 → 目标(平台要负责;见 §4 D3)
c. 目标机承租约拉起(epoch+1)
d. 归属原子更新
4. 【收尾】源数据按策略归档(默认留 N 天)→ 清理源目录 → audit 记录 → 通知用户/管理员
5. 【回滚】归档可解压恢复;迁移可反向再执行一次
⚠️ 顺序不可换:先停源、再在目标机拉起(否则两机同 home ⇒ 双写)。这一点现有 API 已正确实现。
4. 缺口清单(要开发的功能)
| # | 功能 | 解决的问题 | 现状 | 优先级 |
|---|---|---|---|---|
| D1 | bootstrap-worker(节点自举脚本,幂等) |
新节点上线全靠手工(本次 6 件事) | ✅ 已交付:scripts/bootstrap-worker.sh(47 / 106 自检全绿) |
P0 |
| D2 | 建号自动化(uid 由 Manager 投递,agent 在 spawn 前确保账号) | 新用户落 worker 无人建号;uid 依赖 SQLite 在集群下失效 | ✅ 已交付:src/worker/agent.ts 的 ensureOsAccount(端到端已验证) |
P0 |
| D3 | 迁移含数据同步 | 路由注释明说"数据不搬家" ⇒ 现在必须人工搬 | ❌ 无 | P0 |
| D4 | 迁移收尾自动化(归档 + 清理 + 通知) | 现在靠人手打包删源 | ❌ 无 | P1 |
| D5 | 门户「迁移」入口 + 节点状态面板 | admin 现在只能调 API | ⚠️ API 有 | P1 |
| D6 | 节点角色切换清理 | 106 的旧控制面已手工执行并验证(见 §5)⇒ 该流程本身仍未自动化 | ❌ 无 | P1 |
D2 的技术债已确认:uid-for-user 是 user?.uid ?? hashUid(id, baseUid),
而 hashUid = baseUid + hash % 100000 ⇒ 实测 106 上算 guest 得 184656,与真实 uid 100002 不符 ⇒ uid 只能从 PG 读(47 之所以能跑是因为它读 SQLite)。
⚠️ 实测:106 需要能读 PG —— 但 47 的 PG 只监听 127.0.0.1:15432,106 无法直连。
⇒ D2 的正解不是"给脚本换个 --db 参数",而是 Manager 在创建用户时把 uid 下发给 worker(走已有 agent 通道),或让 worker 通过隧道读 PG。这是设计问题,应随 D2 一起定。
D1 的补充判据(实测 dshs doctor 的覆盖缺口):106 上跑 doctor 只报了 node/cgroup/bwrap/setpriv/systemd-run/nft/dsh/降权/dataRoot/DB —— 不覆盖本次真正踩到的四项:/var/lib/dshs/users 权限、dsh-runtime 缺失、provisioner 单元缺失、旧角色残留。
⇒ D1 的自检清单必须把这四项补进去(否则"doctor 全绿"仍会起不来)。
5. ✅ 已执行:106 上的旧控制面已停用、禁用并删除(用户 2026-09-16 00:31 定)
决策过程:先定为「不处理、隔离即可」→ 人工复核隔离面后,用户改判为「停用并禁用 + 删除数据」。
已执行(2026-09-16 00:3x):
systemctl stop+disable dsh-users-platform(同时移除multi-user.target.wants链接)- 删除数据与程序:
/var/lib/dsh-users-platform/(176 K,含 2 个用户目录 + state)·/root/dsh-users-platform/(144 M 平台程序)·/etc/dsh-users-platform.env·/etc/systemd/system/dsh-users-platform.service systemctl daemon-reload
删除前备份(留退路):/opt/dsh/backups/legacy-106-platform-20260916-002931.tar.gz(6.5 K:数据根 + env + 单元文件)。
验证(全部通过):单元文件 / 服务查询 / 残留进程 / 数据根 / 程序目录 / env 文件 全部不存在;3080 端口已释放;
新架构未受影响 —— dsh-100002-43771a07.scope(guest)与 dsh-100008-3099bb14.scope 仍 running,dshs-worker active。
删除前核销的隔离面(留档):数据根 / 用户目录 / OS 账号 / 端口 / systemd 单元五面均不重叠;
唯一残留耦合是"两者共用 BASE_UID=100000 的同一 uid 空间"(理论上可能撞 uid)—— 现已随删除彻底消除。
6. 立即能做的(不依赖 D1–D6 开发)
- ffmpeg/ffprobe/jq/rg:已改为 106 内网
dnf install(本轮已完成,几十秒,不再是遗留项)。 - provisioner 铺到 106:⚠️ 不是"给脚本换个
--db参数"就行 —— uid 必须由 Manager 下发(见 §4 D2 说明:哈希兜底会算错,PG 又连不上)。 - 其余(D1 / D3 / D4 / D5 / D6)建议按 §4 优先级排期开发;D6 的手工执行已完成(106 旧控制面已删),只是那条流程仍未自动化。
7. 实施记录(2026-09-16 00:3x–01:0x)
7.1 D2 建号自动化 —— ✅ 已上线
改动:src/worker/agent.ts
- 新增
ensureOsAccount(config, userId, uid):幂等,规格与provision-new-users.sh一致(dsh-<短id>/-M/ nologin / 同 uid),并对齐用户目录属主; - 在
POST /launch的 spawn 之前调用 —— OS 账号缺席时 bwrap 里的setpriv --reuid会直接失败,且不报权限错,只表现为"实例起不来"; - 护栏:参数校验先行(UUID 形状 /
uid ≥ baseUid)、uid 被他人占用则 fail-loud、非 Linux 静默返回;不接受调用方传命令或路径 ⇒ 不违反 agent 的「最小接口」纪律; - 测试:
test/worker-provision.test.mjs(5 例)已并入npm run verify链,npm run verify全绿; - 部署:47 送源码 + 服务器
npm run build;106 送编译产物;两边restart dshs-worker(实例是独立 scope,不受影响); - 端到端验证:以测试用户(uid 199999)调
/launch⇒ 账号被自动创建dsh-11111111222233334444✓(实例本身 crashed 属预期:测试用户无工作区);验证后账号/scope 已清理干净。
7.2 D1 节点自举 —— ✅ 已上线
交付:scripts/bootstrap-worker.sh(幂等;--check 只读自检;--prune-legacy 清旧角色)。
覆盖链路:前置 → dsh 主程序(版本校验 + PATH 契约)→ runtime(jq/rg/ffmpeg/ffprobe 走本机包管理,不从别处搬)→ python 路径契约 → 数据根权限 711(漏了必崩)→ 旧角色残留 → 自检汇总(含 dshs doctor 漏掉的四项)。
实测:47 与 106 均 全绿、退出码 0。
7.3 🔴 事故与教训(我造成的,已修复)
事故:删除 106 的旧控制面目录 /root/dsh-users-platform 后,106 的 worker 起不来了(Cannot find package 'fastify')。
根因:/opt/dshs-cluster/node_modules 与 /usr/local/dshs-cluster/node_modules 都是符号链接 → /root/dsh-users-platform/node_modules;删掉被指向的目录 ⇒ 依赖链断裂。
我的失误:删除前只检查了「有没有 systemd 单元引用它」,没检查文件系统符号链接引用。
修复:从 47 取 package.json + package-lock.json → 106 上 npm ci --omit=dev --ignore-scripts 重建(109 包)⇒ worker 恢复 active、19000 监听正常。
⚠️ 注意:npm ci 会触发 prepare(npm run build),而该目录没有源码 ⇒ 必须加 --ignore-scripts。
教训(已入纪律):删任何目录前,除 systemd 单元外,必须查符号链接引用:
find / -lname '<path>*' -not -path '/proc/*' 2>/dev/null
7.4 未完成 / 待办
- D3(迁移含数据同步):未开始 —— 这是"迁移不再靠人肉搬数据"的关键一条,建议下一步就做。
- 代码尚未 commit:
src/worker/agent.ts、package.json、scripts/bootstrap-worker.sh、test/worker-provision.test.mjs都在代码仓工作树里(按纪律未擅自提交)。
8. 2026-09-16 06:5x 排查:旧控制面彻底清除 + admin 502 根因
8.1 旧控制面彻底清除(用户要求「还是删除」)
⚠️ 关键差别:这次先把依赖搬离再删 —— 昨晚就是栽在这一步。
- 事实:
/opt/dshs-cluster/node_modules与/usr/local/dshs-cluster/node_modules原本都是符号链接 →/root/dsh-users-platform/node_modules; - 做法:
mv实体 →/opt/dshs-cluster/node_modules(109 包)→/usr/local侧改指新位置 → 重启并验证 worker active → 按 §7.3 的新纪律find /opt /usr /var /etc /root -lname "<path>*"确认无引用 → 删除/root/dsh-users-platform; - 结果:旧控制面残留 0、
dsh-users*单元 0、3080 无监听、worker active、19000 正常。
8.2 admin 访问 502 的根因 = 冷启动时序(非崩溃、非超时)
证据链(全部实测):
- nginx 在 06:53:40 记
upstream prematurely closed connection ... request: "GET /" ... host: "admin.alotbuy.com"(即用户看到的那次 502); - Manager(
dshs)日志里该请求只有incoming request、没有request completed⇒ 连接被上游主动断开,不是超时(该站点proxy_read_timeout= 3600s); - 06:53:28 出现
GET /wake.html?next=https://admin.alotbuy.com/⇒ 触发了唤醒流程,说明当时 admin 实例处于 stopped(被空闲回收); - 06:56 我调 agent
/launch返回already-running⇒ 实例在那之前已被拉起; - 06:56:34 对同一 URL 的请求 ⇒ 401(2 ms) ⇒ 链路本身健康。
⇒ 结论:实例被空闲回收后,用户访问触发唤醒;唤醒页跳回 / 时代理转发到实例,而实例端口尚未就绪 ⇒ 转发失败、连接被断 ⇒ nginx 502。
(这类"回收后再访问"的窗口期对低频访问但非新用户尤其明显 —— admin 管理台正是这种。)
修复方向(按优先级):
- 代理转发失败时做短重试(覆盖"实例已起但端口未 ready"的数秒窗口)—— 根因修复,改
src/supervisor/proxy.ts的转发错误路径; - 或把
/api/dsh/enter的就绪判据从"拿到 launch token"加严到"端口可连"再返回; - 运维缓解(不治本):admin 属高频管理入口,可对其不做空闲回收。
8.3 ⚠️ 顺带发现一个必须修的架构缺口(迁移引入的真实影响)
现象:POST /api/me/locale 对 guest(已迁到 106) 返回 500:
ENOENT: no such file or directory, open '/var/lib/dshs/users/4092b965-…/home/settings.yaml'(06:54 出现两次)。
根因:src/web/home-files.ts 的 writeHomeFile 直接操作本地文件系统,没有走集群文件面 ⇒
Manager 上任何"直接写用户 home"的路由,对不在本机的用户都会 500(不只是 locale)。
影响面:凡迁到 106 的用户,在 47 的门户上做"改语言/改设置"类操作会 500 —— 这是集群化的通用缺口,不是 locale 一处的问题。
修复方向:这些路由改走 app.userFs(集群模式下即 RemoteUserFs,会代理到该用户所在 worker),
而不是直接 fs.writeFile。⇒ 建议与 D3 一并排期。