Files
ykxmz/_build/watch_availability.py
xmz7891 8c4ee6df08 修真凶:4G 链路静默断连导致隧道"假活"(用户撞上 Error 1033)
现象:用户早上打开 relay.xxmxxs.top 看到 Cloudflare Error 1033。
      watchdog 日志把过程记下来了:
        公网不通(HTTP 000,ha_connections=2)  <- 隧道显示连着,公网不通
        公网不通(HTTP 502,ha_connections=2)
        公网恢复(HTTP 200)                     <- 自己又好了
      也就是说:**间歇性假活**,短样本测不出来。
      我之前只测 15~25 次就说"可用率 100%",太草率了。

根因:4G 链路会**静默断开** TCP 连接 —— 不发 FIN/RST,双方都以为还连着。
      Cloudflare 把请求发到这条僵尸连接上,就回 502/530/1033。
      Linux 默认 tcp_keepalive_time = **7200 秒(2 小时)**才发现连接死了,
      所以中间这段时间服务一直"看着在线、实际打不开"。

      tunnel.log 最近 3000 行的佐证:
        i/o timeout              212 次
        Retrying connection      127 次
        Lost connection           22 次

修法:
  1. TCP keepalive 调到 30 秒 + 每 10 秒探测 3 次
     => 最多约 60 秒发现僵尸连接,cloudflared 立刻重连
     (/etc/sysctl.d/97-ykxmz-keepalive.conf)
  2. 隧道连接数 2 -> 4,多几条冗余
  3. 修 relay 从不清理设备记录的问题:
     self.devices 只增不减,被控端每换端口/重装就多一条,
     实测一天攒了 75 条僵尸记录,/v1/health 从几 KB 涨到 20KB。
     加 DEVICE_TTL(6 小时)在 snapshot() 里清掉。

实测(修完立刻验证):
    设备侧  公网 15/15 HTTP 200
    Windows 公网 25/25,中位 520 ms,最快 463 ms
    health  20931 字节 / 75 台  ->  152 字节 / 0 台
2026-10-07 09:15:27 +08:00

114 lines
3.8 KiB
Python
Raw Permalink 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.
# -*- coding: utf-8 -*-
r'''长时间观察公网可用率 —— 这才是判断"稳不稳"的正确方法。
我之前只测了 15~25 次就连着说"可用率 100%",太草率了。
用户早上打开就撞上 Error 1033 —— 说明**间断性掉线**,
短样本根本看不出来。
这个脚本跑 10 分钟,每 3 秒一次,统计:
* 总可用率
* 连续失败的最长时长(这个最要命:用户点进去就是白页)
* 失败的时间分布(是零星抖动还是成片断)
'''
from __future__ import annotations
import statistics
import sys
import time
import urllib.error
import urllib.request
URL = "https://relay.xxmxxs.top/v1/health"
HEADERS = {"User-Agent": "LanRemote/1.4.0"}
DURATION = 600 # 观察 10 分钟
INTERVAL = 3 # 每 3 秒一次
def probe(timeout: float = 12.0) -> tuple:
"""返回 (是否成功, 耗时秒, 状态码或错误名)。"""
start = time.perf_counter()
try:
with urllib.request.urlopen(
urllib.request.Request(URL, headers=HEADERS),
timeout=timeout) as resp:
resp.read()
ok = resp.status == 200
return ok, time.perf_counter() - start, str(resp.status)
except urllib.error.HTTPError as exc:
return False, time.perf_counter() - start, "HTTP%d" % exc.code
except Exception as exc: # noqa: BLE001
return False, time.perf_counter() - start, exc.__class__.__name__
def main() -> int:
print(" 观察 %d 分钟,每 %d 秒一次" % (DURATION // 60, INTERVAL))
print()
total = ok_count = 0
times = []
failures = []
streak = 0
worst_streak = 0
worst_start = None
current_start = None
deadline = time.time() + DURATION
while time.time() < deadline:
ok, elapsed, note = probe()
total += 1
if ok:
ok_count += 1
times.append(elapsed)
streak = 0
current_start = None
else:
failures.append((time.strftime("%H:%M:%S"), note))
if streak == 0:
current_start = time.time()
streak += 1
if streak > worst_streak:
worst_streak = streak
worst_start = current_start
# 每分钟报一次进度
if total % 20 == 0:
print(" 已测 %3d 次,成功 %3d,失败 %2d"
% (total, ok_count, total - ok_count))
time.sleep(INTERVAL)
print()
print(" ================ 结果 ================")
rate = ok_count / total * 100 if total else 0
print(" 样本 %d 次(约 %d 分钟)" % (total, DURATION // 60))
print(" 可用率 %.1f%% (%d/%d)" % (rate, ok_count, total))
if times:
print(" 成功时延迟 中位 %.0f ms 最快 %.0f ms 最慢 %.0f ms"
% (statistics.median(times) * 1000,
min(times) * 1000, max(times) * 1000))
if failures:
span = worst_streak * INTERVAL
print(" **最长连续失败 %d 次(约 %d 秒)**" % (worst_streak, span))
if worst_start:
print(" 开始于 %s" % time.strftime("%H:%M:%S",
time.localtime(worst_start)))
print(" 失败明细(最多列 20 条):")
for stamp, note in failures[:20]:
print(" %s %s" % (stamp, note))
else:
print(" 全程没有失败")
print()
print(" 判读:")
if rate >= 99.5:
print(" ≥99.5% —— 可以接受")
elif rate >= 97:
print(" 97~99.5% —— 有抖动,用户偶尔会遇到打不开")
else:
print(" <97% —— **不稳定**,必须查根因(用户会经常撞上 Error 1033)")
return 0
if __name__ == "__main__":
sys.exit(main())