mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 10:10:43 +00:00
用户报「网站还是容易挂」。查到最后是个 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 个阶段全部通过。
127 lines
5.2 KiB
Python
127 lines
5.2 KiB
Python
# -*- 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())
|