Files
ykxmz/_tests/test_net_tune.py
T
xmz7891 6adf7acbfb 修「网站容易挂」的真因:nettune 和隧道之间的死锁
用户报「网站还是容易挂」。查到最后是个 systemd 顺序死锁 ——
而且**是我自己引入的**(换掉设备上那份过期 unit 之后才露出来)。

== 死锁链条 ==

    lanremote-nettune.service:
        Before=lanremote-tunnel.service        <- 我必须先于隧道启动完
        ExecStart=/bin/bash net-tune.sh

    net-tune.sh 第 4 步("让已经建立的隧道用上新参数"):
        systemctl restart lanremote-tunnel     <- 在这里永久阻塞

  nettune 在**自己还没结束**的时候去 restart 一个"必须等自己结束
  才能启动"的服务 —— systemd 判定顺序冲突,restart 一直不返回。

  证据(设备上抓的):
      CGroup: /system.slice/lanremote-nettune.service
              ├─13388 /bin/bash net-tune.sh
              └─13428 systemctl restart lanremote-tunnel
      那个 systemctl 的 etime 是 09:32 —— 卡了 9 分半。

  后果:nettune 一直 activating -> 隧道被 Before= 挡着起不来 ->
        公网 530。而 journalctl 里只看得到 nettune 停在 activating,
        没有任何报错,特别难查。

