修「网站容易挂」的真因: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 个阶段全部通过。
This commit is contained in:
xmz7891
2026-10-08 18:56:44 +08:00
parent 4ba18fdae9
commit 6adf7acbfb
8 changed files with 431 additions and 1 deletions
+14
View File
@@ -555,6 +555,20 @@ RemainAfterExit=yes
# 它会自己再去调 net-tune.sh(MSS / 接口 MTU / 路径探测的唯一出处)——
# 早先这里写了两条 ExecStart,两边都设一遍 MSS 和 MTU,
# 结果"到底哪个值生效"很难查。现在收成一个入口。
#
# ⚠ NETTUNE_SERVICE_CALL=1 告诉 net-tune.sh:这次是**服务**在调它,
# 不是人工跑的。脚本据此跳过"重启隧道让新参数生效"那一步。
#
# 为什么必须跳过:本 unit 有 Before=lanremote-tunnel.service,
# 脚本在自己还没结束时去 restart 隧道,systemd 会判定顺序冲突 ——
# restart 永久阻塞、nettune 一直卡在 activating、隧道被 Before=
# 挡着永远起不来,表现就是"网站挂了",而日志里什么都看不出来。
#
# 实测卡过 9 分半。手动跑 net-tune.sh 却完全正常(那时没有 Before=
# 约束),所以这个坑特别难发现 —— 设备上那份老 unit 因为 ExecStart
# 里的 `||` 先报错退出,反而把死锁遮住了,直到我把 unit 换成
# 正确形状才露出来。
Environment=NETTUNE_SERVICE_CALL=1
ExecStart=/bin/bash $RELAY_DIR/deploy-tune.sh
[Install]
+5
View File
@@ -314,6 +314,11 @@ Before=lanremote-relay.service lanremote-tunnel.service
[Service]
Type=oneshot
RemainAfterExit=yes
# ⚠ NETTUNE_SERVICE_CALL=1 让 net-tune.sh 跳过"重启隧道"那一步。
# 本 unit 有 Before=lanremote-tunnel.service,脚本在自己还没结束时
# 去 restart 隧道会和 systemd 的顺序约束死锁 —— 实测卡过 9 分半,
# 表现是"网站挂了"而日志里什么都看不出来。
Environment=NETTUNE_SERVICE_CALL=1
ExecStart=$RELAY_DIR/net-tune.sh
[Install]
+29 -1
View File
@@ -154,7 +154,33 @@ fi
# 开机时不会走到这一步(nettune 服务 Before=lanremote-tunnel.service,
# 规则先于隧道生效);但**手动重跑本脚本**时必须重建,
# 否则改了等于没改。
echo " 4) 让已经建立的隧道用上新参数"
# ════════════════════════════════════════════════════════════════════
# 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) 让已经建立的隧道用上新参数"
RESTARTED=0
for svc in lanremote-tunnel lanremote-tunnel-h2; do
if command -v systemctl >/dev/null 2>&1 \
@@ -165,4 +191,6 @@ for svc in lanremote-tunnel lanremote-tunnel-h2; do
done
[ "$RESTARTED" = "0" ] && echo " 没有正在跑的隧道,跳过"
fi
echo " 网络调优已应用(MSS=$MSS_VALUE, MTU=$MTU_VALUE, 网卡=$IFACE)"