Files
dsh_ai1net_server/docs/集群与实例/集群化改造方案_Manager-Worker_20260914.md
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

81 KiB
Raw Permalink Blame History

DSH 平台集群化改造方案 —— Manager / Worker 模式

项 值
版本 v0.1(草案,2026-09-14)
目标 单机 local 模式 → 1 组 Manager(≥2 台,负载均衡 + 主备)+ N 台 Worker 承载用户实例;支持实例跨 Worker 迁移
现状基线 生产 = 单机 dshs(systemd 127.0.0.1:3080)+ bwrap/uid/scope 隔离 + SQLite;实测宿主 1.8 G ⇒ 1–3 人
代码基线 D:\github\dsh_shenxian @ 1d72e8f(dsh 0.1.2-rc.1,官方包零改动)
性质 纯增量改造,不触碰官方 dsh(R2);隔离机制与平台自研能力原样保留
归档 ⚠️ 本文件是草案(放项目根)。定稿后再占号入 dsh-server-docs/04-调整方案/(档案只增不改,不宜冻结草案)

0. 一句话方案

Manager 只管「门户 + 路由 + 归属」,Worker 只管「跑实例」。 归属用 PG 租约(原子 CAS)、数据放 用户专属共享存储、跨机流量走 内网 + 白名单。 于是「迁移」= 改一行归属 + drain 一次进程,不需要搬数据。

已有代码资产可直接复用(本次核对):

资产 位置 复用方式
Spawner 抽象 src/supervisor/spawner.ts(头注释:route 层只依赖此接口) 新增第 3 个后端,路由层不改
代理「TCP 目标 / Host 头分离」 src/supervisor/proxy.ts:299 vs :108 跨机代理零改动(Host 头恒为 127.0.0.1:<port>,通过 dsh 信任栅栏)
栅栏判定「只看主机名不看端口」 官方 packages/client/connection/src/api-request-trust.ts:103 + proxy.ts:30 STRIP_HEADERS 转发端口 ≠ 实例端口也安全
租约语义 src/supervisor/leader.ts:200-216(replace + resourceVersion 乐观并发) 照抄成 PG 版
fencing src/supervisor/k8s-spawner.ts:112 stampFencing 照抄,加在每次实例操作上
跨机文件面 src/fs/k8s-user-fs.ts + src/web/file-service.ts(每用户 sidecar) 泛化为 RemoteUserFs
跨机端口暴露原语 src/tcp-bridge.ts(dshs tcp-bridge <listen> <target>) Worker 侧一条命令暴露实例
PG 后端 src/db/index.ts:19(DSHS_DB_URL 非空即切) Manager 组多节点的硬前提(见 §6 P0-1)
每用户 uid 一致性 uid = baseUid + row_id,由 DB 决定 跨机天然一致,不需要 uid 分配协议

1. 架构

1.1 角色划分

                    ┌── 入口(保留:腾讯云 nginx + CF,443 / 通配证书) ──┐
                    │        *.domain → Manager VIP / SLB              │
                    └──────────────────┬───────────────────────────────┘
                                       │
        ┌──────────────────────────────┴──────────────────────────────┐
        │                     Manager 组(≥2 台)                      │
        │  【门户层 · 双活】  dshs 全功能 web:登录/审核/门户/密钥/技能   │
        │  【控制层 · 主备】  迁移 / 回收 / 归属租约续期(PG advisory)  │
        │  【代理层】         Host → 归属 → 实例(本机直连 / 跨机直连)  │
        └───────┬──────────────────────────────────────┬──────────────┘
                │ 5432(内网)                          │ 9000-9999(内网 + nft 白名单)
        ┌───────┴────────┐                   ┌─────────┴─────────┐
        │  PG(共享状态) │                   │  Worker 组(N 台)│
        │  归属/租约/用户 │                   │  每台:worker agent│
        │  会话/凭据/插件 │                   │  + bwrap 实例      │
        └───────┬────────┘                   └─────────┬─────────┘
                │                                      │
        ┌───────┴──────────────────────────────────────┴──────────────┐
        │  共享存储(用户专属目录,位置无关):NAS RWX 或 对象存储     │
        │  <user>/{home,ws}  ← 任意 Worker 挂载即可接手                │
        └──────────────────────────────────────────────────────────────┘

1.2 职责边界(这条决定改造量)

Manager Worker
门户 / 认证 ✅ 独占 ❌
控制面数据库(用户/会话/凭据/候选池/归属与租约) ✅ 唯一写入者 ⚠️ 不是"不许有数据库" —— 见 §1.3:控制面数据只有 Manager 能写;Worker 完全可以有自己的库
归属租约(谁托管谁) ✅ 唯一写入者 ❌ 只被通知
实例进程生命周期 ❌ 不 spawn 本地进程 ✅ 独占(bwrap/uid/scope 全在本机)
代理转发 ✅ 唯一出入口 ❌ 不直连公网
熔断 / 内存配额 / 插件探活 ❌ 读 Worker 上报 ✅ 本地自管(保持现有语义不变)
文件读写(门户「我的文件」) 转发 ✅ 在本机(worker 有用户卷)
实例业务数据(插件库 / 会话日志 / 工作产出) ❌ ✅ 实例独占(放 home,跟用户走)

1.3 数据分层(2026-09-15 用户纠正:Worker 不是"不许有数据库")

用户原话口径:「不一定 worker 不连数据库,有些插件有可能有大量数据需要保存和管理、长期使用」。 ⇒ 原表述「Worker ❌ 不碰 DB」过窄:它想说的其实只有一件事 —— 控制面数据(尤其归属)不能由 Worker 写。 插件的大量长期数据与这件事根本不在同一层,不该被这条规则挡住。

数据类 例子(都是实测存在的) 唯一写者 落点
控制面元数据 用户 / 会话 / 凭据 / 插件候选池 / 归属与租约 仅 Manager Manager 的 PG
实例业务数据 home/.dsh/mcn-plugin.db(插件自带 SQLite)、会话日志、.univer、工作产出 Worker(该实例) 实例 home(位置无关存储)→ 跟着用户迁移
Worker 运维数据 实例台账、指标、日志、心跳历史 Worker 本地 Worker 本地库/文件(SQLite 足够)——不进控制面 PG
控制面数据的只读副本(可选) 某些插件需要用户/插件元数据 只读 Worker 本地缓存 + 明确失效策略;绝不写归属

三条判据(以后按这个判,别再用"碰不碰 DB"):

  1. 是"归属/租约"吗? ⇒ 是:只有 Manager 能写(否则就是双写归属 = 脑裂)。
  2. 跟着用户走吗? ⇒ 是:放实例 home / 位置无关存储,不要塞控制面 PG。
  3. 是本机运维用的吗? ⇒ 是:放Worker 本地库,同样不进控制面 PG。

为什么"大量数据 + 长期使用"不该塞控制面 PG:会把控制面变成混合负载 ⇒ 备份窗口、锁竞争、扩容、故障半径全部耦合;而且业务数据量随用户数线性增长,会让控制面 PG 的容量规划被业务数据绑架。

给插件的三种落点(按数据量与查询形态选):

场景 落点 迁移行为
单用户、量不大、无需跨用户查询(现状:mcn-plugin.db) 实例 home 里的 per-user SQLite 跟 home 走,天然迁移 ✓
单用户、量大、需要真 DBMS 每用户一个 database/schema(Worker 本地 PG/MySQL,数据目录放在位置无关存储上) 迁移 = dump/restore 或共享存储
跨用户聚合分析(运营看板之类) 不要在 Worker 上做 ⇒ 由 Manager 侧的离线汇总表承接 —

与 S3 实现的关系:agent 目前不需要DB(apiKey/uid 由 Manager 随 launch 投递)—— 这是权限收窄选择(凭据不经网络下发 vs. 让 Worker 能读全量凭据库),符合 R5。 但这不妨碍 Worker 承载自己的数据库:插件的 per-user SQLite 一直在实例 home 里、由实例自己读写;将来接 DBMS 按上表落点即可,与本设计的 Manager/Worker 边界不冲突。

关键设计选择:把「实例本地概念」留在 Worker 自管(熔断、配额推导、崩溃退避、插件探活回滚),Manager 只读上报 —— 这样 档案 34/58/74/78/81 那批能力一行不用重写,这是本方案相对 k8s 路线最大的成本优势。


2. 网络拓扑

2.1 流量路径(4 条)

# 路径 协议/端口 说明
1 用户浏览器 → 入口 nginx(腾讯云) 443(CF 回源) 不变,通配证书 *.domain 仍在入口
2 入口 nginx → Manager 组 443 或 3080 经 VIP(keepalived)或云 SLB
3 Manager → Worker(内网) 9000–9999 代理转发 + 实例控制 API(launch/stop/status)
4 Manager ⇄ PG 5432(内网) 归属租约 + 全部业务表

2.2 端口矩阵

节点 端口 用途 暴露面
入口 80 / 443 用户入口 公网
Manager 3080 dshs(门户+代理) 仅入口 nginx 与内网
Manager 5432 出 PG 客户端 内网
Worker 9000–9999 ① 实例 tcp-bridge ② agent 控制口 仅 Manager 组 IP(nft 白名单)
Worker 22 运维 跳板机
Worker 443 出 模型 API(DeepSeek 等) 出网(可收敛 CIDR)

2.3 防火墙(nft)三条铁律

  1. Worker 的 9000–9999 只允许 Manager 组 IP;其余一律 drop(替代现在单机的 portGuard iptables owner-match 语义,见 §6 P0-2)。
  2. Worker 不监听公网;实例仍只绑 127.0.0.1(dsh 硬禁 --host 0.0.0.0,官方 startup.ts:74-76 原文 "it would expose remote code execution to the network")。
  3. Manager 之间只放 keepalived VRRP + PG + 代理端口,不留其它互通。

✅ 现成的护栏可直接复用:src/supervisor/firewall.ts 的 createPortGuard()(iptables OUTPUT owner-match)在 Worker 上语义不变 —— 它防的是"本机其它 uid 直连实例端口",而实例仍以 dsh-<uid> 运行、桥仍由平台账号监听。⇒ Worker 侧本机护栏零改动复用,无需重新设计。

控制口与代理口分离(建议):

