# ykxmz 优化实测数据(2026-10-07) > 这份文件记录本轮所有优化的**实测数字**和**对比依据**。 > 每一项都写清楚:改之前是多少、改之后是多少、怎么测的、受什么限制。 > 目的:以后有人问"这个改动到底有没有用",能直接翻数字,不用靠感觉。 --- ## 零、测量方法上的两个坑(先说清楚,否则下面的数字会被误读) ### 0.1 「从哪台机器测」会差很多 设备是随身 WiFi(4G 上行)。如果在**设备自己**上跑 curl,请求会走: ``` 设备 → 4G → Cloudflare 边缘 → 隧道 → 回到设备自己 ``` **同一段 4G 链路被算了两遍**,数字比真实用户差得多。 真实用户(WiFi/宽带/5G)只有后半段走 4G: ``` 用户 → 好网络 → Cloudflare 边缘 → 隧道 → 设备 4G ``` 所以**能从 PC 测的指标就用 PC 测**。两边都测过的项,下面会分开列。 ### 0.2 用 `curl -I` 验证 GET 的响应头是错的 `-I` 发的是 HEAD。我在这上面白查了半天缓存头 —— 看到的一直是 `do_HEAD` 分支的响应, 而真正的 GET 早就改对了。**验证缓存头必须用 `curl -sD - -o /dev/null`。** --- ## 一、下载吞吐 **改之前**:relay 给所有响应发 `Cache-Control: no-store`,Cloudflare 完全不缓存, 每次下载都要穿隧道回 4G 源站。 | 文件 | 改之前 | 改之后 | 倍数 | |---|---|---|---| | `ykxmz-setup-1.8.2.exe`(15 MB) | 237568 字节后就断了(17 KB/s) | **695 KB/s,完整 15067199 字节** | **41×** | | `ykxmz-watch-1.8.2.apk` | 3 KB/s | **101 KB/s** | **33×** | | `ykxmz-server-1.8.2.zip` | 67 KB/s | 87 KB/s | 1.3× | 改完之后三个文件的 `cf-cache-status` 都是 **HIT**(不再穿隧道回源), 而且**不消耗 4G 流量**。 改动:`/download/*` → `public, max-age=604800, immutable`;`/` → `max-age=60`; `/shot/*`、`/favicon.ico` 保持 `max-age=86400`;`/v1/*` 保持 `no-store`。 顺带修的:`_send()` 无条件先发 `no-store` 导致**两个 Cache-Control 头** (调用方的被吃掉);`do_HEAD` 和 `do_GET` 的缓存头不一致。 --- ## 二、隧道稳定性与公网可用率 ### 2.1 最终配置 | 项 | 值 | 依据 | |---|---|---| | 实例 1 `lanremote-tunnel` | `--protocol quic --ha-connections 8 --metrics 127.0.0.1:20241` | QUIC 连上之后丢包恢复优于 TCP | | 实例 2 `lanremote-tunnel-h2` | `--protocol http2 --ha-connections 2 --metrics 127.0.0.1:20242` | QUIC 拨号只有约 72% 成功率,靠它兜底 | | 两个实例 | 连**同一个具名隧道** | Cloudflare 把请求分摊到全部 10 条连接 | | wlan0 MTU | 1400 | QUIC 走 UDP,TCPMSS 规则管不到它 | | TCP MSS | 1360 | 和 MTU 1400 配套 | | TCP keepalive | 30 / 10 / 3 | 4G 会静默丢 TCP,系统默认 7200 秒才察觉 | ### 2.2 为什么"两条一起跑"(受控对照,40 次/组) | 配置 | 可用率 | 中位 | P90 | P99 | |---|---|---|---|---| | quic 5 + h2 5 | 100% | 1490 | 2346 | 3295 | | quic 3 + h2 7 | 97% | 1271 | 2558 | **13934** | | **quic 8 + h2 2** | **100%** | **1250** | **1715** | **2632** | 结果反直觉但机制清楚:**QUIC 的弱点是「连不上」,不是「连上之后不好」。** 一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。 ### 2.3 QUIC 拨号 72% 的根因(**不是**"运营商封了 UDP") 曾经以为是运营商限制 UDP。**那个判断是错的** —— 依据来自一个手搓的简化 UDP 探测包,格式不对,Cloudflare 本来就不该回应它。 用 `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 完全通。** 真因是: ``` QUIC 建连要 6+ 个包往返;TCP 的 SYN 只有 3 个包、内核还按 1s/2s/4s 激进重传。 约 5.4%/包的丢包下: (1 - 0.054)^6 ≈ 72% ← 和实测 72% 对得上 ``` ### 2.4 MTU 1400 的效果 | | 建齐 10 条连接耗时 | dial 失败次数 | |---|---|---| | MTU 1500 | 28 秒 | 2 | | **MTU 1400** | **13 秒** | **1** | 原因:QUIC 的包一旦超过路径 MTU 就被中间设备**静默丢掉**(UDP 丢了不回 ICMP), 只能等超时。把接口 MTU 压到 1400(路径 MTU 实测 1472)之后就好了。 ### 2.5 长时可用率 | 时段 | 样本 | 可用率 | 中位 | P90 | P99 | |---|---|---|---|---|---| | 25 分钟(双协议初测) | 50 | **100%** | 1505 | 3218 | 14245 | | **1 小时 44 分(2 小时长测)** | **361** | **98.61%** | **1280** | **2386** | **14242** | > 长测原定 2 小时,实际跑到 1 小时 44 分就停了 —— > 设备在 19:11 换了网段(`192.168.10.105` → `192.168.2.156`), > 测量脚本随之断开。361 个样本足够说明问题。 **改之前**是 96~98%。失败不集中在某个时段(按小时分桶是均匀的, 没有"运营商高峰"特征),绝大多数抖动 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 秒) ``` 失败的 5 次: ``` 17:27:20 HTTP 502 1407ms 17:31:42 HTTP 530 862ms 17:56:46 HTTP 000 25000ms 17:57:06 HTTP 502 4652ms 19:00:20 HTTP 502 2101ms ``` PC 侧(完全不同的网络)在同一个窗口也失败 —— **说明问题在 Cloudflare 边缘 或隧道这一侧,不是某一端的本地链路。** ### 2.6 连接数波动(86 个采样点 / 约 1.5 小时) ``` QUIC 连接数: 最低 1 最高 8 平均 7.5 HTTP/2 连接数: 最低 1 最高 2 平均 2.0 QUIC 连接数分布: 8 条 76.7% <- 大部分时间健康 7 条 15.1% 6 条 3.5% 5 条 2.3% 1 条 2.3% <- 塌陷(1.5 小时里约 2 分钟) QUIC 塌陷(<=2 条): 2 次,占 2.3% 塌陷时 HTTP/2 >=1 条: 2 次(100%) 塌陷时 HTTP/2 也归零: 0 次 ``` **这就是双协议兜底的价值,有数字支撑:QUIC 塌的时候 HTTP/2 一次都没塌。** 一次典型塌陷的时间线(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 次 公网连续 3 次不通,但连接健康(合计 8 条)—— 再等等,不重启 17:57:11 公网不通(HTTP 502,quic=1, h2=1) 连续第 4 次 <- 又塌,且 h2 也掉了 连续 4 次公网不通 —— 重启 [两个实例] 17:57:37 本次已建齐(合计 10 条) <- 26 秒恢复 ``` **这次重启是对的**:连接总数掉到 2 条属于确认故障, 重启把中断缩短到 26 秒,比等 QUIC 自然恢复(可能几分钟)划算。 **目标「0 掉线」没能做到** —— 4G 链路上 QUIC 会周期性塌陷。 能做的是「加协议冗余 + 加快恢复」,这两件都做了, 把影响压到 2.3% 的窗口且 HTTP/2 全程兜住。 --- ## 三、长轮询(目标:稳定贴近 60000ms + 最小开销) 30 次真实 poll(每次挂 60 秒),改完之后: ``` 成功 29/30 = 96.7% 耗时: 最小 60920 中位 61439 P90 126604 P99 126716 ``` **中位 61439ms**(理想 61000)—— 隧道只加了约 1.4 秒开销,非常好。 **4 次异常长尾(~126.6 秒 = 正好 2 倍 hold)**:发生在隧道重启的窗口里。 Cloudflare 在隧道重连时会把请求重发给源站,源站又 hold 了一次 60 秒。 由此把**客户端超时从 120 秒放宽到 180 秒**(留 120 秒网络余量)。 超时余量的演进全是被实测逼出来的: ``` 90 秒 -> 实测出现 93589ms 不够 120 秒 -> 实测出现 126604ms 不够 180 秒 -> 60 hold + 120 余量 ``` **不缩短 hold 的理由**:这条链路的根本问题是丢包抖动,**请求越少越不容易撞上**。 放宽客户端超时只是"别把慢但成功的请求冤枉掉",不增加任何请求数。 --- ## 四、端到端延迟(局域网 vs 跨网络) 用 `_tests/bench_lan_vs_relay.py` 测,同一套客户端写法,两边各 30 次 `cmd:ping`: | 路径 | 握手 | 成功率 | 中位 | P90 | 最大 | |---|---|---|---|---|---| | **局域网直连** | 16 ms | **30/30 (100%)** | **0 ms** | **0 ms** | 1 ms | | 跨网络(本地中转) | 46 ms | 30/30 (100%) | 15 ms | 29 ms | 32 ms | 跨网络那 15ms 是**本地中转**的协议开销(长轮询往返 vs 直连 TCP)。 真实跨网络的 1.0~1.5 秒来自隧道 + 4G,是另一回事(见 2.5 的耗时数据)。 **结论:局域网确实是不折不扣的"毫秒级、不耗流量、不怕隧道抖"的那条路。** 中位 0ms vs 15ms,而且完全不经过任何外网。 --- ## 五、局域网优化的四个真问题("不能忘本") | # | 原来怎么错 | 现在怎么对 | |---|---|---| | 1 | `sameSubnet()` 只比前三段(等于写死 /24)。在 10.x/8、172.16/12 上,`10.8.1.5` 和 `10.8.2.5` 被判成"不同网络"→ **明明能直连却走中转还费流量** | 按**网卡真实掩码**比较;拿不到掩码才退回 /24 | | 2 | 广播发现**只发一次包**,UDP 丢一个就整轮搜不到电脑 | 按 `{0, 350, 900, 1800, 3000}ms` 重发 5 轮 | | 3 | 只探**一个**网段(拿到第一个 wlan 就 return)—— 多网卡手表搜不到另一网段的电脑 | `localNets()` 返回全部,定向广播全探 | | 4 | 走中转之后**再也回不到局域网**(只在"刚连上"那一刻试一次直连) | 定期回切(起手 15s,退避到最多 2min)。**先做便宜的 TCP ping,通了才动真格**,不破坏正在工作的中转连接 | --- ## 六、"按一下没反应"的真因(本轮最重要的修复) ### 现象 用户报:局域网能搜到电脑、IP/端口/配对码都对,**连上后没有任何反应**。 ### 证据链(插桩复现) ``` Exception in thread client-relay: server.py:407 session.send(reply) relay.py:166 self._link.send_to_client(data) relay.py:357 self._post("/v1/agent/send", ...) relay.py:766 raise RelayError(f"请求失败: {last_error}") lanremote.relay.RelayError: 请求失败: Idle ``` 线程一死:`client-relay` 没了 → 电脑再也不回任何东西; 而 `relay-link` 还活着、还在长轮询 → **中转那边看着"设备在线"**。 手表看到的就是「连上了,按一下毫无反应」。 ### 根因:一条 HTTP/1.1 连接被两个线程同时用 ```python with self._http_lock: connection = self._http # ← 锁到这里就放了 ... connection.request("POST", ...) # ← 请求在锁外发 ``` * `relay-link` 线程跑 `/v1/agent/poll` —— **长轮询,一条请求挂 180 秒** * `client-relay` 线程同时 POST `/v1/agent/send` HTTP/1.1 一条连接同一时刻只能有一个在途请求 → 第二个线程撞上 `http.client` 状态机 → `CannotSendRequest("Idle")`。 ### 修法 1. **发命令走独立连接**(`_post` 加 `dedicated` 参数) 2. 两条连接各自一把锁**盖住整个「发请求 + 读响应」周期** 3. 普通请求若发现缓存连接正忙,临时另开一条用完即关 4. **`_client_loop` 兜住回包异常** —— 发失败最多踢掉这个会话让手表重连, 绝不能让线程消失 ### 效果 ``` 修复前:pair OK -> ping OK -> ping 卡死 -> 线程崩溃,永久哑 修复后:pair 21ms -> ping 82ms -> ping 4ms -> ping 6ms,零异常 _tests/test_relay_local.py:之前完全卡死 -> 现在 21/21 全过 ``` **这个测试之前一直是红的**,说明"回归全绿"这件事本身就不成立过 —— 某个阶段卡住时看起来像环境问题而不是缺陷。 --- ## 七、独立验证发现的问题(对抗性复核) 派了一个专门的验证智能体,**只读验证 + 自己构造边界场景**,找到 9 个问题, 其中 3 个严重(都是本轮改动引入的),已全部修复: | 严重度 | 问题 | 修法 | |---|---|---| | 🔴 | **TTL 清理 + 关掉接入开关 = 设备永久锁死**:`register()` 用 `device_id in self.devices` 判断老设备,但超时清理(>6 小时)把设备从 devices 和 codes 都删了。**电脑关机过夜必然触发**,而且重启也救不回来(清 codes 时没落盘,重启后又当孤儿清一遍,自我维系)。管理员在页面上还看不到这台设备 | 状态文件里单独存 `knownDevices` 设备历史,超时清理绝不动它;清理后补 `_save()` | | 🔴 | **管理页 JS 语法错误 → 所有按钮失效**:`admin.py` 里 `ADMIN_JS` 是普通三引号字符串,在 Python 源码里写的 `\n` 被变成真实换行,拼进 JS 就是字符串里的裸换行 → 整段 `