From e611b07fb1ef1f8bd9a39e3fdf49cd1e3eaf23cd Mon Sep 17 00:00:00 2001 From: xmz7891 Date: Wed, 7 Oct 2026 18:22:19 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=A0=E6=AD=A3=E4=B8=80=E4=B8=AA=E4=BC=A0?= =?UTF-8?q?=E5=87=BA=E5=8E=BB=E7=9A=84=E9=94=99=E8=AF=AF=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=EF=BC=9A=E4=B8=8D=E6=98=AF=E3=80=8C=E8=BF=90=E8=90=A5=E5=95=86?= =?UTF-8?q?=E5=B0=81=E4=BA=86=20UDP=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 我在给部署脚本的指示里写过: 实测这条线路上运营商对 UDP 有限制 —— 12 个 Cloudflare edge IP:TCP 12/12 通、UDP 0/12 通 它照这个把注释写进了 relay/deploy.sh。 **但这个测量是无效的。** relay-stability 后来自己纠正了: 那个 UDP 探测包是手搓的简化包,格式不对, Cloudflare 本来就不该回应它 —— 「0/12 无回应」什么都证明不了。 用 cloudflared --loglevel debug 跑临时实例,真相是: UDP Connectivity region1/region2 PASS QUIC connection successful SUMMARY: Environment is healthy. cloudflared will use 'quic' Registered tunnel connection (握手 1 秒) **UDP 完全通。** 真因是:QUIC 建连要 6+ 个包往返,而 TCP 的 SYN 只有 3 个包、 内核还会按 1s/2s/4s 激进重传。约 5.4%/包 丢包下: (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上 所以 QUIC 的弱点在「连不上」,不在「连上之后不好」; 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 结论的**用途**没变(还是要两条协议一起跑),但留着错误的根因注释会: * 误导后人以为 UDP 被封 -> 可能做出「干脆别用 QUIC」的错误决定 * 掩盖真正可优化的方向(QUIC 握手对丢包敏感是链路物理属性, 只能靠 HTTP/2 兜底来绕,不是「运营商限制」) 改动:relay/deploy.sh 两处、_build/test_deploy_sh.sh 一处。 教训记一笔:**手搓协议包去探测可达性非常容易得出错误结论** —— 那个包格式不对,对方根本没义务回应。要么用官方工具的自检 (cloudflared 自己会打 UDP Connectivity PASS/FAIL), 要么别下结论。 --- _build/_cmsg5.txt | 57 ------------- _build/fix_udp_claim.py | 141 +++++++++++++++++++++++++++++++++ _build/lbg.txt | 20 +++++ _build/rp_pc_long.tsv | 21 +++++ _build/test_deploy_sh.sh | 3 +- _tests/test_lan_bench_guard.py | 127 +++++++++++++++++++++++++++++ _tests/verify_all.ps1 | 11 +++ relay/deploy.sh | 40 +++++++--- 8 files changed, 353 insertions(+), 67 deletions(-) delete mode 100644 _build/_cmsg5.txt create mode 100644 _build/fix_udp_claim.py create mode 100644 _build/lbg.txt create mode 100644 _tests/test_lan_bench_guard.py diff --git a/_build/_cmsg5.txt b/_build/_cmsg5.txt deleted file mode 100644 index 57b3afd..0000000 --- a/_build/_cmsg5.txt +++ /dev/null @@ -1,57 +0,0 @@ -修局域网/中转对比基准:它自己有两个 bug,害产品背了黑锅 - -这个基准一直是红的(中转那条路报「连接阶段失败: timed out」), -而且报告里写着"这是中转链路的产品 bug"。**查下来不是产品的问题。** - -== bug 1:两条路用了同一个配对码 == - -基准为了排除互相干扰,给两条路**各起一台全新被控端**(设计是对的), -但两台都用了写死的 code="246813"。 - -中转有「一个配对码只属于一台设备」的规则: - 第 1 台(跑局域网)注册成功 - -> stop_agent 杀掉进程,但**中转还以为它在线**(last_seen 没过期) - 第 2 台(跑中转)用同一个码注册 -> **被拒** - 中转日志:配对码 246813 已被 ... 占用,拒绝 ... - 而 start_agent 只检查"有没有设备在线" —— 第 1 台僵尸还挂着, - 于是**以为新的一台登记成功了** - -> 没有任何进程在回包 -> 超时 - -对照实验(新增 _build/verify_code_reuse.py): - 场景 A 同一个码 -> **不通** - 场景 B 各用不同码 -> 通 - -修法:两条路各用不同码;start_agent 的判据从"有没有在线设备" - 改成"有没有**配对码正好是这一台要用的码**的在线设备"。 - -== bug 2:attach 用的还是旧码(我第一版补丁也漏了这处)== - -relay_connect() 里 `client.attach(code)` 用的还是局域网那个码 —— -attach 到一台已经死掉、但中转以为在线的设备上,pair 自然没人回。 - -修成 `attach(relay_code)`。 - -== 顺带更新一段过时注释 == - -原文写着「当前版本经中转只稳定支撑前 1~2 次交互……这是中转链路的 -产品 bug」,并据此把中转的轮数调小、超时调短。 - -那个 bug **已经修好了**(根因:一条 HTTP/1.1 连接被长轮询和发命令 -两个线程同时用,撞状态机抛 CannotSendRequest("Idle"), -异常冒到 _client_loop 外面把 client-relay 线程打死)。 -注释不更新会误导后人以为产品还有这个问题。 - -== 修好之后的实测(本地中转,30 次/路)== - - 局域网 握手 16ms 成功 30/30 (100%) 中位 0ms P90 0ms 最大 1ms - 中转 握手 46ms 成功 30/30 (100%) 中位 15ms P90 29ms 最大 32ms - -这是本地中转,量的是**协议本身的开销**(长轮询往返 vs 直连 TCP); -真实跨网络那条腿的 1.0~1.5 秒来自隧道和 4G,是另一回事。 - -结论:局域网确实是"毫秒级、不耗流量"的那条根本路径 —— -中位 0ms vs 15ms,而且不走任何外网。 - -踩坑记录:这两处都是"看起来像产品 bug、实际是测试自己的 bug"。 -先怀疑测量工具,比先怀疑产品更省时间 —— -上一次(管理页缓存头)也是我拿 curl -I 测 GET 白查了半天。 diff --git a/_build/fix_udp_claim.py b/_build/fix_udp_claim.py new file mode 100644 index 0000000..d1b9bc5 --- /dev/null +++ b/_build/fix_udp_claim.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +r'''纠正一个我自己传出去的**错误结论**。 + +== 错在哪 == + +I(Lead) 在给部署脚本智能体的消息里写过: + + 「实测这条线路上运营商对 UDP 有限制 —— 对 12 个 Cloudflare edge IP + 探测,TCP:7844 12/12 可达,UDP:7844 0/12 有回应」 + +它照这个说法把注释写进了 relay/deploy.sh。 + +**但这个测量是无效的。** relay-stability 后来自己纠正了: +那个 UDP 探测包是我用 Python 手搓的简化包,**格式不对, +Cloudflare 本来就不该回应它** —— "0/12 无回应"什么都证明不了。 + +用 `cloudflared --loglevel debug` 跑临时实例,真相是: + + INF | UDP Connectivity region1.v2.argotunnel.com PASS QUIC connection successful + INF | UDP Connectivity region2.v2.argotunnel.com PASS QUIC connection successful + INF | SUMMARY: Environment is healthy. cloudflared will use 'quic' as primary protocol + INF Registered tunnel connection (握手 1 秒) + +**UDP 完全通,握手成功时只要 1 秒。** + +== 真因 == + + QUIC 握手需要 6+ 个包的往返; + TCP 的 SYN 只有 3 个包,而且内核会按 1s/2s/4s 激进重传。 + +在约 5.4%/包 的丢包下: + + 成功率 ≈ (1 - p)^6 = (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上 + +所以 QUIC 的弱点是「**连不上**」,不是「连上之后不好」; +一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 + +== 为什么要改 == + +结论错了但**结论的用途是对的**(所以要两条协议一起跑), +可是留着一句错误的根因注释会: + * 误导后人以为 UDP 被封 -> 可能做出"干脆别用 QUIC"的错误决定 + * 掩盖真正可优化的方向(QUIC 握手对丢包敏感,这是链路的物理属性, + 只能靠 HTTP/2 兜底来绕,不是"运营商限制") +''' +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") + +NEW_REASON = """为什么两条一起跑 —— QUIC 握手对丢包敏感: + 它要 6+ 个包往返才建得起来,而 TCP 的 SYN 只有 + 3 个包、内核还会按 1s/2s/4s 激进重传。 + 在约 5% 丢包下 (1-p)^6 只有 ~72% 能建成 —— + 所以刻意让 HTTP/2 占几条连接兜住那 28%。 + (UDP 本身是通的:cloudflared 自检 PASS, + 握手成功时只要 1 秒。不是"运营商封了 UDP"。)""" + +NEW_BLOCK = """# 根因也查清了:**QUIC 握手对丢包敏感**。 +# +# QUIC 注册 525 失败 197 掉线 48 拨号成功率 72% +# HTTP/2 注册 13 失败 0 掉线 7 拨号成功率 100% +# +# QUIC 建连要 6+ 个包的往返,TCP 的 SYN 只有 3 个包、内核还会 +# 按 1s/2s/4s 激进重传。约 5.4%/包的丢包下: +# +# (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上 +# +# 注意:**这不是"运营商封了 UDP"**。曾经这么以为过,但那个判断 +# 来自一个格式不对的手搓 UDP 探测包(Cloudflare 本来就不该回应它)。 +# 用 cloudflared --loglevel debug 实测: +# +# INF | UDP Connectivity region1.v2.argotunnel.com PASS +# INF | UDP Connectivity region2.v2.argotunnel.com PASS +# INF | SUMMARY: Environment is healthy ... +# INF Registered tunnel connection (握手 1 秒) +# +# UDP 是通的。QUIC 的弱点在"连不上",不在"连上之后不好"—— +# 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 +# 所以对策是「多 QUIC + 少量 HTTP/2 兜底」,而不是放弃 QUIC。""" + + +def patch(path: Path, old: str, new: str, label: str) -> bool: + if not path.is_file(): + print(" [跳过] %s 不存在" % path.name) + return True + text = path.read_text(encoding="utf-8") + if old not in text: + if "握手对丢包敏感" in text: + print(" • %s 已经是新说法" % label) + return True + print(" ✗ %s 没找到锚点" % label) + return False + if text.count(old) != 1: + print(" ! %s 锚点出现 %d 次" % (label, text.count(old))) + return False + path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n") + print(" ✓ %s" % label) + return True + + +def main() -> int: + ok = True + + ok &= patch( + ROOT / "relay" / "deploy.sh", + """为什么两条一起跑 —— 运营商对 UDP 有限制 + (12 个 edge IP:TCP 12/12 通、UDP 0/12 通),""", + NEW_REASON, "deploy.sh --help 里的说法") + + ok &= patch( + ROOT / "relay" / "deploy.sh", + """# 根因也查清了:**运营商对 UDP 有限制**。 +# +# 对 12 个 Cloudflare edge IP 探测: +# TCP:7844 12/12 可达(169~252ms) +# UDP:7844 0/12 有回应 +# +# 也就是 UDP 被链路限制,QUIC 先天不利,所以让它当主力、 +# HTTP/2 兜底。""", + NEW_BLOCK, "deploy.sh 的根因注释") + + ok &= patch( + ROOT / "_build" / "test_deploy_sh.sh", + """# 运营商限 UDP(12 个 edge IP:TCP 12/12 通、UDP 0/12 通)""", + """# QUIC 握手要 6+ 个包往返,TCP SYN 只要 3 个且激进重传, +# 约 5% 丢包下 (1-p)^6 ≈ 72%(不是"运营商封 UDP",那是错的测量)""", + "test_deploy_sh.sh 的说明") + + print() + if not ok: + print(" 有锚点没匹配") + return 1 + print(" 纠正完成") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_build/lbg.txt b/_build/lbg.txt new file mode 100644 index 0000000..a56d753 --- /dev/null +++ b/_build/lbg.txt @@ -0,0 +1,20 @@ + [PASS] 基准脚本存在 + ---- 一、两条路用不同的配对码 ---- + [PASS] 定义了局域网用的 code + [PASS] 定义了中转用的 relay_code + [PASS] 两个码不一样(否则第 2 台会被一码一机规则拒掉) + ---- 二、attach 要用 relay_code ---- + [PASS] 能定位到 relay_connect + [PASS] relay_connect 里用的是 relay_code + [PASS] relay_connect 里没有裸 attach(code) + ---- 三、start_agent 的判据要认码 ---- + [PASS] start_agent 接受码参数 + [PASS] 没有「只看有没有在线设备」的松判据 + [PASS] 判据里比对了配对码 + ---- 四、真跑一遍短基准(两条路都要通)---- + [PASS] 基准跑完并输出了 BENCH_RESULT + [PASS] 局域网那条路通了 + [PASS] 中转那条路通了 + +======================================================== + 通过 13 项,失败 0 项 diff --git a/_build/rp_pc_long.tsv b/_build/rp_pc_long.tsv index b1c029f..5a98346 100644 --- a/_build/rp_pc_long.tsv +++ b/_build/rp_pc_long.tsv @@ -152,3 +152,24 @@ time code ms 2026-10-07 18:14:59 200 1089 2026-10-07 18:15:15 200 987 2026-10-07 18:15:31 200 999 +2026-10-07 18:15:48 200 1426 +2026-10-07 18:16:04 200 1222 +2026-10-07 18:16:20 200 1397 +2026-10-07 18:16:37 200 1127 +2026-10-07 18:16:53 200 1010 +2026-10-07 18:17:09 200 1140 +2026-10-07 18:17:25 200 1032 +2026-10-07 18:17:41 200 1367 +2026-10-07 18:17:57 200 1111 +2026-10-07 18:18:13 200 1285 +2026-10-07 18:18:30 200 1169 +2026-10-07 18:18:46 200 1380 +2026-10-07 18:19:02 200 1017 +2026-10-07 18:19:18 200 1121 +2026-10-07 18:19:34 200 1184 +2026-10-07 18:19:51 200 1386 +2026-10-07 18:20:07 200 1351 +2026-10-07 18:20:23 200 1381 +2026-10-07 18:20:40 200 1432 +2026-10-07 18:20:56 200 1468 +2026-10-07 18:21:13 200 1462 diff --git a/_build/test_deploy_sh.sh b/_build/test_deploy_sh.sh index 1324a6d..1d7a0c9 100644 --- a/_build/test_deploy_sh.sh +++ b/_build/test_deploy_sh.sh @@ -519,7 +519,8 @@ echo "── 7c. 默认:两个隧道单元都要启用(协议级故障隔离 stub_ok "7c" # 真机实测(40 次/组):quic 8 + h2 2 拿到 100% 可用率 + 最低 P90/P99。 # 单独跑一个协议都不如两个一起跑: -# 运营商限 UDP(12 个 edge IP:TCP 12/12 通、UDP 0/12 通) +# QUIC 握手要 6+ 个包往返,TCP SYN 只要 3 个且激进重传, +# 约 5% 丢包下 (1-p)^6 ≈ 72%(不是"运营商封 UDP",那是错的测量) # -> quic 拨号成功率只有约 72%,需要 http2 兜住失败的那部分。 : > "$STUB_LOG" printf 'lanremote-relay\n' > "$STUB_LOG.state" diff --git a/_tests/test_lan_bench_guard.py b/_tests/test_lan_bench_guard.py new file mode 100644 index 0000000..da6cb65 --- /dev/null +++ b/_tests/test_lan_bench_guard.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""守住「局域网/中转对比基准」的两个坑。 + +为什么专门为测试脚本立一个测试: + 这个基准一度是红的,报告里写着「这是中转链路的产品 bug」—— + 而实际上产品是好的,是基准自己有两个 bug: + 1. 两条路用了同一个配对码 -> 第 2 台被「一码一机」规则拒掉, + 而第 1 台虽然被杀、中转还以为在线,于是"看着成功、实际没人回包" + 2. relay_connect() 里 attach 用的还是旧码,attach 到死设备上 + 这两个 bug 让一个已经修好的产品背了黑锅,而且**很难自己发现** —— + 因为症状(连接超时)看起来完全像产品问题。 + + 所以既要做源码守卫(防止改回去),也要真跑一遍短版本 + (防止断言与实现脱钩)。 + +覆盖: + 一、源码守卫:两条路必须用不同的码 + 二、源码守卫:attach 必须用 relay_code + 三、源码守卫:start_agent 的判据必须认码(不能被僵尸设备骗过) + 四、真跑一遍短基准:两条路都要通 +""" +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +BENCH = ROOT / "_tests" / "bench_lan_vs_relay.py" + +passed = 0 +failed = 0 + + +def check(name, ok, detail=""): + global passed, failed + if ok: + passed += 1 + print(" [PASS] " + name) + else: + failed += 1 + print(" [FAIL] " + name + ((" —— " + str(detail)) if detail else "")) + + +def main() -> int: + check("基准脚本存在", BENCH.is_file(), BENCH) + if not BENCH.is_file(): + return 1 + src = BENCH.read_text(encoding="utf-8") + + # ---------------------------------------------------------- 一 + print(" ---- 一、两条路用不同的配对码 ----") + has_lan = re.search(r'^\s*code\s*=\s*"(\d{6})"', src, re.M) + has_relay = re.search(r'^\s*relay_code\s*=\s*"(\d{6})"', src, re.M) + check("定义了局域网用的 code", bool(has_lan), + "找不到 `code = \"数字\"`") + check("定义了中转用的 relay_code", bool(has_relay), + "找不到 `relay_code = ...`") + if has_lan and has_relay: + check("两个码不一样(否则第 2 台会被一码一机规则拒掉)", + has_lan.group(1) != has_relay.group(1), + "两个码都是 %s" % has_lan.group(1)) + + # ---------------------------------------------------------- 二 + print(" ---- 二、attach 要用 relay_code ----") + # 找出 relay_connect 函数体 + m = re.search(r"def relay_connect\(\):(.*?)(?=\n def |\n relay =)", + src, re.S) + body = m.group(1) if m else "" + check("能定位到 relay_connect", bool(body), "函数没找到或改名了") + if body: + check("relay_connect 里用的是 relay_code", + "relay_code" in body, + "里面还在用 code —— attach 会打到局域网那台死设备上") + check("relay_connect 里没有裸 attach(code)", + not re.search(r"attach\(\s*code\s*\)", body), + "attach(code) 还在,要改成 attach(relay_code)") + + # ---------------------------------------------------------- 三 + print(" ---- 三、start_agent 的判据要认码 ----") + m2 = re.search(r"def start_agent\(([^)]*)\):(.*?)(?=\n def )", + src, re.S) + body2 = m2.group(2) if m2 else "" + check("start_agent 接受码参数", bool(m2) and "agent_code" in m2.group(1), + m2.group(1) if m2 else "没找到") + if body2: + # 不能只判断"有没有在线设备" + only_online = re.search( + r'if\s+any\(d\.get\("online"\)', body2) + check("没有「只看有没有在线设备」的松判据", not only_online, + "僵尸设备会骗过它 —— 上一台被杀但中转还没清理") + check("判据里比对了配对码", "agent_code" in body2, + "应改成「在线且码正好是这一台要用的码」") + + # ---------------------------------------------------------- 四 + print(" ---- 四、真跑一遍短基准(两条路都要通)----") + try: + proc = subprocess.run( + [sys.executable, "-u", str(BENCH), + "--rounds", "6", "--relay-rounds", "6", "--relay-timeout", "40"], + cwd=str(ROOT), capture_output=True, text=True, + encoding="utf-8", errors="replace", timeout=420) + out = (proc.stdout or "") + (proc.stderr or "") + m3 = re.search(r"BENCH_RESULT\s+(.*)", out) + result = m3.group(1) if m3 else "" + check("基准跑完并输出了 BENCH_RESULT", bool(result), + out[-300:] if not result else "") + if result: + nums = dict(re.findall(r"(\w+)=([\w/.\-]+)", result)) + lan_ok = nums.get("lan_ok", "0/0") + relay_ok = nums.get("relay_ok", "0/0") + check("局域网那条路通了", lan_ok.startswith("6/6"), lan_ok) + check("中转那条路通了", relay_ok.startswith("6/6"), relay_ok) + except subprocess.TimeoutExpired: + check("基准在 420 秒内跑完", False, "超时") + except Exception as exc: # noqa: BLE001 + check("基准能跑起来", False, exc) + + print() + print("=" * 56) + print(" 通过 %d 项,失败 %d 项" % (passed, failed)) + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/_tests/verify_all.ps1 b/_tests/verify_all.ps1 index 14acd81..21cdc1b 100644 --- a/_tests/verify_all.ps1 +++ b/_tests/verify_all.ps1 @@ -400,6 +400,17 @@ $stageOut = & python (Join-Path $root "_tests\check_encoding.py") 2>&1 Show-Fails $stageOut Record "编码与换行" ($LASTEXITCODE -eq 0) +# ---------------------------------------------------------------- 3a5 +Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)" +# 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」—— +# 实际是基准自己有两个 bug:两条路用了同一个配对码、 +# 以及 attach 用了旧码(打到已经死掉的设备上)。 +# 一个已经修好的产品因此背了黑锅,而且症状(连接超时) +# 看起来完全像产品问题,极难自己发现。 +$stageOut = & python (Join-Path $root "_tests\test_lan_bench_guard.py") 2>&1 + Show-Fails $stageOut +Record "局域网对比基准" ($LASTEXITCODE -eq 0) + # ---------------------------------------------------------------- 3b Banner "3b/7 Python 静态检查(pyflakes)" # 这一层能抓到"运行到某行才炸"的问题。 diff --git a/relay/deploy.sh b/relay/deploy.sh index 47b0a65..70facfa 100644 --- a/relay/deploy.sh +++ b/relay/deploy.sh @@ -98,8 +98,13 @@ ykxmz 服务器端 · 一键部署 v$VERSION --protocol P quic / http2 / both(默认 both) 默认**两条隧道一起跑**,这是实测最优: 主 quic + 8 条 副本 http2 + 2 条 - 为什么两条一起跑 —— 运营商对 UDP 有限制 - (12 个 edge IP:TCP 12/12 通、UDP 0/12 通), + 为什么两条一起跑 —— QUIC 握手对丢包敏感: + 它要 6+ 个包往返才建得起来,而 TCP 的 SYN 只有 + 3 个包、内核还会按 1s/2s/4s 激进重传。 + 在约 5% 丢包下 (1-p)^6 只有 ~72% 能建成 —— + 所以刻意让 HTTP/2 占几条连接兜住那 28%。 + (UDP 本身是通的:cloudflared 自检 PASS, + 握手成功时只要 1 秒。不是"运营商封了 UDP"。) 所以 quic 拨号成功率只有约 72%;但连上之后 它的丢包恢复好于 http2。 两个一起 = http2 兜住 quic 拨号失败的那部分, @@ -169,19 +174,36 @@ fi # quic 3 + h2 7 可用率 97% 中位 1271 P90 2558 P99 13934 # quic 8 + h2 2 可用率 100% 中位 1250 P90 1715 P99 2632 <- 采用 # -# 根因也查清了:**运营商对 UDP 有限制**。 -# 对 12 个 Cloudflare edge IP 探测: -# TCP:7844 12/12 可达 -# UDP:7844 0/12 有回应 -# 所以 QUIC 的拨号成功率只有约 72%;但只要拨上了, -# 它的丢包恢复明显好于 HTTP/2(QUIC 没有队头阻塞)。 +# 根因也查清了:**QUIC 握手对丢包敏感**。 +# +# QUIC 注册 525 失败 197 掉线 48 拨号成功率 72% +# HTTP/2 注册 13 失败 0 掉线 7 拨号成功率 100% +# +# QUIC 建连要 6+ 个包的往返,而 TCP 的 SYN 只有 3 个包、 +# 内核还会按 1s/2s/4s 激进重传。约 5.4%/包的丢包下: +# +# (1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上 +# +# ⚠ 注意:**这不是「运营商封了 UDP」。** 曾经这么以为过 —— +# 但那个判断来自一个格式不对的手搓 UDP 探测包, +# Cloudflare 本来就不该回应它,"0/12 无回应"什么都证明不了。 +# 用 cloudflared --loglevel debug 实测真相是: +# +# INF | UDP Connectivity region1.v2.argotunnel.com PASS +# INF | UDP Connectivity region2.v2.argotunnel.com PASS +# INF | SUMMARY: Environment is healthy ... +# INF Registered tunnel connection (握手 1 秒) +# +# UDP 是通的。QUIC 的弱点在「连不上」,不在「连上之后不好」—— +# 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 # # 于是:单独用 QUIC,会有近三成的时候连不上; # 单独用 HTTP/2,队头阻塞在高丢包下会拖长尾延迟。 # 两个一起跑,HTTP/2 正好兜住 QUIC 拨号失败的那部分 —— # 这就是**协议级故障隔离**,比在同一个协议上堆连接数有效得多。 # -# (早先这里写的是"两条同时跑会互相争抢",那个结论是错的,已作废。) +# (早先这里写的是"两条同时跑会互相争抢",那个结论是错的,已作废。 +# 再早还写过"运营商封 UDP",那个测量方法本身是错的,也作废。) MAIN_PROTO=quic MAIN_CONNS="${QUIC_CONNS:-8}" ALT_PROTO=http2