端口 用途 策略
9000 控制口(agent API:launch/stop/status/list) 短超时(10s)、强鉴权(HMAC)、nft 只放行 Manager 组
9001–9999 代理口(每实例一个 tcp-bridge) 长超时(SSE/WS 不限时)、Host 头透传

2.4 负载均衡与主备(按你的要求:≥2 台)

维度 严格主备(active-standby) 推荐:门户双活 + 控制主备
门户层 备机闲置 两台都服务,容量 ×2
控制层(迁移/回收/续租) 主机独占 PG advisory lock 抢主,同一时刻单写者
代价 浪费一台 多一个 advisory lock 与"控制任务幂等"要求
实现 keepalived + VIP 漂移 同上 + 控制任务租约

✅ 已定(2026-09-14 用户拍板):门户层「双活」,同时必须支持「单活」部署。 实现要点:门户无状态 ⇒ 双活与单活是同一套代码,1..N 台都成立,不需要为单活写分支:

场景 行为
N 台(双活) LB/VIP 分发;控制任务靠 PG advisory lock 抢主,同一时刻单写者
1 台(单活) 撤掉 LB,域名直指该机;advisory lock 自然由它持有 ⇒ 控制层零改动

单活只需注意两处(都不是新增代码):

  1. 会话/门户状态必须落 PG(已满足:会话查 DB、cookieDomain 跨子域)⇒ 否则切机即掉登录;
  2. 长连接断开后前端要能自动重连(WebSocket 由 Manager 代理持有;Manager 重启/VIP 漂移会断流)⇒ 需实测 dsh 前端重连行为,必要时由代理层注入重连脚本(现有 401 自愈注入机制「档案 50」可复用同一注入位)。

状态机建议:dsh_hosts.status 同族的 manager 角色标记 + 一条 PG advisory lock,禁止任何"必须 2 台"的硬假设(例如配置里写死 peer 地址)。

⚠️ 备案坑(已有实测记录):阿里云公网 SLB 对未在阿里云备案的域名返回 403 Non-compliance ICP Filing(D:\github\dsh_shenxian\docs\k8s-deploy.md §7.4)。⇒ 入口继续留在腾讯云 nginx,Manager 组只在内网/专线可达,不要在阿里云侧直接对公网提供域名入口。


3. 数据模型

3.1 新增/扩展(迁移 v7)

-- 主机(Worker)注册表:容量准入的依据
CREATE TABLE dsh_hosts (
  id            TEXT PRIMARY KEY,          -- worker-01
  endpoint      TEXT NOT NULL,             -- 10.0.1.11:9000
  agent_token   TEXT NOT NULL,             -- 内部 HMAC 密钥(加密存储)
  capacity_mb   INTEGER NOT NULL,          -- 该机可用内存预算
  used_mb       INTEGER NOT NULL DEFAULT 0,-- 由心跳上报
  status        TEXT NOT NULL DEFAULT 'up',-- up|draining|down
  last_heartbeat BIGINT
);

-- 归属(租约):一行 = 一个用户的 main 实例归属
ALTER TABLE dsh_instances ADD COLUMN host_id     TEXT;
ALTER TABLE dsh_instances ADD COLUMN epoch       BIGINT NOT NULL DEFAULT 0;
ALTER TABLE dsh_instances ADD COLUMN heartbeat_at BIGINT;
ALTER TABLE dsh_instances ADD COLUMN lease_until  BIGINT;

3.2 租约的原子抢占(这是整套方案的承重墙)

UPDATE dsh_instances
   SET host_id = $me, epoch = epoch + 1, heartbeat_at = now(), lease_until = now() + ttl
 WHERE user_id = $u AND role = 'main'
   AND (host_id IS NULL OR lease_until < now());   -- 过期才可抢
-- 受影响行数 = 1 ⇒ 抢到;= 0 ⇒ 有人在管,退让
  • 语义照抄 leader.ts:200-216(replace + resourceVersion 乐观并发);
  • 每次实例操作带 expected_epoch,不匹配即拒绝(fencing,参照 k8s-spawner.ts:112);
  • 建议时序:TTL 30 s / 续期 10 s / 重试 3 s(不变量:TTL > 2 × 续期,否则自身抖动误判)。

4. 关键流程

4.1 冷启动(用户首次访问子域)

浏览器 → 入口 → Manager(代理层查不到归属)
  → 租约抢到(affected=1)
  → 选 Worker(容量准入:used_mb + 实例配额 ≤ capacity_mb)
  → POST worker:9000/instances/launch {userId, folder, patch, epoch}
  → Worker:ensure profile(本机)→ tcp-bridge 暴露 → bwrap+scope 起实例 → 回传 {port, launchToken}
  → Manager 写归属(host_id=worker-01, port)
  → 代理转发(Host 头仍 127.0.0.1:<port>)→ 用户看到页面

⚠️ launchToken 必须回传:本地模式是解析实例 stdout 拿到的(Instance.launchToken 注释),跨机后 Worker 要把它交给 Manager,否则「登录直达会话」(档案 06/13/15)与 401 自愈(档案 24/51)会失效。这是 P0 功能缺口,别漏。

4.2 迁移(计划内)

admin POST /api/admin/instances/:id/migrate {targetHost}
  ① 加迁移锁(PG advisory)→ 目标 Worker 预约容量
  ② drain:SIGTERM → 等落盘(≤10 s)→ 确认 sessions 已写盘(SIGKILL 仅兜底)
  ③ 目标 Worker launch(数据在共享存储,无需搬运)
  ④ 原子更新归属:host_id=目标, epoch+1   ← 旧 Worker 的 fencing 立即失效
  ⑤ 旧 Worker 确认实例已退、端口已释放
  ⑥ 观察 60 s(无崩溃)→ 迁移结束;失败则按 ④ 反向改回归属(回滚 = 改一行)

4.3 故障转移(计划外)

Worker 心跳超时(> TTL)
  → 该机 status=down,其上所有归属的 lease_until 到期
  → Manager(控制主)逐个在新 Worker launch(epoch+1)
  → 旧 Worker 若复活:fencing 拦下它的一切写入  ← 唯一防止「双写坏数据」的机制

判定"对方已死"本质上不可靠(同 R9 的教训:无心跳时不得单方面接管)。因此必须靠 lease TTL 硬等 + fencing 兜底,并接受"复活窗口内可能有短暂双活"。 自动转移的前置 = 共享存储;未上共享存储前,只能做计划内迁移。


5. 实现细节(落地清单)

