mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 21:50:44 +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 个阶段全部通过。
149 lines
6.4 KiB
Python
149 lines
6.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
r'''永久修掉 nettune 死锁。
|
||
|
||
== 死锁回顾 ==
|
||
|
||
lanremote-nettune.service Before=lanremote-tunnel.service
|
||
net-tune.sh 第 4 步 systemctl restart lanremote-tunnel
|
||
|
||
nettune 在"自己还没结束"的时候去 restart 一个"必须等自己结束才能启动"
|
||
的服务 —— systemd 认为顺序冲突,restart 永久阻塞。
|
||
|
||
实测卡了 9 分半,nettune 一直 activating,隧道被 Before= 挡着起不来,
|
||
公网 530。这就是"网站容易挂"的真因之一。
|
||
|
||
== 修法 ==
|
||
|
||
第 4 步的本意是"让**已经建好**的隧道用上新参数"(改完 MTU/MSS 之后
|
||
手动重跑脚本时用)。**开机时根本不该走到那一步** ——
|
||
开机顺序本来就是 nettune 先、隧道后,那时隧道还没起。
|
||
|
||
所以:unit 里加 Environment=NETTUNE_SERVICE_CALL=1,
|
||
脚本看到这个变量就跳过第 4 步。
|
||
|
||
为什么不干脆删掉第 4 步:
|
||
手动改完网络参数后,已经建立的隧道还在用旧参数,
|
||
需要重启它才生效。这个功能是有用的,只是不该在服务启动时跑。
|
||
|
||
为什么不用 `systemctl restart --no-block`:
|
||
那只是把阻塞变成异步,nettune 立刻退出、隧道随后重启 ——
|
||
开机时仍然是无谓的一次重启(隧道刚起来就被停掉),
|
||
而且没解决"不该在开机时重启"这个语义问题。
|
||
'''
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
sys.path.insert(0, str(ROOT / "_build"))
|
||
|
||
from devssh import upload, run # noqa: E402
|
||
|
||
TUNE = ROOT / "relay" / "net-tune.sh"
|
||
|
||
OLD = '''echo " 4) 让已经建立的隧道用上新参数"'''
|
||
|
||
NEW = '''# ════════════════════════════════════════════════════════════════════
|
||
# 4) 让已经建立的隧道用上新参数 —— **只在手动跑的时候做**
|
||
# ════════════════════════════════════════════════════════════════════
|
||
#
|
||
# ⚠ 这一步绝对不能由 lanremote-nettune.service 执行。
|
||
#
|
||
# 那个 unit 里写着 Before=lanremote-tunnel.service
|
||
# (意思是"我必须先于隧道启动完")。如果脚本在自己还没结束的时候
|
||
# 去 `systemctl restart lanremote-tunnel`,systemd 会认为存在
|
||
# 顺序冲突 —— restart 永久阻塞、nettune 一直卡在 activating、
|
||
# 隧道被 Before= 挡着永远起不来。
|
||
#
|
||
# 实测卡了 9 分半,表现就是"网站又挂了",而日志里什么都看不出来
|
||
# (只有 nettune 停在 activating)。手动跑本脚本却完全正常
|
||
# (因为那时没有 Before= 约束)—— 所以这个坑特别难发现。
|
||
#
|
||
# 开机时本来也不需要这一步:顺序就是 nettune 先、隧道后,
|
||
# 那时候隧道还没起来。
|
||
if [ "${NETTUNE_SERVICE_CALL:-0}" = "1" ]; then
|
||
echo " 4) 由 nettune 服务调用,**跳过重启隧道**"
|
||
echo " (unit 里有 Before=lanremote-tunnel.service,"
|
||
echo " 在这里 restart 会和 systemd 的顺序约束死锁)"
|
||
echo " (开机顺序本来就是本脚本先、隧道后,不需要这一步)"
|
||
elif [ "${SKIP_TUNNEL_RESTART:-0}" = "1" ]; then
|
||
echo " 4) SKIP_TUNNEL_RESTART=1,跳过重启隧道"
|
||
else
|
||
echo " 4) 让已经建立的隧道用上新参数"'''
|
||
|
||
|
||
def main() -> int:
|
||
raw = TUNE.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig")
|
||
|
||
if "NETTUNE_SERVICE_CALL" in text:
|
||
print(" · net-tune.sh 已经修过了")
|
||
elif OLD not in text:
|
||
print(" ✗ 找不到锚点")
|
||
return 1
|
||
else:
|
||
text = text.replace(OLD, NEW, 1)
|
||
# 原来的 for 循环 + RESTARTED 逻辑要收进 else 分支
|
||
# 找到 "echo \" 网络调优已应用" 之前的部分,补一个 fi
|
||
tail_anchor = 'echo " 网络调优已应用'
|
||
i = text.find(tail_anchor)
|
||
if i < 0:
|
||
print(" ✗ 找不到结尾锚点")
|
||
return 1
|
||
# 在结尾那行之前插入 fi(缩进对齐 for 循环之外)
|
||
text = text[:i] + "fi\n\n" + text[i:]
|
||
TUNE.write_bytes((b"\xef\xbb\xbf" if bom else b"")
|
||
+ text.encode("utf-8"))
|
||
print(" ✓ net-tune.sh 加了 NETTUNE_SERVICE_CALL 判断")
|
||
|
||
# ---- 推到设备 + 给 unit 加环境变量 ----
|
||
upload(str(TUNE), "/root/lanremote-relay/net-tune.sh")
|
||
print(" ✓ 已上传 net-tune.sh")
|
||
|
||
unit = '''[Unit]
|
||
Description=ykxmz 网络调优(MSS 钳制 + 路径 MTU 探测)
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
Before=lanremote-relay.service lanremote-tunnel.service lanremote-tunnel-h2.service
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
# ⚠ NETTUNE_SERVICE_CALL=1 告诉 net-tune.sh:这次是**服务**在调它,
|
||
# 不是人工跑的。脚本据此跳过"重启隧道让新参数生效"那一步。
|
||
#
|
||
# 为什么必须跳过:本 unit 有 Before=lanremote-tunnel.service,
|
||
# 脚本在自己还没结束时去 restart 隧道,systemd 会判定顺序冲突,
|
||
# restart 永久阻塞 -> nettune 卡在 activating -> 隧道起不来 -> 公网 530。
|
||
# 实测卡过 9 分半。
|
||
Environment=NETTUNE_SERVICE_CALL=1
|
||
ExecStart=/bin/bash /root/lanremote-relay/net-tune.sh
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
'''
|
||
import tempfile
|
||
tmp = Path(tempfile.gettempdir()) / "lanremote-nettune.service"
|
||
tmp.write_text(unit, encoding="utf-8", newline="\n")
|
||
upload(str(tmp), "/etc/systemd/system/lanremote-nettune.service")
|
||
print(" ✓ 已更新 unit(加了 NETTUNE_SERVICE_CALL=1)")
|
||
|
||
out = run("bash -n /root/lanremote-relay/net-tune.sh && echo SH_OK; "
|
||
"systemctl daemon-reload && "
|
||
"systemctl restart lanremote-nettune && sleep 6 && "
|
||
"echo \"nettune: $(systemctl is-active lanremote-nettune)\"; "
|
||
"echo \"tunnel: $(systemctl is-active lanremote-tunnel)\"; "
|
||
"echo \"h2: $(systemctl is-active lanremote-tunnel-h2)\"; "
|
||
"echo \"公网: $(curl -s -o /dev/null -w '%{http_code}' "
|
||
"--max-time 20 https://relay.xxmxxs.top/v1/health)\"",
|
||
timeout=180)
|
||
for ln in out[1].strip().splitlines():
|
||
print(" " + ln)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|