docs: 工作区根项目文档批量入仓(04-调整方案 113–128 / 交接单 T09–T21 / ops / archive)

起因:用户 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 代码面)。
This commit is contained in:
admin committed 2026-09-17 18:24:19 +08:00
1 parent bc0dd2c96d
commit 04776af4b1
36 files changed
+11840 -43

No files matched your search

@@ -0,0 +1,416 @@
# 参数表 · 覆盖网络(**唯一一张**)
> **线**:覆盖网络线 | **序**:⑤(参数表 · 观测 · 权限评估)| **产出**:执行棒 2026-09-17
> **唯一来源**:`覆盖网络_应用场景与待完善清单_20260916.md` **§五 第 5 项** + **§四 P1 行** + `交接单_443兜底_20260917.md` **§8.8**
> **本表要做的事**:把散在 6 份文档里的输入参数 + 代码里已固化的常量**收成一张可复算的表**(每行带**来源等级**与**来源定位**),并填掉 443 单留下的 **45% 口径空位**。
> ⛔ **本表不引入新模型、不引第三方依赖、不做架构改动**;性质是「**固化**」,不是重新推演。
> 🔴 **`待测` 项一个都不许编数** —— 编出来的数会让整张表失去"可复算"的资格。`待测` 行的**值列留空**。
---
## §0 来源等级(四档,⛔ 不许混用)
| 等级 | 含义 | 判据 |
|---|---|---|
| **实测** | 本机 / 远端**命令原文**能复现的数 | 本表给出一条能跑的命令 |
| **推导** | 由表内其它行**按公式算出来**的数 | 本表给出复算式,独立手算能得同值 |
| **估值** | 文档里的行业口径 / 旧记录 / 推演假设 | 指到**文件:行**,且**不得**被当成实测引用 |
| **待测** | 本单取不到真值 | **值留空**,写明"由序⑥ 用 3–5 台真机换掉" |
> ⚠️ 旧记录里两个数**本次已复核判为不可用**(⛔ 别再引用):
> ① 47 规格"1.8 GB / 2 核"(09-08)⇒ 本次实测 **1870 MB / 2 核**(量级巧合,但**必须用实测值复算**);
> ② 跨云带宽 **~22 KB/s**(`覆盖网络_千台全场景推演_20260916.md:38`)⇒ 本次实测**上行下界 ≥ 192 KB/s**(≈ 9×),原值作废。
---
## §1 用法约定(**机器可读** —— 探针脚本靠它取阈值)
- **参数行格式**:``| `KEY` | 值 | 单位 | 等级 | 来源定位 | 复算式 |``
- **观测阈值行格式**:``| `OBS-NN` | 指标 | 阈值(引用键或字面值) | 判据 |``
- ✅ **`scripts/overlay-probe.cjs` 的每一个阈值都从本表读**,⛔ 脚本内**不许有魔数**(判据 = 交接单 §6 **E6**)。
- 探针运行方式(**一条命令**,cwd = 工作区根):
`cd "E:/ProgramData/AI技能/aliyun-dsh-server" && node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"`
---
## §2 运行坐标
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `SSH_TARGET_47` | `bt-server` | — | 实测 | `~/.ssh/config`(`HostName 47.77.182.89`) | `ssh -p 22 bt-server hostname` |
| `SSH_TARGET_106` | `test106` | — | 实测 | 同上(`HostName 106.54.21.172`) | `ssh -p 22 test106 hostname` |
| `SSH_PORT` | 22 | — | 实测 | 交接单 §2 注(`bt-server` 配置写的 32022 **已陈旧**) | `ssh -p 22 bt-server true` |
| `W47_HOSTNAME` | `iZrj99af19cibck1ge93tqZ` | — | 实测 | 本次 S0 P6 | `ssh -p 22 bt-server hostname` |
| `W106_HOSTNAME` | `VM-0-8-opencloudos` | — | 实测 | 本次 S0 P8 | `ssh -p 22 test106 hostname` |
| `RELAY_STATUS_URL` | `http://127.0.0.1:20080/status` | — | 实测 | relay ExecStart `--port 20080` | `ssh -p 22 bt-server 'curl -s 127.0.0.1:20080/status'` |
| `PORTAL_URL` | `http://127.0.0.1:3080/` | — | 实测 | 平台门户(⚠️ `http2 on` ⇒ **必须 `--http1.1`**,否则假 404) | `ssh -p 22 bt-server 'curl -s --http1.1 -H "Host: alotbuy.com" 127.0.0.1:3080/'` |
| `PORTAL_HOST_HEADER` | `alotbuy.com` | — | 实测 | 同上 | 同上 |
| `RELAY_NETWORK_ID` | `ops` | — | 实测 | `/etc/dshs.env` `DSHS_OVERLAY_NETWORK_ID` | `ssh -p 22 bt-server 'grep NETWORK_ID /etc/dshs.env'` |
| `SSH_TIMEOUT_MS` | 20000 | ms | 推导 | 探针脚本**单次 ssh** 的超时(⚠️ 放这里是为了让 `overlay-probe.cjs` **零魔数**;实测单次 ssh 往返 ≈ 1 s,取 20× 余量) | — |
> 🆕 **序⑦ 真机演练坐标**(`scripts/overlay-failover-drill.cjs` 用它取数 ⇒ 脚本内**零魔数**):
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `DRILL_RELAY_UNIT` | `dshs-relay` | — | 实测 | 两台机的 relay 单元名(`systemctl cat dshs-relay`) | `ssh -p 22 bt-server systemctl cat dshs-relay` |
| `DRILL_MANAGER_UNIT` | `dshs` | — | 实测 | 47 的 Manager 单元名(演练只读它的 journal 判别器) | 同上 |
| `DRILL_POLL_MS` | 500 | ms | 推导 | 演练轮询 journal 的间隔(≤ `RELAY_FAILOVER_CHECK_MS` 的 5×,保证不漏一次切换)。🆕 **序⑨:2000 → 500** —— 原 2000 让读数带 **0–2000 ms 系统性高估**,使「是否超 `RELAY_FAILOVER_DEADLINE_MS`」**两端都可能误判**(序⑨ RC-4)⇒ 这是**测量修正,不是调参**。⚠️ 改前/改后的读数**不可混比** | — |
| `DRILL_SAMPLE_N` | 5 | 次 | 推导 | 🆕 序⑨:`overlay-failover-drill.cjs --sample` 的**默认采样轮数**(口径 = 每轮都做完整归零:两台 relay `start` → `restart dshs` → 等新的 `AUTH OK host=ops/manager` → 杀入口 → 等新 `[relay-switch]`) | `node scripts/overlay-failover-drill.cjs --sample 5` |
| `DRILL_COOLDOWN_OBSERVE_MS` | 90000 | ms | 推导 | 幕 3 的「冷却期内不回跳」观察窗;⛔ **必须 < `RELAY_FAILOVER_COOLDOWN_MS`**,否则判据不成立 | — |
| `DRILL_SWITCH_MATCH_106` | `106.54.21.172` | — | 实测 | 断言 `[relay-switch]` 的目标是不是 **106**(用**目录里那个 host**,⛔ 不是 ssh 别名 `test106`) | `curl -s https://alotbuy.com/dshs-overlay/bootstrap` |
| `DRILL_KILLED_MATCH` | `alotbuy.com` | — | 实测 | 幕 1 被杀入口的标识(= 目录 `relays[]` 首位的 host);脚本据此判定「Manager 当前通道**是否就是**将被杀的那台」——不在 ⇒ 记 **SKIP** 而不是假装 PASS | 同上 |
| `DRILL_DETECT_BUDGET_MS` | 120000 | ms | 推导 | 幕 1/幕 2 的**观察窗**;⛔ 必须 ≥ **静默失效检测时延**(半开检测 = `2.5 × HB_SEC`,即 75 s)—— 首轮实测用 1× deadline(30 s) **必然漏判** | — |
---
## §3 输入参数(清单 §三 点名的 **5 类**;散落点收口)
> 散落点计数(P10,⛔ 只计数不重读):`调研_游戏网络特征…:19 处` / `千台全场景推演…:18` / `瓶颈落地方案…:9` / `游戏专项…:7` / `清单…:7` / `百台规模推演…:2` / `骨干层方案…:2` ⇒ **合计 64 处命中,正是"不可复算"的成因**。
### 3.1 设备占比(分层)
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `DEV_L1_PUBLIC_IP` | 10 | 台 | 估值 | `覆盖网络_百台规模推演_20260916.md:53` | — |
| `DEV_L2_HOLE_PUNCHABLE` | 30 | 台 | 估值 | `覆盖网络_百台规模推演_20260916.md:54` | — |
| `DEV_L3_RELAY_ONLY` | 45 | 台 | 估值 | `覆盖网络_百台规模推演_20260916.md:55`(原文区间 40–55,取中值) | `(40 + 55) / 2` |
| `DEV_SHARE_L3` | 0.45 | — | 估值 | `覆盖网络_百台规模推演_20260916.md:119`「L3 占 **45%**(v1 只算 15%)」+`:132` | 45 / 100(口径 = **全网节点数**的比例) |
| `FLEET_TARGET` | 1000 | 台 | 估值 | `覆盖网络_千台全场景推演_20260916.md`(标题与全文口径) | — |
| `FLEET_RELAY_DEMAND` | 450 | 台 | 推导 | 本表 §5 | `FLEET_TARGET × DEV_SHARE_L3` = 1000 × 0.45 |
### 3.2 打洞率
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `HOLE_PUNCH_RATE_INDUSTRY` | 0.90–0.94 | — | 估值 | `覆盖网络_全球架构复盘_20260916.md:38`(业内 UDP 打洞 ≈ 94%) | — |
| `HOLE_PUNCH_RATE_BY_CLASS` | 90% / 40% / 10% | — | 估值 | `方案规划方法_覆盖网络线提炼_20260916.md:45`(三档标注实例) | — |
| `HOLE_PUNCH_RATE_LOCAL` | **分层(n=3 台)**:① 云机×云机(47↔106)—— 均为 L1 直连,**不是打洞场景**;② 国内家宽/办公 NAT(本机)↔ 47 / ↔ 106 —— **对端 → 本机方向 10/10 收包 = 可打洞**;本机 → 云机方向**取不到**(云安全组拦 UDP 入站)。**两对中两对可打洞** | — | **实测** | 本单 §8.4 E6 + `overlay-holepunch.cjs --stun`(⚠️ 首选路径的 47 UDP 观察器**实测不可用**:零收包 ⇒ 降级为**第三方 STUN**,见 §8.4 `OBS-4D`) | 判定式=`对(47,本机)`、`对(106,本机)` 各「至少一个方向成功」⇒ 2/2;⚠️ **样本 n=3 对,云节点占 2/3,不代表家宽场景** |
### 3.3 每玩家带宽
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `PER_PLAYER_BW_TEXT` | 0.5 | KB/s | 估值 | `覆盖网络_调研_游戏网络特征与群聊上限_20260916.md:15` | — |
| `PER_PLAYER_BW_BATTLE` | 2–5 | KB/s | 估值 | 同上 `:15`(另一口径 `:43`「单玩家 2–20 KB/s」) | — |
| `PER_PLAYER_BW_SIEGE` | 10–20 | KB/s | 估值 | 同上 `:15` | — |
| `PER_PLAYER_BW_LOCAL` | **9.8**(n=10 玩家档)/ **3.9**(n=50 玩家档)—— 判据见来源列;聚合天花板 ≈ **200–350 KB/s** | KB/s | **实测** | 本单 §8.4 E7 + `overlay-wan.cjs --players`(合成 200 B 消息 × 5/20/50 msg/s × 10/50 玩家 × 60 s,经 relay 真机间) | 判据=`p95 ≤ 2×p50 且丢包 = 0`;⚠️ 原始丢包含**尾部在途**伪影(≈`rate × RTT`);扣掉后满足的**最高档 = 10 玩家 @50 msg/s = 9.77 KB/s/玩家**(50 玩家档最高 3.91)。**边界**:本值 = **传输层上限**;游戏协议的真实需求仍是估值(`PER_PLAYER_BW_TEXT/BATTLE/SIEGE`)——⛔ 不得读成"实测出的游戏需求" |
### 3.4 消息频率 / 扇出
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `MSG_RATE_GLOBAL` | 50 | msg/s | 估值 | `覆盖网络_千台全场景推演_20260916.md:75` | — |
| `FANOUT_AVG` | 50 | — | 估值 | 同上 `:75`("均扇出 50") | `50 × 50 = 2500 投递/秒` |
| `ROOM_FANOUT_LIMIT_100` | 100 | 人 | 估值 | `覆盖网络_调研_游戏网络特征与群聊上限_20260916.md:97`(≤100 纯扇出即可) | — |
| `PRESENCE_FANOUT_1000ROOM` | 16700 | 次/秒 | **推导** | `覆盖网络_千台全场景推演_20260916.md:79` | 千人房 presence ≈ 1000 × 16.7 次/秒(原文直接给 16,700) |
### 3.5 心跳 / 链路质量
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `HB_SEC` | 15 | s | **实测**(代码常量=运行真值) | `src/net/relay/server.ts:60` `DEFAULT_HB_SEC = 15`;运行期由 `HELLO_ACK` 下发(`server.ts:816`) | `grep -n DEFAULT_HB_SEC src/net/relay/server.ts` |
| `HB_SEC_DOC` | 20 | s | 估值(**文档口径,与代码不一致**) | `覆盖网络_千台全场景推演_20260916.md:39`「心跳间隔 20 s(沿用现有隧道自愈定时器)」 | ⚠️ 该行属**旧隧道时代**口径 —— 现已换 relay(`hbSec=15`)⇒ **文档待更正**,参数以 `HB_SEC=15` 为准 |
| `HALF_OPEN_MS` | 37500 | ms | 推导 | `src/net/relay/client.ts:602` | `max(3000, HB_SEC × 1000 × 2.5)` = 15 × 1000 × 2.5 |
| `IDLE_TIMEOUT_MS` | 45000 | ms | 实测(代码常量) | `src/net/relay/server.ts:58` | `grep -n DEFAULT_IDLE_TIMEOUT_MS src/net/relay/server.ts` |
| `JITTER_LIMIT_MS` | 20 | ms | 估值(业界口径) | `覆盖网络_调研_游戏网络特征与群聊上限_20260916.md:32`+`覆盖网络_千台全场景推演_20260916.md:90` | — |
| `JITTER_LINK_MEASURED` | **3**(判定值 = `p95(|ΔRTT|)`;另记 `mdev` = 1.37–1.38 ms、`avg` = 148.3 ms、丢包 1.3–1.7%) | ms | **实测** | 本单 §8.4 E5 + `overlay-jitter.cjs --icmp`(47→106 与 106→47 各 300 包 × 0.2 s) | **达标**:3 ms < `JITTER_LIMIT_MS`(20 ms,⚠️ 该限值是**业界估值**口径)。⚠️ 这条**推翻了**"当前跨云链路上 jitter 不可能达标"的旧定性结论 —— 旧结论是拿 `RELAY_RTT_W106` 当链路 RTT 推的(口径错,见下行) |
| `RELAY_RTT_W106` | 336 → **保留数值,但加口径修正**:该值是**心跳往返**(`server.ts:810`:测 RTT / 察觉半开 / 保 NAT 表项),**含应用层处理与验签,⛔ ≠ 网络 RTT** | ms | **实测(口径受限)** | 本单 §8.4 E5(b) 三方对比:ICMP 148 ms | TCP 握手 median 153 ms(min 138.5)| relay 心跳 344–376 ms ⇒ **差 2.3×,触发"加口径备注"判据** | 判据(S3(b)):`relay rttMs` 与 ICMP 差距 > 2× ⇒ 加备注 + 在 `OBS` 侧登记"relay `rttMs` 不得当链路 RTT 用" |
| `RELAY_RTT_MANAGER` | 7 | ms | **实测** | `/status` → `sessions[hostId=manager].rttMs` | 同上 |
| `RELAY_TTFB_S` | 0.68 | s | **实测** | 本次 S0 P7a:47 → relay 回环口 → w-106 任一端口,`%{time_starttransfer}`(6 次样本 0.676–1.083) | `ssh -p 22 bt-server 'curl -s --http1.1 -o /dev/null -w "%{time_starttransfer}\n" http://127.0.0.1:44133/'` |
| `WAN_UP_BOUND_KBPS` | 192 ⇒ **已被上行实测取代(值作废,保留作历史)** | KB/s | ~~实测(下界)~~ **作废** | 原 S0 P7 样本被 405 提前截断(131072 B / 0.685 s,含 RTT)⇒ 只是**下界**,且下界偏低 **64×** | 用 `WAN_STEADY_THROUGHPUT` 的上行值 12213 作准 |
| `WAN_STEADY_THROUGHPUT` | **352(106→47)/ 12213(47→106)** —— 取**绑定方向**(对端→中继机,即"内容从 worker 上来"那条)= **352** | KB/s | **实测** | 本单 §8.4 E4 + `overlay-wan.cjs --download|--upload`(各 5 样本) | **口径三要素**:① 谁到谁 = 106 的 `w-106p:19777` ↔ **47 本机 relay 回环端点**;② **经 relay**(非裸链路);③ 稳态段 **30 s**(弃前 3 s,爬升段字节单独计)。**中位数 352**(min 345.6 / max 352);上行侧 12213(min 11784 / max 12452)。⚠️ 非对称的成因 = 106(云轻量)**公网出带宽封顶 ≈ 2.8 Mbps**,实测三向互证:`/status` 会话计数 `in=1.905 GB` + 106 侧进程 `rchar=1.905 GB` + 下载恒定 165×64 KB/30 s |
| `WAN_UP_BOUND_KBPS` | 192 ⇒ **已被上行实测取代(值作废,保留作历史)** | KB/s | ~~实测(下界)~~ **作废** | 原 S0 P7 样本被 405 提前截断(131072 B / 0.685 s,含 RTT)⇒ 只是**下界**,且下界偏低 **64×** | 用 `WAN_STEADY_THROUGHPUT` 的上行值 12213 作准 |
### 3.6 presence(节点在线态 · 序⑲)—— **帧率类**参数
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `PRESENCE_BATCH_MS` | 1000 | ms | **实测**(代码常量=运行真值) | `src/net/relay/server.ts:106` `DEFAULT_PRESENCE_BATCH_MS = 1_000` | `grep -n DEFAULT_PRESENCE_BATCH_MS src/net/relay/server.ts` |
| `PRESENCE_GRACE_MS` | 10000 | ms | **实测**(代码常量=运行真值) | `src/net/relay/server.ts:89` `DEFAULT_PRESENCE_GRACE_MS = 10_000` | `grep -n DEFAULT_PRESENCE_GRACE_MS src/net/relay/server.ts` |
| `PRESENCE_OFFLINE_DEBOUNCE_MS` | 30000 | ms | **实测**(代码常量=运行真值) | `src/net/relay/server.ts:97` `DEFAULT_PRESENCE_OFFLINE_DEBOUNCE_MS = 30_000` | `grep -n DEFAULT_PRESENCE_OFFLINE_DEBOUNCE_MS src/net/relay/server.ts` |
| `PRESENCE_TTL_MS` | 45000 | ms | 推导(代码常量 × 心跳) | 服务端 `src/net/relay/server.ts:604`;客户端保守档 `src/net/relay/client.ts:329` `DEFAULT_PRESENCE_TTL_MS = 45_000`(两处同值,实测一致) | `HB_SEC × 1000 × DEFAULT_PRESENCE_TTL_FACTOR` = 15 × 1000 × 3(因子 = 3 见 `server.ts:115`) |
| `PRESENCE_SUB_MAX` | 0 | 个 | **实测**(代码常量;**0 = 不限**) | `src/net/relay/server.ts:122` `DEFAULT_PRESENCE_SUB_MAX = 0` | `grep -n DEFAULT_PRESENCE_SUB_MAX src/net/relay/server.ts` |
> ⚠️ 这五个键只驱动**在线态事件**(谁在线 / 何时改口),⛔ 与 `RELAY_FAILOVER_*` / `HB_SEC` / burst **无耦合** —— 序⑲ 全程一字未动(E11 的 D1 自证)。
> ⚠️ `PRESENCE_TTL_MS` 是**安全网**(兜"漏掉 close 事件"那条路),⛔ **不是常规下线路径**:常规下线走 `PRESENCE_GRACE_MS + PRESENCE_OFFLINE_DEBOUNCE_MS` = 40 s。
> ⚠️ 口径来源 = D3(最终一致:允许 5–15 s 陈旧),⛔ 三个阈值均按代码常量取,**不许改口径去凑判据**(E5)。
> 🔴 **值格必须纯数字**(夹注 / 混写单位 ⇒ 探针 `NaN` ⇒ 假红,序⑫ 已踩)。
---
## §4 代码已固化常量(**实测**=直接可 grep 的源码真值)
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `RELAY_PORT` | 20080 | — | 实测 | `src/net/relay/server.ts:52`+单位文件 ExecStart | `grep -n DEFAULT_RELAY_PORT src/net/relay/server.ts` |
| `RELAY_BIND` | 127.0.0.1 | — | 实测 | 主单元 ExecStart(无 `--host` ⇒ 默认回环) | `ssh -p 22 bt-server "ss -lntp \| grep 20080"` |
| `RELAY_INSTANCE_BASE` | 19000 | — | 实测 | 主单元 ExecStart `--base 19000` | 同上 |
| `RELAY_INSTANCE_SPAN` | 3000 | — | 实测 | 主单元 ExecStart `--span 3000` ⇒ 可声明窗口 `[19000, 22000)` | 同上 |
| `AUTH_DEADLINE_MS` | 5000 | ms | 实测 | `src/net/relay/server.ts:54` | `grep` |
| `AUTH_WINDOW_MS` | 60000 | ms | 实测 | `src/net/relay/server.ts:56` | `grep` |
| `CAPACITY_RETRY_AFTER_MS` | 5000 | ms | 实测 | `src/net/relay/server.ts:63` `DEFAULT_CAPACITY_RETRY_AFTER_MS` | `grep` |
| `SHUTDOWN_GRACE_MS` | 300 | ms | 实测 | `src/net/relay/server.ts:69` | `grep` |
| `MAX_STREAMS_PER_PORT` | 64 | 条 | 实测 | `src/net/relay/server.ts:71` | `grep` |
| `QUEUE_MAX_BYTES` | 1048576 | B | 实测 | `src/net/relay/server.ts:73`(`1 << 20`) | `grep` |
| `WS_PEER_HIGH_WATER` | 262144 | B | 实测 | `src/net/relay/server.ts:76`(`256 * 1024`) | `grep` |
| `CLIENT_HIGH_WATER` | 262144 | B | 实测 | `src/net/relay/client.ts:230` | `grep` |
| `CLIENT_QUEUE_MAX` | 1048576 | B | 实测 | `src/net/relay/client.ts:229` | `grep` |
| `DIRECTORY_REFRESH_SECONDS` | 300 | s | 实测 | `src/net/relay/directory.ts:60` | `grep` |
| `DIRECTORY_FETCH_TIMEOUT_MS` | 5000 | ms | 实测 | `src/net/relay/directory.ts:521` | `grep` |
| `DIRECTORY_MAX_ENTRIES` | 8 | 条 | 实测 | `src/net/relay/directory.ts:63` `MAX_ENTRIES` | `grep` |
| `DIRECTORY_MAX_ENTRY_LEN` | 512 | 字符 | 实测 | `src/net/relay/directory.ts:64` `MAX_ENTRY_LEN` | `grep` |
| `DIALER_POOL` | 64 | 口 | 实测 | `src/net/relay/dialer.ts:55` `DEFAULT_POOL`+`/etc/dshs.env` `DSHS_RELAY_DIAL_POOL` | `grep`+`ssh -p 22 bt-server 'grep DIAL_POOL /etc/dshs.env'` |
| `DIAL_PORT_BASE` | 25000 | — | 实测 | `/etc/dshs.env` `DSHS_RELAY_DIAL_PORT_BASE` | 同上 |
| `DIAL_PORT_SPAN` | 1000 | — | 实测 | `/etc/dshs.env` `DSHS_RELAY_DIAL_PORT_SPAN`(⚠️ 实绑只到 `base+POOL`) | 同上 |
| `DIAL_POOL_BOUND` | 25000–25063 | — | **实测** | 47 上 `ss -lntp`(64 口全绑)+journal `[relay-dialer] 本机落点池就绪:64 个口` | `ssh -p 22 bt-server "ss -lntp \| grep -c 250"` |
| `W47_AGENT_PORT` | 19100 | — | 实测 | `/etc/dshs.env` `DSHS_CLUSTER_AGENT_URL=http://127.0.0.1:19100` | `ssh -p 22 bt-server 'grep AGENT_URL /etc/dshs.env'` |
| `LOCAL_INSTANCE_PORT` | 20000 | — | 实测 | 47 上 `ss -lntp`(`127.0.0.1:20000`) | `ssh -p 22 bt-server "ss -lntp \| grep 20000"` |
| `PEER_AGENT_PORT` | 19000 | — | 实测 | `/status` → `endpoints[]`/`online[]`(对端 w-106 的 `ports=19000/21000`) | `ssh -p 22 bt-server 'curl -s 127.0.0.1:20080/status'` |
| `PEER_INSTANCE_PORT` | 21000 | — | 实测 | 同上 | 同上 |
| `RELAY_FAILOVER_MIN_ATTEMPTS` | 3 | 次 | 实测 | `src/net/relay/switcher.ts#relayFailoverThresholds`(env 可覆写;**置 0 = 总开关关闭**) | `grep -n RELAY_FAILOVER_MIN_ATTEMPTS src/net/relay/switcher.ts` |
| `RELAY_FAILOVER_GRACE_MS` | 15000 | ms | 实测 | 同上(与 `MIN_ATTEMPTS` 取**或**:106 侧首连窗口长,只看次数会误切) | `grep` |
| `RELAY_FAILOVER_COOLDOWN_MS` | 300000 | ms | 实测 | 同上(与 `DIRECTORY_REFRESH_SECONDS` 对齐;冷却期内**不回跳**) | `grep` |
| `RELAY_FAILOVER_DEADLINE_MS` | 30000 | ms | 实测 | 同上(**验收判据**:从不健康到切换完成的允许上限) | `grep` |
| `RELAY_FAILOVER_CHECK_MS` | 2000 | ms | 实测 | 同上(健康巡检周期;独立于目录刷新周期) | `grep` |
| `RELAY_FAILOVER_UP_TIMEOUT_MS` | 12000 | ms | 实测 | 同上(**只用于换址**:新通道必须真到 `up` 才算「建起来了」)。⚠️ 序⑨:**死候选提前失败后它不再是恒等代价**(实测死候选 ≈ 0.1–1 s 就返回,慢候选仍享受完整 12 s) | `grep` |
| `RELAY_GRACEFUL_BURST_MS` | 15000 | ms | 🆕 实测 | **序⑨ 参数表化**:`client.ts` 的 `gracefulBurstMs`("计划内下线 ≠ 故障"的快速重试窗口)原先是**全仓唯一一个不可配的时延常量**(只有 `?? 15_000` 一处),现可经 env 覆写;⛔ **默认值语义逐字不变**。⚠️ 它就是**检测段 15.0 s 地板的真身**:窗口内 `attempts` 恒为 0 ⇒ `unhealthy()` 只能靠 `graceMs` 成立(序⑨ §1.2-RC-2)⇒ 改它 = 改"计划内重启不触发切流"的窗口,**先回写交接单**(D3) | `grep -n gracefulBurstMsDefault src/net/relay/client.ts` |
| `RELAY_FAILOVER_EXEMPT` | 1 | — | 实测 | 同上(🆕 序⑧:**一跳豁免总开关**,`1`=开 / `0`=关;`RELAY_FAILOVER_MIN_ATTEMPTS=0` 是**另一层**,⛔ 别混) | `grep -n RELAY_FAILOVER_EXEMPT src/net/relay/switcher.ts` |
| `DRILL_COOLDOWN_MS` | 90000 | ms | 推导 | 🆕 序⑧:**演练期**冷却覆盖值(经 `dshs.service.d/zz-drill-override.conf` 注入)——⛔ **生产默认恒为 `RELAY_FAILOVER_COOLDOWN_MS=300000`**。取值口径:必须 ① ≫ 静默失效检测时延(实测 ≈ 29 s,否则"冷却未到期"判据根本来不及观测)② ≪ `DIRECTORY_REFRESH_SECONDS`(300 s)(让冷却先过期、目录巡检后到) | — |
| `DRILL_NO_SWITCH_OBSERVE_MS` | 45000 | ms | 推导 | 🆕 序⑧:幕 4b 的**"预期不切换"观察窗**;⛔ 必须 ≥ 失效检测时延(否则 D6 现场还没形成)**且** < `DRILL_COOLDOWN_MS`(否则会跨过冷却期满、把"自然回归"误判成"切了") | — |
> 🆕 **序⑦ 新增 5 键**(中继失败切流):默认值 = 源码真值(可直接 grep);
> ⛔ 值格**必须纯数字**(夹注 ⇒ `NaN` ⇒ 假红,见 §8.8-2 教训);
> 🔑 **回滚开关 = `RELAY_FAILOVER_MIN_ATTEMPTS=0`** ⇒ 监管器**永不触发**,行为回到「原地退避重试」的现状(§7 回滚第 1 层)。
>
> 🆕 **序⑧ 新增 3 键**(切流冷却语义拆分):`RELAY_FAILOVER_EXEMPT`(代码侧,默认 `1`)+
> `DRILL_COOLDOWN_MS` / `DRILL_NO_SWITCH_OBSERVE_MS`(**只在演练脚本里用**)。
> 🔑 **本单首选回滚点 = `RELAY_FAILOVER_EXEMPT=0`** ⇒ 只关掉豁免、序⑦ 的切流能力**全部保留**;
> ⛔ 它**不是** `MIN_ATTEMPTS` 的同义词(后者关整个监管器)。
> ⚠️ 演练期改冷却**只能**经 `DRILL_COOLDOWN_MS` 注入 drop-in;⛔ `RELAY_FAILOVER_COOLDOWN_MS` 的代码默认值不动(D9)。
> ⚠️ **键名口径**:`LOCAL_*` = 中继机(47)**本机**的实例/代理口;`PEER_*` = **对端**节点(今天= w-106)的等价口。
> (键名里**不带** `106` 是为了让 `overlay-probe.cjs` 的**零数字纪律**成立 —— 见交接单 §6 E6。)
---
## §5 本单新增:**45% 口径填值**(443 单 §8.8 留下的空位)
> **443 单 §8.8 登记行原文**:「兜底启用后,relay 容量须按 **45% 的节点走中继**核算(异构纪律,⛔ 不是同构的 15%)」。
> **口径落点**(按交接单 §4.1-3):填**单台中继的 `--max-hosts`**,⛔ **不填"全网 45%"** —— 后者只作**校验**用。
### 5.1 输入(全部来自本表其它行,⛔ 无外部魔数)
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `MEM_TOTAL_MB` | 1870 | MB | **实测** | S0 P6:`free -m` 第 1 行 | `ssh -p 22 bt-server 'free -m \| head -2'` |
| `MEM_AVAILABLE_MB` | 1002 | MB | **实测** | S0 P6:`free -m` 的 `available` 列 | 同上 |
| `MEM_BUDGET_MB` | 1002 | MB | 推导 | 本表 | `= MEM_AVAILABLE_MB`(47 同时跑 PG+Manager+nginx,故**只取 available**,⛔ 不用 total) |
| `CPU_CORES` | 2 | 核 | **实测** | S0 P6:`nproc` | `ssh -p 22 bt-server nproc` |
| `FD_LIMIT` | 262144 | 个 | **实测** | S0:`/proc/<relay pid>/limits` `Max open files` | `ssh -p 22 bt-server 'cat /proc/$(systemctl show -p MainPID --value dshs-relay)/limits \| grep "open files"'` |
| `RELAY_RSS_KB` | 72888 | KB | **实测** | S0:`ps -o rss= -p <relay pid>`(used=2 时,含 Node 基座) | `ssh -p 22 bt-server 'ps -o rss= -p $(systemctl show -p MainPID --value dshs-relay)'` |
| `MEM_PER_HOST_MB` | **0.06** | MB/台 | **实测**(原为推导 2) | 本单 §8.4 E8 + `relay-mem-calibrate.mjs`:本机独立 relay 实例 + 同进程合成 client N=2/10/25/50/100/150,每点 7 次采样取中位数 ⇒ 线性回归 `RSS(N) = 48078 + 46.7·N` KB,**R² = 0.942**(≥ 0.9 ⇒ 有效) | `46.7 KB/台 × 1.2 余量 = 56.0 KB = 0.0547 MB ⇒ ceil 到 0.01 MB 位 = 0.06`。⚠️ 原推导 2 MB **高估 36×**:它把 **per-stream** 的 256 KB 高水位算进了 **per-host**;本值是**空闲会话**口径,带流量的 per-stream 开销**未测**(登记于 §9) |
| `FD_PER_HOST` | 4 | 个/台 | 推导 | 1 条 WS + 每声明端口 1 个回环 listener + 2 个临时 ⇒ 上界 4 | 保守取上界 |
| `DESIGN_MARGIN` | 0.45 | — | 估值 | **443 单 §8.8** 原文(异构纪律;⛔ 非同构 15%) | — |
### 5.2 推导式(⛔ 可独立手算,E2 判据)
```
C_MEM = floor(MEM_BUDGET_MB / MEM_PER_HOST_MB) = floor(1002 / 0.06) = 16700 # ← 序⑥ S7 重算(原 floor(1002/2)=501)
C_FD = floor(FD_LIMIT / FD_PER_HOST) = floor(262144 / 4) = 65536
C_RELAY = min(C_MEM, C_FD) = 16700 # 带宽仍不参与取 min,见 §5.4
RELAY_MAX_HOSTS = floor(C_RELAY × DESIGN_MARGIN) = floor(16700 × 0.45) = 7515 # ← 原 225
```
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `C_MEM` | **16700** | 台 | 推导 | 本表 | `floor(1002 / 0.06)` |
| `C_FD` | 65536 | 台 | 推导 | 本表 | `floor(262144 / 4)` |
| `C_RELAY` | **16700** | 台 | 推导 | 本表 | `min(16700, 65536)` |
| `RELAY_MAX_HOSTS` | **7515** | 台 | 推导 | 本表 | `floor(16700 × 0.45)`(⚠️ **本单元格必须是纯数字** —— 探针 `KEY_RE`/`cleanValue` 不认识"(原 225)"这类夹注,会把整格当值 ⇒ `NaN` ⇒ `OBS-02` 假红;旧值 225 记在此列)。🔴 🔴 **序⑥ S7 回头条件已触发并执行**(2026-09-17):`MEM_PER_HOST_MB` 2 → 0.06、`WAN_STEADY_THROUGHPUT` 待测 → 352/12213 ⇒ 已**重下发** 47 与 106 两台的 `capacity.conf`(`--max-hosts 7515`),`OBS-02` 复验 PASS |
**校验(三条,全部满足才生效)—— 序⑥ S7 重跑记录**
- ① **防自锁**:`RELAY_MAX_HOSTS > used × 4` ⇒ `7515 > 4 × 4 = 16` ✅(验收时 `used = 4`)
- ② **设计余量自洽**:`7515 / 16700 = 45.0% ≈ DESIGN_MARGIN` ✅
- ③ **千台需求校验**:`FLEET_RELAY_DEMAND` = 450 **≤** `RELAY_MAX_HOSTS` = 7515(原为 `450 > 225`)⇒ 🔴 **结论变更**:**"容量上必须 ≥2 台中继"这条理由随实测消失**(单台容量已足够)。2 台的依据改为 ① `清单 §3 关键决定`「有公网 IP 的节点自动升格中继候选」② **跨机真容灾**(106 已升格,见 §8 ④)。⚠️ "零余量"这一旧事实**不再成立**,代之以下面那条:**绑定约束从"内存"搬到了"带宽/时延"**(见 §5.4)。
### 5.3 `--max-hosts` 的**分母口径**(⛔ 必须写清,否则数字会被误读)
`--max-hosts` 是**每台中继**的**在册会话(host)数上限**,它**不是**"全网 45%",分母是**这一台中继**;`FLEET_RELAY_DEMAND`(450)**只是校验上界**。今天生产上只有 **1 台中继**(47)⇒ 本值直接生效。
### 5.4 为什么带宽**不参与** `min`(⛔ 不是漏了)—— 序⑥ S7 用实测重判
- **判据(S7-2 原文)**:若新实测吞吐使"225 台(今为 7515 台)的控制面 + 实例面流量"逼近实测吞吐 ⇒ 带宽进 `min`。
- **控制面**(用实测值重算):`1 / HB_SEC` = 0.067 次/秒/台 × 7515 台 ≈ **504 次/秒**;每台每秒控制面流量 `≈ 100 / HB_SEC = 6.7 B/s` ⇒ 7515 台 ≈ **50 KB/s**,对比绑定方向实测 **352 KB/s**(余量 7×)⇒ **控制面不绑定**。
- **实例面**:不是"稳态负载"而是"**单次传输**",正确的判据是**时延**不是容量 —— 单次 46.3 MB 冷启动 ÷ 352 KB/s ≈ **135 s**(原推算按 192 KB/s 是 247 s,按 22 KB/s 旧值则是 36 min)。⇒ ⛔ **仍不进 `min`**,但登记为**实例面单次传输的时延上界**,并给出唯一的改进方向(提升对端公网出带宽,而非加中继)。
- ⚠️ **本条的边界**:352 KB/s 是 **106(云轻量)出带宽封顶**造成的,不是 relay 栈上限(上行方向实测 12213 KB/s 证明栈本身没到这个量级)。
### 5.5 `/status.capacity` 的**语义纠正**(P2 实测 vs 交接单期望)
| 事实 | 原文 |
|---|---|
| `max = 0` 时**只有** `{max, used}`,**没有 `free`** | `src/net/relay/server.ts:502-506`:`this.maxHosts > 0 ? {max, used, free} : {max, used}`;实测 `/status` = `"capacity": {"max": 0, "used": 2}` |
| `free` **只有**在 `max > 0` 时出现 | 同上 ⇒ **E3 的 `free = max - used` 判据只在设值后成立**(设值前 `free` 字段不存在,不是 `null`/不是 `0`) |
| 满载时的 `free` 恒为 `0` | `server.ts:761`:`capacity: { max, used, free: 0 }`(拒绝载荷里) |
| `used` = **在册会话数**(= host 数,不是流数、不是端口数) | `server.ts:505`:`used: this.sessions.size` |
| `retryAfterMs` 口径 = 满载时给对端的**排队建议** | `CAPACITY_RETRY_AFTER_MS` = 5000 ms;客户端**不消耗退避**地排队(`client.ts:532 / 881-896`,附A 已核) |
### 5.6 443 单 §8.7③ 要求的回答:`relays[]` 顺序语义
> **本单必须回答**:443 单留下的「若将来 `relays[]` 引入非首位更优的显式优先级语义 ⇒ 需重新定义同源优先与它的先后关系」。
**回答**:本单**不引入**该语义。`relays[]` 的顺序语义固定为「**主入口首位**」—— 首位是主入口,其余是兜底候选;**不存在**"非首位更优"的显式优先级字段。⛔ 本表**不新增**任何优先级键(新增即等于把这条语义改了)。
---
## §6 观测阈值(探针脚本的**唯一**取数来源;⛔ 脚本内无魔数 = E6)
| ID | 指标 | 阈值 | 判据 |
|---|---|---|---|
| `OBS-01` | relay 在册会话数(`used`) | `MIN_HOSTS` | `used ≥ MIN_HOSTS` |
| `OBS-02` | `capacity.max` / `free` | `RELAY_MAX_HOSTS` | `max = RELAY_MAX_HOSTS` 且 `free = max - used` |
| `OBS-03` | 身份强制与受信签名者 | `MIN_TRUSTED_SIGNERS` | `identityRequired = true` 且 `trustedSigners ≥ MIN_TRUSTED_SIGNERS` |
| `OBS-04` | 通过节点凭据校验的注册数 | `MIN_IDENTITY_OK` | `identityOk ≥ MIN_IDENTITY_OK` |
| `OBS-05` | 吊销清单规模 | `MAX_REVOKED_HOSTS` | `revokedHosts ≤ MAX_REVOKED_HOSTS` |
| `OBS-06` | **判别器计数三件套存在** | (字段存在性) | `typeof dial/dialDenied/dialFailed === 'number'` |
| `OBS-07` | 鉴权失败累计 | `MAX_AUTH_FAILED` | `authFailed ≤ MAX_AUTH_FAILED` |
| `OBS-08` | 端点表全在线 | (无 `false`) | `endpoints[].online` 全为 `true` 且非空 |
| `OBS-09` | 双实例探活(`w-47` 实例面 / `w-106` 实例面经 relay) | `PROBE_CODE_SET` | 两个 HTTP 码均 ∈ `PROBE_CODE_SET` |
| `OBS-10` | 门户可达 | `PORTAL_CODE` | `= PORTAL_CODE` |
| `OBS-11` | 零新增暴露面(**监听集合** / **nft 入站 accept 集合** / relay 只绑回环) | `LISTEN_REQUIRED` / `LISTEN_ALLOWED` / `LISTEN_ALLOWED_RANGES` / `DIAL_POOL_BOUND` / `NFT_ALLOW_INBOUND` / `RELAY_BIND` | **三集包含式**:`必在 ⊆ 实际 ⊆ 必在 ∪ 允许 ∪ 区间(含拨号池) ∪ 派生`,差集**逐条点名**;且 `nft 入站 accept ⊆ NFT_ALLOW_INBOUND`;`RELAY_PORT` 只出现在 `RELAY_BIND` 上。🔴 **序⑫:旧口径「计数相等」已退役** —— 它对实例/端点在线态**敏感 ⇒ 假红**,对"一进一出"替换式变化**不敏感 ⇒ 假绿** |
| `OBS-12` | relay 进程内存 | `RELAY_RSS_MAX_KB` | `RSS ≤ RELAY_RSS_MAX_KB` |
| `OBS-13` | **presence 帧率(稳态)** | `PRESENCE_STEADY_FRAMES_MAX` / `PRESENCE_SAMPLE_HITS_MIN` | **两次 `/status` 采样的增量**:`Δpushed ≤ 0` **且** `ΔstatusHits ≥ 1`(活性证明:⛔ 不许"因为读不到所以看起来是 0");并附口径一致(`presenceTiming` 逐项 == `PRESENCE_*`)+ 判别器齐全(`subs/pushed/snaps/rejected/statusHits` 皆 number)+ `snaps ≤ pushed` |
| `OBS-14` | **首帧即全量 `SNAP`(⛔ 无 N+1)** | (`snaps` / `pushed` / `subs` 三者关系) | `snaps ≤ pushed` 且 **`subs > 0 ⇒ snaps ≥ 1`**(有订阅者却一帧 `SNAP` 都没发 ⇒ 首帧走"逐台拉",判 FAIL) |
| `OBS-15` | **在线态表不撒谎 + 陈旧度** | `PRESENCE_STALE_P95_MAX_MS` / `PRESENCE_TTL_MS` | 活跃会话(`lastSeenAgoMs ≤ PRESENCE_TTL_MS`)**必须**被 `presence[]` 覆盖且 `online = true`;且 `p95(lastSeenAgoMs over devices>0)` ≤ `PRESENCE_STALE_P95_MAX_MS` |
**阈值取值(同为参数行,⛔ 不是脚本魔数)**
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `MIN_HOSTS` | 2 | 台 | 实测 | S0 P2 `/status.online[]`(manager + w-106) | `online.length` |
| `MIN_TRUSTED_SIGNERS` | 1 | 个 | 实测 | S0 P2 `trustedSigners = 1` | — |
| `MIN_IDENTITY_OK` | 2 | 次 | 实测 | S0 P2 `identityOk = 2`(⛔ 不是 3:序③ 注释里的 3 是三台节点**当时**的口径) | — |
| `MAX_REVOKED_HOSTS` | 0 | 个 | 实测 | S0 P2 `revokedHosts = 0`+`/etc/dshs/revocations.json` 空清单 | — |
| `MAX_AUTH_FAILED` | 50 | 次 | 推导 | 累计值会随时间涨(⚠️ 探针自己也污染它,见交接单 §5 S6);取"远大于稳态自然增长、又能挡住暴力破解"的档 | 经验上界 |
| `PROBE_CODE_SET` | 200,401 | — | 实测 | S0 P7a:两个面在无凭据时回 `401`;带门户 Host 时回 `200` | `curl -w '%{http_code}'` |
| `PORTAL_CODE` | 200 | — | 实测 | S0 P2/S6:`curl -s --http1.1 -o /dev/null -w '%{http_code}' -H "Host: alotbuy.com" 127.0.0.1:3080/` | 同左 |
| `LISTEN_COUNT` | 79 | 行 | 实测 | ⛔ **已退役(序⑫ 集合判据替代)—— 仅对账用,不得再作为判据**。原 S0 P5:`ss -lntp \| wc -l`(与 443 单收口态**逐字一致** ✅;⚠️ 该值含"47 有活跃实例"那一档,故与无实例态恒差 `1` 层) | 同左 |
| `NFT_RULES` | 72 | 行 | 实测 | ⛔ **已退役(序⑫ 集合判据替代)—— 仅对账用,不得再作为判据**。原 S0 P5:`nft list ruleset \| wc -l`(与 443 单收口态**逐字一致** ✅;⚠️ **行数不是暴露面**:47 的 `input` 链**一条规则都没有**、policy=`accept`) | 同左 |
| `RELAY_RSS_MAX_KB` | 800000 | KB | 推导 | 由 §5 `MEM_BUDGET_MB` × 0.8 得 ≈ 800 MB(**不随 `MEM_PER_HOST_MB` 变**,故序⑥ 校准后仍保留该值)。⚠️ 原注"每 host 2 MB 假设的哨兵"**已作废**:实测斜率 46.7 KB/台 ⇒ 7515 台时预计 RSS ≈ `48078 + 46.7×7515` ≈ **390 MB**,本哨兵(781 MB)仍留 **2× 余量**;且因 per-stream 开销未测(§9),**运行期真正的操作约束就是本哨兵**,不再是 `--max-hosts` | `MEM_BUDGET_MB × 1000 × 0.8` |
**序⑲ 新增阈值键(`OBS-13/14/15` 的取数来源)**
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `PRESENCE_STEADY_FRAMES_MAX` | 0 | 帧 | 实测(口径 = 交接单 §6 **E1**) | 稳态"变化驱动"⇒ 无状态变化则一帧都不推 | 判据本身(帧数,⛔ 不是时长) |
| `PRESENCE_SAMPLE_HITS_MIN` | 1 | 次 | 推导 | 探针自己连读两次 `/status` ⇒ `ΔstatusHits` **至少 1**;用来把"读不到"与"确实为 0"分开(本线反复踩的假绿) | 探针一次采样 = 1 次读 |
| `PRESENCE_SAMPLE_GAP_MS` | 3000 | ms | 推导 | 必须 **≥ 3 × `PRESENCE_BATCH_MS`**(否则批窗口还没过去,增量无意义) | `PRESENCE_BATCH_MS × 3` = 1000 × 3 |
| `PRESENCE_STALE_P95_MAX_MS` | 15000 | ms | 推导(= 心跳周期) | D3:允许 5–15 s 陈旧;心跳每 `HB_SEC` 刷新一次 `lastSeen` ⇒ 稳态陈旧度的**上界就是心跳周期** | `HB_SEC × 1000` = 15 × 1000 |
> 🆕 **序⑫ 新增 4 键(`OBS-11` 集合判据的白名单,⛔ 取代 `LISTEN_COUNT` / `NFT_RULES`)**
> ⚠️ 值格**必须纯数字/纯 `地址:端口`**(夹注 ⇒ `NaN` ⇒ 假红);⛔ 值内**不得含 `|`、`*`、反引号**
> (探针 `cleanValue()` 会**剥掉 `*` 与反引号** ⇒ 写 `*:443` 会被改成 `:443` ⇒ 判据全红)。
| 键 | 值 | 单位 | 等级 | 来源定位 | 复算式 |
|---|---|---|---|---|---|
| `LISTEN_REQUIRED` | `0.0.0.0:22,[::]:22,0.0.0.0:80,0.0.0.0:443,0.0.0.0:888,127.0.0.1:3080,127.0.0.1:20080` | — | 实测 | 🆕 序⑫ S1:**必在集**(缺一条 ⇒ FAIL)。口径 = **平台工作必需 且 不受实例/端点在线态影响**的固定口。值按 47 上 `ss -lntp` **原文**逐条写;⚠️ `3080` 的口在 47 上是 `127.0.0.1:3080`(⛔ **不是** `0.0.0.0:3080` —— 原规划稿示例写错,以原文为准) | `ssh -p 22 bt-server "ss -lntp"` |
| `LISTEN_ALLOWED` | `0.0.0.0:58888,0.0.0.0:8765,127.0.0.1:15432,127.0.0.1:19100` | — | 实测 | 🆕 序⑫ S1:**允许集(非必在)** —— 出现合法、消失**不红**(只是某个可选项没开:宝塔面板口 / 管理 UI / 控制面 PG / 本机 worker agent) | 同上 |
| `LISTEN_ALLOWED_RANGES` | `127.0.0.1:20000-20999,127.0.0.1:21000-21999` | — | 实测 | 🆕 序⑫ S1:**区间允许集** = 两台 worker 的实例端口区间。⚠️ **span 取 1000**(⛔ 不是规划稿示例的 100):`dshs-worker` 单元实测 `DSHS_INSTANCE_PORT_BASE=20000`(47,经 `/proc/<pid>/environ`)/`=21000`(106,经 `/etc/dshs-worker.env`)+ 两者 `DSHS_INSTANCE_PORT_SPAN=1000` ⇒ 并集 `[20000,22000)` 正好落在 relay 声明窗口 `[19000,22000)`(`--base 19000 --span 3000`)**之内**。**拨号池⛔不在此键内**(复用 `DIAL_POOL_BOUND` 派生,避免两处漂移) | `ssh -p 22 bt-server "tr '\0' '\n' < /proc/$(ss -lntpH 'sport = :19100' \| grep -oP 'pid=\K[0-9]+' \| head -1)/environ \| grep INSTANCE_PORT"` |
| `NFT_ALLOW_INBOUND` | `tcp:22,tcp:80,tcp:443,tcp:888,tcp:3080,tcp:58888` | — | 实测 | 🆕 序⑫ S4:**入站 accept 白名单**(`nft -j` 归一后 `acceptSet ⊆` 本键)。归一形态 = `<proto>:<dport>`;「无 dport 匹配的 accept 规则」归一为 `<proto>:any` —— ⚠️ 47 的 `input` 链**目前 0 条规则** ⇒ 集合为 ∅、本键暂无标记位;**将来若出现必须显式加进来**(⛔ 不许放宽判据) | `ssh -p 22 bt-server "nft -j list ruleset"` |
> 📌 **成员归属判据(序⑫ S1 明文要求,防下一棒再吵)**:
> - **必在**(`LISTEN_REQUIRED`)= 它不在 ⇒ **平台本身坏了**(`sshd` / nginx `80`·`443` / 宝塔 `888` / 平台门户 `3080` / relay 回环口)
> - **允许**(`LISTEN_ALLOWED` + `_RANGES`)= 它不在 ⇒ 只是**某个可选项没开**(面板口 / 管理 UI / PG / worker agent / 实例档 / 拨号池)
> - 🔑 **"有/无实例态"⛔不编码进参数表**(那等于把"对状态敏感"这条缺陷写进单一来源 = 净退化,违 **R11**)⇒ 实例档直接进**允许区间**:出现不报、消失不红。
> ⚠️ **附注(序⑥ S3(b) 明文要求登记的 OBS 侧口径)**:relay `/status.sessions[].rttMs` 是**心跳往返**
> (含应用层处理与验签),**⛔ 不得当链路 RTT 用**,**⛔ 更不得据它下"跨云不可玩"的结论** ——
> 实测三方对比:ICMP 148 ms | TCP 握手 median 153 ms | relay 心跳 344–376 ms(**差 2.3×**)。
> 真判据请用 `overlay-jitter.cjs --icmp/--tcp`。
---
## §7 待测项汇总(⛔ 一个都不许编数;**与表内 `待测` 单元格一一对应**)
> 🔢 **计数口径(E1 判据)**:表内 `待测` 单元格 **4 个 → 0 个**(序⑥ 全部换成实测,见下表「结果」列)。
> 第 **5** 条是「**推导值待校准**」(`MEM_PER_HOST_MB`)—— 序⑥ 已校准为 **实测 0.06 MB/台**。
| # | 键 | 换测条件 | 归属 | 序⑥ 结果 |
|---|---|---|---|---|
| 1 | `HOLE_PUNCH_RATE_LOCAL` | 3–5 台真机(分层:家宽 / CGNAT / 移动 / 企业网) | 序⑥ | ✅ **实测(分层)**:n=3 对,2/2 对可打洞(对端→本机 10/10);⚠️ 首选观察器不可用(云安全组拦 UDP 入站)⇒ 走第三方 STUN;**不代表家宽场景** |
| 2 | `PER_PLAYER_BW_LOCAL` | 同上 | 序⑥ | ✅ **实测**:9.8 KB/s/玩家(n=10)/ 3.9(n=50);聚合天花板 200–350 KB/s |
| 3 | `WAN_STEADY_THROUGHPUT` | 两端可控载荷(本次两个面在无凭据时只回 24–68 B) | 序⑥ | ✅ **实测**:352 KB/s(106→47)/ 12213 KB/s(47→106),5 样本中位数,经 relay,稳态段 30 s |
| 4 | `JITTER_LINK_MEASURED` | 同上;⚠️ 这是**游戏可玩性的决定项**(`调研…:118`) | 序⑥ | ✅ **实测**:`p95(|ΔRTT|)` = **3 ms ⇒ 达标**(限值 20 ms 属估值口径) |
| 5 | `MEM_PER_HOST_MB`(**推导值待校准**) | 压到 ≥ 20 台再量 RSS 斜率 | 序⑥ | ✅ **实测**:N=2..150 六点,斜率 **46.7 KB/台**,**R²=0.942**;原推导 2 MB **高估 36×** |
> ✅ **序⑨ 收口(2026-09-17 14:xx)**:**无新增待测项**(表内 `待测` 仍为 **0** 个)。本轮新增的两个键
> (`DRILL_SAMPLE_N` = 5 / `RELAY_GRACEFUL_BURST_MS` = 15000)**都是实测/推导值**,⛔ 不含 `待测`;
> `DRILL_POLL_MS` 由 2000 改 500 属**测量修正**(剔掉 ≤2 s 量化误差),⛔ 不是待测项换测。
> 另:序⑨ 的四段分解登记在 **§9 第 9 行**(属"已知边界/实测记录",不进本表 §7 的"待测"口径)。
**合计:`待测` 4 项 → 0 项;待校准推导 1 项 → 已校准。(E1 判据达标)**
> 🆕 **序⑦(2026-09-17)复算**:仍为 `待测` **0 项** —— 序⑦ 新增的 `RELAY_FAILOVER_*` 5 键**全部取默认值(= 源码真值)**,不产生新的「待测」;同时 §9 第 5 行(无失败切流)改判**已闭环**。⛔ §9 第 6 行(per-stream 内存)**仍未测**,仍是序⑦ 之后的回头条件。
>
> 🆕 **序⑧(2026-09-17)复算**:仍为 `待测` **0 项** —— 序⑧ 新增 3 键(`RELAY_FAILOVER_EXEMPT` = 源码默认值 `1`;
> `DRILL_COOLDOWN_MS` / `DRILL_NO_SWITCH_OBSERVE_MS` = **演练脚本自用的推导值**)**均不产生「待测」**。
> ⚠️ 两条**口径**要记住:① `DRILL_COOLDOWN_MS` 是**演练期覆盖值**,⛔ 不是生产值(生产恒为 `RELAY_FAILOVER_COOLDOWN_MS=300000`);
> ② 幕 4b 的结论**标注了非生产冷却值**方可复现(D9)。§9 第 5 行追加"冷却语义拆分已闭环";§9 第 6 行**仍未测**。
> 🔴 **回头条件(已执行,2026-09-17 序⑥ S7)**:`MEM_PER_HOST_MB` 与 `WAN_STEADY_THROUGHPUT` **双双换成实测** ⇒ §5.2 已**重算并重下发**两台中继的 `capacity.conf`(225 → 7515),三条校验已重跑,`OBS-02` 复验 PASS。⛔ 未触发项:无(两项都换成了实测)。
---
## §8 权限影响评估(R5;**只有产出,无动作**)
> 交接单 §4.1-7 的**固定 6 列**:`对象 / 是否扩大权限面 / 扩大到哪一类 / 是否已可收窄 / 证据 / 结论`。
> ⛔ **结论列只允许「收窄」或「维持」**;若某项确需扩大 ⇒ 停下报告(命中 R5)。
| # | 对象 | 是否扩大 | 扩大到哪一类 | 是否已可收窄 | 证据(代码行 / 命令原文) | 结论 |
|---|---|---|---|---|---|---|
| ① | **虚拟网卡驱动**(打洞所需) | **否(本单零动作)** | 若将来做 ⇒ **权限位**(管理员/内核态驱动) | ✅ 可收窄:**不做虚拟网卡**(纯 relay 中继形态已闭环),要打洞也只走 **UDP 用户态**(无驱动) | 本表 §3.2 `HOLE_PUNCH_RATE_LOCAL` = 待测;现网 2 节点全走 relay(`/status.networks[0].sessions = [manager, w-106]`);⚠️ 我方 relay 只做 TCP/WS,**代码里没有任何 TUN/TAP 调用**(`grep -rn "tun\|tap\|ip tuntap" src/net/relay/` = 0 命中) | **维持** |
| ② | **骨干节点开端口** | **否** | — | ✅ 已收窄:骨干成员**只拨出、不开入站**(worker 永远只拨出,106 入站 = 0) | 主单元 ExecStart 无 `--host` ⇒ 默认绑 `127.0.0.1`;实测 `ss -lntp` 中 `20080` 只出现在 `127.0.0.1:20080`;`覆盖网络_骨干层方案_20260916.md`「骨干不得被默认征用」 | **维持** |
| ③ | **`nft` 打洞规则** | **否(本单零动作)** | 若将来做 ⇒ **入站面**(放行 UDP 入站) | ✅ 可收窄:现实测 `nft list ruleset` = **72 行**且与 443 单收口态逐字一致 ⇒ 打洞规则**一条都没加** | S0 P5:`ssh -p 22 bt-server 'nft list ruleset \| wc -l'` → `72`;对比交接单 §6 E9 的收口态 `72` | **维持** |
| ④ | **第二中继机(L3 跨机真容灾)** | **否(已实施;R5 要求的"实施态"逐项如下)** | 若实施 ⇒ 入站面(新机新公网口)+凭据面(新节点密钥签发) | ✅ **已实施且两项都没扩大**:**新增监听口 = 0**(relay 仍只绑 `127.0.0.1:20080`;对外**复用 106 既有 nginx 的 443**,证书 = 本机**既有** Let's Encrypt(SAN 覆盖 IP `106.54.21.172`),⛔ 未下发任何证书/私钥)|**新增凭据 = 0**(relay 侧只需**公钥类**文件:`relay-keys.json`、`overlay-signers.json`、`revocations.json`;⛔ `node-*.key` 与 `overlay-signer-key.pem` **一律留在 47**)|**106"入站 = 0"这条已收窄成果**:443 本来就开着(宝塔 nginx),本次**只在既有 server 块内加一条 `location`** ⇒ `ss -lntp` 计数**未变** | `systemctl cat dshs-relay`(106)|`ss -lntp`|106 的 `extension/106.54.21.172/relay-b.conf` | **维持** |
| ⑧ | **临时 UDP 入站面**(序⑥ S4 打洞探测的观察器) | **是(临时,已关闭)** | 入站面(UDP 高位口) | ✅ **已收窄**:对象 = 47 的 `0.0.0.0:21100/21101`(`overlay-holepunch.cjs --observer`),**开放时长 ≈ 75 s**、进程退出即释放;关闭证据 = `ss -lunp \| grep 2110[01]` ⇒ **空**。⚠️ 实测该口**零收包**(连同机发出的都收不到)⇒ 47 的**云安全组拦 UDP 入站**(⛔ 不是 nft:`input policy = accept`)⇒ S4 改走第三方 STUN | `ss -lunp`(空)|本单 §8.4 E6 | **维持** |
| ⑨ | **106 的 443 relay 路由**(序⑥ S8 新增 `location /dshs-relay`) | **是(新增可路由路径,⛔ 未新增监听口)** | 入站**路径**面(不是新口) | ✅ 已收窄:只放行 `/dshs-relay` **一个端点**(⛔ 不写 `location /`、不复制站点任何路径;未知路径 404);relay 侧仍强制 HMAC +节点凭据(`identityRequired = true`) | 本单 §8.6 + `curl -H "Upgrade: websocket" … https://106.54.21.172/dshs-relay` = **101**|`/nope` = **404** | **维持(监听口零新增)** |
| ⑤ | **443 兜底入口**(复核是否真零扩大) | **否** | — | ✅ **已收窄**(相对 sshd 时代**净减 1 个公网口**) | 实测:`32022` 已回收(`ss -lntp` 无该口);443 由 **nginx 复用**(`ss -lntp` 显示 `0.0.0.0:443` + `nginx` 4 个进程),**未新增监听**;`/etc/dshs.env` 的 `DSHS_OVERLAY_BOOTSTRAP_SEEDS` 含 `relay-direct.alotbuy.com/dshs-relay`,而 `DSHS_OVERLAY_ADDR_OVERRIDES=relay-direct.alotbuy.com=47.77.182.89` ⇒ 走的是**既有** 443 | **维持** |
| ⑥ | **relay 拨号白名单 `dialers`**(R5 引入) | **否** | — | ✅ **已收窄**:白名单是**准入收窄**(默认拒绝,⛔ 不是放开);且**构造时定型、运行期不可改** | `src/net/relay/server.ts:355` `private readonly dialers`;`:404` `normalizeDialers()` 在构造期;`:747` `const wantDialer = this.dialers.get(network)?.has(hostId) === true`(默认拒绝);drop-in `dialers.conf` = `Environment="DSHS_RELAY_DIALERS=ops:manager"`(只有 **1** 个拨号方) | **维持** |
| ⑦ | **每机独立密钥与信任根保管** | **否** | — | ✅ **已收窄**:私钥**只在本机**、信任根在**离线**签发;relay 侧只有**公钥**与**签名者集合** | `/etc/dshs.env` `DSHS_OVERLAY_NODE_KEY_FILE=/etc/dshs/node-manager.key`(**私钥路径仅本机**);drop-in `identity.conf` 里**只有** `DSHS_OVERLAY_ROOT_PUBKEYS=<公钥>` + `DSHS_OVERLAY_SIGNER_SET_FILE` ⇒ **私钥从不出现在 relay 的配置面**;`:399` `this.trustedSignerKeys`/`:401` `this.requireIdentity` 均 `readonly` | **维持** |
**两条附注**
- ⚠️ 本表 §5.1 的「**每 host 0.06 MB**」是**空闲会话**的实测斜率 ⇒ 它不覆盖 per-stream 缓冲(见 §9 新登记行)。
- ⛔ 本表口径仍成立:**零新增公网端口、零新增入站面、零凭据外发**;序⑥ 的两处"潜在扩大项"(④ 第二中继 / ⑧ 临时 UDP / ⑨ 443 路由)**逐条列在表内**,结论均为**维持**或**收窄**。
---
## §9 未纳入本表的已知边界(**登记,不改**)
| # | 边界 | 为什么不动 | 归属 |
|---|---|---|---|
| 1 | 引导链缓存两支无"答出者"信息 ⇒ 切兜底有 ≤ `DIRECTORY_REFRESH_SECONDS`(300 s)**收敛期** | 交接单 §8.7④ 明确"本单把 300 s 收进参数表并写明该收敛期,⛔ 不改缓存结构" | 已闭环(本表 §4) |
| 2 | 106 的 agent 面不吃引导链(`DSHS_RENDEZVOUS_URL` 被当 relay URL 直接用) | 交接单 §2 P8:**只记录、⛔ 不许顺手撤**(撤掉 = `tunnel===undefined` 生产回归) | 若要改,归序⑥ |
| 3 | D2 字面判据不可满足(CF 泛解析) | 未做灰云记录 ⇒ 属"解析层也不经 CF"的更大改动 | 序⑥ 后候选 |
| 4 | relay 进程**没有** `MemoryMax`(只有本表的软口径 `--max-hosts`) | 加 cgroup 内存上限 = 改单元语义 + 可能 OOM-kill relay(新失败模式)⇒ **超出本单范围**,且 R11(不许净变差) | 序⑥ 候选(与 `MEM_PER_HOST_MB` 校准一起做)→ ⚠️ 序⑥ 只做了**测量与登记**,**仍未加** `MemoryMax`(同上理由,且实测后 `--max-hosts` 已非绑定约束) |
| 5 | 🔴 **无"失败切流"**(序⑥ E9 后半判红的**根因**):客户端把 relay url **在首次解析后钉死** —— `main.js --client` 无重解析;worker 走 `DSHS_RENDEZVOUS_URL` 同样不吃引导链;只有 Manager 的**拨号通道**有周期性重解析(`web/server.ts#refreshOverlay`),而它的换址条件是"**目录里的地址变了**",与"当前 relay 挂了"**无关** | 修它 = **新功能**(需"连接失败后重解析 + 排除已失败 relay"),⛔ 属单外发现,只报告不动手(R7) | ✅ **已闭环(序⑦ · 2026-09-17)**:候选集不再退化成单点(`listOverlayRelayCandidates`);三处客户端(Manager 拨号 / worker 实例面 / `relay --client`)接同一个 `RelayFailoverSupervisor`;阈值见本表 §4 `RELAY_FAILOVER_*`。⛔ 服务端零改动(D8)**|序⑧:冷却语义拆分已闭环**(2026-09-17)—— 冷却表结构化(键仍按 url,新增 `kind`)+ `replace()` 增 `origin`(health 可一跳豁免 / directory ⛔ 不可)+ 豁免有界(每 url 每冷却周期一次)+ 双开关与判别器(`RELAY_FAILOVER_EXEMPT`、`exemptSwitches`、`|豁免` 日志标记)|真机判据 = 演练幕 4 系列(`--scene 4 / 4b / 4c`) |
| 6 | **per-stream 内存开销未测**:`MEM_PER_HOST_MB` = 0.06 MB/台 只是**空闲会话**斜率;带流量时每条流最多 `QUEUE_MAX_BYTES`(1 MB) 缓冲 | 需带流量的压测(S6 的已定口径是"每个声明 1–2 个端口的空闲会话") | 序⑦ 候选(**回头条件**:`--max-hosts` 若重新收紧,必须先有本数) |
| 7 | 106 的第二中继**不提供** `/dshs-overlay/bootstrap`(目录由 47 的 `127.0.0.1:3080` 签发;实测 3080 只绑回环、106→47:3080 `TCP_BLOCKED`) | 两条绕法都有代价:走 47:443(**同一失败域**,等于没增益)/搬证书私钥(命中 R5 扩大) | **已登记**;功能影响 = 0(客户端 `resolveOverlayRelay` ③ 对取不到的 origin `continue`,且 `relays[]` 首位是主入口) |
| 8 | 106 的 443 vhost 归**宝塔面板**管理:本次把 relay location 放进 `extension/106.54.21.172/*.conf`(面板重写 vhost 主文件**不会**丢它) | ⛔ 若将来面板重建该站点,需复查 `nginx -T \| grep dshs-relay` | 运维注意(已写进 `relay-b.conf` 头部注释) |
| 9 | **换址墙钟的四段分解**(序⑨ 实测 · 2026-09-17,n=5+n=5 真机样本,逐行对齐 journal 时间戳):**检测 15.0–16.6 s**(= `RELAY_GRACEFUL_BURST_MS` 地板 + 0–0.7 s 重试相位)+ **首试延迟 0.3–5.0 s**(`RELAY_FAILOVER_CHECK_MS` 相位 + 拨号耗时)+ **白等 12.02 s → 0.09 s**(修前 `waitUpOn` 对"必然失败的同机候选"吃满 `upTimeoutMs`;修后终态早退)+ **建连 2.7–5.2 s** ⇒ 总 **30.7–34.1 s → 20.9–23.9 s**(`RELAY_FAILOVER_DEADLINE_MS` 30000 内侧 5/5) | 白等段**已修**(`waitUpOnStatus` 终态早退,三处装配点共用一份);检测段 ⛔ **不改**(D3:15 s 的 burst 窗口是"计划内下线不是故障"的有意设计,改它 ⇒ 每次 relay 重启都切流) | ✅ **序⑨ 已闭环**(本表 §4 的 `RELAY_GRACEFUL_BURST_MS` / `RELAY_FAILOVER_UP_TIMEOUT_MS`;复算工具 = `scripts/overlay-failover-drill.cjs --trace`) |
---
## §10 指纹
- **本节口径**(**推荐核对用**,可复现):**整个 §10 不计入** ⇒ 复核命令
`sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum` ⇒ **`13de5f9b77c486d71e5b83ec909b17b2`**(**序⑲ 执行棒收口值 · 2026-09-17 18:1x**;本轮变更 = 新增 §3.6 `PRESENCE_*` 五键 + §6 新增 `OBS-13/14/15` 三行与 `PRESENCE_STEADY_FRAMES_MAX` / `PRESENCE_SAMPLE_HITS_MIN` / `PRESENCE_SAMPLE_GAP_MS` / `PRESENCE_STALE_P95_MAX_MS` 四阈值键;上一版 = `8f08e74b026e6e5b5e1b3db813f031ae`(序⑰ 收口)→ `99e9e17b0c1ce0550e4bc7626a5a0494`(序⑨ 收口)→ `e6b669c257d8e8964273b3b400238351`(序⑧ 收口)→ `24cf2efdbcdcbe61267126ed65dba006`(序⑦ 收口)→ `9641f3d67fbc2e67cadf6f78e516f24c` → `44af9ea5ca15ae21f2604a9cd3a935b8` → 序⑥ 收口 = `db1317c2f7aaef7b47785c1f4fc9de03`)
- **全文件 md5**:请用 `md5sum 参数表_覆盖网络_20260917.md` 现取 —— ⛔ 此处**故意不内嵌数值**:它包含本节自身,写进去即刻失效(自指)。
---
## §11 补记(2026-09-17 11:1x,**在 §10 指纹口径之外**)
- ✅ **"第 4/5 台真机来源"已拍板**(登记给 §9 第 5 行那条线的同批遗留):用户原话「**1 本机内存大 可以模拟多台**」⇒ 选 **本机模拟多台**,放弃"用户自备设备"与"新开云主机"。
- 本机实测:总内存 **47.6 GB** / 空闲 27.4 GB / **32 核**;relay 单实例 ≈ **48 MB** ⇒ 可模拟数十台。⚠️ **共用同一出口 IP** ⇒ 对"切流逻辑"够用,对"家宽 / 运营商 NAT 差异"无增量。
- ⛔ **不改本表任何数值**:`HOLE_PUNCH_RATE_LOCAL`(分层实测 2/2)与其样本口径**保持原样**;本补记只登记"多实例可作第 4/5 台"这一环境决定。
- 🔒 本补记位于 §10 之后 ⇒ **复核指纹 `db1317c2f7aaef7b47785c1f4fc9de03` 仍然有效**(后续引用无需换值)。
- **谁能改这张表**:任何一次实测替换(`待测` 换真值 / `MEM_PER_HOST_MB` 校准)**都会改指纹** ⇒ 改完请**同步更新本值**,并在工作区日志里记一笔"哪个键从什么换成什么"。