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)、记忆修复前备份。
This commit is contained in:
commit
ce8e6ceed9
396 files changed
+66045
No files matched your search
@@ -0,0 +1,284 @@
|
||||
# 会合 / 中继从 Manager 拆分 — 只读取证 + 可执行改造方案(2026-09-16)
|
||||
|
||||
> 🟢 **状态:已完成使命 · 仅存档(2026-09-17 16:4x 由序 ⑯ 规划棒复核后标注)**
|
||||
> **复核结论 = 判「不做」**:本方案 §4 的四个耦合点(C1–C4)与两步改造(S3 / S4)已**逐条**被后续序次覆盖,**未覆盖部分 = 无**。
|
||||
> C1 → 序② P0-2(`DSHS_RENDEZVOUS_URL` 优先、`DSHS_TUNNEL_TARGET` 降兜底,`src/config.ts:485-490`)|C2 → **R5**(Manager 只拨出、落点搬到本机回环池 `127.0.0.1:25000+`,`src/config.ts:498-500`)|C3 → S2/P2(`dsh_hosts.via` + `RendezvousRegistry`)|C4 → 本方案 §8.1 实测**前提不成立**|S3 → 本方案 §9.1 自我证伪后改「实例端口区间隔离」|S4 → R2(`dshs-relay` 独立单元)+ 序⑥ S8(106 升格第二中继)+ 序⑦(多实例切流实测)。
|
||||
> ⛔ **勿再按 §4 的 S0–S4 开工**。复核证据 = 工作区根 `交接单_在册收尾_20260917.md` **§0.1**(逐条对照表)。
|
||||
> ⚠️ 正文**不改**(历史档案属性)—— 上表即勘误入口。
|
||||
|
||||
> **来源**:`接续入口_覆盖网络线_20260916.md §2 第 2 条`(第一优先 · 真活)。
|
||||
> **本步只读**:⛔ 未改代码、未重启、未动服务器。全部结论来自 `D:\github\dsh_shenxian` 工作树(HEAD `4e3a1a4`)的源码静态阅读。
|
||||
> **为什么是前置**:中继绑在 Manager 上 ⇒ 多区域 / 多中心 / 骨干层**全部**做不了。这一刀不切,后面九大瓶颈方案里的中继容量、45% 中继规划都无从落地。
|
||||
|
||||
---
|
||||
|
||||
## 0. 结论先行
|
||||
|
||||
| 判定 | 内容 |
|
||||
|---|---|
|
||||
| **现状** | 「会合 + 中继」**不是一个组件,而是 Manager 主机上 sshd 的副作用** —— 会合点 = `47.77.182.89:32022`,中继落点 = Manager 的 `127.0.0.1`。 |
|
||||
| **真问题** | 功能能跑(档案 103 已判"现有架构给出 80% 形状")。⚠️ **卡点是"耦合写死在 4 处",不是"协议不对"**。 |
|
||||
| **因此第一步不是换协议** | ⛔ 不要一上来换 WireGuard / TURN。**先把会合地址与中继落点抽成配置与接口**,让 SSH 隧道退化成"第一个可替换实现"。**每一步独立可验、独立可回滚。** |
|
||||
| **分层口径(已定,勿破)** | 会合 = 位置视图(可多实例、可无状态);中继 = 数据面(可多实例);**归属 / 租约 / 骨干资格 = 只能控制面写**(本方案不碰)。 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 取证:现状到底长什么样
|
||||
|
||||
### 1.1 链路(跨机形态,实测形态)
|
||||
|
||||
```
|
||||
Worker 106 Manager 47
|
||||
┌────────────────────────────┐ ┌──────────────────────────────┐
|
||||
│ dshs-worker.service │ │ dshs.service (deployMode= │
|
||||
│ WorkerAgent :19000 (lo) │ │ cluster) │
|
||||
│ └─ SshTunnel ──── ssh -R ─┼───────────────▶│ sshd :32022 │
|
||||
│ 每实例 :PORT (lo) │ ControlMaster │ └─ 反转到 127.0.0.1:PORT │
|
||||
└────────────────────────────┘ │ RemoteSpawner → agentUrl │
|
||||
│ = http://127.0.0.1:19000 │
|
||||
┌─────────────────────────▶ proxy.ts → out.host = 127.0.0.1:PORT
|
||||
│ │ PostgreSQL :15432 (静态转发)│
|
||||
│ └──────────────────────────────┘
|
||||
用户浏览器 → alotbuy.com → 47
|
||||
```
|
||||
|
||||
### 1.2 关键代码位(行号 = 本次阅读的 HEAD)
|
||||
|
||||
| 角色 | 文件:行 | 事实 |
|
||||
|---|---|---|
|
||||
| 隧道实现 | `src/worker/tunnel.ts:96-120` | `ssh -M -N -f -S <ctl> -i <key> -R <port>:127.0.0.1:<port>` —— **两端同端口号** |
|
||||
| 隧道加/减 | `src/worker/tunnel.ts:147` / `:163` | `ssh -O forward` / `-O cancel`,靠 **ControlMaster 复用同一条长连接** |
|
||||
| 隧道自愈 | `src/worker/agent.ts:183-219` | `healTunnel()` 20 s 定时器 + 心跳里顺带 `-O check` |
|
||||
| Worker 侧配置 | `scripts/switch-C-worker.sh:19-20` | `[email protected]:32022`、`DSHS_TUNNEL_IDENTITY=/root/.ssh/tunnel_ed25519` |
|
||||
| 隧道开关 | `src/worker/agent.ts:154` | `tunnelTarget === '' ⇒ 完全不建隧道`(同机形态零影响) |
|
||||
| Manager 侧配置 | `src/config.ts:350-354` | `DSHS_CLUSTER_HOST_ID` / `_AGENT_URL` / `_AGENT_TOKEN` / `_INSTANCE_HOST` |
|
||||
| 装配 | `src/web/server.ts:256` | `hostsProvider` 把 `dsh_hosts.endpoint` **直接当 `agentUrl`**,`instanceHost` 取全局 `clusterInstanceHost` |
|
||||
| 访问面 | `src/supervisor/proxy.ts:109` | `out.host = 127.0.0.1:<port>` —— 代理**写死 loopback** |
|
||||
| 数据面契约 | `src/supervisor/remote-spawner.ts:24-34` | `ClusterHost{hostId, agentUrl, token, instanceHost}` —— **没有"经谁可达"这个字段** |
|
||||
| 注册 | `src/web/routes/admin.ts:184-203` | `POST /api/admin/hosts`(join 脚本调用,幂等) |
|
||||
| 心跳 | `src/worker/agent.ts:255-262` | `/healthz` 回 `{ok, hostId, instances, tunnel:{ready,ports}}` |
|
||||
| 选机 | `src/web/server.ts:300-318` | 粘性优先 → 容量准入(`capacityMb<=0` 不限 / `-1` 禁用) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 四个耦合点(这是本方案的靶子)
|
||||
|
||||
| # | 耦合点 | 具体表现 | 不拆的后果 |
|
||||
|---|---|---|---|
|
||||
| **C1** | **会合地址硬编码在 Worker 的 env** | `[email protected]:32022` 写死在 `switch-C-worker.sh` + `/etc/dshs-worker.env` | 会合点换机 / 加第二台 ⇒ **每台 Worker 都要改 env 重启**;会合点不能独立部署 |
|
||||
| **C2** | **中继落点命名空间 = Manager 的 loopback(且两端同端口号)** | `tunnel.ts` 用 `-R <port>:127.0.0.1:<port>`;`proxy.ts:109` 写死 `127.0.0.1` | ① 端口空间与 Manager **全局共享**(冲突面 = 所有 Worker 实例端口并集)② **第三台机器无法直达 Worker**(只有 Manager loopback 有那个端口)③ 中继不可多实例(多实例无从分配端口) |
|
||||
| **C3** | **可达性登记把"经谁中转"磨掉了** | `dsh_hosts.endpoint` 存的是 `127.0.0.1:19000` —— 语义上**不是 worker 的地址,是 Manager 本地一个转发口**;`hostsProvider` 原样当 `agentUrl` | 换会合/中继组件时,表里无法表达「via(经哪个中继)+ 真实可达地址」⇒ **必须改表**,越晚改代价越大 |
|
||||
| **C4** | **控制面 PostgreSQL 也走同一条隧道(静态转发)** | `DSHS_TUNNEL_STATIC_PORTS`(`src/worker/agent.ts:157`),设计上含控制面 PG | 会合组件一换,**控制面库的连接路径一起断** ⇒ 回滚面比看上去大 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标形状(分层,且**不新建权威状态**)
|
||||
|
||||
| 组件 | 职责 | 可以多实例? | 权威状态 |
|
||||
|---|---|---|---|
|
||||
| **会合 (rendezvous)** | 收 Worker 注册、回"该拨谁"、下发中继分配;**不承载数据面流量** | ✅ 无状态可复制 | ❌ 只有位置视图 |
|
||||
| **中继 (relay)** | 数据面:Worker 拨它 → Manager / 其他节点经它到 Worker | ✅ | ❌ |
|
||||
| **控制面 (Manager)** | 归属 / 租约 / 骨干资格 / 容量准入 | ❌ **单点** | ✅ 唯一写入者 |
|
||||
|
||||
**硬约束**:会合与中继**不得**写入 `dsh_instances.host_id` / `epoch` / 骨干资格 —— 否则就是双写脑裂(与 `集群化改造方案 §1.3` 一致)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 分步方案(S0–S4 必做 · S5 后续)
|
||||
|
||||
### S0 — 抽接口(**零行为变化**)
|
||||
|
||||
- **改哪些文件**
|
||||
- 新增 `src/net/rendezvous.ts`:`interface Rendezvous { id: string; dialTarget(): string; resolve(hostId): Promise<Reachability|undefined> }`
|
||||
- 新增 `src/net/reachability.ts`:`interface Reachability { hostId: string; via: string; address: string; scheme: 'http'|'https' }`
|
||||
- 改 `src/supervisor/remote-spawner.ts:24-34`:`ClusterHost` 增 `reachability?: Reachability`,`agentUrl` 降级为派生 getter(保向后兼容)
|
||||
- **接口怎么切**:`RemoteSpawner` / `RemoteUserFs` / `proxy.ts` 三处**都只消费 `Reachability`**,不再各自拼 `http://127.0.0.1:<port>`。
|
||||
- **怎么验**:`npm run build` 通过 + `node scripts/verify-cluster-cross.mjs` 全绿(**行为零变化**是唯一验收标准)。
|
||||
- **怎么回滚**:纯新增文件 + 可选字段,`git revert <commit>` 即可。
|
||||
|
||||
### S1 — 会合地址出 env(打掉 **C1**)
|
||||
|
||||
- **改哪些文件**:`src/worker/agent.ts:154-172`、`src/worker/tunnel.ts:26-65`、`src/config.ts:350-354`
|
||||
- **接口怎么切**:新增 `DSHS_RENDEZVOUS_URL`(`ssh://[email protected]:32022` 或未来的 `https://...`);`DSHS_TUNNEL_TARGET` **保留为兜底**(未设 `DSHS_RENDEZVOUS_URL` 时走它)⇒ 两台机器可分先后改。
|
||||
- **怎么验**:改一台 Worker 的 env → `systemctl restart dshs-worker` → `curl -H <token> http://127.0.0.1:19000/healthz` 里 `tunnel.ready === true`;Manager 侧 `verify-cluster-cross.mjs` 通过。
|
||||
- **怎么回滚**:删掉新 env 即在走旧路径(代码同时保留两条),**不需回滚代码**。
|
||||
|
||||
### S2 — `dsh_hosts` 增 `via` 列(打掉 **C3**)
|
||||
|
||||
- **改哪些文件**:`src/db/adapter.ts:115-121`、`src/db/pg.ts:602-646`、`src/db/repo.ts:601-625`、`src/db/sqlite.ts:343-366`、`src/web/server.ts:256-266`、`src/web/routes/admin.ts:184-203`
|
||||
- **接口怎么切**
|
||||
- 迁移:`ALTER TABLE dsh_hosts ADD COLUMN via text NOT NULL DEFAULT 'manager-ssh'; ADD COLUMN address text;`
|
||||
- 回填:把现 `endpoint` 里的 `127.0.0.1:19000` 拆成 `via='manager-ssh'` + `address='127.0.0.1:19000'`(**`endpoint` 列保留不删**)
|
||||
- `hostsProvider`:**先读 `via` → 选对应 `Rendezvous` 实现 → 解析成 `Reachability`**;`via` 未设时回退 `endpoint` 原语义。
|
||||
- **怎么验**:脚本断言「迁移前 `agentUrl`」与「迁移后 `resolve()` 结果」**逐条相等**(这就是验收判据,不靠肉眼)。
|
||||
- **怎么回滚**:列有默认值 ⇒ 旧代码读 `endpoint` 完全不受影响,**列可留着不删**。
|
||||
|
||||
### S3 — 中继落点命名空间可配(打掉 **C2 的一半**)
|
||||
|
||||
- **改哪些文件**:`src/worker/tunnel.ts:142-160`、`src/supervisor/proxy.ts:109`
|
||||
- **接口怎么切**:新增 `DSHS_RELAY_LOCAL_NAMESPACE`(默认 `127.0.0.1`)⇒ 允许每条转发落在**独立回环地址**(`127.0.0.2`、`127.0.0.3`…),由会合**下发端口**而非"两端同号"。
|
||||
- ⚠️ **同号是 `endpointFor` 免映射表的前提** ⇒ 改不同号**必须**同时给 `ClusterHost` 加 `portMap`。所以本步**验收必须含两条端到端**,不能只看健康检查。
|
||||
- **怎么验**:① `verify-cluster-cross.mjs` 全绿 ② **跨机实例页能打开** ③ **跨机工作区文件能读能写**(文件面与实例面同一份路由,见 `server.ts:274-288` 的注释)。
|
||||
- **怎么回滚**:不设该 env 即回到"同号"原状。
|
||||
|
||||
### S4 — 中继独立成单元 + 会合可换机(打掉 **C2 的另一半 + C1 的尾巴**)
|
||||
|
||||
- **做什么**:把"接收 Worker 反拨"这半边从 Manager 的 sshd 里搬出来 → **独立 systemd 单元 `dshs-relay.service`**(第一版仍用 sshd,但**独立端口 + 独立 `authorized_keys` + 独立系统账号**)。Manager 通过 `via` 指过去。
|
||||
- **为什么这样切**:**不换协议、只换绑定关系** ⇒ 已跑通的隧道逻辑 0 改动,风险面最小;换来的是"中继可换机、可多实例、Manager 不再兼任会合"。
|
||||
- **怎么验(三条,缺一不可)**
|
||||
1. 单中继实例下全链路通(实例面 + 文件面)
|
||||
2. **停掉中继** ⇒ 同机形态实例仍可用、跨机代理失败但**不崩**(验证"中继非必需依赖")
|
||||
3. 会合指向**第二个**中继实例,新 Worker 注册后能被 Manager 正确解析(验证多实例)
|
||||
- **怎么回滚**:`dsh_hosts.via` 改回 `manager-ssh` + `systemctl disable --now dshs-relay`。
|
||||
|
||||
### S5 — 443/TCP 兜底通道(**后续,非本方案前置**)
|
||||
|
||||
已定口径(异构设计:企业 / 校园网常封 UDP)。落地时挂到 **relay** 上做传输变体,不动会合接口。
|
||||
|
||||
---
|
||||
|
||||
## 5. 待现场复核(本轮只读,未在服务器执行任何命令)
|
||||
|
||||
| # | 待复核项 | 为什么重要 | 若为否的影响 |
|
||||
|---|---|---|---|
|
||||
| 1 | 106 上 `DSHS_TUNNEL_STATIC_PORTS` 是否真含控制面 PG 端口 | 决定 **C4** 是否成立、回滚面是否含 DB | 仅影响回滚清单;S2/S3 不变 |
|
||||
| 2 | `dsh_hosts` 里 `w-106` 那条 `endpoint` 的实际值 | 决定 S2 回填脚本的取值 | 需先取真实值再写迁移 |
|
||||
|
||||
**下令复核的命令(只读,未执行)**
|
||||
```bash
|
||||
# 远程服务器 106
|
||||
systemctl show dshs-worker -p Environment
|
||||
# 远程服务器 47
|
||||
sudo -u postgres psql -d dshs -c "SELECT id, endpoint, status, capacity_mb FROM dsh_hosts ORDER BY id;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与反模式
|
||||
|
||||
| 风险 | 处置 |
|
||||
|---|---|
|
||||
| 把会合做成"藏了权威状态的隐形控制面" | 会合只读位置视图;归属写入路径**一律**仍走 `db.claimInstance`(`epoch+1` fencing 不变) |
|
||||
| 一步到位换传输协议 | ⛔ 反模式。S0–S4 全程**不换协议**,只换绑定与寻址 |
|
||||
| S3 只看健康检查就收工 | ⛔ 端口映射一改,`endpointFor` 的隐含前提就变 ⇒ 必须跑通"实例页 + 文件读写"两条端到端 |
|
||||
| 先改表再改代码(或反之) | `via` 列有默认值 ⇒ 顺序必须是**先加列 → 再改代码 → 最后回填**,任一步中断都不崩 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付边界(本步)
|
||||
|
||||
- ✅ 产出:本方案(改哪些文件 / 接口怎么切 / 分几步 / 每步怎么验 / 怎么回滚)。
|
||||
- ⛔ **未做**:未改任何代码、未重启任何服务、未动 47 / 106。
|
||||
- **下一步**:按 S0 开工即可 —— 纯新增文件 + 类型扩展,零行为变化,是整条链里风险最低的入口。
|
||||
|
||||
---
|
||||
|
||||
# 8. 现场复核结论 + 方案勘误 + S0 执行记录(2026-09-16 13:1x 追加)
|
||||
|
||||
> 本节的复核命令**全部只读**(`systemctl show` / `cat` / `ss` / `psql SELECT`),未改服务器任何状态。
|
||||
> 执行环境:本机 → `[email protected]`(22)+ `test106`(`~/.ssh/id_ed25519_test106`)。
|
||||
|
||||
## 8.1 §5 两项待复核 —— 已复核
|
||||
|
||||
| # | 项 | 实测结论 |
|
||||
|---|---|---|
|
||||
| 1 | 106 是否真含控制面 PG 的静态转发 | ❌ **不含**。106 `/etc/dshs-worker.env` **没有** `DSHS_TUNNEL_STATIC_PORTS`;47 的 worker env **也没有**(且 47 根本没设 `DSHS_TUNNEL_TARGET` ⇒ 同机形态**完全不建隧道**)⇒ `staticPorts` 只有 agent 自身端口 ⇒ **C4 在本形态下不成立,回滚面不含 DB** |
|
||||
| 2 | `dsh_hosts` 里各条 `endpoint` 的实际值 | `w-106` = `http://127.0.0.1:19000`|`w-47` = `http://127.0.0.1:19100` |
|
||||
|
||||
**47 端口面实测**(`ss -lntp`):`0.0.0.0:32022` sshd(会合点)|`127.0.0.1:19000` sshd=**106 的隧道落点**|`127.0.0.1:43937` sshd=**106 上那个实例的端口,同号反转**|`127.0.0.1:19100` node=w-47 的 agent|`127.0.0.1:15432` postgres。
|
||||
⇒ §2 的 C2「中继落点 = Manager loopback + 两端同号」**实测成立**;`/healthz` 亦证实 `w-106` 的 `tunnel.ports = [19000, 43937]`。
|
||||
|
||||
## 8.2 三处必须勘误(照字面做会出事故)
|
||||
|
||||
**① 🔴 `proxy.ts:109` 不是中继连接目标 —— ⛔ S3 绝不能改它。**
|
||||
`buildUpstreamHeaders()` 里的 `out.host = \`127.0.0.1:${port}\`` 是**发给上游 dsh 的 HTTP `Host` 头**,源码注释已写明原因:dsh 的 `/api` 信任栅栏要求 Host 是 loopback(或 `--trusted-host` 授权项),否则**每条 `/api` 调用都 403**。
|
||||
真正的连接目标在 **`proxy.ts:138-139`** 的 `{ host: endpoint.host, port: endpoint.port }`(来自 `endpointFor`)。
|
||||
⇒ S3 做「中继落点命名空间可配」时,**只动连接地址那一侧**;改动 `proxy.ts:109` 会让实例(不只跨机)的 `/api` 全量 403。
|
||||
|
||||
**② 🔴 两条 `endpoint` 字符串同形、语义不同 ⇒ S2 回填不能一刀切 `via='manager-ssh'`。**
|
||||
- `w-47` → `127.0.0.1:19100` 是 **node 自己监听的端口**(同机直连)⇒ `via = 'local'`
|
||||
- `w-106` → `127.0.0.1:19000` 是 **47 上 sshd 的隧道落点** ⇒ `via = 'manager-ssh'`
|
||||
|
||||
§4 S2 原文只写了后者(把 `127.0.0.1:19000` 拆成 `manager-ssh`)⇒ **按行回填别按字面**。
|
||||
|
||||
**③ ⚠️ 47 的 cluster 配置在 systemd drop-in,不在 `/etc/dshs.env`。**
|
||||
实际位置 = `/etc/systemd/system/dshs.service.d/cluster.conf`(`DSHS_DEPLOY_MODE=cluster` / `DSHS_DB_URL` / `DSHS_CLUSTER_*` 十条;`/etc/dshs.env` 里**没有** cluster 段)。§5 的回滚命令(删 drop-in → `daemon-reload` → `restart dshs`)**正确,无需修改**,只是换文件位置要照此。
|
||||
|
||||
## 8.3 S0 执行记录(**已落地本机 · 未部署**)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/net/reachability.ts` | **新增**:`Reachability` + `agentBaseUrl()` + **`agentBaseUrlOf()`(全仓唯一取址入口)** + `parseReachability()`/`toEndpoint()`(互逆)+ `VIA_LOCAL` / `VIA_MANAGER_SSH` |
|
||||
| `src/net/rendezvous.ts` | **新增**:`Rendezvous` 接口 + `LocalRendezvous` + `ManagerSshRendezvous` + `RendezvousRegistry`(**S2 才接调用点**,本轮不消费) |
|
||||
| `src/supervisor/remote-spawner.ts` | `ClusterHost.agentUrl` → **可选**、新增 `reachability?`;构造期归一化移除(移到取址处、幂等);`call()` / `touch()` 改走 `agentBaseUrlOf()` |
|
||||
| `src/web/server.ts` | 两处 `agentFor` 改 `agentBaseUrlOf(h)` —— **类型收紧后必须一起改,方案 §4 S0 未列出** |
|
||||
| `test/reachability.test.mjs` | **新增** 8 条断言:把**现网真实两行 endpoint** 写死为判据 ⇒「解析前后 agent 基址逐字相等」**不靠肉眼** |
|
||||
| `package.json` | `test` / `verify` 挂上新测试 |
|
||||
|
||||
- **验收实测**:`npm run build` RC=0 | `npm test` **44 项 / 0 失败**(新增 8 项全绿)。`lib/` 产物里已无 `host.agentUrl` 直接拼接。
|
||||
- **为什么未部署**:S0 零行为变化 ⇒ 部署**没有任何可观察收益**,却要 `restart dshs` 打断在线实例 ⇒ **与 S1 合并上线**(一次重启换一个真实变化)。⛔ 这不是"漏了部署"。
|
||||
- **有意未收敛的两处**(避免本轮动 5 个文件,留给 S2 与 `via` 列一起改):
|
||||
① `leased-spawner.ts` 的 `agentFor` 契约仍是 `{agentUrl, token}`;
|
||||
② `remote-user-fs.ts:57/73` 自己又做了一次 `.replace(/\/$/,'')`。
|
||||
两处的取值**都已来自经过 `agentBaseUrlOf()` 的 `agentFor`** ⇒ **行为正确**,只是还没换成 `Reachability` 形态。
|
||||
- **回滚**:`git checkout -- src/supervisor/remote-spawner.ts src/web/server.ts package.json` + 删 `src/net/` + `test/reachability.test.mjs`(纯新增 + 可选字段,无数据迁移)。
|
||||
- **下一步**:S1(会合地址出 env,打掉 C1)—— 新增 `DSHS_RENDEZVOUS_URL`、`DSHS_TUNNEL_TARGET` 降为兜底,两台机器可分先后改。
|
||||
|
||||
---
|
||||
|
||||
# 9. 第二轮勘误(2026-09-16 15:2x · 本机 + 双机只读实测)
|
||||
|
||||
> 本轮复核了 §4 的可执行性,**结论:S3 的机制不可用,须换做法**;另外 S1 的部署面在本方案里写漏了 47。
|
||||
> 操作层细节(命令 / 验收 / 回滚 / 清理)已落 **`交接单_覆盖网络落地执行_20260916.md` v2 §0 + §3**,本节只记**设计层的判断**。
|
||||
|
||||
## 9.1 🔴 S3 的 `DSHS_RELAY_LOCAL_NAMESPACE`(回环别名)**实测无效** —— 机制换成「端口区间隔离」
|
||||
|
||||
**实测**(本机 → 106 → 47,测完已清理):自 106 发起
|
||||
`ssh -R 127.0.0.2:19999:127.0.0.1:19000 -o ExitOnForwardFailure=yes root@47.77.182.89:32022`
|
||||
⇒ 47 上落点实际是 **`127.0.0.1:19999`**(另有 `[::1]:19999`),**`127.0.0.2` 被静默改写**;ssh 侧**零报错**(`ExitOnForwardFailure` 未触发、日志为空)。
|
||||
|
||||
**根因**:47 `sshd -T` ⇒ **`gatewayports no`**(OpenSSH 默认)⇒ sshd 把远端转发**强制绑到回环**,**客户端指定的绑定地址被丢弃**。
|
||||
|
||||
⇒ **§4 S3 原设计(`127.0.0.2`、`127.0.0.3`… 独立回环别名)作废**,改做 **实例端口区间隔离**:
|
||||
- 新增 `DSHS_INSTANCE_PORT_BASE` / `DSHS_INSTANCE_PORT_SPAN`,`findFreePort()` 改为**在区间内挑端口**;
|
||||
- 各 worker 配**互不重叠**的区间(`w-47`=42000+,`w-106`=43000+)⇒ 同一台 Manager 的 `127.0.0.1` 上永不撞号;
|
||||
- ✅ 副作用收益:**不需要 `portMap`**(不改"不同号")⇒ §4 S3 里"必须同时给 `ClusterHost` 加 `portMap`"**一并作废**;`src/worker/agent.ts` 的 `instanceHost` 也**不用改**。
|
||||
- ⛔ 若将来确需回环别名:只能开 `GatewayPorts clientspecified`,且**只能开在新建的 relay sshd 上** —— 开在主 sshd `32022` 等于让持隧道密钥者可把端口绑到 `0.0.0.0`,命中 **R5(权限只准收窄)**。
|
||||
|
||||
## 9.2 🔴 S3 的真实必要性上修为「**在修一个真 bug**」,不是"管道美化"
|
||||
|
||||
`findFreePort()`(`src/supervisor/spawn.ts:53`)只在 **worker 自己那台机器** 上随机取端口(调用点 `src/supervisor/orchestrator.ts:595`)⇒ **两台 worker 取到同号是常态**;
|
||||
而 `tunnel.forward()` 的返回值在 **`src/worker/agent.ts:303` 与 `:205` 两处都被忽略** ⇒ 撞号时 `-R` 失败但**静默**;
|
||||
Manager 随后仍按 `127.0.0.1:<port>` 拨 ⇒ **打到另一个用户的实例**。
|
||||
`w-47` 的本地实例与 `w-106` 的隧道落点**共享 47 的 `127.0.0.1` 端口空间** ⇒ **两台机器即可触发**。
|
||||
|
||||
## 9.3 🔴 S1 的部署面:本方案与 v1 交接单**都漏了 47**
|
||||
|
||||
S0 的 `src/web/server.ts`、`src/supervisor/remote-spawner.ts`、`src/net/*` 跑在 **Manager(47)**;S2 的 `hostsProvider` 也在那儿。
|
||||
实测:**47 `/opt/dshs/lib/` 至今无 `net/`**,`config.js` 指纹 ≠ 本机 ⇒ **47 是 S0 之前的版本**;S1 只上了 **106(Worker)** 侧。
|
||||
⇒ 执行必须先做一步 **P1:把 47 对齐到 `640813e`**(零行为变化,风险最低),再上 S2。
|
||||
|
||||
## 9.4 ⚠️ S4 有**方案缺口**:中继一旦不在 Manager 主机上,"Manager 拨落点"就断
|
||||
|
||||
隧道落点在**接收方那台机器的 `127.0.0.1`**。中继换到别的机器 ⇒ 落点在**那台**机器上,Manager 拨不到。
|
||||
⇒ 需要**额外一跳**:① Manager 侧向中继建出向隧道(`ssh -L`,把中继回环落点搬到 Manager 自己的回环),或 ② 中继侧把落点暴露到**非回环**地址 + nft 收窄(暴露面变大)。
|
||||
**倾向 ①**(中继 sshd 的 `gatewayports no` 可保持不动 = 零权限扩大,且支持异地中继)。
|
||||
⇒ 本轮 S4 **只做同机 relay**;⛔ 原 §4 S4 验收第 3 条"会合指向第二个中继实例"**本轮不作判据**(那要先定这一跳)。
|
||||
|
||||
## 9.5 ⚠️ S2 减负:**不要加 `address` 列**
|
||||
|
||||
`endpoint` 本身就是"要拨的地址",`via` 只补"经谁";再加 `address` = **同义双真相**(回填后必然逐字相等,违反单一来源)。⇒ §4 S2 的 `ADD COLUMN address text` **作废**,**只加 `via`**。
|
||||
|
||||
## 9.6 验收工具的现实约束(§4 未提,会直接卡住执行)
|
||||
|
||||
`scripts/verify-cluster-cross.mjs`:默认 `MANAGER=http://127.0.0.1:13080`(**演练端口**,现役是 **3080**)、`AGENT_TOKEN=cross-machine-token`(现役 `dshs-worker-7f3a91c05e`)、需 `ADMIN_PW`;
|
||||
且它**有生产副作用**(幂等注册 `w-106` 并把容量改 4096 / 新建 `crossuser*` 用户 / 在 106 拉真实例 / 可选注册 `w-106b` 并迁移实例)⇒ **跑完必须清理**;脚本头部注释"控制面 PG 在 106"**已过时**(PG 现在在 47)。
|
||||
|
||||
## 9.7 §8 三处勘误的复验结论
|
||||
|
||||
> ⚠️ 行号微差(本轮实测):§1.2 / §8 写的 `proxy.ts:138-139` 实测应为 **`136-137`**(`host/port` 两行);`admin.ts` 的注册路由实测在 **`:181`(`POST /api/admin/hosts`)**,不是 `:184`。**结论不变**,只是行号偏了一两行。
|
||||
|
||||
- **① `proxy.ts:109`** ✅ **再次确认**仍是"发给上游 dsh 的 `Host` 头",⛔ 不可改;连接目标在 **`proxy.ts:136-137`**(`host/port` 取自 `endpointFor`)。
|
||||
- **② 两条 `endpoint` 同形不同义** ✅ 确认(`w-47`=`local`/`w-106`=`manager-ssh`),别一刀切回填。
|
||||
- **③ 47 cluster 配置在 drop-in** ✅ 确认。
|
||||
|
||||
Reference in new issue
Block a user