- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
60 KiB
交接单 · 检测时延与 deadline(覆盖网络 · 序 ⑨)
一单只做一件事:把「杀中继 ⇒ 切流完成」的 30 s 墙钟拆开量化,并用分解结果决定改哪里 —— 目标是让真机样本回到
RELAY_FAILOVER_DEADLINE_MS(30000) 的内侧,⛔ 不是把 deadline 调大。上游依据(均已核实,⛔ 不必重读全文):
交接单_切流冷却语义_20260917.md§8.8-2(现象 + 口径提示"该值含静默失效检测时延 ≈ 2.5×HB_SEC")—— ⚠️ 本单 §1.2 已用实测勘误该口径,见RC-5- 同上 §8.8-3(OBS-11 回头条件)⇒ 本单 §2-P7 已复取,判定见下
- 同上 §8.8-4(
RELAY_FAILOVER_COOLDOWN_MS=0自锁)⇒ 本单 §7 回滚 ⛔ 禁用该值- 同上 §9(
switcher.ts阈值六键默认值300000 / 30000 / 15000 / 3 / 2000 / 12000+ 三个装配点 + env 覆盖生效)交接单_中继失败切流_20260917.md§8.8-2(五样本全景30563 / 27878 / 29176 / 32550 / 29586;触发条件 = 样本 > 30000 ⇒ 立即回头)—— 本单的立项依据- 代码单一来源:
D:/github/dsh_shenxian/src/net/relay/switcher.ts(+client.ts/web/server.ts)本单性质:规划棒产出。⛔ 只出单、不改服务器、不改代码(规划与执行分离)。
§1 目标
1.1 一句话
先证明这 30 s 花在哪两段上,再决定改「检测」还是改「拨号」 —— 现在证据指向:检测 ≈ 15 s(优雅重启的 burst 窗口地板)、拨号 ≈ 12 s(对一个必然失败的同机候选白等 upTimeoutMs)、真正建连只要 ≈ 2.8 s。
1.2 本单的核心判定:30 s 去哪了(用现有日志分解,不需要新埋点)
方法:[relay-client] down / [relay-skip] / [relay-switch] 三种行本来就带毫秒级时间戳(journalctl -o short-unix)⇒ 分解是"读现有日志",不是"加新探针"。
样本 ① · 序⑧ 幕 4 收口(drill 读数 29586 ms,与五样本里的 29586 同一轮) —— 逐行原文:
| 事件 | epoch | 相对 BYE | 段长 |
|---|---|---|---|
[relay-client] down (peer bye: server restarting (was up)); attempt #0 [graceful, burst window 15000ms] |
1789617465.999982 |
0 | — |
attempt #1, retry in 975ms(burst 窗口耗尽,退避才从 0 起算) |
1789617481.084742 |
+15.085 s | ① 检测 = 15.08 s |
[relay-skip] ⛔ 新通道起不来(wss://relay-direct.alotbuy.com/dshs-relay)… 该候选进冷却 300000ms |
1789617493.375446 |
+27.376 s | ② 白等 = 12.00 s(= waitUpOn 吃满 upTimeoutMs) |
[relay-switch] #1 wss://alotbuy.com/… -> wss://106.54.21.172/…(原因:当前通道不健康(… attempts=4 unhealthyForMs=29376 …)) |
1789617496.196091 |
+30.196 s | ③ 建连 = 2.82 s |
样本 ② · 序⑦ 30563 ms 那一轮 —— 同结构、逐行原文:
| 事件 | epoch | 相对 BYE |
|---|---|---|
peer bye: server restarting (was up) … burst window 15000ms |
1789616881.632718 |
0 |
[relay-skip] ⛔ 新通道起不来(relay-direct)… 进冷却 300000ms |
1789616910.195416 |
+28.56 s |
[relay-switch] #1 alotbuy -> 106(… attempts=5 unhealthyForMs=30563 …) |
1789616912.919233 |
+31.29 s(drill 读数 30563 ⇒ 偏移 0.73 s = systemctl stop 返回晚于 BYE 的时间) |
⇒ 两样本结构完全一致:15.0(检测)+ ~1.5(tick 相位)+ 12.0(白等)+ 2.1(建连)≈ 30.6 s。
分解结论(一句话):
检测段 = 15 s 不是"半开检测",而是
gracefulBurstMs的地板;拨号段 = 12 s 里 12 s 全是白等(waitUpOn不对终态失败早退),真正的跨机建连只要 2.8 s。 ⇒ 两段各占一半;其中 12 s 是纯浪费(生产目录前两条候选同在 47)。
五样本按同一结构反算(⛔ 未逐行复核,须由 §5-S2 验证):
| 样本 | 总 | 检测 | tick 相位 | 白等 | 建连 | 是否自洽 |
|---|---|---|---|---|---|---|
| 30563 | 30.56 | 15.00 | ~1.5 | 12.00 | ~2.1 | ✅ 已逐行复核 |
| 29586 | 30.20 | 15.08 | ~0.3 | 12.00 | 2.82 | ✅ 已逐行复核 |
| 27878 | 27.88 | 15.00 | ~0.2 | 12.00 | ~0.7 | ⚠️ 反算自洽,未复核 |
| 29176 | 29.18 | 15.00 | ~0.2 | 12.00 | ~2.0 | ⚠️ 反算自洽,未复核 |
| 32550 | 32.55 | 15.10 | ~0.2 | 12.00 | ~5.2 | ⚠️ 反算自洽,未复核 |
🔴 可被证伪:以上结构的硬地板 = 15.0 + 12.0 = 27.0 s。若 §5-S2 采到任一 < 27 s 的样本 ⇒ 本单的分解被证伪,立刻停下报告(⛔ 不许带着错分解去改代码)。
根因清单(按可省时长排序):
| # | 根因 | 证据 | 可省 |
|---|---|---|---|
| RC-1 | waitUpOn 对"终态失败"不早退 —— server.ts:499-506 只轮询 state === 'up',直到 deadline 才返回 false ⇒ 每个死候选固定吃满 upTimeoutMs。生产目录 [alotbuy.com(47), relay-direct.alotbuy.com(47), 106] 前两条同机(server.ts:517-519 注释原文已承认"这个坑一定会踩到"),杀 47 ⇒ 第一次 tick 必然先试 relay-direct ⇒ 白等 12 s |
两样本的 [relay-skip] ⛔ 新通道起不来 行,间隔 ≡ upTimeoutMs |
~12 s |
| RC-2 | gracefulBurstMs(15_000) 期间 attempts 恒为 0(client.ts:565-580 三处 this.attempts = 0)⇒ unhealthy()(switcher.ts:244 = attempts ≥ 3 ∨ unhealthyForMs ≥ graceMs)只能靠 graceMs=15000 成立 ⇒ 优雅停机(systemctl stop/restart、部署)路径的检测地板 = 15.0 s |
两样本 attempt #0 [graceful …] → attempt #1 间隔 = 15.085 / 15.06 s |
受 DP-4 约束,本单⛔不改 |
| RC-3 | tick 周期 2000 ms ⇒ 判不健康 → 发起换址之间存在 0–2 s 相位误差 | 实测 0.29 / 1.48 s | 0–2 s |
| RC-4 | DRILL_POLL_MS = 2000 ms ⇒ 演练读数本身有 0–2 s 系统性高估 |
drill waitSwitch 每 2 s 轮询一次 |
测量误差,非产品时延 |
| RC-5 | 🔴 勘误:上单 §8.8-2 的口径提示"该值含静默失效检测时延 ≈ 2.5×HB_SEC(= 37.5 s)"在优雅停机路径上不成立 —— 两样本都没走到半开检测(half-open suspected 行为零命中)。⇒ 该口径只在丢包型静默失效时生效;HB_SEC 不在这条关键路径上 ⇒ ⛔ 别为了它去动心跳/控制面流量模型 |
样本 ①②的原文链条里无 half-open 行 |
0(防误改) |
1.3 三问的判定
① 30 s 里多少是检测、多少是拨号?
判定:检测 15.0–15.1 s(50%)/拨号 12.0 s 白等 + 0.7–5.2 s 建连(50%)。 依据见 §1.2 两个逐行复核样本。
② 该先改哪里?
判定:先改「拨号段的白等」(RC-1),⛔ 不动 deadline、⛔ 不动 HB_SEC。
- 为什么不是调 deadline:deadline 是验收判据(上单 §8.8-2 原文:"放宽 = 作废本轮判据")⇒ 调大 = 把问题藏起来,明确违反"要解决问题、不将就妥协"。
- 为什么不先动检测段(RC-2):15 s 的 burst 窗口是有意设计 ——
client.ts:30注释原文"计划内下线不是故障:gracefulRetryMs后立刻重连"。把它改成"graceful 也触发切流"⇒ 每次 relay 重启/部署都切一次流 = 正是 D5 要防的抖动。⇒ 判为已定项,⛔ 不动(§4.1-D3)。 - 为什么拨号段是"真缺陷":12 s 不是"等一个慢候选",是等一个必然失败的候选(
waitUpOn不看终态),且生产目录结构决定每次从 47 切走都必然命中 ⇒ 纯浪费、且有确定性的修法。
③ 判据怎么改才算"不将就"?
判定:把"总时长 ≤ deadline"细化为"三段各自的预算",并把测量误差从判据里剔掉。
- 总判据保留(
kill → switch ≤ 30000 ms),但必须同时看三段:检测 ≤ 16 s(graceful)/白等 ≤ 1 s(修后)/建连 ≤ 6 s。 DRILL_POLL_MS2000 → 500:否则 ≤2 s 的量化误差会让"是否超 deadline"的判定既可能假红也可能假绿(RC-4)。这是测量修正,不是调参。
§2 只读前置(⛔ 只读,不改;P1–P10 逐条核实后才允许进 S 段)
| # | 命令(原文) | 期望 |
|---|---|---|
| P1 | bash "D:/github/dsh_shenxian/dsh-server-docs/scripts/handoff-guard.sh" --claim-exec "覆盖网络线-序9执行棒" |
✓ 已持全局执行锁。抢不到 ⇒ 有会话在跑 ⇒ 停下报告 |
| P2 | "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AI技能/aliyun-dsh-server/state.py" |
锁 owner = 本会话名;HEAD = 640813e;入口 = 接续入口_覆盖网络线_20260916.md,其 §2 指向本单 |
| P3 | cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum |
e6b669c257d8e8964273b3b400238351(【实测·本棒复取】;不一致 ⇒ 参数表被人动过,停下报告) |
| P4 | cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §8 回报格式/,$d' 交接单_检测时延与deadline_20260917.md | md5sum |
与 §8.9 里记载的"本单出单值"逐字一致(⚠️ 该值故意只写在 §8 之内 —— §8 本身不计入哈希,写在这里会让哈希自指失效) |
| P5 | 上两单前缀:sed '/^## §8 回报格式/,$d' 交接单_切流冷却语义_20260917.md | md5sum ⇒ aa3a6ec0d66dd81f465cea2a3a08ad27;…交接单_中继失败切流_20260917.md | md5sum ⇒ 419abf308c00b7668e8898aaa91ba9e8 |
两个值逐字一致(【实测·本棒复取】)⇒ 底稿未被改动 |
| P6 | cd "D:/github/dsh_shenxian" && grep -n "gracefulBurstMs|halfOpenMs" src/net/relay/client.ts && grep -n "const waitUpOn" -A 8 src/web/server.ts && grep -n "private unhealthy|async open|deps.open" src/net/relay/switcher.ts |
锚点存在性(⛔ 不验行号):client.ts ≈ 137/146/567/631;server.ts ≈ 499(waitUpOn 只轮询 up、不看终态)/524;switcher.ts ≈ 244(unhealthy)/300(await deps.open) |
| P7 | cd "E:/ProgramData/AI技能/aliyun-dsh-server" && node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs" > /tmp/seq9-probe-s0.txt 2>&1; echo $? |
12/12 PASS、exit 0。【实测·本棒 13:3x 复取】当前为 10/12(OBS-09 / OBS-11 红) ⇒ 见下方口径说明 |
| P8 | ssh -p 22 bt-server "systemctl is-active dshs-relay dshs || true; curl -s http://127.0.0.1:20080/status" + ssh -p 22 test106 "systemctl is-active dshs-relay dshs-worker || true" |
两台 relay active;47 /status 的 capacity.used = 2、online[] 含 manager + w-106。⚠️ 106 nginx 归宝塔托管 ⇒ is-active nginx=inactive 是正常态,⛔ 别据以判故 |
| P9 | npm.cmd test(必须 Node v22) |
155 tests / 154 pass / 0 fail / 1 skipped(序⑧ 收口基线;⚠️ 若因新增用例上浮,写清"新增 n / 基线 155") |
| P10 | 🔴 分解复现(本单的核心前置):ssh -p 22 bt-server "journalctl -u dshs --since -6h -o short-unix --no-pager | grep -E 'relay-switch|relay-skip|relay-client. down'" ⇒ 抽 §1.2 的两个样本,逐行算出四段数值 |
样本①(unhealthyForMs=29376)= 检测 15.085 s + 白等 12.00 s + 建连 2.82 s;样本②(unhealthyForMs=30563)= 28.56 / 12.00 / 2.72 s。复现不出 ⇒ 停下报告(说明 §1.2 的分解是错的,⛔ 不许带着它进 S3) |
2.1 P7 的口径(承上单 §8.8-3 的回头条件)—— 本棒已复取,判定:未命中
- 复取值:
ss -lntp | wc -l= 77(= 76 socket + 1 行表头);两次连采同值(sample1=77/sample2=77);nft=72、relay 口绑回环1/1、门户200、两台 relayactive。 - 环境态钉因:
LOCAL_INSTANCE_PORT(20000) 未监听 ⇒ 47 无活跃实例(/opt/dsh/users/main存在但无实例进程,ps里只有控制面 /w-47worker / relay 三个 node)。⇒ 口径目标 = 78("无活跃实例"档),不是 79。 - 判定:
78 − 77 = 1,差值 1 < 2 ⇒ 未命中上单 §8.8-3 的回头条件("差值 ≥ 2");且另一分支要求"有活跃实例时仍 ≠ 79",本棒无活跃实例 ⇒ 分支不适用。另:相对序⑧ 收口值(同为 77)零变化。 - ⛔ 只报告不动手(R7):
78 → 77这1 个口从未被定位(上单已核销 9 个固定口 / 64 个拨号池口 /nft/ relay 回环绑定)。⇒ 记为在册未办,本单不修;只把"完整端口清单留档"作为 §5-S0 的一个动作(取证,不是修复),让下次漂移可比对。
2.2 已核实的代码事实(⛔ 执行棒不必重复探索)
waitUpOn不会对失败早退(RC-1 的代码真身):server.ts:499-506每 100 ms 查一次client.status().state === 'up',直到deadline才return false。⇒ 死候选(连不上 / 握手失败)与慢候选在这一层不可区分,代价恒为upTimeoutMs。upTimeoutMs有三处消费点(改一处不够):web/server.ts:524(Manager 拨号通道 · C1)/worker/relay-tunnel.ts:104与:158(worker 实例面 · C2)/net/relay/main.ts:315(独立relay --client· C3)。⚠️ 序⑦ 的 D1 就是为"同一件事别在多处各写一份"立的 —— 要么抽公共,要么每处都改并逐处验收。gracefulBurstMs是"写死的默认值、不可配":client.ts:137是可选 option,全仓grep只有client.ts:567的?? 15_000,无任何 env 键 / 无任何装配点赋值 ⇒ 运行时无法调(对比RELAY_FAILOVER_*全部可经 env 覆盖)。- burst 期间
attempts被强制归零:client.ts:565-580三个分支各自this.attempts = 0(queued/graceful/burst)⇒ 与switcher.ts:244的unhealthy()组合出"15 s 检测地板"(RC-2)。 - 半开检测参数与触发:
client.ts:631halfOpenMs ?? max(3000, hbSec×1000×2.5)= 37.5 s;:632巡检tick = halfOpen/4;:636判据age > halfOpen。⚠️ 仅在state === 'up'时计时 ⇒ 只在"连着但不来帧"(丢包)时生效(RC-5)。 - 演练读数的两个系统性偏差:①
waitSwitch每DRILL_POLL_MS(2000) 轮询一次 ⇒ 读数高估 0–2 s;②t0 = Date.now()在stopRelay返回之后 ⇒ 比 relay 的BYE行晚 ≈ 0.6–0.73 s(两样本实测;这正好解释为什么 drill 读数与unhealthyForMs数值接近)。 [relay-switch]行自带state/attempts/unhealthyForMs/阈值⇒ 三段的边界不需要新埋点,只需时间戳对齐(D2 的"⛔ 不新造探测帧"继续成立)。- 本机 = 生产的前身 ⇒ 改动会经下一次
scp传导到 47/106。⛔ 推送前先git status --short确认待传清单只含本次真实改动(当前 = 43 项)。 src/net/relay/**在 git 里 untracked ⇒ 改动用git diff看不到,只能用grep -c/wc -l/git status --short核(⚠️ 目录里还留着一个client.ts.bak-seq7-*)。
§3 范围
3.1 要改的(且只有这些)
| # | 文件 | 改动 | 理由(为什么非改不可) |
|---|---|---|---|
| 1 | src/web/server.ts(+ 对齐 worker/relay-tunnel.ts / net/relay/main.ts) |
waitUpOn 增加"终态失败早退":新通道一旦进入终态失败态(建议判据 = 已发生一次明确的失败退避且不在 burst 窗口内,或 lastFatalReason 命中)⇒ 立即 return false,⛔ 不再等满 upTimeoutMs。三处消费点逐处验收;能抽公共就抽公共(D1) |
RC-1 的唯一落点 ⇒ 直接省掉 12 s。判据必须"不误伤慢候选":仍能连上只是慢的候选,依旧享受完整 12 s |
| 2 | scripts/overlay-failover-drill.cjs |
新增 --trace:从 journal 抽 down/skip/switch 三类行,逐样本算出四段(检测 / tick 相位 / 白等 / 建连)并落盘原始行 |
E1–E5 的判据本体(分解必须可被第三方复现)。⛔ 原有 --scene 1|2|3|4|4b|all 语义一字不改 |
| 3 | test/relay-failover.test.mjs |
新增用例:死候选 ⇒ open() 提前失败(注入"永不 up 且已进 backoff"的假客户端)/慢候选 ⇒ 仍等满超时(护栏,⛔ 防"早退"误伤) |
本线纪律:先红后绿已实证两次;"早退"必须有护栏用例否则会把慢候选误杀(= R11 净退化) |
| 4 | src/net/relay/client.ts |
把 gracefulBurstMs 参数表化(新增 env 键 RELAY_GRACEFUL_BURST_MS,默认值 15_000 不变) |
消除"唯一一个不可配的时延常量"(符合本线既有的"阈值零魔数"纪律,overlay-probe E6)。⚠️ 只加可配性,⛔ 不改值 |
| 5 | 参数表_覆盖网络_20260917.md |
① DRILL_POLL_MS 2000 → 500;② 新增 DRILL_SAMPLE_N = 5;③ 新增 RELAY_GRACEFUL_BURST_MS = 15000;④ **§9 表格新增一行「时延分解(序⑨ · 实测)」**记录四段数值 + 两个样本的 epoch 原文;⑤ §7 计数回填;⑥ §10 指纹更新 |
参数表 = 单一来源。🔴 值格必须纯数字(上单 §8.6 的假红坑:夹注 ⇒ NaN ⇒ OBS-02 假红) |
| 6 | overlay-failover-drill.cjs 的 waitSwitch / 参数表 DRILL_POLL_MS |
轮询周期 2000 → 500 ms(随第 5 项) | RC-4:把 ≤2 s 的量化误差从判据里剔掉 —— 否则"是否超 deadline"两端都可能误判 |
3.2 ⛔ 不动什么(防顺手扩大)
- ⛔ 不动
RELAY_FAILOVER_DEADLINE_MS(30000)。这是本单的纪律核心:放宽判据 = 作废判据。若 §5-S7 复测仍超界 ⇒ 写进 §8.8 报告,⛔ 不许自行改值。 - ⛔ 不动
gracefulBurstMs的默认值(只加 env 可配性 = §3.1-4);⛔ 不动client.ts的 burst 语义("计划内下线不触发切流"是有意设计,见 §1.3-②)。 - ⛔ 不动
HB_SEC(15) —— 分解已证half-open(37.5 s) 不在关键路径上(RC-5);动它等于为一个没发生的路径改控制面流量模型。 - ⛔ 不动
RELAY_FAILOVER_COOLDOWN_MS(300000);🔴 ⛔ 绝对不许把COOLDOWN_MS=0写进任何回滚 / 演练路径(上单 §8.8-4:归零连带废掉"失败候选必须被排除" ⇒ 候选链自锁,实测 121–123 s 无切换)。演练期要缩短只走DRILL_COOLDOWN_MS。 - ⛔ 不动
RELAY_FAILOVER_MIN_ATTEMPTS/GRACE_MS/CHECK_MS(本单不改判据阈值;若要改,先回 §8 报告)。 - ⛔ 不做"同机候选去重 / 并发竞速 open"(§4.2-DP-2):运行时拿不到"同机"维度(候选只是 url 字符串),要做得在取址层新增 host 维度 = 扩大改造面;并发 open 会同时建两条通道(多占口池口 + 多一次 AUTH)⇒ 暴露面/资源面变化需另行评估。记为在册未办(R7 只报告)。
- ⛔ 不新增任何监听口 / 凭据 / 对外可读端点;⛔ 不动 nft / 安全组 / nginx;⛔ 不改 relay 服务端协议。
- ⛔ 不动 worker 会合面(
DSHS_RENDEZVOUS_URL/DSHS_TUNNEL_TARGET—— 已判"撤掉 ⇒ 判不执行",别再复议);⛔ 不动 directory 引导链。 - ⛔ 不重做序 ②–⑧;⛔ 不做 presence / 房间层 / 内容分发(清单第 7 步);⛔ 不做打洞实现。
- ⛔ 不 commit / 不 push(未获授权)。
- ⛔ 范围外的缺陷先报告、不顺手改(R7)。在册:OBS-11 的 1 口差(§2.1)|OBS-09 实例面 000|guest(w-106) 实例页 502|
mksess*.cjs失效|unhealthyForMs记账起点偏早(上单 §8.8-5)|src/net/relay/**untracked 留档缺口|client.ts.bak-seq7-*残留。
§4 决策点
4.1 已定项(规划棒已拍 —— 执行棒不得自行更改;要改必须回写 §8 并说明理由)
| # | 已定 | 理由 / 反例 |
|---|---|---|
| D1 | 顺序 = 先量化再调参:§2-P10 的分解复现成功之前,⛔ 不许改任何 RELAY_FAILOVER_* 的生产值(含 deadline / minAttempts / grace / upTimeout) |
用户明令 + 本单立项初衷。分解已经能用现有日志做出来(§1.2)⇒ 没有理由跳步 |
| D2 | 判据来源 = 现有日志行的时间戳,⛔ 不新增探测帧 / 不新开端点 / 不引外部监控 | 承序⑦ D2;[relay-switch] 行自带 attempts / unhealthyForMs / 阈值,信息已足够 |
| D3 | 15 s 的 burst 检测地板⛔ 不改(保持"计划内下线不触发切流") | client.ts:30 原文语义;改它 ⇒ 每次 relay 重启都切流 = D5 要防的抖动(R11 净退化) |
| D4 | 首选修法 = RC-1 的"死候选早退";upTimeoutMs 降值只作降级替代 |
降 upTimeoutMs 会误伤"慢但能通"的候选(实测 106 建连 2.7–2.8 s ⇒ 降到 4 s 已贴边)⇒ 只有在 RC-1 拿不到 12 s 时才考虑 |
| D5 | DRILL_POLL_MS 2000 → 500(测量修正,非调参) |
RC-4:≤2 s 量化误差会让判定两端误判 |
| D6 | deadline 保持 30000,且必须继续按 30000 判(⛔ 不设"宽限窗口"、⛔ 不写"接近即视为通过") | 判据一旦软化,本单与上单的结论都不可比 |
| D7 | 三处装配点必须逐处验收(server.ts / relay-tunnel.ts / main.ts) |
序⑦ D1 的教训("同一件事在多处各写一遍,其中一处悄悄漏了") |
| D8 | 服务端零改动(relay 侧最多只读) | 缩小失败域(承序⑦ D8 / 序⑧ D12) |
| D9 | 若最终确需动 upTimeoutMs ⇒ 必须同时给出"慢候选不被误杀"的判据(例如"某台成功建连耗时的 P95 + 余量")并回填参数表 |
⛔ 不许凭感觉给一个更小的数 |
4.2 交给执行棒自决(⛔ 不上升为提问)
"终态失败"的具体判据(用 state === 'backoff'?用"已发生 ≥1 次非 burst 退避"?用 lastFatalReason 是否存在?还是给 RelayClient 加一个只读的 failedTerminally() 投影)|早退判据的代码落点(waitUpOn 内联 vs 抽 src/net/relay/ 公共工具)|是否把三处 waitUpOn/waitUp 合并|--trace 的输出格式(四段数值 + 原文行)/落盘路径|DRILL_SAMPLE_N 的采法(是否复用 --scene ctrl)|RELAY_GRACEFUL_BURST_MS 的键名与是否需要 DRILL_ 前缀的演练覆盖|§9「时延分解」行的表格字段设计|日志文案细节。
4.3 真需要用户拍板的(命中才问,且一轮只问这一句)
本单 = 空。
三问的判定全部落在"技术实现 / 测量与判据"边界内(§1.3 已逐条给出判定与依据);最容易被误当成"业务意图"的 D3(relay 计划内重启该不该触发切流) 已有明确的设计原文依据(client.ts:30 注释 + burst 窗口的存在理由)⇒ 属"客观可判",自行拍掉、不上抛。
4.4 技术实现裁决顺序(⚠️ 与 dsh-decision-method §4.4 一致)
① 先复用现成能力(现有日志行的时间戳 + RelayClient.status() 的 state/attempts + 既有 [relay-skip] 判别器)→ ② 再改现有模块(waitUpOn 一处/三处)→ ③ 最后才新增(--trace、两个单测、参数表两个键)→ ⛔ 不引第三方依赖。
4.5 权限影响评估(按红线 R5 要求先出;结论:未命中 R5,暴露面零变化)
| 维度 | 本单影响 | 判定 |
|---|---|---|
| 新增监听口 | 0(只改客户端换址时序;候选集不变) | ✅ 不扩大 |
| 新增凭据 | 0 | ✅ 不扩大 |
| 放宽访问控制 | 0(早退只是"更早放弃一个已失败的候选",⛔ 绝不放宽为"连目录外地址") | ✅ 不扩大 |
| 入站 | 106 入站仍为 0;47 无新增入站口 | ✅ 不扩大 |
| 新增可读面 | 0(D2:⛔ 不新开端点;--trace 只读本机 journal) |
✅ 不扩大 |
| 暴露面净变化 | 0 | ✅ |
⚠️ 若执行中为"能切过去"而允许连目录外的地址 ⇒ 立刻停下:那才是真的 R5(扩大信任面 = 任意重定向),必须回来出评估。
§5 步骤(S0–S9;每步自带一次可执行的验证)
S0 · 只读取证(= §2 P1–P10 + 端口清单留档)
- 动作:按 §2 逐条跑;P3/P5 指纹逐字一致;P10 的分解必须复现;额外把 47 的完整端口清单落盘(
ss -lntp | tail -n +2 | awk '{print $4}' | sort -n | uniq > /tmp/seq9-ports-<ts>.txt)—— 这是取证,用于下次78 → 77类漂移的可比对(⛔ 不是修复)。 - 验证:P1–P10 全部 ✅;P10 的四段数值与 §1.2 一致(容差:检测段 ±0.5 s、白等段 ±0.3 s、建连段 ±1.5 s)。
- ⛔ 硬门:任一 ❌ ⇒ 停下报告,不进 S1。
S1 · 时间轴抽取(只读,先出工具)
- 动作:给
scripts/overlay-failover-drill.cjs加--trace:一次journalctl取回窗口内relay-client. down/relay-skip/relay-switch三类行(⚠️--since一律用@<epoch>;⚠️ 远端读状态命令一律|| true;⚠️ 保留JOURNALCTL-ERR哨兵),按样本切分(以down (… (was up))或进程号变化为界)算出四段,并原样打印所依据的行。 - 验证:对 §1.2 的两个历史样本跑
--trace⇒ 四段数值与 §1.2 表格逐项对得上。 - ⚠️ 口径:
--trace只读,⛔ 不停 relay、⛔ 不改 env、⛔ 不写远端文件。
S2 · N=5 采样(判据分布的地基)
- 动作:跑 5 次"杀当前入口"(每轮之间必须归零:① 两台 relay
systemctl start② 重启 Manager(restart dshs)⇒ 通道回到目录首位 ③ 等 47 的 relay 日志出现新的AUTH OK host=ops/manager);每次用--trace落盘。 - 验证:5 份 trace 齐全;每份的"检测 + 白等"≥ 27.0 s ⇒ 若出现 < 27 s 的样本 ⇒ 🔴 分解被证伪,立刻停下报告(§1.2 的可证伪条款)。
- ⚠️ 注意事项:① 必须避开
--scene ctrl(COOLDOWN=0)那类轮次 —— 会污染候选顺序;② 每轮记录DRILL_POLL_MS值(S6 改前后各一组,读数不可混比)。
S3 · 按分布选方案(门 D1 在此解锁)
- 动作:算三段的中位数与极值:
- 若白等段中位数 ≥ 8 s ⇒ 做 D4 首选(S4 的死候选早退);
- 若白等段中位数 < 8 s ⇒ 说明 RC-1 不是主因 ⇒ 停下报告,把精力转向 RC-3/RC-2 的重新评估(⛔ 不许硬套)。
- 验证:一段结论 + 三行数据(中位数 / 最小值 / 最大值)写进 §8。
- ⛔ 硬门:S2 未跑完 ⇒ ⛔ 不许进 S4。
S4 · 死候选早退(RC-1 落点)
- 动作:
waitUpOn(及另两处等价逻辑)在轮询里增加终态失败判定 ⇒ 立即return false;⛔ 不得把"还在 burst 窗口内的短间隔重试"误判成终态(那会让 relay 的计划内重启被当成死候选 ⇒ 每次部署都切流)。 - 验证:单测 先红后绿;
npm.cmd test⇒ 155 + 新增 / 全绿(基线 155)。 - ⚠️ 易错点:三处消费点(
server.ts:524/relay-tunnel.ts:104、:158/main.ts:315)逐处确认;⛔ 别只改一处就宣布完成。
S5 · 护栏用例(防"早退"误杀慢候选)
- 动作:新增单测:慢候选(连得上、只是
up来得晚,<upTimeoutMs)必须仍然成功;死候选(进 backoff 且出 burst 窗口)必须提前失败。 - 验证:两条同时绿;且既有用例一条不红。
- ⚠️ 不变量:这条是"早退"的护栏 —— 没有它,早退可能变成"更频繁地切到第三候选"甚至"全部候选都判失败"(R11)。
S6 · 参数表闭环
- 动作:
DRILL_POLL_MS2000 → 500;新增DRILL_SAMPLE_N、RELAY_GRACEFUL_BURST_MS;§9 新增「时延分解(序⑨ · 实测)」行(四段数值 + 两个样本 epoch 原文);§7 计数回填;§10 指纹复取回填。 - 验证:
grep -nE 'DRILL_POLL_MS|DRILL_SAMPLE_N|RELAY_GRACEFUL_BURST_MS' 参数表_覆盖网络_20260917.md⇒ 值格纯数字、无夹注。 - ⚠️ 假红坑:值格带夹注 ⇒
NaN⇒OBS-02假红(上单 §8.6 原文教训)。
S7 · 真机复测(修后 N=5,判据不许软化)
- 动作:S4 部署后重跑 S2 的 5 轮(
scp+restart dshs;⚠️ relay 真身在/opt/dsh-relay/lib/,两处都要铺)。 - 验证:五样本 kill→switch ≤ 30000 ms(用新的 500 ms 轮询口径读数);且白等段 ≤ 1 s。
- ⛔ 硬门:若仍有样本 > 30000 ms ⇒ 写进 §8.8 报告并给出下一步候选(含
upTimeoutMs的降值方案 + 慢候选 P95 证据),⛔ 不许自行改 deadline。
S8 · 不退化
- 动作:
--scene all(1/2/3/4/4b)+overlay-probe+npm test+ 门户200+ 双实例面 ∈PROBE_CODE_SET+ss -lntp | wc -l与 §2-P7 对照。 - 验证:见 E9/E10。幕 1/2/3/4 结果不得变红。
- ⚠️ 观察点:若出现新的
[relay-skip]或[relay-switch]形态变化 ⇒ 事件行必须原样抄进 §8(含attempts / unhealthyForMs)。
S9 · 收口
- 动作:清理临时产物(trace 落盘件先移
_中间产物_待清理/);释放锁;登记下一棒 automation(scheduledAt= 收口时刻 + 2~5 分钟);用陈述句告知接续;推进入口 §2;写工作区日志。 - 验证:见 E11/E12。
§6 验收(判据清单;命令 + 期望输出,可被第三方复现)
| # | 判据 | 命令 / 观察点 | 期望 |
|---|---|---|---|
| E1 | 🔴 分解可复现(D1 的解锁条件) | §2-P10 + --trace 对两个历史样本 |
检测 15.02–15.60 s;白等 11.7–12.3 s;建连 1.3–4.3 s;三段之和 ≡ 墙钟(误差 ≤ 0.5 s) |
| E2 | 分解未被证伪 | S2 的 5 份 trace | 每份"检测 + 白等" ≥ 27.0 s;出现 < 27 s ⇒ 停下(§1.2 可证伪条款) |
| E3 | 白等段有原文证据 | --trace 输出里出现 [relay-skip] ⛔ 新通道起不来(wss://relay-direct.alotbuy.com/dshs-relay) |
该行与 open 发起时刻的间隔 ≥ 11.7 s |
| E4 | 测量误差已剔除(D5) | `grep '^ | DRILL_POLL_MS' 参数表_覆盖网络_20260917.md` + drill 实跑 |
| E5 | 样本量达标 | S2 / S7 各 5 轮 | 5/5 份 trace 齐全,且每轮都记录了归零动作 |
| E6 | 🔴 修后 5/5 在 deadline 内侧 | S7 | kill → switch ≤ 30000 ms 5/5;白等段 ≤ 1 s |
| E7 | 不误伤慢候选(护栏) | 单测(S5) | "慢候选仍成功" + "死候选提前失败" 两条同时绿 |
| E8 | 三处装配点逐处验收(D7) | grep -n "waitUpOn" -A 12 src/web/server.ts src/worker/relay-tunnel.ts src/net/relay/main.ts |
三处的早退语义一致;报告逐处给出证据 |
| E9 | 不退化 | --scene all + npm test(基线 155)+ overlay-probe + 门户 200 + 双实例面 ∈ PROBE_CODE_SET |
与 §2-P8/P9 对照逐项一致;幕 1/2/3/4 结果不变红 |
| E10 | deadline 未被放宽(D6) | grep -n 'RELAY_FAILOVER_DEADLINE_MS' src/net/relay/switcher.ts 参数表_覆盖网络_20260917.md |
仍是 30000;⛔ 参数表 / drop-in / 演练脚本里零处把它改成别的值 |
| E11 | 上单遗留不恶化 | §2.1(ss -lntp | wc -l)|OBS-09/11|nft 72 |
与 §2-P7 逐项一致;若变红 ⇒ 停下报告(非本单范围) |
| E12 | 无越界 | git status --short + grep -rc "COOLDOWN_MS=0|COOLDOWN_MS= 0"(drill / 参数表 / drop-in) |
只含 §3.1 列的文件;⛔ 无 commit / push;🔴 COOLDOWN_MS=0 在回滚 / 演练路径里零命中 |
| E13 | 收口四件套 | 锁 --release-exec 已释放;下一棒 automation 已登记并已用陈述句告知;入口 §2 已推进;工作区日志已写 |
✅ |
§7 回滚
三层,均秒级:
- 配置层(不改代码即可回到现状) —— 本单新增的键全部有安全默认值:
RELAY_GRACEFUL_BURST_MS删掉 ⇒ 回落到代码默认15_000(逐字回到现状)。DRILL_POLL_MS改回2000⇒ 只影响演练读数,不影响产品。- ⇒ 改完
daemon-reload→systemctl restart dshs(47)/restart dshs-worker(106,如涉及)。 - 🔴 ⛔ 禁用项:不许用
RELAY_FAILOVER_COOLDOWN_MS=0当"回滚/对照"开关(上单 §8.8-4:归零 ⇒ 候选链自锁,实测 121–123 s 无切换)。真要缩短冷却只走DRILL_COOLDOWN_MS。
- 代码层:用出单后立即做的
.bak-seq9-<ts>备份还原被改文件(⛔git diff对src/net/relay/**无效 —— 它 untracked)→npm run build→scp lib/到 47/opt/dshs/lib/+/opt/dsh-relay/lib/;106/opt/dshs-cluster/lib/+/opt/dsh-relay/lib/→restart dshs+restart dshs-worker。- 落点自证:改的是
server.ts的waitUpOn与client.ts的 burst 键 ⇒ 建议用一个稳定的可 grep 标记(例如早退分支里的专属日志片段)在远端lib/里grep -c自证,三处 lib 全部。
- 落点自证:改的是
- 服务层:S2/S7 采样若停了 relay ⇒
systemctl start dshs-relay(两台)。两台 relay 的 drop-in(capacity.conf)本单不动。
⚠️ 回滚后必须复验:overlay-probe(按 §2.1 口径判:OBS-09 / OBS-11 属环境态)|ss -lntp | wc -l = 77(无活跃实例档,与 §2.1 同值)|双实例面 ∈ PROBE_CODE_SET|门户 200|--scene 1 仍能在 deadline 内侧切换。
§8 回报格式(执行棒按此格式收口;沿用序⑤–⑧ 单的分节)
✅ 执行棒回报(覆盖网络线 · 序⑨ 执行棒 | 2026-09-17 13:41–14:3x)
一句话结论:§2-P10 的四段分解复现成功(D1 解锁)⇒ 「死候选白等」12.03 s → 0.09 s(省掉 12 s)⇒ 真机样本 5/5 从「全部超 deadline」变成「全部在 deadline 内侧」(30.2–34.1 s → 19.8–23.9 s)。 ⛔ 未动:
RELAY_FAILOVER_DEADLINE_MS(30000) /HB_SEC(15) / burst 语义 / 服务端零改动 / 未 commit·未 push。 🔴 三条硬门:D1 ✅(分解先复现,再改值 —— 且本单一个生产阈值都没改)|R7 ✅(范围外只报告:§8.8)|⛔COOLDOWN_MS=0✅(本单新增行零命中,见 E12)。
8.1 S0 快照(P1–P10)
| # | 命令(原文摘要) | 记录 | 判定 |
|---|---|---|---|
| P1 | handoff-guard.sh --claim-exec "覆盖网络线-序9执行棒" |
✓ 已持全局执行锁 |
✅ |
| P2 | state.py |
锁空闲→抢到;HEAD 640813e;入口 = 接续入口_覆盖网络线_20260916.md(§2 指向本单);工作区 43 处改动 |
✅ |
| P3 | sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum |
e6b669c257d8e8964273b3b400238351 |
✅ 逐字一致 |
| P4 | sed '/^## §8 回报格式/,$d' 交接单_检测时延与deadline_20260917.md | md5sum |
d903b4eeabf25ef381379cdbaac77e8a |
✅ 逐字一致 ⇒ 单未被改动(收口后仍为此值,见 8.9) |
| P5 | 上两单前缀 | 序⑧ aa3a6ec0d66dd81f465cea2a3a08ad27|序⑦ 419abf308c00b7668e8898aaa91ba9e8 |
✅ 两值逐字一致 |
| P6 | grep -n gracefulBurstMs|halfOpenMs client.ts;grep -n "const waitUpOn" -A8 server.ts;switcher.ts 锚点 |
client.ts:137/146/567/631;server.ts:499-506(只轮询 up、不看终态);switcher.ts:244/300;waitUpOn 三处 = server.ts:499+524|relay-tunnel.ts:104+189|main.ts:155+315(⚠️ 单 §2-P6 记的 main.ts 函数在 155、调用在 315,单内写 315 指调用点 ⇒ 一致) |
✅ 锚点全部存在 |
| P7 | node scripts/overlay-probe.cjs(cwd = 工作区根) |
10/12(OBS-09 实例面 000|OBS-11 监听口 77 vs 阈值 79),exit=1 |
✅ 与 §2.1 / §9.1 逐项同值(环境态:47 无活跃实例) |
| P8 | 两台 systemctl is-active + 47 /status |
47 dshs=active / dshs-relay=active;capacity.used=**2**、online[] = manager(session=6959e3e1fb5a2956) + w-106(session=416e7764a86752db ports=19000);106 dshs-worker=active / dshs-relay=active |
✅ |
| P9 | npm.cmd test(Node 22) |
155 tests / 154 pass / 0 fail / 1 skipped | ✅ 与序⑧ 收口基线逐字一致 |
| P10 | journalctl -u dshs --since -6h -o short-unix | grep -E 'relay-switch|relay-skip|relay-client. down' 抽两样本逐行对齐 |
见 §8.2 —— 逐项复现(误差 ≤ 0.03 s) | ✅ D1 解锁 |
8.2 时延分解表(本单的核心产出)
口径:四段全部由 journal 现有行的时间戳算出(⛔ 无新埋点,D2);
--trace 的白等锚点 = 「新候选客户端首败」→「放弃」(⑨ 修前修后同一口径,见 8.2.3)。
8.2.1 单内 §1.2 的两个历史样本 —— 逐项复现(§6-E1 / D1 的解锁证据)
| 样本(epoch 原文) | 检测 | 首试延迟 | 白等 | 建连 | 总 | unhealthyForMs |
与 §1.2 比对 |
|---|---|---|---|---|---|---|---|
1789616881.632718 |
15.119 | 1.416 | 12.029 | 2.724 | 31.287 | 30563 | 单内记 15.00 / ~1.5 / 12.00 / ~2.1 ⇒ ✅ 逐项一致 |
1789617465.999982 |
15.085 | 0.258 | 12.033 | 2.821 | 30.196 | 29376 | 单内记 15.085 / 0.29 / 12.00 / 2.82 ⇒ ✅ 逐字一致 |
所依据的行原文(样本② 1789616881,节选三行):
1789616881.632718 … [relay-client] down (peer bye: server restarting (was up)); attempt #0 [graceful, burst window 15000ms], retry in 300ms
1789616896.751232 … [relay-client] down (transport error); attempt #1, retry in 1054ms ← 检测段终点(burst 耗尽)
1789616898.166825 … [relay-client] down (transport error); attempt #1, retry in 799ms ← **新候选客户端首败**(白等起点)
1789616910.195416 … [relay-skip] ⛔ 新通道起不来(wss://relay-direct.alotbuy.com/dshs-relay)… 进冷却 300000ms ← 放弃点
1789616912.919233 … [relay-switch] #1 … -> wss://106.54.21.172/…(unhealthyForMs=30563) ← 样本终点
🔴
--trace对"白等起点"的识别依据(可被第三方复核):open()里的候选客户端是全新实例 (attempts=0、无 burst 窗口)⇒ 它第一次拨号失败就打不带方括号标签的attempt #1; 而老通道客户端在 burst 耗尽时也打一条attempt #1,那条正好等于 burst 耗尽行本身(被排除) ⇒ 窗口内"最后一条不带标签的attempt #1"唯一指向新候选客户端。
8.2.2 全量样本(--trace --since -6h,19 个样本中 15 个"单跳")
| # | 样本 epoch | 检测 | 首试延迟 | 白等 | 建连 | 总 | 签名 |
|---|---|---|---|---|---|---|---|
| 1 | 1789616881.632718 |
15.119 | 1.416 | 12.029 | 2.724 | 31.287 | 修前 |
| 2 | 1789617465.999982 |
15.085 | 0.258 | 12.033 | 2.821 | 30.196 | 修前 |
| 3 | 1789620716.466165 |
15.209 | 1.269 | 12.035 | 3.718 | 32.232 | 修前 |
| 4 | 1789621434.115527 |
15.490 | 1.830 | 12.034 | 3.768 | 33.122 | 修前 |
| 5 | 1789623996.90519 |
15.291 | 0.985 | 12.041 | 2.716 | 31.034 | 修前(S2-1) |
| 6 | 1789624064.033677 |
15.136 | 0.808 | 12.026 | 2.721 | 30.692 | 修前(S2-2) |
| 7 | 1789624132.71806 |
15.298 | 1.660 | 12.044 | 4.221 | 33.223 | 修前(S2-3) |
| 8 | 1789624194.346892 |
15.738 | 0.468 | 12.021 | 3.717 | 31.943 | 修前(S2-4) |
| 9 | 1789624258.224431 |
15.671 | 2.818 | 12.033 | 3.621 | 34.143 | 修前(S2-5) |
| 10 | 1789624473.578564 |
16.645 | 3.031 | 0.092 | 2.717 | 22.485 | 修后(S7-1) |
| 11 | 1789624534.000883 |
15.229 | 0.332 | 0.090 | 5.223 | 20.873 | 修后(S7-2) |
| 12 | 1789624587.689803 |
15.017 | 1.480 | 0.092 | 4.719 | 21.307 | 修后(S7-3) |
| 13 | 1789624640.289734 |
15.096 | 5.016 | 0.092 | 3.730 | 23.933 | 修后(S7-4) |
| 14 | 1789624693.055419 |
15.262 | 1.585 | 0.090 | 4.719 | 21.656 | 修后(S7-5) |
| 15 | 1789625104.721054 |
15.386 | 1.610 | 0.093 | 2.721 | 19.810 | 修后(幕 4-A) |
- 修前(n=9):检测 15.085–15.738(中位 15.29)|白等 12.021–12.044(中位 12.033)|建连 2.716–4.221|总 30.196–34.143 ⇒ 9/9 全部 >
deadline30000 - 修后(n=6):检测 15.017–16.645(中位 15.26)|白等 0.090–0.093(中位 0.092)|建连 2.717–5.223|总 19.810–23.933 ⇒ 6/6 全部 ≤
deadline - 自洽:15/15 样本
检测 + 首试延迟 + 白等 + 建连 ≡ 总(误差 ≤ 0.05 s)✅
8.2.3 白等段的口径说明(⛔ 防误读)
- 单内 §1.2 把白等写成
≡ upTimeoutMs(12.000 s)—— 那是修前的定义式。本棒用可直接测的锚点替代它: 白等 =t(放弃点) − t(新候选客户端首败)⇒ 修前实测 12.021–12.044(与定义式 12.000 相差 21–44 ms = 拨号耗时), 修后实测 0.090–0.093 s。⇒ 同一个度量在修前修后都成立,修前的 12 s 是"真等待",修后只剩 0.09 s。 - ⚠️ 单内 §1.2 的"tick 相位"在本口径下改称首试延迟(=
RELAY_FAILOVER_CHECK_MS相位 + 拨号耗时), ⛔ 不是"测量被篡改":四段划分与总长自洽校验逐样本通过(见 8.2.2)。
8.3 代码改动清单
| # | 文件 | 改动点 | wc -l 前后 |
先红后绿 |
|---|---|---|---|---|
| 1 | src/net/relay/client.ts |
① 接口+status() 新增只读投影 inGracefulBurstWindow;② 新增 gracefulBurstMsDefault()(env 参数表化,默认 15_000 逐字不变);③ 新增 openedChannelFailedTerminally()(RC-1 判据);④ 新增 waitUpOnStatus()(三处装配点收口成一份,D7) |
1306 → 1403 | ✅ |
| 2 | src/net/relay/index.ts |
导出上述 3 个符号 | 2 行改 | — |
| 3 | src/web/server.ts(C1) |
waitUpOn 改为委托 waitUpOnStatus + 判死日志([relay-failover] ⛔ 新通道终态失败…) |
908 → 917 | ✅ |
| 4 | src/worker/relay-tunnel.ts(C2) |
同上 | 220 → 224 | ✅ |
| 5 | src/net/relay/main.ts(C3) |
同上(+ log 透传) |
359 → 363 | ✅ |
| 6 | scripts/overlay-failover-drill.cjs |
新增 --trace(四段分解·只读)+ --sample N(N 轮采样,含完整归零序列);⛔ 原有 --scene 1/2/3/4/4b/4c/ctrl 语义一字未改(仅在 --help 追加 2 行) |
720 → 1032 | — |
| 7 | test/relay-failover.test.mjs |
新增 F18(护栏·慢候选)+ F19(死候选早退)+ F20(burst 窗口不许判死)+ F21(判据四反例)+ F22(env 参数表化) | 731 → 844 | ✅ |
E8 · 三处装配点逐处验收(D7):grep -n waitUpOnStatus ⇒ server.ts(C1)/relay-tunnel.ts(C2)/main.ts(C3)三处全部委托同一实现;
落点自证(远端 lib 四份):grep -c "openedChannelFailedTerminally" ⇒ 47 /opt/dshs/lib/net/relay/client.js = 3、47 /opt/dsh-relay/lib/… = 3、106 /opt/dshs-cluster/lib/… = 3、106 /opt/dsh-relay/lib/… = 3;
grep -c "新通道终态失败" ⇒ /opt/dshs/lib/web/server.js = 1、/opt/dshs-cluster/lib/worker/relay-tunnel.js = 1。⚠️ 属主按原值 197108:197121 复原(⛔ 不引入 R10 的 root 属主问题)。
8.4 单测与本地验证
npm.cmd test(Node 22)= 160 tests / 159 pass / 0 fail / 1 skipped(基线 155 + 新增 5)。- 先红后绿(实证):改完用例先对旧
lib/产物跑 ⇒SyntaxError: The requested module '../lib/net/relay/index.js' does not provide an export named 'openedChannelFailedTerminally'(红);npm run build后node --test test/relay-failover.test.mjs⇒ 22/22 全绿;全量npm test⇒ 159/0。 - 护栏用例(E7 的断言切面):
- F19(红→绿的分水岭):假客户端在 100 ms 处进
backoff+attempts=1+非 burst ⇒ 断言ok===false且ms < 2000(旧实现必然 12 000 ms)。 - F18(护栏):慢候选
connecting → up@800ms⇒ 断言ok===true且等到 ~800 ms ⇒ ⛔ 早退不许误杀慢候选(R11 不变量)。 - F20(D3 保护):
backoff attempts=1但inGracefulBurstWindow=true⇒ 必须继续等,窗口过后up⇒ok===true(⛔ 不许在窗口内判死)。 - F21:判据四反例(
connecting/handshaking/queued(attempts=0) / burst 窗口内)一律 false。 - F22:
RELAY_GRACEFUL_BURST_MS默认 15 000 逐字不变;空串/非法/负数 ⇒ 回落默认;显式覆写才生效。
- F19(红→绿的分水岭):假客户端在 100 ms 处进
8.5 真机复测(修复前 N=5 / 修复后 N=5)
读数口径:切换耗时 = --sample 的墙钟(含 DRILL_POLL_MS 的量化误差);四段另由 --trace 从 journal 时间戳复算(与轮询口径无关)。
| 轮 | 修复前 DRILL_POLL_MS=2000 |
≤30000? | 修复后 DRILL_POLL_MS=500 |
≤30000? |
|---|---|---|---|---|
| 1 | 33914 ms | ❌ | 25465 ms | ✅ |
| 2 | 31646 ms | ❌ | 25342 ms | ✅ |
| 3 | 34078 ms | ❌ | 21985 ms | ✅ |
| 4 | 35321 ms | ❌ | 27389 ms | ✅ |
| 5 | 41252 ms | ❌ | 23318 ms | ✅ |
| 合计 | 5/5 超界(中位 34078) | ❌ | 5/5 在内侧(中位 25342) | ✅ |
每轮的归零动作(E5):① 两台 relay systemctl start → ② systemctl restart dshs(通道回到目录首位 = 47)→ ③ 等 47 relay 日志出现新的 AUTH OK host=ops/manager(修复前 AUTH 就绪 2359–4112 ms/修复后 2834–4761 ms)→ ④ 记 t0 → systemctl stop dshs-relay(47) → 等新 [relay-switch] → ⑤ 复原 relay。10/10 轮全部执行归零;⛔ 全程未施加任何演练 env 覆盖(尤其未出现冷却归零)。
同轮的四段读数(journal 口径):修复前白等 12.021–12.044/修复后 0.090–0.093(见 8.2.2 第 5–14 行)。
8.6 参数表改动
| 键 | 旧 → 新 | 值格自证(纯数字) | 说明 |
|---|---|---|---|
DRILL_POLL_MS |
2000 → 500 | 500 |
测量修正(剔掉 ≤2 s 量化误差 ⇒ 超/不超 deadline 两端都可能误判,RC-4)。⚠️ 改前/改后读数不可混比(8.5 已分档) |
DRILL_SAMPLE_N |
🆕 5 |
5 |
--sample 的默认轮数(= S2/S7 的 N) |
RELAY_GRACEFUL_BURST_MS |
🆕 15000 |
15000 |
把"全仓唯一一个不可配的时延常量"参数表化(gracefulBurstMs);⛔ 默认值语义逐字不变、⛔ 本单未改其值 |
| §9 表格 | 🆕 第 9 行「换址墙钟的四段分解」 | — | 四段数值 + 修前修后两档总长 |
| §7 计数 | 回填一行 | — | 待测项 仍为 0(新增两个键都是实测/推导值) |
值格纯数字自证:grep -nE '^\| (DRILL_POLL_MS|DRILL_SAMPLE_N|RELAY_GRACEFUL_BURST_MS)' ⇒ 值格依次为 500 / 5 / 15000(无夹注 ⇒ ⛔ 不会引发 OBS-02 假红)。
8.7 不退化(S0 对照)
| 项 | S0(§8.1) | 收口复取 | 判定 |
|---|---|---|---|
npm test |
155/154/0/1 | 160/159/0/1(+5 新用例) | ✅ 无红 |
--scene all(幕 1/2/3/4) |
序⑧ 全绿 | 11 PASS / 1 FAIL(幕2-B,见 8.8-1);幕 4-A/B/C 全绿(豁免切回 47 22213 ms ≤ 30000) | ⚠️ 见 8.8-1(归因 = drill 状态依赖,正确前置下 3/3 PASS) |
overlay-probe |
10/12(OBS-09/OBS-11 红) |
10/12,同样只有 OBS-09/OBS-11 红 |
✅ 逐项一致 |
ss -lntp | wc -l(47) |
77 | 77 | ✅ |
nft 规则行数 |
72 | 72 | ✅ |
门户 http_code |
200 | 200 | ✅ |
| 双实例面 | 000/000(无活跃实例) |
000/000 |
✅ 同值(OBS-09 环境态) |
| 两台 relay 单元 | active/active | active/active | ✅ |
演练 env 残留(RELAY_FAILOVER_* 计数) |
— | 0 | ✅ 生产值未被演练污染 |
git status --short 计数 |
43 | 43 | ✅ 无越界(新增的 4 个文件均在本单 §3.1 范围内,且 src/net/relay/**·relay-tunnel.ts·drill·test 本就是 untracked) |
8.8 未过项 / 遗留
1. ✅ 序⑩ 已修(2026-09-17 14:4x)· 原报:🔴 --scene all 的幕 2-B 判 FAIL(归因:drill 自身的状态依赖,⛔ 非本单改动引入)
- ① 现象:
幕2-B 留下「无候选 ⇒ 原地退避」的判别器证据(D6)⇒ 窗口内 0 行[relay-skip];同窗口内[relay-client] down/[relay-switch]也全为 0 行(journal 取证:1789624835±的 120 s 窗口里 Manager 一行 relay 日志都没有 ⇒ 它的通道根本没被打断)。 - ② 已做到哪一步:定位到 drill 代码本身 —— 幕 2 的注释写"两台全杀",但实现只停 106(
await stopRelay(h106),overlay-failover-drill.cjs幕 2 块首行),而幕 1 已把 Manager 的通道切到了 47 ⇒ 幕 2 停的是一台 Manager 已不在用的 relay ⇒ 无失效、无判别器。在正确前置下重跑 ⇒ 3 PASS / 0 FAIL(幕2 前置:47 relay=inactive / 106 relay=inactive,判别器原文:[relay-skip] ⚠ 豁免尝试也起不来(wss://alotbuy.com/dshs-relay)⇒ **保持原通道**;重置该候选冷却…)。⇒ 与本单改动无关(本单只改"新通道等待",不触碰"无候选"分支;且该窗口内 Manager 完全无事件)。 - ③ 回头条件:只要有人需要
--scene all一键全绿(如 CI 化 / 交给第三方复跑)⇒ 必须先把幕 2 改成"停掉当前通道所在的那台 relay"(与幕 1 同一套killTarget判定),否则它永远依赖上一轮留下的通道归属。⛔ 本单不动(§3.1 明令"原有--scene语义一字不改"+R7 范围外先报告)。
2. 🔴 在册未办(本单只报告、不动手**)**
- ✅ 序⑩ 已清(现
grep -rho 'COOLDOWN_MS=0' scripts/ | wc -l=0)· 原报:COOLDOWN_MS=0在演练脚本里仍有 5 处命中(overlay-failover-drill.cjs:--scene 4c/--scene ctrl的文案与实现)—— 全部是序⑧ 遗留,本单新增行零命中(git diff -U0 | grep -c '^+.*COOLDOWN_MS=0'= 0)。按上单 §8.8-4(归零 ⇒ 候选链自锁、实测 121–123 s 无切换)与 §3.2 禁令 ⇒ 建议下一棒删掉--scene ctrl及其 4 处文案(4c可保留:它走DRILL_COOLDOWN_MS覆盖,不碰生产键)。回头条件:任何一次把--scene ctrl当"回滚/对照"用之前必须删掉。 - ✅ 序⑩ 已定位(见下方 §8.8-4)· 原报:OBS-11 的
78 → 77那 1 个口:本棒复取仍 = 77(与 S0/序⑧ 同值),未定位(R7 只报告)。 - 承上单:
OBS-09(47 无活跃实例 ⇒ 实例面000)|guest(w-106) 实例页 502|106 agent 面不吃引导链(已判不执行)|mksess*.cjs失效|src/net/relay/**untracked 留档缺口|client.ts.bak-seq7-*残留。 - ⚠️ 本棒对"当前通道归属"有副作用(幕 4 / S2/S7 采样会把 Manager 留在 106):收口时已
systemctl restart dshs归零回 47 并复验(used=2、online[]含manager+w-106、probe 回到 10/12)。
3. ⛔ 明确未做(保持单内 §3.2):未动 deadline/HB_SEC/burst 语义/MIN_ATTEMPTS/GRACE_MS/CHECK_MS/生产 COOLDOWN_MS;未做"同机候选去重 / 并发竞速 open";未新增监听口·凭据·端点;未改服务端;未 commit / 未 push。
4. ✅ 序⑩ 收口回填(2026-09-17 14:23–14:5x · 执行棒 · 三件技术债全清)
| # | 遗留 | 结果 | 证据(现取) |
|---|---|---|---|
| ① | 幕 2 状态依赖(幕 2 只停 106 ⇒ 停的是不用的那台) | ✅ 已修 | 修法 = 幕 2 起手现场重读权威通道归属(lastManagerAuthOn 比对)后两台都停(幂等);--scene all = 12 PASS / 0 SKIP / 0 FAIL(原 11/1);--scene 2 单独复跑 = 3 PASS / 0 FAIL(前置行原文:本幕开始时活跃通道 = bt-server(epoch 47=…/106=…)) |
| ② | COOLDOWN_MS=0 × 5 处(--scene 4c / --scene ctrl) |
✅ 已清,0 命中 | grep -rho 'COOLDOWN_MS=0' scripts/ | wc -l = 0;两个场景名整体移除并显式拒绝(--scene 4c / ctrl ⇒ 退出码 2 + 提示走 DRILL_COOLDOWN_MS),⛔ 不静默空跑;node --check 通过 |
| ③ | OBS-11 的 78 → 77 那 1 口 |
✅ 已定位(有名字) | 逐口对账见下 ⇒ 差额 = 20000(47 实例档,当前无活跃实例) |
③ 逐口对账(应然 78 行 vs 实际 77 行 · ss -lntp,含表头)
| 组 | 应然条数 | 实际 | 说明 |
|---|---|---|---|
九固定口 22/80/443/888/3080/8765/15432/19100/20080 |
9 | ✅ 全在 | — |
[::]:22(sshd 的 IPv6 第二条绑定行) |
1 | ✅ 在 | 计数是"行"不是"端口",故 22 占 2 行 |
拨号池 25000–25063 |
64 | ✅ 64 口全在 | 脚本逐口点名验证 |
| w-106 落点(relay 动态分配) | 1 | ✅ 在(值 = 46147) |
/status.endpoints[0].localPort=46147,与 relay 20080 同 PID 779508 |
BT-Panel 58888 |
1 | ✅ 在 | — |
47 实例档 20000 |
1 | ❌ 缺 | 就是这 1 口 —— 47 当前无活跃实例(ss 无 127.0.0.1:20000) |
| 表头 | 1 | ✅ 在 | — |
⇒ 76 条 socket 逐条点名、零无名;78 − 77 = 1 的名字 = 20000(实例档随实例上线/下线),不是暴露面消失。
⇒ 🔴 附带订正(口径书写错误):原清单把 39463 写成固定项 —— 它其实是 relay 为 w-106 端点动态分配的落点(现值 46147,同 PID)。⛔ 口径里不能写固定值,否则每次重启都会误报"口变了"。
⇒ ⚠️ 仍无名的 1 条(在册未办):参数表 LISTEN_COUNT = 79(S0 基线,78 socket)比"应然·无实例态 77"高 2、比"应然·有实例态 78 行"高 1。这 1~2 条无法定名,卡点 = S0 原始 ss 清单未留档(已查工作区与 04-调整方案/,均无)。回头条件:① 47 恢复活跃实例后复取(应回到 78 行)② 届时若仍 77 ⇒ 另有 socket 确实消失 ⇒ 按"零新增暴露面"重定基线,⛔ 不许把差值当噪声放过。
🔴 ③ 的副产品(⛔ 本棒不动手 · 属方案改动 ⇒ 转规划棒):OBS-11 用「计数相等」当暴露面判据有两个结构性弱点 —— ⓐ 对状态敏感(实例档/端点落点在线态一变就红 ⇒ 假红);ⓑ 对替换式变化不敏感(一进一出计数不变 ⇒ 假绿,正是本项目最忌的静默失效)。⇒ 建议下一棒规划 白名单集合判据(实际监听行 ⊆ 允许集,并打印差集)替代计数相等;连带处理 LISTEN_COUNT 的"有/无实例态"两值。⛔ 本棒只报告(§3.2 范围 + D1 纪律)。
8.9 指纹(收口后的可复现核对口径)
- 参数表:
cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum⇒ 现取 =99e9e17b0c1ce0550e4bc7626a5a0494(本单出单值 =e6b669c257d8e8964273b3b400238351⇒ 已随 §3.1-5 的四处改动更新) - 本交接单前缀(§8 及其后不计入):
cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §8 回报格式/,$d' 交接单_检测时延与deadline_20260917.md | md5sum⇒ 现取 =d903b4eeabf25ef381379cdbaac77e8a= 出单值 ✅ 回填 §8 后未变(底稿零改动) - 交叉证据(上两单,收口复取):
切流冷却语义_20260917.md=aa3a6ec0d66dd81f465cea2a3a08ad27|中继失败切流_20260917.md=419abf308c00b7668e8898aaa91ba9e8(两者均未变) - 本单全文件 md5 请现取(⛔ 不内嵌,自指)
- 序⑩ 原始取证落盘:
_中间产物_待清理/seq10/(ss47-actual.txt= 47 监听口全量 77 行;drill-all-after.txt=--scene all12 PASS 原文;drill-scene2-after.txt= 幕 2 单跑 3 PASS 原文) - 原始取证落盘(供第三方复核):
_中间产物_待清理/seq9-trace/(s2-before.json修前 5 轮、s7-after.json修后 5 轮、full-6h.json全窗口 19 样本、seq9-sample-*.json采样明细)
§9 附录 · 本单的取证基线(供执行棒复核,⛔ 不必重跑)
| 事实 | 出处 |
|---|---|
relayFailoverThresholds(env = process.env) ⇒ env 覆盖生效;六键默认值 300000 / 30000 / 15000 / 3 / 2000 / 12000 + exempt |
switcher.ts:55-95 |
唯一换址入口 replace(targetUrl, reason, origin);open() 在 :300 被 await |
switcher.ts:204-310 |
unhealthy() = state==='backoff' && (attempts ≥ minAttempts ∨ unhealthyForMs ≥ graceMs) |
switcher.ts:244 |
tick() 周期 = checkMs(2000);D6 现场 ⇒ noCandidateChecks + [relay-skip](节流) |
switcher.ts(tick 内) |
waitUpOn 只轮询 state === 'up',直到 deadline 才 false(RC-1 真身) |
web/server.ts:499-506 |
open 的三处消费点 |
web/server.ts:524(C1)/worker/relay-tunnel.ts:104、:158(C2)/net/relay/main.ts:315(C3) |
| 生产目录 3 条候选、前 2 条同机 47 —— 注释原文已承认 | web/server.ts:517-519 |
gracefulBurstMs 默认 15_000、不可配(全仓只有 ?? 15_000 一处) |
client.ts:137/:567 |
burst 三个分支各自 attempts = 0(RC-2) |
client.ts:559-580 |
gracefulRetryMs 默认 300;reconnectMinMs 1000/reconnectMaxMs 30000;退避含 ±25% 抖动 |
client.ts:554-588 |
半开检测 halfOpen = max(3000, hbSec×1000×2.5) = 37.5 s;仅在 state==='up' 计时 |
client.ts:631-638 |
DEFAULT_HB_SEC = 15(运行真值;HB_SEC_DOC=20 是旧隧道时代文档口径,⚠️ 待更正) |
server.ts:60/参数表 §4 |
演练读数偏差:waitSwitch 每 DRILL_POLL_MS 轮询;t0 在 stopRelay 返回之后(比 BYE 晚 0.6–0.73 s) |
overlay-failover-drill.cjs(waitSwitch / runScene1) |
| 本单两个逐行复核样本的 epoch | ① 1789617466 / …7493.375 / …7496.196(unhealthyForMs=29376)② 1789616882 / …6910.195 / …6912.919(unhealthyForMs=30563) |
9.1 本棒(规划棒)已做的只读取证(⛔ 执行棒不必重做)
| 项 | 读数 | 出处 |
|---|---|---|
| 工作区 HEAD | 640813e;git status --short = 43 |
state.py / git status |
| 参数表指纹 | e6b669c257d8e8964273b3b400238351 |
现取 |
| 上两单前缀指纹 | aa3a6ec0d66dd81f465cea2a3a08ad27 / 419abf308c00b7668e8898aaa91ba9e8 |
现取 |
overlay-probe |
10/12(OBS-09 / OBS-11 红);exit=1;原始输出落 /tmp/seq9-probe-s0.txt |
现取 |
ss -lntp | wc -l(47) |
77(76 socket + 表头);连采两次同值;nft=72 |
现取 |
| 47 端口清单 | 9 固定口(22/80/443/888/3080/8765/15432/19100/20080)+ 拨号池 25000–25063(64) + 39463(w-106 落点)+ 58888(BT-Panel) |
现取 |
| 47 活跃实例 | 无(20000 未监听;ps 只有控制面 / w-47 / relay) ⇒ OBS-09 红属环境态 |
现取 |
47 relay /status |
capacity.used=2;online[] = manager + w-106;dialers=["manager"] |
现取 |
| 时延分解 | 检测 15.08 s / 白等 12.00 s / 建连 2.82 s(+ 相位 0.29 s) | §1.2 样本① |
§10 指纹
- 本节口径(推荐核对用,可复现):整个 §10 不计入 ⇒
cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 交接单_检测时延与deadline_20260917.md | md5sum - ⚠️ 本单另有 §8 前缀口径(§8 及其后不计入)⇒ 见 §8 回报格式 内 §8.9 的取值处(收口时回填)。
- 全文件 md5:请现取(⛔ 本行故意不内嵌数值 —— 包含本节自身,写进去即刻失效)。