Files
ykxmz/_build/rp_REPORT.md
T
xmz7891 aac43e559d 修正 MTU 测试的过时断言(1500 -> 1400)
test_net_tune.py 原来断言 net-tune.sh 里写着 mtu 1500,
依据是更早一次实测「接口 MTU 降到 1400 会掉到 50%」。

那个结论后来被推翻了 —— 当时漏了一件事:
**MSS 钳制只管 TCP,QUIC 走 UDP 完全不受保护。**
路径 MTU 实测 1472(1500 的包不通),QUIC 默认发 ~1452 的包会被
中间设备静默丢掉,而 UDP 丢了不回 ICMP,只能等超时。
把接口 MTU 压到 1400 之后,QUIC 建齐 10 条连接从 28 秒缩到 12 秒。

所以现在是**故意**设 1400。断言跟着改,
不然它会永久假失败,还会诱导后人把真正的改进改回 1500。

顺带记录:全量回归 38 个阶段全部通过。
新增的三个阶段(局域网优化 / 手表局域网切换 / 编码与换行 /
管理页设备控制)都是这轮加的。
2026-10-07 18:10:30 +08:00

274 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# task-3 中转稳定性收尾 · 数据报告
> 起草于 2026-10-07 17:40,最终数字等 2 小时长测结束后填入。
## 一、最终采用的配置
| 项 | 值 | 为什么 |
|---|---|---|
| 隧道实例 1 | `lanremote-tunnel` | QUIC 主力 |
| | `--protocol quic --ha-connections 8 --metrics 127.0.0.1:20241` | 实测 QUIC 一旦连上,丢包恢复优于 TCP |
| 隧道实例 2 | `lanremote-tunnel-h2` | HTTP/2 兜底 |
| | `--protocol http2 --ha-connections 2 --metrics 127.0.0.1:20242` | QUIC 拨号只有 72% 成功,靠它保证总有可用连接 |
| 两个实例 | 连**同一个具名隧道** `lanremote` | Cloudflare 把请求分摊到全部 10 条连接 |
| wlan0 MTU | `1400` | QUIC 走 UDP,TCPMSS 规则管不到 |
| TCP MSS | `1360` | 和 MTU 1400 对齐 |
| TCP keepalive | `30 / 10 / 3` | 4G 会静默丢 TCP,默认 7200 秒才察觉 |
| 中转长轮询 | `RELAY_POLL=60` | 减少 4 倍请求数 |
| watchdog | 双实例 + 自适应阈值(317 行) | 见下 |
## 二、根因分析
### 2.1 QUIC 拨号失败 28% —— 握手对丢包比 TCP 敏感
**先纠正一个我中途搞错的结论。** 我一开始用 Python 发了个简化版 QUIC 包探测
UDP 可达性,得到 "UDP 0/12 无回应",据此判断"运营商封了 UDP"。
**这个测量是无效的** —— 那个包格式不对,Cloudflare 本来就不该回应。
后来用 `--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 (09:45:00 -> 09:45:01,握手 1 秒)
DBG Received transport parameters MaxUDPPayloadSize=1360
DBG QUIC MTU updated to 1296 -> 1328 -> 1344
```
**UDP 完全通,握手成功时只要 1 秒。**
那为什么历史统计里拨号成功率只有 72%?
```
QUIC 注册 525 失败 197 掉线 48 拨号成功率 72%
HTTP/2 注册 13 失败 0 掉线 7 拨号成功率 100%
```
**机制是:QUIC 握手需要 6+ 个包的往返,而 TCP 的 SYN 只有 3 个包且内核会激进重传。**
在 4G 这种有几个百分点丢包的链路上,多轮往返的成功率是指数衰减的:
每次握手约 6 个关键包,单包丢失率 p
成功率 ≈ (1-p)^6
实测 72% -> p ≈ 5.4%/包
而 TCP 的建连即使 SYN 丢了,内核会按 1s/2s/4s 重传(`tcp_syn_retries=6`),
所以 100% 能连上 —— 代价是慢,但那几条 HTTP/2 连接就是用来兜住这个的。
**这也顺带解释了为什么"多 QUIC"反而更快**:QUIC 的弱点是"连不上",
不是"连上之后不好";一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。
排除的另一个假设:
* `--edge-ip-version 6` 用不了 —— 设备 0 个全局 IPv6 地址,IPv6 到 Cloudflare 也不通
(Cloudflare 的 SRV 记录确实返回了 AAAA,但本机没有 IPv6 出口)
### 2.2 为什么"多 QUIC"反而更快(反直觉)
| 配置 | 可用率 | 中位 | P90 | P99 |
|---|---|---|---|---|
| quic5 + h2 5 | 100% | 1490 | 2346 | 3295 |
| quic3 + h2 7 | 97% | 1271 | 2558 | **13934** |
| quic8 + h2 2 | **100%** | **1250** | **1715** | **2632** |
QUIC 拨号成功率低,但**一旦连上,它的丢包恢复明显优于 HTTP/2 over TCP**
(没有跨流的队头阻塞)。所以最优组合是"尽量多 QUIC + 少量 HTTP/2 兜底"。
### 2.3 MTU 1400 的效果
QUIC 走 UDP,我们那条 `iptables -j TCPMSS --set-mss` 规则**只对 TCP 生效**。
路径 MTU 实测 1472,而 debug 日志显示 Cloudflare 报的 `MaxUDPPayloadSize=1360`,
QUIC 自己也在做 MTU 探测(1296 -> 1328 -> 1344)。
把 wlan0 MTU 从 1500 降到 1400 后:
```
QUIC 建齐 6 条连接 28 秒 -> 13 秒
dial 失败次数 2 -> 1
```
### 2.5 周期性 QUIC 崩溃:每 8~10 分钟一次
长测期间观察到一个反复出现的模式:**QUIC 的连接数会突然从 8 掉到 1,几秒后又回到 8。**
连接数采样(每 60 秒)里抓到的几个瞬间:
```
17:30:53 quic=7 h2=2
17:31:59 quic=1 h2=2 <- QUIC 塌了
17:33:04 quic=8 h2=2 <- 自己回来了
...
17:55:33 quic=8 h2=2
17:57:06 quic=1 h2=2 <- 又塌
17:58:12 quic=8 h2=2
```
**关键观察:QUIC 塌到 1 条时,HTTP/2 那 2 条始终存活。**
也就是说:
* 这是**链路级**的突发事件(不是单条连接的问题),QUIC 对它特别敏感
* 而 HTTP/2 over TCP 靠内核的 SYN/重传扛住了
watchdog 日志同时段记录:
```
17:56:39 公网不通(HTTP 000,quic=1, h2=2) 连续第 1 次
17:56:59 公网不通(HTTP 000,quic=7, h2=2) 连续第 2 次 <- 自己回来了
17:57:09 公网不通(HTTP 502,quic=6, h2=2) 连续第 3 次
17:57:09 公网连续 3 次不通,但连接健康(合计 8 条)—— 再等等,不重启
17:57:11 公网不通(HTTP 502,quic=1, h2=1) 连续第 4 次 <- 又塌,且 h2 也掉了
17:57:11 连续 4 次公网不通 —— 重启 [两个实例]
17:57:37 本次已建齐(合计 10 条) <- 26 秒恢复
```
**这次重启是对的**:连接总数掉到 2 条,属于确认故障。
而且**重启把中断缩短到 26 秒** —— 比等 QUIC 自然恢复(可能要几分钟)更划算。
**一个值得后续验证的改进方向**:
QUIC 塌掉时只剩 http2=2 在扛,容量很薄。把 HTTP/2 从 2 条提到 4 条,
可能能在这种窗口里多撑一会儿。但注意第 2.2 节的配比对照是
**2 分钟的短样本**,抓不到这种 9 分钟周期的事件 —— 要验证得跑更长的对照。
### 2.6 故障不集中在某个时段
把 watchdog 记录的"公网不通"按小时分桶,全天分布是均匀的,
**没有出现"运营商某时段抖动"的特征**:
```
16:14 / 16:21 / 16:22 / 16:30 / 16:34 / 16:40 / 16:41 / 16:42
16:49 / 16:50 / 17:00 / 17:10 / 17:11 / 17:12 / 17:13 ...
```
多数是 1 秒内自愈(`公网不通` 紧接着 `公网恢复`)。
真正造成可见中断的只有三次:
```
16:30:19 -> 16:34:28 (4 分 09 秒)
16:50:39 -> 16:53:04 (2 分 25 秒)
17:00:20 -> 17:02:27 (2 分 07 秒)
```
### 2.7 客户端与设备同时故障 = 源站侧问题
PC 侧长测(WiFi/宽带,与设备完全不同的网络)在同一个窗口也失败了:
```
PC 侧: 17:56:57 code=0 42093ms
17:57:13 code=502 674ms
17:58:57 code=0 25818ms
设备侧: 17:56:46 code=000 25000ms
17:57:06 code=502 4652ms
```
**两个独立网络同时受影响,说明问题在 Cloudflare 边缘或隧道这一侧,
不是某一端的本地链路。**
30 次长轮询里出现两次 ~126.7 秒(正好是 2 倍 hold + 开销):
```
#1 17:30:42 开始 -> 17:31:40 watchdog 重启了两个实例 -> 实际 126604ms
#10 17:40:59 开始 -> 17:41:56 公网 000、17:42:46 重启 h2 -> 126716ms
其余 61~62 秒正常
```
**对齐时间线后确认:异常长轮询都发生在隧道被重启的窗口里**,
不是稳态行为。而重启有一部分正是看门狗过于激进的策略引起的
(见 3.2),所以"收紧重启策略"本身就直接改善了长轮询的尾部延迟。
**这带来一个需要 Lead 决定的点**:客户端 `POLL_TIMEOUT` 是 120000ms,
126 秒会被判失败。要么把超时放宽到 180 秒,要么把 `RELAY_POLL` 降到 45 秒
(这样 2 倍也只有 ~93 秒)。两者都涉及 android/ 和 windows/,超出我的授权范围。
## 三、修掉的两个真 bug
### 3.1 metrics 端口和实例的对应关系会翻转(严重)
两个服务的 ExecStart **都没写 `--metrics`**,都用 cloudflared 默认的 20241。
正常启动顺序下看着是"主=20241 副=20242",**但那只是巧合**。
实测:把主实例 stop 掉、再重启副实例,**副实例就抢到了 20241**。
于是"20241 是谁"翻转,watchdog 和所有按端口读指标的脚本全部读错实例
(我自己的 A/B 对照就因此出了一组完全反向的数据)。
修法两层:
1. 给两个服务显式钉死 `--metrics`,并实测验证(停主实例后副实例不再抢)
2. watchdog 改成从服务配置里读端口,以后谁改了端口它跟着走
### 3.2 watchdog 在双实例下有盲点
第三版:`METRICS` 写死 20241,**只看得到 QUIC 那个实例**;
`restart_tunnel` 只重启 `lanremote-tunnel`,副实例僵死永远不会被拉起来。
第四版(193 → 317 行):
* 分别读两个实例,分实例判断
* 某实例端口无响应 **或** 连接数为 0 且过了宽限期 → 只重启那一个
* 两个都垮 / 公网不通 → 重启两个
* "建齐"阈值按总连接数算
**另外发现它在"帮倒忙"**:
```
17:30:59 公网不通(HTTP 000,连接 7+2=9 条,健康)
17:31:19 公网不通(HTTP 000,连接 8+2,健康)
17:31:40 公网不通(HTTP 000,连接 7+2,健康)
17:31:40 连续 3 次 —— 重启两个实例
17:32:35 重启后只剩 7 条(6+1)
```
连接明明健康,它却因为"公网 3 次不通"把隧道重启了。
而**重启会主动切断全部连接**,接下来 60 秒真的不可用 ——
等于把一次"抖一下就好"的故障升级成确定的 1 分钟中断。
加了自适应阈值:
* 连接健康(≥6 条)→ 要连续失败 **8 次**(约 2.5 分钟)才重启
* 连接降级 → 保持 **3 次**快速重启
* 日志僵死 + 公网不通 → 立即重启(原逻辑保留)
4 个场景的桩测试全部通过(第 8 次 / 第 1 次 / 第 3 次 / 永不)。
## 四、实测数据
### 4.1 长轮询:30 次真实 poll(每次挂 60 秒)
```
成功 29/30 = 96%
失败 #22(000,130001ms —— 撞上 17:57 那次隧道重启,客户端 130 秒超时)
耗时分布(成功的):
最小 60920 P50 61439 P90 126604 P99 126716 最大 126716 ms
理想值 = 60000 + 隧道开销 ≈ 61000
```
**分布是双峰的**:
| 档位 | 次数 | 说明 |
|---|---|---|
| 61 秒 | 26 | 正常,开销只有 1~1.5 秒 |
| 126 秒 | 3 | 约 2 倍 hold,**全部对齐到隧道重启窗口** |
对照三次异常的时间线:
```
#1 17:30:42 开始 -> 17:31:40 watchdog 重启两个实例 -> 126604ms
#10 17:40:59 开始 -> 17:41:56 公网 000、17:42:46 重启 h2 -> 126716ms
#18 17:50:17 开始 -> 17:50:42~17:51:25 连续三次不通 -> 126363ms
#22 17:55:28 开始 -> 17:57:11 重启两个实例 -> 超时 130001ms
```
**结论:稳态下长轮询极其干净(61~62 秒,抖动 < 2 秒);
126 秒那种是隧道被重启打断造成的,不是协议或中转的问题。**
### 4.2 设备侧公网可用率(2 小时长测,每 15 秒一次)
数据见第 4.4 节的最终汇总。
### 4.3 PC 侧(真实用户视角)
PC(WiFi/宽带)发起,只有"边缘 -> 隧道 -> 设备 4G"这一跳是慢的。
这与真实用户的情况一致(用户也是好网络)。
### 4.4 待 2 小时长测结束后填入
## 五、受链路物理条件限制、做不到 100% 的部分
(待填)