# 改动 文件 量级
1 迁移 v7(dsh_hosts + 归属列) src/db/schema.ts 小
2 PG 租约模块(抢/续/过期扫描) src/supervisor/lease.ts(新) 中
3 RemoteSpawner implements Spawner src/supervisor/remote-spawner.ts(新) 中
4 Worker agent(复用 LocalSpawner,内部 HTTP) src/cli.ts 加 dshs worker、src/worker/agent.ts(新) 中大
5 选机(容量准入) 并入 #2/#3 小
6 文件面泛化(复用 file-service) src/fs/k8s-user-fs.ts → remote-user-fs.ts 小
7 模型落地层改造(现在直写主机 home) src/web/server.ts:132 writeHomeFile / :165 landModels 中(必须改)
8 角色补丁 / picker(主机侧脚本) orchestrator.ts:248 ensurePickerProfile、ensure-role-profile-patch.cjs 中(必须改)
9 Worker 上报(熔断/配额/状态) agent 暴露 breakerInfo/quotaInfo 小
10 admin 迁移/主机管理 API src/web/routes/admin.ts(同族 /api/admin/users/:id/dsh/*) 小
11 部署基线(同镜像 + nft + bwrap + uid) 新装机脚本 中
12 路由 proxy.ts 不改 ✅ 0

Worker 数量 → 容量(按现有配额实测值:实例 384–672 MiB)

8C16G Worker 并发活跃实例(保守 672 MiB) 并发活跃实例(典型 384 MiB)
每台 12–14 20–24
目标 目标并发 所需 Worker 说明
50 人试点 15 2–3 台 含 1 台冗余
300 人 60–90 6–8 台 20–30% 同时在线
1000 人 200–300 16–25 台 需配合分钟级空闲回收

6. 潜在问题点(P0 / P1 / P2)

P0(不解决就不能上)

# 问题 后果 应对
P0-1 Manager ≥2 台 ⇒ PG 是硬前提。SQLite 放共享存储多写者不安全 组不了 Manager 组 切 PG 只有一个环境变量(db/index.ts:19),但要真做数据迁移与回归(db/pg.ts 508 行,本项目从未在生产启用 —— 档案 19 §C8)
P0-2 实例端口对内网暴露 = 权限扩大(触 R5) 同 VPC 任意主机可直连实例端口(只剩 dsh 自身 token/cookie 一层) nft 只放行 Manager 组 IP + 桥只绑内网网卡 + tcp-bridge 加共享密钥校验;先出权限影响评估
P0-3 脑裂双写同一 home 会话日志 append 冲突 → 数据损坏 租约 + fencing(§3.2/§4.3);宁可放弃自动转移,也不裸奔
P0-4 模型落地层直写主机 home(writeHomeFile + chown) 档案 87「模型设置」在集群下整块失效 改为经 Worker agent 写(或写共享存储后由实例读)
P0-5 角色补丁 / picker 是主机侧脚本 非 admin 隐藏模型分区、目录选择器收敛失效 并入 Worker launch 流程(Worker 本机执行,逻辑不变)
P0-6 launchToken 回传缺失 直达会话、401 自愈失效(档案 06/13/24/51) Worker 必须把 token 交 Manager(协议字段)
P0-7 文件面跨机:userFs 直读本机 dataRoot/users/<id>/ws 门户「我的文件」对非本机用户 404/空 复用 file-service(已有实现)或共享存储直读

P1(影响可用性/运维)

# 问题 应对
P1-1 熔断/退避/配额是进程内存态(LocalSpawner) 留在 Worker 自管(推荐,零重写);但 Worker 重启即失忆 ⇒ 需接受"熔断态随重启清零"或落本机轻量状态文件
P1-2 实例随机端口 → 防火墙难写 收敛到固定段(9000–9999),findFreePort() 限定范围
P1-3 容量准入缺失:现在只有本机 maxIdleInstances Manager 按 dsh_hosts.capacity_mb 选机,否则某台被打满
P1-4 长连接集中在 Manager(WebSocket 全由代理持有) 按连接数扩容 Manager;监控单副本连接数上限
P1-5 时钟漂移影响租约 全集群 NTP;TTL 留足余量(≥2×续期)
P1-6 日志/可观测分散到 N 台 集中日志(Loki/SLS)+ /healthz 聚合;现有单机 journald 不够
P1-7 备份窗口随用户数膨胀(见 §7) 分层备份
P1-8 Worker 升级需同基线(bwrap / /etc/passwd 白名单 / 镜像) 统一镜像 + 灰度;/etc 白名单是运行时实测最小集(档案 39),换发行版会失效

P2(可延后)

# 问题
P2-1 uid 是否需要在每台 Worker 建 OS 账号 —— 代码走 setpriv --reuid <数字>,但 /etc/passwd 被绑入沙箱(orchestrator.ts:640,为 os.userInfo())⇒ 需 5 分钟实测确认(若不建号也可,uid 分发零成本)
P2-2 迁移期间用户正在对话的体验(drain 10 s 内断连)
P2-3 单用户跨机后 portGuard(iptables owner-match)语义变化,需重做等价护栏

7. 备份方案(回答「有没有比文件备份更高效的方式」)

7.1 现状(实测)

  • /opt/dsh/backup.sh(入库 scripts/backup-platform.sh):每周日 05:00 全量 tar.gz + 60 天保留;含 var/lib/dshs + env + systemd + artifacts + nginx vhost;SQLite 用 .backup 一致性快照;
  • 实测首跑 8.7 MB / 6919 条目(用户数还少)—— 现在全量 tar 毫无压力,问题在用户数上去之后。

7.2 五种方式的对比(结论:单靠文件备份不是最高效的)

方式 机制 CPU/IO 成本 优点 缺点
① 全量 tar(现状) 每次重打包 高(随数据量线性增长) 简单、可读 全量重传;数千小文件(node_modules)是 tar 最慢的部分
② rsync 增量 按 mtime/size 比对 中 差量传 需目标端为文件系统;无去重、无加密、版本管理弱
③ restic / borg 去重增量 内容分块去重 + 快照 中低 增量 + 去重 + 加密 + 保留策略,天然适合小文件与历史版本 需目标为 S3/OSS 或本地仓库;首跑要建立索引
④ 存储层快照(最省) NAS / ESSD 快照 近零(不经过应用) 秒级、卷级原子、增量块级 粒度是卷不是用户;恢复单用户要挂快照再拷
⑤ 只备「不可重建」子集 选择性打包 最低 体积可砍掉大头 需要明确"什么可重建"

7.3 推荐:分层备份(组合 ④ + ③ + ⑤)

层 对象 方式 频率 恢复目标
L1 共享存储卷(全部用户数据) NAS/ESSD 自动快照 每日(保留 7–30 天) 整卷回滚(分钟级)
L2 关键子集 → 对象存储(异地) restic 增量 + 去重 每日 单用户/单文件恢复
L3 DB PG 物理备份 + WAL 归档(PITR) 连续 任意时间点
L4 平台配置(env/systemd/nginx/artifacts) 现有 tar(体积小,保持全量) 每周 重建平台

L2 的「关键子集」= 只备不可重建的部分(这是最大的效率来源):

备份 不备份 理由
sessions/(对话历史)、.credentials.yaml、skills/、ws/(产出物) profiles/web/node_modules 数千小文件、体积最大,且可由「dsh.profile.bundles 清单 + 镜像版本」完整重建
平台注入的 patch / picker / 模型落地文件的清单 其内容 由代码 + DB 重建(renderPatch / landModels 幂等)
— 各类缓存(.cache / 临时产物) 无价值

体积预估:排除 node_modules 后,单用户数据通常从数百 MB 降到 数 MB–数十 MB ⇒ L2 的窗口从"小时级"回到"分钟级"。 必须保留:credentials(不可再生成)与 sessions(对话不可重建)一律备份,不要省。

附加要求(现状已有先例):备份必须配恢复演练(对照 02-运维手册 §C.8 的演练记录)+ 迁移/备份期间避免"实例正在写 sessions 时取半写快照"⇒ 优先用存储层快照的原子性,其次 drain 后备份。


8. 落地成本

8.1 机器(量级参考,需以实际报价核)

角色 规格 数量 说明
入口 现有腾讯云机 1 复用,不新增
Manager 4C8G 2 双活门户 + 控制主备;主要是代理长连接的带宽/连接数
Worker 8C16G 见 §5 表(2–25 台,随目标并发) 主要成本项
PG 4C8G / 复用现有 RDS 1(HA 则 2) 共享状态
共享存储 NAS(或 ESSD+快照) 按用户数据量 容量 + 快照存储费

8.2 工程改动面(比人日更可靠的口径)

类别 内容 判断
新组件 worker agent、RemoteSpawner、租约模块 3 个
必须改的现有能力 模型落地层、角色补丁/picker(P0-4/5) 2 处
数据层 PG 切换(本项目从未在生产用过)+ 迁移 v7 高风险项
不需要改 代理层、路由层、隔离机制(bwrap/uid/scope)、熔断/配额/插件探活 ✅ 本方案最大收益
运维新增 装机基线、nft、集中日志、租约监控、备份四层 中

参考量级(仅表示规模,不作为承诺):一个可上线的第一版 ≈ 一次性投入数周(含 PG 真机回归);之后每加一个能力(自动转移、弹性调度)都是独立增量。

8.3 三阶段路线

⚠️ 分期必须互相兼容(一期做完,二期只是加机器/加开关/加运维,不重写代码)⇒ 约束、兼容矩阵、反模式与一期 DoD 见 §14。

期 内容 出口验收
一期(可用) 1a:PG 切换 + 归属/租约 + worker agent + RemoteSpawner + 2 台 Manager(其中 1 台兼任 Worker-01,现有数据原地不动)→ 1b:加 Worker-02 + NAS,只做计划内迁移(渐进路径见 §15.3) 50 人真实使用;kill 一台 Manager 门户不中断;管理员迁移一个用户成功且会话无损
二期(稳) 文件面跨机、容量准入、集中日志、四层备份、灰度升级 300 人;单 Worker 可下线维护;备份恢复演练通过
三期(弹性) 心跳故障转移、自动回收、容量自动扩缩、可观测告警 1000 人;Worker 故障 5 分钟内自动恢复

8.4 回滚

变更 回滚方式
归属/租约(v7) 新增列/表,不影响旧代码路径;deployMode=local 单机模式保留可用(代码不删)
PG 切换 保留 SQLite 路径;PG 出问题可切回单机(需数据回灌,这是最大风险点,二期前必须演练)
Worker 扩容 缩容 = 迁走用户 + status=draining
单次迁移失败 归属改回原 Worker(改一行 + epoch+1)

9. 决策状态(2026-09-14 更新)

# 事项 状态 结论
1 Manager 组形态 ✅ 已定 门户双活 + 控制主备,且支持单活部署(同一套代码,1..N 台)→ 见 §2.4
2 共享存储选型 ✅ 已定方向 平台侧做成「存储后端可插拔」:本地盘 / 自建 NFS / 云 NAS / 云块存储都能接 → 详解见 §12
3 PG 承载方式 ✅ 已定方向 一期自建(独立机 + 四层备份),300 人后再评估迁 RDS → 详解见 §13

定项以外的技术细节(端口段、TTL 数值、对账周期、备份频率)由实现侧自主决定,不再上抛。

10. 动工前必须先做的 5 分钟实测(把未知数清掉)

✅ 本清单已于 2026-09-14 21:17–21:25 在真实两台服务器上执行完毕 ⇒ 结果与结论见 §17(第 1、2 项已有答案,第 3–5 项依赖共享存储,尚未做)。

# 验证项 判据
1 跨机代理:实例经 tcp-bridge 暴露后,用带 Host: 127.0.0.1:<非实例端口> 的请求打 /api/* 403 = 栅栏拒(理论被推翻)/ 401 = 栅栏放行(理论成立)
2 uid 是否需 OS 账号 不建号直接 setpriv --reuid <数字> 能否起实例 + os.userInfo() 是否报错
3 共享存储上的 watcher skill-filesystem.watchUsePolling=true 后 skill 目录事件正常;credentials 热重载是否失效(最坏只需重启)
4 共享存储冷启动耗时 一个真实 home(含 node_modules)从 NAS 起的冷启动 vs 本地盘,差多少
5 每用户数据体积 实测 sessions/skills/ws(含 node_modules)各占多少 ⇒ 定 L2 备份子集与 NAS 容量

11. Manager ⇄ Worker 连接保障与故障恢复(可靠性核心)

11.1 先立一条设计原则:连接不是关键路径

权威归属在 PG、用户数据在共享存储 ⇒ 「连接断了」本身不会损坏数据,最坏只是"暂时访问不到"。 所有可靠性设计都服务于这一条:宁可短暂不可用,不可双写。

11.2 连接形态:单向、Manager 主动拨入

通道 方向 协议 用途 超时
控制通道 Manager → Worker:9000 HTTP + HMAC launch / stop / status / list / drain 10 s
心跳通道 Manager → Worker:9000 HTTP GET /healthz 存活 + 容量 + fencing 广播(见 §11.5) 2 s
数据通道 Manager → Worker:9001-9999 HTTP + WebSocket 用户流量代理 常规 30 s;SSE/WS 不限

Worker 不反向连接 Manager、不直连 DB ⇒ Worker 实现简单、无需知道 Manager 地址、控制口是它唯一的"被拨入口"。

为什么心跳用「Manager 拉」而不是「Worker 推」:① Worker 无需 DB 依赖(保持 §1.2 边界);② 多台 Manager 各自拉 = 天然冗余探测;③ 避免 Worker 侧维护"往哪个 Manager 推"的状态。

11.3 连接"确保"的六条工程手段

# 手段 具体做法 依据
1 网络层 同 VPC 内网;跨可用区走内网固定 IP;跨地域必须 WireGuard/专线,不得公网明文 —
2 半开连接防护(必须做) 控制通道 keepAliveTimeout + request timeout;代理通道复用现有做法:连接出错 → agent.destroy() + new Agent() → 短连接重试一次 proxy.ts:307/450-455 记的 2026-09-09 事故:keep-alive 池缓存已回收实例的 socket → 请求永久挂起。跨机后 NAT/防火墙会静默丢空闲连接,这个问题只会更严重
3 幂等键(必须做) 每个控制请求带 operation_id(= epoch + 序号);Worker 保留最近 N 个并回放上次结果 解决"Manager 发出去了但响应丢了"——否则超时重试可能起两个实例。Worker 侧按 userId 幂等(已运行则返回现有实例信息),与 LocalSpawner 的 AlreadyRunningError 语义对齐
4 鉴权 + 最小接口 X-DSH-Agent-Token(HMAC,密钥加密存 dsh_hosts.agent_token);agent 只接受白名单动作,参数受限(folder 必须在该用户根下、patch 有长度上限) 安全红线:agent 若接受任意命令,Worker 沦陷 = 全集群沦陷
5 重试与退避 控制请求重试 3 次(200 ms / 1 s / 3 s),仍失败则标记该 Worker 异常并告警 区分"抖动"与"真故障"
6 连接池容量 maxSockets 按 Worker 数 × 平均实例数 调整(当前 32) 现状值是按单机定的

11.4 故障恢复矩阵(10 个场景)

# 场景 检测方式 恢复动作 用户感知
1 控制请求超时(网络抖动) 请求超时 幂等重试 无(冷启动略慢)
2 Worker 心跳连续失败,但未过 TTL 3 次 /healthz 失败 只告警,不接管 无
3 Worker 失联 超过 TTL 租约过期 先尝试 stop(网络若恢复);确认不可达后 → 新 Worker 用 epoch+1 重启;旧实例若复活被 fencing 拦下 约 30–60 s 不可用(TTL 30 s + 冷启动)
4 Worker 计划内维护 人工置 status=draining 先迁后停:逐个 drain + migrate,全部迁完才停机 每次迁移数秒
5 Worker agent 崩(机器/OS 正常) 心跳恢复但实例列表为空 对账:归属在该机但实例不在 → 按会话活跃度决定重拉或删归属 冷启动延迟
6 Manager 挂 1 台 LB 健康检查 另一台接管(门户双活);控制主经 advisory lock 自动转移 无(长连接断开,前端自动重连)
7 Manager 全挂 LB 无后端 Worker 上实例继续运行(无人代理 = 不可访问);Manager 恢复后从 DB 读归属 → 逐个 Worker 核对 → 重新暴露 门户中断期间全员不可用 ⇒ 所以 2 台 Manager 要跨可用区
8 实例进程崩(Worker 本地) Worker 本地检测 Worker 自愈(现有退避 + 熔断 + 插件自愈),Manager 不参与 与现状一致
9 PG 不可用 连接失败 拒绝新冷启动;已代理流量继续转发(依赖归属缓存,建议 TTL 60 s) 新用户进不来,在线用户不受影响
10 网络分区(脑裂) fencing 校验失败 老 holder 的一切操作被拒;新 holder 接管;分区恢复后自动对账 分区侧请求失败,恢复后收敛

11.5 心跳的三个职责(别只当存活检测)

  1. 存活:连续失败计数 → 决定是否标记 down;
  2. 容量上报:used_mb / 实例数 → 供选机(§5 容量准入);
  3. fencing 广播(关键):Manager 在心跳响应里下发该机的 expected_epoch 清单。Worker 发现自己持有的实例 epoch 落后 ⇒ 主动停掉自己那个实例(self-fencing)。

⇒ 它是"防双写"的最后一道防线:即使 Manager 判定错误,复活的老 Worker 也会因为 epoch 落后而自杀,而不是继续写共享存储。

11.6 对账(reconcile)设计

  • 只有控制主跑(PG advisory lock),周期 30 s;
  • 一次调用拿回整机实例列表(GET /instances),而不是逐用户查询 —— 这是相对 k8s 版 reconcile.ts 逐个 ensureUserResources/reap(O(N) API 调用)的直接改进;
  • 差异处理(全部带 epoch、全部幂等):
    • DB 有 / Worker 无 → 有活跃会话则重拉,否则删归属
    • Worker 有 / DB 无 → 孤儿,stop
    • host_id 指向 status=down 的机 → 迁走
  • Worker 响应的 epoch 与 DB 不一致 ⇒ 立即 self-fence 该 Worker。

11.7 恢复动作必须可审计 + 不可逆操作守纪律

  • 每次迁移/接管/孤儿清理写 audit_log(表已存在)+ 平台状态目录留痕;
  • 不可逆破坏性动作(删归属、删用户数据)一律先出清单,与现有纪律一致(CODEBUDDY.md §3 R8);
  • 自动接管默认关闭,一期只做"告警 + 人工确认接管" ⇒ 与 R9(无心跳不臆断对方已死)的教训保持一致,等 TTL + fencing 在真机跑稳后再开自动。

12. 存储选型详解("服务存储"与"接入存储空间"到底差什么)

12.1 先澄清:这是四种产品形态,不是两种

"服务存储"vs"接入存储空间"这个说法容易把两件不同的事混在一起 —— 真正的分类轴是**「能不能被多台机器同时挂载」**:

类型 例子(云厂商产品) 能不能多机同时读写 本质
① 本地盘 实例自带的本地 SSD/NVMe、物理机 RAID ❌ 只能本机 硬件直连
② 块存储 阿里云 ESSD、腾讯云 CBS ❌ 一块盘挂一台(多重挂载需集群文件系统,实践罕见) 一块"虚拟硬盘"
③ 文件存储 阿里云 NAS、腾讯云 CFS / CFS Turbo ✅ RWX 多机同时挂(NFS/SMB 协议) 网络文件系统
④ 对象存储 阿里云 OSS、腾讯云 COS、S3 ✅ 但是 HTTP API,不是文件系统 Key-Value 桶

12.2 我原来那两个选项,本质就是 ③ 与 ② 之争

维度 ③ 文件存储(NAS RWX) ② 块存储(ESSD + 定期同步)
多机共享 ✅ 天然(这正是"位置无关"的实现方式) ❌ 只有挂它的那台能读写 ⇒ 靠定时同步间接共享
小文件性能 ⚠️ 一般(元数据操作走网络,node_modules 数千小文件是典型受害者) ✅ 好(本地/近本地块设备)
迁移代价 不搬数据,改归属即完成 必须搬数据(或接受"迁移后数据不是最新")
可用区约束 可跨可用区(多可用区版) 必须与实例同可用区 ⇒ 会限制 Worker 调度
快照 支持(文件系统级) 支持(块级,更常见更成熟)
计费 按容量(+吞吐/性能等级) 按容量 + 性能等级(PL0→PL3 陡增)
我们方案的定位 跨机共享层(主选) Worker 本地高性能层(可选加速)

性能更快的版本:文件存储不是只有"通用型"——阿里云有 NAS 极速型 / CPFS、腾讯云有 CFS Turbo,小文件性能大幅改善,代价是单价高。若实测通用型 NAS 起不来实例,第一选择是换极速型,而不是放弃共享存储。

12.3 "也是云厂商的服务吗" —— 是,但都能自建(重要)

类型 云厂商托管 自建等价物
块存储 ESSD / CBS 本地盘、Ceph RBD
文件存储 NAS / CFS 自己搭 nfs-kernel-server(一台机器导出 NFS)
对象存储 OSS / COS / S3 MinIO、Ceph RGW
数据库 RDS PostgreSQL 自己装 PG(见 §13)

⇒ 所以"支持服务存储,也支持接入存储空间"这句话在架构上应该落成:平台不绑定任何一种,只依赖一个「存储后端」抽象。

12.4 怎么做到"两种都支持"(成本很低)

现状代码里所有用户路径都从 config.dataRoot 派生(dataRoot/users/<id>/{home,ws})⇒ 把 dataRoot 指向挂载点即可切换后端,业务代码零改动。真正要新增的只有能力探测 + 明确降级:

探测项 为什么要测 失败时怎么办
原子 rename / fsync 会话日志与 .credentials.yaml 的原子写依赖它 降级告警,禁用依赖该能力的路径
NFS/网络文件系统识别(statfs 的 fs 类型) 决定是否自动开 skill-filesystem.watchUsePolling=true(官方开关,index.ts:610-619) 自动开轮询
小文件写延迟采样(启动时写 1000 个小文件计时) 预判 node_modules 冷启动是否可接受 超阈值 → 告警并建议换极速型
可用容量 / inode 余量 NAS 的 inode 耗尽比容量耗尽更常见 低于阈值拒绝新实例

判据必须"启动时自证":宁可启动就报"我这台存储后端是 NFS 通用型、小文件延迟 8 ms、轮询已开",也不要等用户跑起来才发现慢。做法照我们已有的「按序探测 + 可观测降级」模式(src/web/dsh-install.ts,档案 88)。

12.5 推荐:按目录分层挂载(比"整卷选一种"更优)

不同子目录的特性完全不同,混在一层是浪费:

子目录 特性 建议落点
home/profiles/**/node_modules 数千小文件、可完整重建 不入共享层:镜像提供 + 清单重建(同时是备份 L2 的排除项,§7.3)
home/sessions、home/.credentials.yaml 小文件多、总量小、不可重建 共享层(NAS)或随实例走 + 迁移时同步
home/skills 小、不可重建 共享层
ws/(用户工作产出) 可能有大文件、增长快 共享层(NAS)或块存储

12.6 选型时必须问厂商/自查的 6 个参数

  1. 是否支持 RWX(多挂载点)与跨可用区;
  2. 小文件(4 KB 级)随机写 IOPS/延迟(不要看顺序吞吐的宣传值);
  3. inode 上限与扩容方式;
  4. 快照能力与恢复粒度(整卷 / 单文件);
  5. 单价口径(容量 / 吞吐 / IOPS 分别计费的部分);
  6. 与 Worker 的可用区关系(块存储必须同可用区 ⇒ 会锁死调度)。

本期建议:一期先把 dataRoot 放在现有服务器本地盘(单机阶段无跨机需求)→ 上多 Worker 时切云 NAS 通用型(省事、位置无关)→ 实测不行则升极速型,或对 ws/ 单独用块存储加速。


13. PG 承载详解(自建 vs 云 RDS)

13.1 四种形态

形态 说明 适用
A. 与 Manager 同机 PG 和门户抢资源;Manager 挂 = DB 也挂 ❌ 不建议
B. 独立 ECS 自建 单独一台跑 PG(+ 可选流复制备机) ✅ 一期推荐
C. 云 RDS(RDS PostgreSQL / TencentDB) 厂商托管,主备自动切换 + 自动备份 ✅ 二期可迁
D. CNPG on K8s 若将来走 k8s,deploy/01-dsh-pg.yaml 已写好 未来

13.2 对比

维度 B. 自建(独立机) C. 云 RDS
月成本(4C8G 量级) 只付机器 约 2–4×
延迟(同 VPC) 0.2–1 ms 0.2–1 ms(同 VPC 相当)
高可用 自己做(流复制 + 手工/脚本切换) 自带主备自动切换
备份 / PITR 自己做(本方案 §7.3 的 L3) 自带(可视化 + 时间点恢复)
参数/扩展 全自由(任意扩展、任意参数) 部分受限,白名单/权限有约束
运维负担 高(升级、监控、切换演练) 低
风险 ⚠️ 我们从未在生产跑过 PG(档案 19 §C8 标注"未使用、未验证") 厂商背书,但同样要做 PG 回归

13.3 三个必须知道的坑

# 坑 说明
1 跨云/跨地域绝对不要 我们的入口在腾讯云;若集群侧在阿里云而 DB 在腾讯云 ⇒ 每条 SQL 走公网 10–30 ms + 抖动,会被放大到每一个请求。⇒ PG 必须与 Manager 同 VPC,跨云只用专线。
2 连接数 dshs 每个 Manager 一个连接池;RDS 小规格常限 100 连接、且每 Request 新建连接是灾难(要复核是否全部走池)。2–3 台 Manager + 未来副本要算总数。
3 两套 DB 实现并行 db/sqlite.ts(含 repo.ts 同步实现)与 db/pg.ts 是两份独立实现 ⇒ PG 回归必须覆盖两者行为一致(同一组 smoke 跑两边对比),这比"切个 URL"重得多。

13.4 推荐与迁移路径

  • 一期:B(独立 ECS 自建)+ §7.3 的四层备份。理由:成本低、延迟可控,而且无论选谁都要做 PG 回归(这是最大风险项,与选型无关);
  • 二期(300 人后):评估迁 C。在线迁移用逻辑复制(原生 logical replication 或 pglogical):源库订阅 → 追平 → 秒级切写 → 改 DSHS_DB_URL 重启 Manager,停机窗口可压到秒级;
  • 若团队无人愿承担 DB 运维 ⇒ 一上来就用 C 也完全合理,代价只是钱。

13.5 无论选哪个都必须先做的回归

  1. 数据迁移脚本(SQLite → PG):注意 identity 列与 uid 列(db/pg.ts 用 BIGINT GENERATED ALWAYS AS IDENTITY,baseUid + row_id 的 uid 派生依赖它);
  2. 跑通现有 smoke:scripts/smoke-*.mjs 全套(admin/auth/dsh/fs/domain/isolation)在 PG 下重跑一遍;
  3. 会话与凭据回归:登录态跨 Manager 生效、credential_vault 加解密读写一致;
  4. 回滚演练:PG 出问题时能否切回 SQLite(这是本方案最大风险点,必须先演练再上量)。

14. 阶段兼容性设计(一/二期不返工)

用户要求(2026-09-14):一期与二期方案必须互相兼容。 落地成一条总原则:分期只分「自动化程度与规模」,不分「机制、数据结构、协议」 —— 机制和结构一期就定死,二期只是加机器、加开关、加运维。

14.1 兼容矩阵(7 个维度)

维度 一期(可用) 二期(稳) 一期必须现在就做对的约束
Manager 数量 2 台(也支持 1 台) 更多 / 跨可用区 门户无状态 + advisory lock;禁止"必须 2 台"的硬假设
Worker 数量 2 台 6–25 台 一期就走 RemoteSpawner + agent 协议(哪怕 Worker 就在本机)⇒ 加机器是纯运维动作,不重写代码
存储 本地盘 云 NAS / 极速型 / 块存储加速 一期就写"能力探测 + 按目录分层挂载"(哪怕全是本地盘);dataRoot 指向挂载点(派生点 80 处已全走 config ✅)
DB 自建 PG(硬要求) 云 RDS 两阶段都是 PG ⇒ 只需 DSHS_DB_URL + 逻辑复制在线迁;⚠️ 一期不能先用 SQLite 顶,因为租约依赖 PG 的原子 UPDATE … WHERE
自动接管 关闭(只告警 + 人工确认) 开启 lease + fencing + self-fencing 一期全实现 —— 分期只分"自动化程度",不分安全机制(fencing 是防双写的,不能二期才加)
备份 四层结构的低频版 高频 + 异地 + PITR 一期就用同一套工具与格式(restic + 快照 + WAL),只是频率低 ⇒ 二期只调参数,不换格式
代理/路由 跨机转发 不变 零改动(已验证:TCP 目标与 Host 头分离 + 栅栏只看主机名)

14.2 六个「现在不做、二期必返工」的点(已核实,全部是真实存在)

# 点 现状(实测) 一期就要做
1 平台状态目录散落 8 处硬编码 /opt/dsh/state/{runtime-baseline,capabilities}.json、/opt/dsh/state(模型托管清单)、/opt/dsh/backups、/var/run/dsh-storage-report.json、/var/log/dsh-crash-breaker.log、/opt/dshs/scripts/ensure-biz-plugins.cjs、/usr/local/dsh-runtime(×3) 先给"平台状态 vs 用户数据 vs 机器基线"三分类(见 14.3),把它们收敛到 1–2 个显式配置的根;否则二期换存储会出现"一半在 NAS、一半在本地"的隐性分裂
2 模型托管清单是"每机本地文件" server.ts:106 DSH_PLATFORM_STATE_DIR/model-landing/<userId>.json 🔴 必须挪到 PG 或共享目录 —— 否则 A 机拉起的实例写过的 ref,B 机不知道 ⇒ 违反档案 87「只碰自己写过的」约束 ⇒ 重复写 / 漏删用户凭据
3 process.cwd() 定位脚本 orchestrator.ts:251 用 cwd 找 poc/workspace-scoped-picker/ensure-workspace-picker.cjs 改成基于安装根的绝对路径(多机/多部署形态下 cwd 由 systemd WorkingDirectory 决定,不稳定)—— 复用 src/web/dsh-install.ts 的按序探测 + 可观测降级(档案 88)
4 两套 DB 实现并行 db/sqlite.ts(+repo.ts)与 db/pg.ts 是独立两份 所有 schema 变更(如 v7)两份同时改;只用两者都支持的 SQL 子集;时间统一 BIGINT epoch ms;布尔沿用现有转换约定
5 dsh_hosts / 归属列 不存在 一期就建(哪怕只注册 1 台)⇒ 二期加机器零 schema 变更
6 uid 是否需要 OS 账号未定 setpriv --reuid <数字>;但 /etc/passwd 是 bwrap 白名单必挂项(orchestrator.ts:640,为 os.userInfo()) 一期测掉(§10 第 2 项)—— 它决定二期加机器要不要同步建号

14.3 状态三分类(先分类,再谈存储)

类别 例子 多机后应放哪 迁移时是否跟着用户走
用户数据 dataRoot/users/<id>/{home,ws} 共享存储(NAS) ✅ 跟走
平台状态 模型托管清单、capabilities、storage-report、crash-breaker 日志 PG 或共享目录(唯一一份) ❌ 平台级,不属于某个用户
机器基线 /usr/local/dsh-runtime(glibc/共享工具)、镜像、bwrap 白名单 每台 Worker 各自一份,必须同基线 ❌ 不能跟着走(跟着走反而错)

这张表是"存储可插拔"能否成立的前提:分类做对,二期只改挂载表;分类不做,二期要翻代码。

14.4 反模式清单(一期"先简单来"会锁死二期的五种偷懒)

❌ 反模式 为什么锁死二期
一期先在本机直接 spawn,二期再做 RPC Worker 抽象被绕过 ⇒ 二期重写实例生命周期全链路
一期先用 SQLite 跑("PG 以后再说") 租约/归属必须 PG;二期要补数据迁移 + 双实现回归,风险集中爆发
一期把心跳/租约做成"能跑就行" fencing 缺位 ⇒ 二期开自动接管 = 直接暴露在双写风险下
一期备份用 tar,二期换 restic 两套格式 ⇒ 恢复路径分叉,演练覆盖不全
一期把平台状态继续写在 /opt/dsh/** 二期换存储时状态分裂,且多 Manager 读不到同一份

14.5 一期 DoD(验收时必须已包含这些"二期就绪项")

  • 至少 1 台"远端 Worker"(即使物理上同机)走完整 RemoteSpawner + agent 协议,含幂等键与 fencing;
  • dsh_hosts + 归属/租约列已建,advisory lock 生效,单 Manager 时自动退化为主;
  • PG 跑通全套 scripts/smoke-*.mjs(SQLite 侧同组对比),回滚演练过;
  • 存储能力探测已输出(启动日志能看出"这台是什么后端、哪些能力被降级");
  • 平台状态三分类已收敛(14.2 的 #1/#2 完成);
  • 四层备份已按同一套工具落地(低频即可),恢复演练通过;
  • 自动接管开关存在且默认关闭,人工接管流程演练过;
  • 单活部署实测通过(撤掉 LB 后域名直指一台,全功能可用)。

15. 部署与管理(方便上线、方便日常运维)

用户要求(2026-09-14 21:06):还要考虑如何方便部署和管理。 判据一句话:凡是"要登录某台机器手工做"的事,都必须能用一条命令替代(系统级故障除外)。

15.1 先定部署单元:Worker 必须整机,不能容器化

角色 交付物 理由
Worker 整机镜像(云自定义镜像 / kickstart·preseed 脚本) 🔴 硬约束:实例隔离依赖 systemd-run --scope + bwrap + 每用户 setpriv 改 uid ⇒ 需要真实 systemd 与特权。容器里做不到(做得到也得放弃现有隔离语义 = 换一套实现)。这是与 k8s 模式最本质的差别,别照搬容器路线。
Manager 整机镜像或容器均可 它不需要特权(纯 Node + Fastify),且无状态 ⇒ 容器化收益大、风险低
产物 同一版本产物分发到所有节点 避免"服务器源码 ≠ git"那类静默漂移(我们踩过)

为什么强烈建议整机镜像而不是"每台手工装":§14.3 已把"机器基线"列为不能跟着用户迁移的一类状态(/usr/local/dsh-runtime、bwrap 白名单、系统包)。手工装必然漂移 ⇒ 二期加机器时性能/行为不一致。整机镜像 = 基线一致 + 回滚容易(换镜像 ID)。

15.2 一条命令加机器(Worker join)

目标:新机器从零到接单 ≤ 3 条命令。

# ① 用平台镜像起机(或用 join 脚本在干净系统上跑)
curl -fsSL https://<入口>/join.sh | bash -s -- --manager https://mgr.internal --token <一次性令牌>
# ② 脚本自检 + 注册 + 起服务
# ③ 在门户「集群」页确认它变绿

join.sh 要做的自检(全部硬失败或显式降级,不许静默):

检查项 依据
内核 cgroup v2 / swap 关闭 / nft 可用 k8s 部署时踩过(docs/k8s-deploy.md §1)
bwrap 可运行、setpriv 可用、每用户 uid 可 setuid 隔离前提;同时验证 §14.2 #6(是否需建号)
/usr/local/dsh-runtime 存在且版本匹配(glibc 运行时,档案 76) 否则 univer 等原生插件装不上
存储后端探测(本地盘/NFS)+ 小文件延迟采样 §12.4
出网 443 可达模型 API;内网可连 Manager 网络矩阵
版本自报(GET /version 上报给 Manager) 防版本漂移

注册是幂等的:同一 host_id 重复执行 = 更新记录,不产生重复行。

15.3 首次上线的渐进路径(关键:共享存储不是开通集群的前置)

我此前把"共享存储"写进了必备项,顺着"方便部署"重新推演后发现可以更好 —— 拆成两步,第一步几乎零风险:

步骤 部署形态 现有用户怎么办 拿到什么 需要共享存储吗
1a Manager-01 + Worker-01 同机(Manager 组里有一台兼任 Worker),dataRoot 原地不动 零迁移(老用户目录不动、路径不变) 归属表 + 租约 + 双活门户 + 跨机协议已验证 ❌ 不需要(本地盘即可)
1b 加 Worker-02 + 挂 NAS,把 dataRoot 指向共享层 需要时把用户从 01 搬一次(rsync,低频计划内) 可迁移(改归属 + drain,不搬数据) ✅ 此时引入
2 Worker-02..N + 容量准入 + 自动转移 按需迁移 300 人规模 + 故障转移 ✅

好处:① 第一步不动现有数据 ⇒ 上线风险极低、随时可退回单机;② NAS 的性能风险(小文件)推迟到 1b 再评估,不阻塞主体工作;③ RemoteSpawner + agent 协议在 1a 就已经被真实流量验证(§14 要求的"哪怕同机也走远程协议"正好落在这里)。

⚠️ 1a 的唯一注意点:Manager-01 兼任 Worker 时,门户与实例抢资源(实例 384–1024 MiB)。给该机留足内存,或把 Manager 的实例承载上限设为 0(只做控制/门户,不接实例)。后者更干净:Manager 默认 capacity=0,需要时显式开。

15.4 管理面(门户新增「集群」页,仅 admin)

沿用现有 admin 管理面形态(admin.html + /api/admin/*),新增一页:

区块 显示什么 能做什么
主机列表 host_id / 内网地址 / 状态 / 版本 / 内存水位 / 实例数 / 最后心跳 置 draining、踢出集群、加机器指引
实例归属 用户 → 哪个 Worker / 端口 / epoch / 心跳 单用户迁移、批量迁移整机、强制重启
存储 每台的后端类型 + 能力探测结果 + 降级项 只读(异常时告警)
备份 上次成功时间 / 体积 / 保留策略 / 下次计划 手动触发、一键恢复演练
告警 Worker down、心跳超时、容量水位、备份失败、版本漂移 静音、确认
审计 迁移/接管/孤儿清理的 audit_log 按用户/时间检索

红线不变:破坏性动作(删归属、删用户数据)仍要"先出清单 + 二次确认",与现有纪律一致。

15.5 一键自检与可观测(把 §10 的实测清单变成可重复命令)

工具 作用
dshs doctor 单机自检:隔离能力 / 存储后端与能力降级 / PG / nft / 版本 —— 把 §10 的 5 项实测固化成命令
dshs cluster status 全集群:每台状态/版本/水位/归属数/心跳
GET /metrics + Prometheus 控制面副本数、PG 连接、每 Worker 实例数、崩溃计数、租约抢主次数、迁移次数
集中日志(Loki/SLS) 多 Worker 日志聚合(现在单机 journald 到集群就失效)
统一 trace id 🔴 一个请求从入口 → Manager → Worker → 实例要有同一个 id,否则跨机排查只能靠人肉 grepping(这是集群化后最容易后悔的地方)
告警规则 控制面 < 2 副本、PG 不可写、心跳超时、Worker 水位 > 85%、备份失败、版本不一致

15.6 灰度升级与回滚(集群化最大的运维红利)

单机时代升级要停机;集群后:

① 置 Worker 为 draining(不再接新实例)
② 迁移 / 等待其上实例自然回收
③ 停服 → 换镜像/产物 → 起服 → 自检 → 置回 up
④ 观察一个周期再推下一台(**一次只动一台**)
  • Manager 升级:先升不持控制主的那台 ⇒ 观察 ⇒ 触发切换 ⇒ 再升另一台(门户始终有活副本);
  • 版本策略:Worker 版本可与 Manager 不同大版本内共存(滚动窗口),但 Manager 必须能兼容 N-1 的 Worker ⇒ 协议要向后兼容(新增字段可选、不删旧字段);
  • 回滚:镜像/产物按版本目录保留,systemd unit 指向"当前版本"符号链接 ⇒ 回滚 = 切链接 + 重启;
  • ⚠️ 发布前必须 CI 绿:现状 scripts/ci.sh 非 0 退出(test/db.test.mjs 一项长期红,属别人 lane)⇒ 上集群前必须让 CI 恢复可信,否则"发布可信度"这个前提不成立。

15.7 运维任务表

频率 任务
每日(自动) 心跳/水位/告警巡检、四层备份(§7)、租约过期扫描
每周 备份产物抽验、版本漂移检查、容量趋势
每月 恢复演练、迁移演练(真迁一个用户)、fencing 演练(模拟老 holder 复活)、PG 备份校验、扩缩容评审
变更时 灰度升级(一次一台)、加/减 Worker、PG 升配或迁 RDS(逻辑复制)
应急 Worker down 接管(一期人工确认)、Manager 单点恢复、PG 故障切换、存储不可达降级

15.8 与现有运维资产的衔接(都需要更新)

现有资产 集群化后
DEPLOY-本部署.md 新增/派生 DEPLOY-集群.md(Manager 组 + Worker 组 + PG + 存储 + join 流程 + 灰度)
02-运维手册.md 新增「集群运维」章(主机管理、迁移、接管、故障矩阵 §11.4、运维任务表)
ops/(nginx / scripts) 增加 join.sh、dshs doctor、集群巡检脚本
scripts/backup-platform.sh 升级为四层备份编排(快照 + restic + WAL)
03-路线图与待办.md / 交接单/ 按一期/二期拆单(本期只出方案,不动文档库 → 见文末说明)

⚠️ 文档库改动纪律:以上文档都在受保护根 dsh-server-docs/ 内 ⇒ 动它们必须先抢全局执行锁,且按"档案只增不改"追加而非改写历史。


16. 域名与组网(只有入口需要域名;跨机房能组网,但有代价)

16.1 域名/证书需求矩阵(结论:只有入口一处需要)

角色 需要公网域名? 需要证书? 说明
入口 nginx(现有腾讯云机) ✅ 唯一需要:domain + *.domain ✅ 通配证书 用户只认这两个名字
Manager ❌ 不需要 可选(内部加密才用) 只是入口的一个 upstream(内网 IP:3080 或 VIP 背后的成员)
Worker ❌ 不需要 ❌ 不需要 只被 Manager 以内网 IP 访问;Worker 上不装 nginx、不配证书
PG ❌ 不需要 ❌ 内网地址
共享存储 ❌ 不需要 ❌ 挂载点(内网 NFS 挂载)

三条容易被误解的点:

  1. 多 Manager 不需要多个域名、也不需要会话粘性 —— 登录态靠"PG 会话表 + 同一个 cookieDomain"共享,而不是靠每台有自己的域名。给 Manager 各配子域(mgr1.domain)不需要也不建议(只会引入粘性会话复杂度);
  2. Manager 之间不需要网络直连 —— 控制面互斥靠 PG advisory lock,只要"都能连同一个 PG"即可(这是本方案的一个结构性优点:Manager 组之间没有额外的对等网络要求);
  3. 备案是域名的属性,不是服务器的属性 —— 服务器本身不需要"配置域名"。若在阿里云侧走公网 SLB 提供域名入口会被 ICP 校验拦(已记录,§2.4);现状生产是 CF 回源到 ECS 直连,不受影响。

建议(非必需):给 Manager/Worker 配内网 DNS 名(mgr.internal、worker-01.internal)只为运维好读;用 IP 完全等价 —— 但防火墙白名单请写网段而不是单个 IP(见 §16.5)。

16.2 组网三档("不在同一个 IP 下的服务器"能否组网)

能,但要分清处在哪一档 —— 三档的能力与代价完全不同:

档 场景 互通方式 延迟 能否任意迁移用户
① 同 VPC / 同内网 同云同地域的多台 ECS 默认内网互通 最低(0.1–1 ms) ✅ 能
② 跨可用区(同 VPC) 同云同地域不同 AZ 内网仍互通 略高(0.5–2 ms),跨 AZ 流量可能计费 ✅ 能(存储在同一地域)
③ 跨云 / 跨地域 / 混合机房 阿里云 + 腾讯云、云 + 自建 必须建重叠网(overlay):WireGuard / Tailscale·ZeroTier / 专线·CEN 10–30 ms+ ❌ 不能(见 16.3)

③ 的三种落地方式:

方式 特点 适用
WireGuard 每个节点一个 wg 接口,组成加密内网(如 10.99.0.0/24);配置轻、性能好 ✅ 首选(自建、可控)
Tailscale / ZeroTier 基于 WireGuard,自带 NAT 穿透与密钥管理 运维最省;节点多、网络复杂时
专线 / 云企业网 CEN / 云联网 企业级、稳定、不走公网 正式多地域生产
SSH 隧道 应急 ❌ 不做生产(连接数/稳定性差)
纯公网直连 + IP 白名单 + HMAC 能用但攻击面大 ❌ 不推荐

16.3 🔴 跨云场景的三个硬约束(必须写进设计,否则会白做)

# 约束 后果
1 PG 必须与 Manager 同侧 跨云 SQL 10–30 ms + 抖动 ⇒ 每个请求都被放大(§13.3 已列为"绝对不要")。⇒ 跨云时 PG 跟着 Manager 走,不能跨。
2 NAS 不能跨云挂载 NFS over 公网既不安全也不现实 ⇒ 每个云一个存储域 ⇒ 用户不能跨云迁移(数据在另一个云的 NAS 上)。⇒ 跨云只能"云内迁移"。
3 代理流量全走 Manager 用户流量经 Manager → Worker ⇒ 跨云时全部流量跨云(带宽成本 + 延迟叠加)。⇒ Worker 应与 Manager 同地域。

⇒ 架构结论:"任意 Worker 可接手"这个能力,隐含前提是"同地域 + 同一存储域"。 跨云不是不能做,而是会降级成"每个云一个独立集群域" —— 那时更像"两个集群"而不是"一个集群"。若目标是"上千人",建议先在一个云内横向扩,不要过早跨云。

16.4 LB 与 VIP 的实现前提(跨 AZ 会踩)

方案 前提 备注
keepalived + VRRP VIP 主备必须在同一二层网络 跨 AZ 通常不成立 ⇒ 别默认选它
云 HAVIP(高可用虚拟 IP) 云厂商支持,可跨 AZ 漂移 需要该产品支持
云 SLB / ALB(推荐) 无二层要求 后端用 IP 注册,不需要域名;⚠️ 阿里云公网 SLB 有 ICP 校验(§2.4)⇒ 入口侧用腾讯云或内网 SLB

16.5 白名单用网段,不要用单个 IP(配套"一条命令加机器")

若防火墙按"Manager 的单个 IP"写白名单,每次加机器/换 IP 都要改 nft ⇒ 与 §15 的"一条命令加机器"目标冲突。

做法:给 Manager 组一个专用子网/overlay 网段(如 10.99.0.0/24),Worker 侧只放行这一段:

# Worker 侧:只放行 Manager 网段 + 只暴露控制/代理口
tcp dport { 9000-9999 } ip saddr 10.99.0.0/24 accept
tcp dport { 9000-9999 } drop

⇒ 加机器 = 加到该网段即可,防火墙零改动。

16.6 落地建议(按现状)

  1. 域名侧零改动:继续 domain + *.domain → 入口 → Manager(内网 IP/VIP);
  2. Manager 组与 Worker 组放同地域同 VPC(档 ①),先不跨 AZ;
  3. PG 放同 VPC(§13);
  4. 暂不跨云:跨云的三个代价(16.3)与"上千人"目标冲突,等单云跑稳再评估;
  5. 若将来确有跨云需求:先上 WireGuard 重叠网,并接受"云内迁移"(放弃跨云迁移)。

17. 现网实测结论(2026-09-14 21:17–21:25)

按用户给定的实际服务器执行:manager = 47.77.182.89、worker-01 = 47.77.182.89(同机,即 §15.3 的 1a 形态)、worker-02 = 106.54.21.172。 实测方式:本机 SSH + 只读探测(无任何写操作、无端口新增)。

17.1 可达性(结论:全都通,但是跨云)

测试 结果
47.77.182.89 SSH ✅ 可达 —— ⚠️ 端口是 32022,不是 22(bt-server 别名已配)
106.54.21.172 SSH ✅ 可达(端口 22,专用密钥 id_ed25519_test106)
47 → 106 TCP ✅ 22 / 80 / 443 / 8888 全开
106 → 47 TCP ✅ 22 / 32022 / 80 / 443 / 58888 全开
ICMP(ping) ❌ 双向被安全组拒绝(100% packet loss)
RTT 47→106 connect=150 ms;106→47 connect=183 ms;本机回环 0.1 ms
HTTP 往返 47→106 首页 total=300 ms, code=200

两条直接结论:

  1. 🔴 实测 RTT 150–183 ms,比本文档原估的"10–30 ms"高 5–10 倍 ⇒ 跨云代理会让每个请求多 300 ms 以上往返,对话式 UI 与 WebSocket 流式体验会明显退化。原 §16.3 的"不要跨云"结论不变,但理由从"贵"升级为"实测不可接受"。
  2. ⚠️ ICMP 不可用 ⇒ 存活检测绝不能用 ping,必须用 TCP/HTTP —— 本文档 §11 把心跳设计成 HTTP GET /healthz(而不是 ping)恰好正确,继续保持。

17.2 两台基线差异(指向 §14.3「机器基线不能跟着迁移」)

项 47.77.182.89(阿里云) 106.54.21.172(腾讯云)
OS Alibaba Cloud Linux 3.2104 U12 OpenCloudOS 9.6
内核 5.10.134 6.6.119
cgroup v1(tmpfs) v2(cgroup2fs)
glibc 2.32 ⇒ 需挂 glibc 2.35 运行时(档案 76 / univer) 2.38 ⇒ 不需要该 hack
CPU / 内存 2 核 / 1870 MB(可用仅 773 MB) 4 核 / 3655 MB(可用 2655 MB)
swap 有(1 个) 有(/www/swap 1G)
bwrap / setpriv / systemd-run / nft / node / dsh 齐 齐(systemd 255)

⇒ 同一用户在两台机器上可能表现不同(最典型:47 上靠 glibc 运行时才能跑的插件,106 上原生可跑)。这正是"整机镜像统一基线"(§15.1)必要性的实证。

17.3 🔴 两个会影响设计的实测发现

发现 1:uid 必须建号(原"待实测"项已有答案)

setpriv --reuid 100001 ... -- id -u        → 100001   ✅ 无需建号
setpriv --reuid 100001 ... -- node -e 'os.userInfo()'  → FAIL ERR_SYSTEM_ERROR   ❌

⇒ uid 可以 setpriv,但 os.userInfo() 在无 /etc/passwd 条目时直接失败 ⇒ 每台 Worker 都必须为要跑的 uid 建号(或至少写入 passwd 条目)。 ⇒ 落地含义:加机器时必须同步账号(漏了的表现 = 实例启动即崩);这也解释了代码为何把 /etc/passwd 列为 bwrap 白名单必挂项(orchestrator.ts:640)。 ⇒ 对应 §14.2 #6 结论 = 需要建号(§10 第 2 项已闭环)。

发现 2:systemd-run 未限制 swap,而两台机器都有 swap 🟠

  • 代码现状(orchestrator.ts:749-751):只设 MemoryMax=<n>M + TasksMax=128,没有 MemorySwapMax;
  • 有 swap 时 MemoryMax 超限会先换出到 swap(变慢),而不是被 OOM kill ⇒ "被 OOM 杀 → 熔断自愈"(档案 20/78)的语义在有 swap 的机器上不成立;
  • 且两台 swap 大小不同 ⇒ 同一用户在不同 Worker 行为不一致。
  • ⇒ 建议:Worker 统一 -p MemorySwapMax=0(或在 join.sh 自检里发现 swap 即告警/关闭)。
  • ⚠️ 这一条是现状单机就存在的隐患(47 也有 swap),不是集群才引入的 —— 建议单独立项核查。

17.4 106.54.21.172 上已有的一套独立平台(不是裸 Worker)

  • /root/dsh-users-platform/(含 lib/、deploy/、Dockerfile.dsh、install.sh、ensure-role-profile-patch.cjs)
  • systemd dsh-users-platform.service:active running(已跑 16 h+),EnvironmentFile=/etc/dsh-users-platform.env,User=root,Restart=always
  • 监听 127.0.0.1:3080(node);数据根不在 /var/lib/dshs(由该 env 文件指定,本次未读取以免触碰密钥行)
  • 另有宝塔面板(:8888)与 nginx(80/443)

⇒ 它不是一台"待接管的空机器",而是另一套完整平台。要用它当 Worker-02 需要先定:

选项 含义 代价
(a) 保留为独立测试环境(推荐) 集群另起 不干扰现有测试
(b) 停掉它,改造为纯 Worker 这台机器整体并入集群 会中断其上任何在跑的实例
(c) 就地把它变成 1a 集群 Manager-01 + Worker-01 都用它 测试环境与新集群合一

17.5 部署建议(基于实测,与 §16.3 一致但更硬)

结论 依据
🔴 这两台机器不能组成"一个"集群 跨云 150–183 ms ⇒ 代理路径每个请求多 300 ms+,不可接受
⇒ 必须选一个云内扩容(新机器与 Manager/Worker 同云) §16.2 档①(同 VPC,0.1–1 ms)
⇒ 或各自独立(106 测试环境 + 47 生产),不互联 等价于"每云一个集群域"(§16.3)
⚠️ 47 不适合做 Manager 可用内存仅 773 MB,门户+代理+PG 客户端会挤掉实例余量(§11 内存账)
✅ 若要试 1a,优先放 106 4 核 / 可用 2655 MB,且集群化所需的 bwrap/setpriv/nft/dsh 全部就绪

17.6 尚未做的实测(依赖共享存储,等 NAS/存储定下来再跑)

  • 共享存储上的 watcher 行为(skill-filesystem.watchUsePolling)
  • 共享存储冷启动耗时(node_modules 小文件)
  • 每用户数据体积(定备份子集与容量)
  • 跨机代理的信任栅栏验证(§10 第 1 项):需 1a 落地后才可测 —— 现阶段已由源码判定(api-request-trust.ts:103 只看主机名)通过,实测留到 1a 联调时补。

18. 与 K8S 模式的对比(回答"k8s 的优势到底是什么、代价多大")

18.1 先回答"镜像主要是什么" —— 是两个镜像,不止 dsh 主程序

镜像 谁用 说明
DSHS_DSH_IMAGE(dsh 主程序/运行时) 每用户 DSH Pod 的主容器 官方 dsh + 我们的运行时基线
DSHS_CONTROL_PLANE_IMAGE(dshs) ① 控制面 Deployment ② 每用户 Pod 的 tcp-bridge sidecar ③ 每用户 files Pod ④ bootstrap Job 复用同一镜像的子命令,是为了避免依赖 docker.io(ACK 拉不动 alpine/socat,见 docs/k8s-deploy.md §7.5)

镜像分发确实是成本,但要说准:它在磁盘/网络上花钱(ACR 拉取、每节点缓存一份),镜像层本身不额外占运行内存(只读层跨容器共享 page cache)。真正多占内存的是下面的 18.2。

18.2 容器在"运行时"到底多占多少(分三层算,别混在一起)

层 开销 依据
① 每用户 sidecar +90–120 MiB/用户 = tcp-bridge(Node 空转 ~30–40 MiB)+ files sidecar(Node+Fastify ~60–80 MiB,实测 64 MiB 跑不起来→提到 256 MiB) k8s-spawner.ts 的两个 sidecar 容器
② 每节点固定 +300–500 MiB/节点(kubelet + containerd + CNI + node-local-dns) k8s 本身
③ 装箱效率(被低估的一项) Pod requests 决定装箱:每用户 1Gi(dsh)+ 32Mi(bridge)+ 128Mi(files)≈ 1.16 GiB request/用户;而实际 RSS 只有 ~250–350 MiB ⇒ 账面内存比实际多 3–4 倍 k8s-spawner.ts 的 resources.requests

对照自研版(local 语义):

  • 每实例没有 sidecar(tcp-bridge / 文件面都在进程内或按需);
  • cgroup 用的是 MemoryMax上限而非预留 ⇒ 天然超卖、按真实可用内存装箱(这就是为什么单台 1.87 G 的机器能跑 1–3 个实例);
  • 每节点没有 kubelet/CRI 固定开销。

⇒ 1000 并发的账面账:k8s 需要 ~1.16 TiB requests;自研版按实际 RSS 约 300–450 GiB 规划。这个差距不是"加 5–8 台"的问题,而是选型级的差别。 ⚠️ 公平地说:requests 是可调的(可降到 256–512 MiB 并接受超卖),但那等于主动放弃 k8s 最可靠的调度依据,回过头来又要自己写容量准入 —— 优势自我抵消。

18.3 K8S 真正的优势(按"对我们这个形态有没有用"排序)

# 优势 对我们有用吗
1 调度与装箱(自动把实例放到有空闲的节点) ⚠️ 有限 —— 我们的实例是有状态的(绑定用户 home、uid、会话日志),不能被任意调度,只能"同存储域内选择"
2 节点故障自愈(Pod 被重新调度到别的节点) ✅ 有用 —— 这是我们自研版要自己写的那部分(心跳 + 租约 + 重建)
3 声明式期望态 + 控制器 ✅ 有用(但我们的 dsh_instances + reconcile 已经是同一思路)
4 网络策略 / PSA / seccomp / 只读 rootfs ⚠️ 与 bwrap 机制不同、强度相当;且我们已完成 bwrap 收窄实测(/etc 可见项 222→12,档案 39)
5 滚动升级 + PDB ✅ 有用(我们自研靠 drain,效果等价)
6 弹性伸缩(HPA / Cluster Autoscaler) ✅ 有用(但用户数增长是缓慢趋势,不是秒级波峰)
7 多副本控制面 + Lease 选举 ⚠️ 自研用 PG advisory lock 几行就够
8 可观测生态(Prometheus/Loki/事件) ✅ 有用(自研也能接,只是多写点)

18.4 K8S 的代价(5 条,都是实打实的)

# 代价 说明
1 内存账面 +3–4 倍(§18.2 ③) 选型级差异
2 每用户 +2 个 sidecar 进程 每用户多 90–120 MiB
3 每节点 300–500 MiB 固定开销 8 台节点 ≈ 白扔一台机器的内存
4 API 对象数 每用户 6–8 个对象(Pod/Svc/NP/Secret/ConfigMap/files×3)⇒ 1000 用户 ≈ 8000 对象,还要分片命名空间(§6 P0-1/P1-5)
5 平台自研能力要重写 6 处 模型落地直写 home、角色补丁/picker、restartAndProbe、breakerInfo/quotaInfo、touch/launchToken、备份与清理脚本(§18.5)

18.5 判定:本方案仍选自研 manager/worker,理由三条

  1. 我们的实例是"有状态长驻进程" ⇒ k8s 的看家本领(任意调度、快速重建)价值被削弱,而它的固定代价(sidecar、requests 保守、节点开销)全额付出;
  2. 隔离与平台能力已经在 local 语义上做完并实测过(bwrap 收窄、模型落地、角色补丁、插件探活、配额观测、熔断)⇒ 走 k8s 等于把这些逐条重写在容器语义上(上一轮列过 6 处缺口);
  3. 自研版需要自己写的,只有"调度 + 自愈 + 租约"三件事 —— 而这三件事的语义在我们这里很窄(同存储域内选机、TTL 租约、drain 重建),本方案 §3/§11 已给出可照抄的实现路径。

什么时候该反过来选 k8s:用户实例变成短生命周期 / 无状态(例如每次会话一个 Job、无用户私有 home),或者团队已经有成熟 k8s 运维能力且不想维护自研控制器时。当前形态不属于这两种。

一句话:k8s 卖的是"你不用自己写调度器和自愈",而对我们这种"每用户一个有状态常驻进程"的形态,这两样恰好是我们能自己写、且已经设计完的部分 —— 却要为它付 3–4 倍的内存账面和一次全量能力重写。


19. 本期验证策略与"访问模式兼容"约束

19.1 跨云环境当"极限场景测试床"(✅ 可行,且推荐)

47(阿里云)↔ 106(腾讯云)实测 RTT 150–183 ms、ICMP 被禁、无内网 ⇒ 这正是一个现成的恶劣网络环境,用来验证容错比局域网更有价值:

用它测什么(✅ 有价值) 用它测什么(❌ 会误判)
心跳/租约在 150 ms RTT 下是否稳定(TTL 30 s 足够宽) 生产性能基线(150 ms 会污染结论)
代理的超时与重试设置是否合理(半开连接、SSE/WS 长流) 用户真实体验(会明显偏慢,不代表同云表现)
断网/丢包故障注入:fencing 是否真的拦下老 holder、重连是否自愈 存储小文件性能(共享存储未上,测不了)
"跨云不可用"的判定验证(把 §16.3 的结论从推理变成实测) 容量/装箱结论(网络慢不影响内存)

但要立一条纪律:跨云 = 故障注入与容错测试床,不是性能基线环境。所有性能/容量结论必须在同云同 VPC 环境取数,否则会被 150 ms 误导。

这条对方案的价值:§11.4 恢复矩阵里那些场景(心跳超时、Worker 失联、Manager 单点、PG 不可达、脑裂)都可以在这个跨云环境里人为制造,比在单机模拟更真实。

19.2 "47 限制只能 admin 访问,留足够内存"(✅ 可行)

可行,且不需要改代码 —— 现有平台本来就是注册审核制(注册 → 待审 → admin 放行),把 47 设为"只有 admin 能用"就是不放行新用户 + 必要时对存量用户按 disable(已有路由 /api/admin/users/:id/disable)。

内存账(47:2 核 / 1870 MB,实测可用 773 MB —— 该值含当时在跑的实例):

项 量
系统基础 ~300 MB
dshs Manager(门户+代理+PG 客户端) ~200–300 MB
admin 实例(档案 81 配额 384–672 MB) ~400–670 MB
合计 900–1270 MB
⇒ 仅留 admin 后 停止其它实例可释放数百 MB ⇒ 可行,但余量很薄(< 300 MB)

⚠️ 但有一个必须知道的坑(1a 形态的固有代价):

LocalSpawner 构造函数会调 cleanAllStaleScopes()(orchestrator.ts:164 + 档案 30:"portal 启动即清掉遗留 scope")⇒ Manager 每次重启都会停掉同机所有实例 scope。 ⇒ 在 1a(Manager 兼 Worker 同机)下,升级/重启 Manager = 该机实例全断,随后按需重建。 规避:① 接受它(admin 自己用,可接受);② 把该机 capacity=0(不承载实例,只做门户/控制);③ 只有在共享存储就绪后才把"Manager 与实例同机"当成正式形态。

结论:47 可以做 Manager(门户),但内存余量薄,且必须接受"Manager 重启清实例"。若求稳,Manager 应放在 106 那类 4 核/3.6 G 的机器上,47 只做实例承载。

19.3 访问模式兼容清单(改造后必须与现在的"单实例访问模式"一致)

维度 现状 改造后必须保持 是否天然满足
门户 URL https://alotbuy.com 不变 ✅(入口不动)
实例 URL https://<用户名>.alotbuy.com 不变 ✅(Host 路由不变)
Cookie Domain=.alotbuy.com,会话查 DB 不变 ✅(已满足)
登录直达会话(enter 带 launch token) 从实例 stdout 解析 token 拼 URL 不变 ⚠️ 需专门做(§4.1 的 launchToken 回传,P0-6)
实例侧 401 自愈(档案 24/50/51) 代理透明重放 / 注入脚本 不变 ⚠️ 依赖上一条
门户 API 路径(/api/**) 不变 不变 ✅
「我的文件」面板 读本机 dataRoot/users/<id>/ws 跨机可读 ⚠️ 需 file-service(P0-7)
单机部署仍可跑 systemctl restart dshs 保留 deployMode=local 路径,代码不删 ✅(§8.4 已定)
用户目录结构 dataRoot/users/<id>/{home,ws} 不变 ✅(§12.4 挂载点即切)
管理员操作入口 门户 admin 页 新增「集群」页,旧页面不动 ✅

⇒ "兼容单例访问模式"的落点集中在 3 处:launch token 回传、401 自愈链路、文件面跨机。其余(URL/cookie/API/目录结构/单机可退)天然满足,不需要额外工作。