mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17: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 个阶段全部通过。
197 lines
11 KiB
Bash
197 lines
11 KiB
Bash
#!/bin/bash
|
||
# 网络调优:修路径 MTU 问题 + 给 QUIC 隧道留出安全包长。
|
||
#
|
||
# 这个脚本是 **MSS / 接口 MTU / 路径 MTU 探测** 的唯一出处。
|
||
# deploy-tune.sh 会调它;不要在别处再设一遍 ——
|
||
# "到底哪个值生效" 一旦有两处来源就会变得很难查。
|
||
#
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 证据链
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 现象:新建 TLS 连接约 30% 失败,但连接一旦建立就接近 100% 成功
|
||
# -> 典型的大包被丢、小包能过
|
||
#
|
||
# ping 实测(到 1.1.1.1,-M do 禁止分片):
|
||
# 包 1448 通 包 1488 丢 包 1500 全丢
|
||
# 接口 MTU 写着 1500,但实际路径扛不住。
|
||
# TLS 握手要发大证书包 -> 被丢 -> 握手失败。
|
||
#
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 实测效果(同一台设备,各 24 次)
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 不夹 MSS(1500) 87%
|
||
# MSS 1300 100%
|
||
# MSS 1360 100% <- 采用(和 MTU 1400 配套)
|
||
# MSS 1400 100%
|
||
# MSS 1440 100%
|
||
#
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 为什么接口 MTU 也要压到 1400(早期结论是错的,这里更正)
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 早先这里写的是「不要改接口 MTU,实测降到 1400 反而掉到 50%」。
|
||
# 那个结论只考虑了 TCP,**漏掉了 QUIC**:
|
||
#
|
||
# * MSS 钳制走的是 iptables 的 TCPMSS 目标,**只对 TCP 生效**;
|
||
# * 隧道用 QUIC 时跑在 UDP 上,整套 TCPMSS 规则对它毫无作用。
|
||
#
|
||
# QUIC 的包一旦超过路径 MTU 就会被中间设备丢掉,而 UDP 丢包
|
||
# 不会回 ICMP,客户端只能干等到握手超时 —— cloudflared 日志里就是
|
||
# "failed to dial to edge with quic: timeout: no recent network activity"
|
||
#
|
||
# 把 wlan0 MTU 从 1500 降到 1400 之后(实测):
|
||
# 建齐 6 条 QUIC 连接 28 秒 -> 13 秒
|
||
# 建齐 10 条 QUIC 连接 28 秒 -> 12 秒
|
||
# 建连期间 dial 失败 2 次 -> 1 次
|
||
#
|
||
# 【重要】MTU 和 MSS 必须配套:
|
||
# MTU 1400 => TCP 段最大 1360(1400 - 20 IP头 - 20 TCP头)
|
||
# 如果 MTU 压到 1400 却还把 MSS 设成 1400,
|
||
# TCP 段(1440) 就超过接口 MTU(1400),包在本地就被分片,等于白改。
|
||
# 两个值在本脚本里一起设,改一个必须改另一个。
|
||
set -u
|
||
|
||
MSS_VALUE=${MSS_VALUE:-1360}
|
||
MTU_VALUE=${MTU_VALUE:-1400}
|
||
|
||
# ────────────────────────────────────────────────────────────────
|
||
# 找对外的那张网卡
|
||
# ────────────────────────────────────────────────────────────────
|
||
# 以前这里写死 wlan0。写死的问题是换机器(或者用 usb0 上网)就错了,
|
||
# 而 MTU 设错网卡是没有任何提示的 —— 改了等于没改。
|
||
IFACE=${IFACE:-}
|
||
if [ -z "$IFACE" ] && command -v ip >/dev/null 2>&1; then
|
||
IFACE=$(ip -4 route show default 2>/dev/null \
|
||
| awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' \
|
||
| head -1)
|
||
fi
|
||
if [ -z "$IFACE" ]; then
|
||
for cand in /sys/class/net/*; do
|
||
name=$(basename "$cand")
|
||
case "$name" in lo|pan0|docker*|veth*|br-*) continue ;; esac
|
||
[ -r "$cand/operstate" ] || continue
|
||
[ "$(cat "$cand/operstate" 2>/dev/null)" = "up" ] || continue
|
||
IFACE="$name"
|
||
break
|
||
done
|
||
fi
|
||
[ -z "$IFACE" ] && IFACE=wlan0
|
||
|
||
echo " 1) 打开路径 MTU 探测"
|
||
if [ -w /proc/sys/net/ipv4/tcp_mtu_probing ]; then
|
||
sysctl -w net.ipv4.tcp_mtu_probing=1 >/dev/null
|
||
echo " tcp_mtu_probing = $(cat /proc/sys/net/ipv4/tcp_mtu_probing)"
|
||
else
|
||
echo " [跳过] 读不到 /proc(不是 Linux 环境)"
|
||
fi
|
||
|
||
echo " 2) 夹 MSS 到 $MSS_VALUE(只限制 TCP 段,不改链路层)"
|
||
if command -v iptables >/dev/null 2>&1; then
|
||
for chain in OUTPUT POSTROUTING; do
|
||
# 先清掉所有旧值,避免重启后越积越多。
|
||
# ⚠ 这个 while 必须有次数上限:
|
||
# 正常情况下 -D 删掉之后 -C 就返回非 0,循环自然结束。
|
||
# 但如果哪天 -D 报成功而规则还在(被包了一层、或 iptables 出异常),
|
||
# 原写法就是死循环,把整个部署卡死。
|
||
# 实测踩过:桩命令一律返回 0,脚本真的转不出来了。
|
||
for old in 1300 1360 1400 1440 1460; do
|
||
guard=0
|
||
while iptables -t mangle -C "$chain" -p tcp --tcp-flags SYN,RST SYN \
|
||
-j TCPMSS --set-mss "$old" 2>/dev/null; do
|
||
guard=$((guard + 1))
|
||
if [ "$guard" -gt 10 ]; then
|
||
echo " ⚠ 清理旧 MSS 规则($old) 超过 10 次仍报存在,停手"
|
||
break
|
||
fi
|
||
iptables -t mangle -D "$chain" -p tcp --tcp-flags SYN,RST SYN \
|
||
-j TCPMSS --set-mss "$old" 2>/dev/null || break
|
||
done
|
||
done
|
||
iptables -t mangle -A "$chain" -p tcp --tcp-flags SYN,RST SYN \
|
||
-j TCPMSS --set-mss "$MSS_VALUE" 2>/dev/null
|
||
done
|
||
iptables -t mangle -L -n 2>/dev/null | grep -i mss | sed 's/^/ /'
|
||
else
|
||
echo " [跳过] 没有 iptables"
|
||
fi
|
||
|
||
echo " 3) 接口 MTU -> $MTU_VALUE(这一条是给 QUIC 用的,见文件头说明)"
|
||
if command -v ip >/dev/null 2>&1; then
|
||
CUR=$(cat "/sys/class/net/$IFACE/mtu" 2>/dev/null || echo "?")
|
||
if [ "$CUR" = "$MTU_VALUE" ]; then
|
||
echo " $IFACE 已经是 $MTU_VALUE,不用改"
|
||
elif ip link set dev "$IFACE" mtu "$MTU_VALUE" 2>/dev/null; then
|
||
echo " $IFACE 的 MTU: $CUR -> $(cat "/sys/class/net/$IFACE/mtu" 2>/dev/null)"
|
||
else
|
||
echo " ⚠ 改 $IFACE 的 MTU 失败(权限或网卡不支持),继续"
|
||
fi
|
||
else
|
||
echo " [跳过] 没有 ip 命令"
|
||
fi
|
||
|
||
# 持久化:wlan0 归 NetworkManager 管,光用 ip 改重启就丢。
|
||
# 写进 NM 自己的连接配置,比塞 rc.local 干净。
|
||
if command -v nmcli >/dev/null 2>&1; then
|
||
CONN=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null \
|
||
| awk -F: -v dev="$IFACE" '$2==dev {print $1; exit}')
|
||
if [ -n "$CONN" ]; then
|
||
nmcli connection modify "$CONN" 802-11-wireless.mtu "$MTU_VALUE" \
|
||
2>/dev/null \
|
||
|| nmcli connection modify "$CONN" ethernet.mtu "$MTU_VALUE" \
|
||
2>/dev/null || true
|
||
echo " 已写进 NetworkManager 连接「$CONN」(重启后仍然生效)"
|
||
fi
|
||
fi
|
||
|
||
# ════════════════════════════════════════════════════════════════
|
||
# 4) 关键:让已经在跑的隧道重建连接
|
||
# ════════════════════════════════════════════════════════════════
|
||
# MSS 和 MTU 只对**新建连接**生效(MSS 改的是 SYN 包里的选项,
|
||
# MTU 影响的是新建的 socket)。隧道如果建在规则生效之前,
|
||
# 它那条连接还带着旧参数 ——
|
||
# 实测:隧道 17:01 建、规则 17:22 生效,可用率一直卡在 76%;
|
||
# 重启隧道之后立刻回到 100%。
|
||
#
|
||
# 开机时不会走到这一步(nettune 服务 Before=lanremote-tunnel.service,
|
||
# 规则先于隧道生效);但**手动重跑本脚本**时必须重建,
|
||
# 否则改了等于没改。
|
||
# ════════════════════════════════════════════════════════════════════
|
||
# 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 \
|
||
&& systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||
echo " $svc 正在跑,重启它让新参数生效"
|
||
systemctl restart "$svc" >/dev/null 2>&1 && RESTARTED=1
|
||
fi
|
||
done
|
||
[ "$RESTARTED" = "0" ] && echo " 没有正在跑的隧道,跳过"
|
||
|
||
fi
|
||
|
||
echo " 网络调优已应用(MSS=$MSS_VALUE, MTU=$MTU_VALUE, 网卡=$IFACE)"
|