新增 docs/优化实测数据.md:本轮所有优化的实测数字汇总

每一项都写清楚「改之前是多少、改之后是多少、怎么测的、受什么限制」,
以后有人问「这个改动到底有没有用」能直接翻数字,不用靠感觉。

内容包括:
  * 测量方法上的两个坑(从哪台机器测差很多、curl -I 验证 GET 是错的)
  * 下载吞吐 41 倍(含改之前/之后对照)
  * 隧道稳定性:配比对照表、QUIC 72% 拨号率的真因、
    MTU 1400 的效果、长时可用率
  * 长轮询 30 次数据(中位 61439ms)
  * 端到端延迟:局域网 0ms vs 本地中转 15ms
  * 局域网优化的四个真问题
  * 「按一下没反应」的完整证据链和根因
  * 独立验证发现的 9 个问题
  * 测试工具自己骗人的两次
  * 服务端部署脚本的三套测试
  * **没做到的部分**(98.5% 可用率的物理限制,如实记录)

这份是内部工程文档,已加进对外清洁度检查的允许清单
(和 docs/项目规矩.md、docs/双平台发布.md 同类)。
This commit is contained in:
xmz7891
2026-10-07 19:02:57 +08:00
parent 0ee91b68de
commit 4cc368431c
5 changed files with 487 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# 看 relay-stability 的 30 次长轮询结果(目标 2)
set -u
echo " ── 汇总文件 ──"
[ -f /root/rp_poll30_summary.txt ] && cat /root/rp_poll30_summary.txt | sed 's/^/ /'
echo
echo " ── 明细(前 5 行看格式)──"
[ -f /root/rp_poll30.tsv ] && head -5 /root/rp_poll30.tsv | sed 's/^/ /'
echo
echo " ── 自己再算一遍 ──"
python3 - <<'PYEOF'
from pathlib import Path
p = Path("/root/rp_poll30.tsv")
if not p.is_file():
print(" 没有文件")
raise SystemExit(0)
rows = []
for line in p.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line:
continue
parts = [x.strip() for x in line.replace(",", "\t").split("\t") if x.strip()]
if not parts:
continue
# 找出状态码和耗时
code = None
ms = None
for x in parts:
if x in ("200", "502", "530", "000", "401", "504"):
code = x
elif x.replace(".", "").isdigit() and "." not in x and len(x) >= 4:
try:
v = int(x)
if 500 <= v <= 300000 and ms is None:
ms = v
except ValueError:
pass
if code:
rows.append((code, ms))
if not rows:
print(" 解析不出样本,原始内容:")
print(p.read_text(encoding="utf-8", errors="replace")[:400])
raise SystemExit(0)
total = len(rows)
ok = sum(1 for c, _m in rows if c == "200")
print(" 样本 %d,成功 %d,可用率 %.1f%%" % (total, ok, ok * 100.0 / total))
good = sorted(m for c, m in rows if c == "200" and m)
if good:
def pc(q):
return good[min(int(len(good) * q), len(good) - 1)]
print(" 耗时: 最小 %d 中位 %d P90 %d P99 %d 最大 %d ms"
% (good[0], good[len(good) // 2], pc(0.9), pc(0.99), good[-1]))
print(" (理想值 = 60000 + 隧道开销;>90000 的算异常长尾)")
outliers = [m for m in good if m > 90000]
print(" 异常长尾(>90 秒): %d 次 %s" % (len(outliers), outliers))
PYEOF
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# 看 relay-stability 在设备上跑到哪了
set -u
echo " ── 它起的进程 ──"
pgrep -af 'rp_|long2h|ab_long|collapse' 2>/dev/null | head -6 | sed 's/^/ /' || echo " (没有)"
echo
echo " ── /root 下它最近写的文件 ──"
ls -la --time-style=+%H:%M:%S /root/rp_* /root/*.tsv /root/*.txt /root/*.out 2>/dev/null \
| sort -k6 | tail -12 | sed 's/^/ /'
echo
echo " ── 那个 2 小时长测的输出 ──"
if [ -f /root/long2h_health.tsv ]; then
echo " 样本数: $(wc -l < /root/long2h_health.tsv)"
echo " 最后 3 行:"
tail -3 /root/long2h_health.tsv | sed 's/^/ /'
fi
echo
echo " ── 配比对照有没有开始(看隧道配置变没变)──"
for s in lanremote-tunnel lanremote-tunnel-h2; do
printf " %-24s %s\n" "$s" \
"$(grep -oE -- '--protocol [a-z0-9]+|--ha-connections [0-9]+' /etc/systemd/system/$s.service 2>/dev/null | tr '\n' ' ')"
done
for p in 20241 20242; do
n=$(curl -s --max-time 4 "http://127.0.0.1:$p/metrics" 2>/dev/null \
| grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}')
printf " 端口 %s -> %s 条\n" "$p" "${n:-读不到}"
done
echo
echo " ── 公网自检 ──"
curl -s --max-time 20 -o /dev/null -w ' HTTP %{http_code} %{time_total}s\n' \
-H 'User-Agent: LanRemote/1.8.2' https://relay.xxmxxs.top/v1/health
echo
echo " ── 设备当前时间 ──"
date '+ %H:%M:%S'
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# A 组长测是否跑完 + 汇总
set -u
echo "════════ 进度 ════════"
echo " 健康样本: $(wc -l < /root/long2h_health.tsv 2>/dev/null || echo 0)"
echo " 连接采样: $(wc -l < /root/long2h_conns.tsv 2>/dev/null || echo 0)"
echo " 轮询: $(wc -l < /root/rp_poll30.tsv 2>/dev/null || echo 0) / 30"
echo " 测试进程: $(pgrep -c -f rp_long2h || echo 0)"
echo
echo "════════ 汇总文件(跑完才有)════════"
if [ -s /root/long2h_summary.txt ]; then
cat /root/long2h_summary.txt
else
echo " (还没生成,测试仍在跑)"
echo " 当前状态码分布:"
awk -F'\t' 'NR>1{print $2}' /root/long2h_health.tsv 2>/dev/null | sort | uniq -c | sed 's/^/ /'
echo " 当前非 200:"
awk -F'\t' 'NR>1 && $2!="200"{print " " $1 " 码=" $2 " " $3 "ms"}' /root/long2h_health.tsv 2>/dev/null
echo " 连接数最新 5 条:"
tail -5 /root/long2h_conns.tsv 2>/dev/null | sed 's/^/ /'
fi
echo
echo "════════ 塌陷窗口预览(总数<=3 的采样)════════"
awk -F'\t' 'NR>1 && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ && ($2+$3)<=3 {
print " " $1 " quic=" $2 " h2=" $3 " 合计=" $2+$3
}' /root/long2h_conns.tsv 2>/dev/null | tail -20
echo
echo "════════ 连接总数分布 ════════"
awk -F'\t' 'NR>1 && $2 ~ /^[0-9]+$/ && $3 ~ /^[0-9]+$/ {print $2+$3}' \
/root/long2h_conns.tsv 2>/dev/null | sort -n | uniq -c | sed 's/^/ /'
+4
View File
@@ -58,6 +58,10 @@ DOCS_ALLOWLIST = {
# 属于描述规则本身,不是对外宣传材料。
"双平台发布.md",
"EzHub-SSH诊断报告.md", # 给服务端运维看的排查报告
# 内部工程数据文档:记录本轮所有优化的实测数字和对比依据,
# 里面会引用"管理页 23 项测试通过"这类工程事实。
# 它不是发给用户的材料,是给自己和后人查数字用的。
"优化实测数据.md",
}
# 官网源码里这些词也不许有(landing.py 渲染出来的页面)
+346
View File
@@ -0,0 +1,346 @@
# 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 小时 35 分(2 小时长测中途) | 330 | **98.48%** | 1274 | 2187 | 14208 |
**改之前**是 96~98%。现在的失败集中在极少数窗口,且**不集中在某个时段**
(按小时分桶是均匀的,没有"运营商高峰"特征)。
失败的 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 连接数仍会周期性波动(未达标项)
实测观察到的周期约 9 分钟:
```
17:56:39 quic=1 h2=2 <- QUIC 塌到 1 条
17:57:11 quic=1 h2=1 <- h2 也掉了,watchdog 重启
17:57:37 建齐 10 条 <- 26 秒恢复
```
**关键**:QUIC 塌的时候 HTTP/2 那 2 条始终存活,这就是双协议兜底的价值。
但只剩 h2=2 时容量确实薄 —— 这是 relay-stability 正在做长窗口配比对照
(quic8+h2 2 vs quic6+h2 4)要回答的问题。
**这一项(目标 0 掉线)受 4G 链路物理条件限制,本轮没能做到 100%。**
---
## 三、长轮询(目标:稳定贴近 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 就是字符串里的裸换行 → 整段 `<script>` 不执行 → 改码/删除/批量删除全变幽灵按键 | 写成 `\\n` |
| 🔴 | **`--relay` / `--relay-status` 崩溃**:`RelayLink.start(url)` 的 url 是必填参数,改默认地址时两处调用没跟着改 | 两处都补上 url |
| 🟡 | 非法 deviceId(控制字符 / 路径分隔符)被接受 | 白名单:字母数字 + `-_.:` |
| 🟡 | `enable_relay()` 第二次调用谎报成功 | 加 `RelayLink.running` 属性,返回值反映真实状态 |
| 🟡 | `snapshot()` 清孤儿码后没落盘 | 补 `_save()` |
| 🟡 | `deploy.sh --prefix` 沙箱泄漏 | 已修 |
| 🟡 | `test_net_tune.py` 断言过时(钉死 MTU 1500,而实现故意用 1400) | 更新断言 |
| 🟢 | 测试自己骗人(变异测试证明有盲区) | 已补 |
**验证智能体自己的总结很到位**:
> 「23 项测试通过」和「功能能用」是两件事 —— 管理页那个 JS bug
> 就是在 23 项全绿的情况下进去的,测试只查 token 存不存在,不查语法。
---
## 八、测试工具自己骗人(本轮踩了两次)
### 8.1 局域网/中转对比基准
这个基准一度是红的,报告里写着"这是中转链路的产品 bug"。
**查下来不是** —— 基准自己有两个 bug:
1. 两条路用了**同一个配对码** → 第 2 台被「一码一机」规则拒掉,
而第 1 台虽然被杀、中转还以为在线,于是"看着成功、实际没人回包"
2. `relay_connect()` 里 `attach` 用的还是旧码(打到已死掉的设备上)
修好之后两条路都通了(见第四节)。
### 8.2 缓存头
用 `curl -I` 验证 GET 的缓存头,看到的一直是 HEAD 分支的响应。见 0.2。
**教训**:症状像产品 bug 时,先怀疑测量工具比先怀疑产品更省时间。
---
## 九、服务端一键部署脚本
`relay/deploy.sh` + `relay/deploy-tune.sh`,随服务端 zip 发布。
* 自动检查并安装 cloudflared
* 部署程序、装 4 个 systemd 服务、网络调优、配隧道、自检打公网地址
* **不传 `--hostname` 且没有 `TUNNEL_TOKEN` 时明确提示要填** ——
不会静默用一个写死的域名(这个中转站是作者自己用的,不对外开放)
* 幂等:重跑 = 升级,已有数据/配对码/管理密码/凭据/手改过的 config.yml 都不动
* 按需重启:程序和 unit 用 `cmp` 比对,一样就不落盘、不重启
测试三套全绿:
| 测试 | 结果 |
|---|---|
| `_build/test_deploy_sh.sh`(Windows 沙箱) | 110 通过 / 0 失败 |
| `_build/test_zip_deploy.sh`(从发布 zip 解压再跑) | 30 通过 / 0 失败 |
| `_build/test_deploy_device.sh`(真机 Debian aarch64) | 39 通过 / 0 失败 |
真机那套桩掉了 `systemctl`/`iptables`/`ip`/`sysctl`/`nmcli`
(`--prefix` 只隔离写文件,不隔离命令副作用),
第 8 节专门验证线上零影响:relay/tunnel/h2 全 active、MTU=1400、
MSS 规则数、keepalive 测前测后完全一致。
---
## 十、没做到的部分(如实记录)
| 目标 | 实际 | 限制 |
|---|---|---|
| 公网可用率 100% | **98.5%**(1h35m) | 4G 链路约 5%/包丢包;QUIC 握手要 6+ 往返,成功率 (1-p)^6 |
| 隧道 0 掉线 | 仍有约 9 分钟周期的 QUIC 塌陷 | 同上,靠 HTTP/2 兜底把影响压到最小 |
| 长轮询 100% | **96.7%**(30 次) | 隧道重启窗口内 Cloudflare 会重发请求,源站二次 hold |
**这些不是代码问题,是这条 4G 链路的物理条件。** 换有线/宽带会立刻好很多。
能做的是"减少请求数、加协议冗余、加快恢复"——这三件都做了。