== 为什么以前没暴露 ==

  设备上原来那份 unit 的 ExecStart 里有个 iptables ... || ...,
  systemd 不支持 ||,所以它**在第 2 条 ExecStart 就报错退出**,
  根本走不到"重启隧道"那一步 —— 死锁被那个 bug 遮住了。
  我把 unit 换成正确形状之后,死锁才露出来。

  另一个迷惑点:**手动跑 net-tune.sh 完全正常**(输出"没有正在跑的
  隧道,跳过"),因为手动跑时不在服务上下文里、没有 Before= 约束。

== 改法 ==

  第 4 步的本意是"改完 MTU/MSS 后让已建立的隧道用上新参数",
  这个功能有用,但**只该在人工重跑时做** —— 开机时顺序本来就是
  nettune 先、隧道后,那时隧道还没起来。

  所以 unit 里加 Environment=NETTUNE_SERVICE_CALL=1,
  脚本看到就跳过第 4 步。三个地方都改了:
      relay/net-tune.sh    认这个开关
      relay/deploy.sh      生成的 unit 带上它
      relay/install.sh     同上(老机器走的安装路径)

  没选 systemctl restart --no-block:那只是把阻塞变异步,
  开机时仍然做一次无谓的重启(隧道刚起来就被停掉),
  也没解决"不该在开机时重启"这个语义问题。

== 另外 ==

  setup-monitor.sh / monitor.sh / rescue.sh 这一整套这次也真的
  在线上跑起来并验证过了:

      18:13:03  紧急预案启动:公网连续 3 次不通
      18:13:07  现场已记录 + [1/5] 重启隧道
      18:13:59  ✅ 第 1 级救回来了,公网已恢复
      18:13:59  已把「第 1 级奏效」记进知识库

  死锁这次就是被监测单元抓到的(fails=3 / last_code=530)。

全量回归 46 个阶段全部通过。
2026-10-08 18:56:44 +08:00

127 lines
5.2 KiB
Python
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.
# -*- coding: utf-8 -*-
"""验证 MTU 调优脚本和安装集成。
为什么值得单独回归:
这是这次可用率从 48% 提到 100% 的关键,而且**很容易被后人误改** ——
比如"顺手把接口 MTU 也降一点"(实测那样会掉到 50%)。
所以把关键决策钉在测试里。
"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
NET_TUNE = ROOT / "relay" / "net-tune.sh"
INSTALL = ROOT / "relay" / "install.sh"
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))
def main() -> int:
print("一、调优脚本本身")
check("net-tune.sh 在", NET_TUNE.is_file())
if not NET_TUNE.is_file():
return 1
text = NET_TUNE.read_text(encoding="utf-8")
# bash 语法(在 Linux 上才有 bash,本机没有就跳过)
check("打开 tcp_mtu_probing", "tcp_mtu_probing=1" in text)
check("夹 MSS", "--set-mss" in text)
check("MSS 默认值合理(1300~1440)",
bool(re.search(r"MSS_VALUE=\$\{MSS_VALUE:-(\d+)\}", text))
and 1300 <= int(re.search(r"MSS_VALUE=\$\{MSS_VALUE:-(\d+)\}", text)
.group(1)) <= 1440,
re.search(r"MSS_VALUE=.*", text).group(0) if "MSS_VALUE" in text else "")
check("OUTPUT 链上夹", "OUTPUT" in text)
check("POSTROUTING 链上夹", "POSTROUTING" in text)
# ⚠ 这里原来断言的是"把接口 MTU 设回 1500",
# 依据是更早的一次实测(接口 MTU 降到 1400 会掉到 50%)。
#
# 那个结论后来被推翻了,原因是我们当时漏了一件事:
# **MSS 钳制只管 TCP,而 QUIC 走 UDP 完全不受保护。**
# 路径 MTU 实测 1472(1500 的包不通),QUIC 默认发 ~1452 的包
# 会被中间设备静默丢掉,而 UDP 丢了不回 ICMP,只能等超时。
# 把接口 MTU 压到 1400 之后,QUIC 建齐 10 条连接从 28 秒缩到 12 秒。
#
# 所以现在**故意**设 1400。这个断言跟着改,
# 否则它会永久假失败,还会诱导后人把真正的改进改回 1500。
check("把接口 MTU 压到 1400(QUIC 走 UDP,MSS 钳制管不到它)",
"mtu 1400" in text or "MTU_VALUE" in text,
"接口 MTU 要压到 1400,否则 QUIC 的大包会被静默丢弃")
check("注释里说明了为什么改接口 MTU(QUIC 不受 MSS 保护)",
"QUIC" in text or "UDP" in text or "MSS 钳制" in text)
# 清理旧规则,避免重启后越积越多
check("会先清掉旧 MSS 规则", "-D" in text and "while" in text)
print()
print("二、安装脚本带上了它")
check("install.sh 在", INSTALL.is_file())
install = INSTALL.read_text(encoding="utf-8")
check("有 [3c] 网络调优段", "[3c]" in install)
check("会拷 net-tune.sh", "net-tune.sh" in install)
check("会写 sysctl 配置", "99-lanremote-mtu.conf" in install)
check("会注册开机服务", "lanremote-nettune.service" in install)
check("服务 enabled", "systemctl enable lanremote-nettune" in install)
check("开机服务在隧道之前跑(新连接就用上新参数)",
"Before=lanremote-relay.service lanremote-tunnel.service" in install)
print()
print("三、打包会带上这两个文件")
package = (ROOT / "_build" / "package_remote.py").read_text(
encoding="utf-8")
check("打包脚本扫描整个 relay 目录(不是硬编码文件名)",
"iterdir" in package or "rglob" in package, "")
check("relay/net-tune.sh 会被打进去(同目录扫描即可)",
NET_TUNE.is_file())
print()
print("四、说明文档里记了实测数据")
check("写了不夹 MSS 的对照(87%)", "87" in text)
check("写了 MSS 各档都 100%", "100%" in text)
check("写了接口 MTU 1400 更差的教训", "50%" in text)
print()
print("=" * 58)
print(f"通过 {passed} 项,失败 {failed} 项")
return 0 if failed == 0 else 1
# ⚠ 绝不能由 nettune 服务去 restart 隧道 —— 那个 unit 有
# Before=lanremote-tunnel.service,脚本在自己还没结束时 restart
# 隧道会和 systemd 的顺序约束死锁(实测卡 9 分半,表现是网站挂了
# 而日志里什么都看不出来)。
# 所以 net-tune.sh 必须认 NETTUNE_SERVICE_CALL 这个开关。
tune_src = (ROOT / "relay" / "net-tune.sh").read_text(encoding="utf-8")
check("net-tune.sh 认 NETTUNE_SERVICE_CALL(防死锁)",
"NETTUNE_SERVICE_CALL" in tune_src,
"没有这个开关 -> 服务启动时会卡死")
# 两个生成 unit 的地方也要带上
for name in ("deploy.sh", "install.sh"):
p = ROOT / "relay" / name
if p.is_file():
body = p.read_text(encoding="utf-8")
check("%s 的 nettune unit 带 NETTUNE_SERVICE_CALL" % name,
"NETTUNE_SERVICE_CALL" in body,
"新机器装出来还是会死锁")
if __name__ == "__main__":
sys.exit(main())