mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +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 个阶段全部通过。
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
# -*- 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())
|