Files
ykxmz/relay/net-tune.sh
T
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

197 lines
11 KiB
Bash
Raw 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.
#!/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)"