Files
ykxmz/_build/break_nettune_deadlock.py
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

93 lines
3.3 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'''解开"nettune 死锁",然后把根因修掉。
== 死锁是怎么回事 ==
lanremote-nettune.service:
Before=lanremote-tunnel.service <- 关键
ExecStart=/bin/bash net-tune.sh
net-tune.sh 第 4 步:
systemctl restart lanremote-tunnel <- 在这里永远阻塞
为什么阻塞:systemd 正在启动 nettune,而 nettune 声明了"我必须在
隧道之前启动完"。脚本却在**自己还没结束的时候**去 restart 隧道 ——
那要求隧道先停、再等 nettune 结束……systemd 检测到这是顺序冲突,
restart 就一直挂着不返回。
证据:CGroup 里能看到 net-tune.sh(13388)下面挂着
systemctl restart lanremote-tunnel(13428),
etime 09:32 —— 卡了 9 分半,nettune 一直是 activating。
后果:nettune 卡住 -> 隧道被 Before= 挡着起不来 -> 公网 530。
这就是用户说的"网站还是容易挂"的真因之一。
== 为什么以前没暴露 ==
设备上原来那份 unit 的 ExecStart 里有个 `iptables ... || ...`,
systemd 不支持 ||,所以它**在第 2 条 ExecStart 就报错退出了**,
根本没走到"重启隧道"那一步 —— 死锁被那个 bug 遮住了。
我把 unit 换成正确形状之后,死锁才露出来。
== 这一步做什么 ==
1. 杀掉卡住的 systemctl 和 net-tune.sh(解开当前死锁)
2. 拉起隧道(网站先恢复)
3. 把 net-tune.sh 的"重启隧道"改成**只在手动跑时做**
'''
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 run # noqa: E402
REMOTE = r'''
echo "════ 1. 解开死锁 ════"
# 先杀卡住的 systemctl(不然它会一直拽着 systemd 的 job)
pkill -f 'systemctl restart lanremote-tunnel' 2>/dev/null && echo " 杀了卡住的 systemctl" || echo " 没有卡住的 systemctl"
sleep 2
# 再让 nettune 的脚本结束(杀它的子进程已经让主脚本继续往下走了)
systemctl stop lanremote-nettune 2>/dev/null
systemctl reset-failed lanremote-nettune 2>/dev/null
sleep 2
echo " nettune 现在: $(systemctl is-active lanremote-nettune)"
echo
echo "════ 2. 拉起隧道(网站先恢复)════"
systemctl reset-failed lanremote-tunnel lanremote-tunnel-h2 2>/dev/null
systemctl start lanremote-tunnel lanremote-tunnel-h2
sleep 15
echo " tunnel: $(systemctl is-active lanremote-tunnel)"
echo " tunnel-h2: $(systemctl is-active lanremote-tunnel-h2)"
echo
echo "════ 3. 验公网 ════"
for i in 1 2 3; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 https://relay.xxmxxs.top/v1/health)
echo " 第 $i 次: $code"
[ "$code" = "200" ] && break
sleep 6
done
echo
echo "════ 4. 隧道连接数 ════"
for p in 20241 20242; do
n=$(curl -s --max-time 5 http://127.0.0.1:$p/metrics 2>/dev/null | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1)
echo " $p -> ${n:-拿不到}"
done
'''
def main() -> int:
out = run(REMOTE, timeout=300)
text = out[1] if isinstance(out, tuple) else str(out)
print(text)
return 0
if __name__ == "__main__":
sys.exit(main())