#!/bin/bash # ============================================================================ # 紧急预案 —— 网站(公网)打不开时,按阶梯自动抢救 # ============================================================================ # # 用户要求: # 「在后台设立检测单元,实时检测网站状态,一旦检测到网站挂了, # 立即启动紧急预案」 # 「如果紧急预案未能解决,则将问题打包成 .txt,然后在后台加一个分区 # 专门放还有下载这个 txt」 # 「解决后将解决问题的方法加入紧急预案」 # # 谁调用它:monitor.sh(每 60 秒检查一次公网,连续失败就调这里) # 也可以人工跑:bash /root/lanremote-relay/rescue.sh # # --------------------------------------------------------------------------- # 设计原则 # --------------------------------------------------------------------------- # # 1. **先记录现场,再动手** —— 重启会把证据冲掉。第一件事永远是把 # 当前状态(内存、连接数、进程、日志尾部)写进报告。 # # 2. **阶梯式,每级都验证** —— 从"代价最小"到"代价最大": # 1 重启隧道 -> 不动中转,在线设备不掉(首选) # 2 重启中转 -> 设备要重连,但很快 # 3 重施调优 -> 网络参数跑偏时用(实测 MTU 顺序错过一次) # 4 重启网卡 -> 4G/WiFi 掉线卡死时用 # 5 重启整机 -> 最后的底牌,默认**不开**(要 RESCUE_REBOOT=1) # 每做完一级立刻再验;好了就停,不做多余的破坏。 # # 3. **全都失败要留证据** —— 生成一份 .txt 报告,放进 rescue_reports/, # 后台管理页有专门的分区可以下载。 # # 4. **成功的招数记下来** —— 每级成功就写进 rescue_knowledge.md。 # 下次同类问题,monitor 可以把这一级提到前面先试。 # (这就是用户说的"解决后将解决问题的方法加入紧急预案") # # --------------------------------------------------------------------------- set -u RELAY_DIR="${RELAY_DIR:-/root/lanremote-relay}" PUBLIC_URL="${PUBLIC_URL:-https://relay.xxmxxs.top}" LOCAL_URL="${LOCAL_URL:-http://127.0.0.1:8080}" REPORTS_DIR="$RELAY_DIR/rescue_reports" KNOWLEDGE="$RELAY_DIR/rescue_knowledge.md" LOCKFILE="/tmp/lanremote-rescue.lock" # 每级之后等多久再验(隧道重连要时间) SETTLE_FAST=8 SETTLE_SLOW=25 # 是否允许用"重启整机"这张底牌。默认关 —— 半夜自动重启一台没人管的 # 机器很吓人,而且如果问题出在供电/上游,重启反而更久才回来。 RESCUE_REBOOT="${RESCUE_REBOOT:-0}" mkdir -p "$REPORTS_DIR" # 同一时间只允许一个预案在跑(monitor 可能和人工同时触发) if ! mkdir "$LOCKFILE" 2>/dev/null; then echo "[rescue] 已经有一个预案在跑,本次退出" exit 0 fi trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT STAMP="$(date +%Y%m%d-%H%M%S)" REPORT="$REPORTS_DIR/$STAMP.txt" LOG="$RELAY_DIR/rescue.log" say() { echo "$*" echo "$(date '+%F %T') $*" >> "$LOG" } # --------------------------------------------------------------------------- # 健康检查:**看公网,不看本机** # --------------------------------------------------------------------------- # ⚠ 这是关键。本机 127.0.0.1 通不代表用户能访问 —— # 隧道断了、Cloudflare 边缘出问题,本机照样 200。 # 所以预案里的"好了没"一律以公网为准。 is_healthy() { local code code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 12 \ "$PUBLIC_URL/v1/health" 2>/dev/null) [ "$code" = "200" ] } # 公网不通时,看是本机的问题还是隧道的问题 local_health() { local code code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 6 \ "$LOCAL_URL/v1/health" 2>/dev/null) [ "$code" = "200" ] } # --------------------------------------------------------------------------- # 现场记录 # --------------------------------------------------------------------------- collect() { { echo "================================================================================" echo " ykxmz 紧急预案诊断报告" echo " 时间:$(date '+%F %T %Z')" echo " 主机:$(hostname) 运行时长:$(uptime -p 2>/dev/null || uptime)" echo " 触发原因:$1" echo "================================================================================" echo echo "---- 一、结论速览 ----" echo echo " 公网 $PUBLIC_URL/v1/health -> $(is_healthy && echo 200-正常 || echo 不通)" echo " 本机 $LOCAL_URL/v1/health -> $(local_health && echo 200-正常 || echo 不通)" echo if local_health && ! is_healthy; then echo " 判断:**本机是好的,问题在隧道或 Cloudflare 那一侧**。" elif ! local_health; then echo " 判断:**本机的中转服务就没起来**,先看第二、三节。" else echo " 判断:两边都通(可能是刚才已经自愈,或者只是偶发抖动)。" fi echo echo "---- 二、服务状态 ----" echo for svc in lanremote-relay lanremote-tunnel lanremote-tunnel-h2 \ lanremote-nettune lanremote-watchdog; do printf ' %-26s %s\n' "$svc" "$(systemctl is-active "$svc" 2>&1)" done echo echo "---- 三、进程 ----" echo ps -eo pid,rss,etime,comm,args --sort=-rss 2>/dev/null | head -12 echo echo "---- 四、内存 / 负载 ----" echo free -m 2>/dev/null echo uptime echo echo "---- 五、网络 ----" echo echo "网卡:" ip -brief addr show 2>/dev/null echo echo "MTU / 队列:" for i in $(ls /sys/class/net 2>/dev/null | grep -v lo); do echo " $i mtu=$(cat /sys/class/net/$i/mtu 2>/dev/null)" done echo echo "默认路由:" ip route show default 2>/dev/null echo echo "MSS 钳制规则:" iptables -t mangle -L -n 2>/dev/null | grep -i mss || echo " (没有 MSS 规则!)" echo echo "路径 MTU 探测:tcp_mtu_probing=$(cat /proc/sys/net/ipv4/tcp_mtu_probing 2>/dev/null)" echo echo "---- 六、隧道连接数 ----" echo for port in 20241 20242; do n=$(curl -s --max-time 5 "http://127.0.0.1:$port/metrics" 2>/dev/null \ | grep '^cloudflared_tunnel_ha_connections' | awk '{print $2}' | head -1) echo " 指标端口 $port -> 连接数 ${n:-拿不到}" done echo echo "---- 七、日志尾部 ----" echo echo "### 中转(lanremote-relay)" journalctl -u lanremote-relay --no-pager -n 25 2>/dev/null | tail -25 echo echo "### 隧道(lanremote-tunnel)" journalctl -u lanremote-tunnel --no-pager -n 15 2>/dev/null | tail -15 echo echo "### 隧道副本(lanremote-tunnel-h2)" journalctl -u lanremote-tunnel-h2 --no-pager -n 15 2>/dev/null | tail -15 echo echo "### 看门狗" tail -20 "$RELAY_DIR/watchdog.log" 2>/dev/null echo echo "### 监测单元" tail -20 "$RELAY_DIR/monitor.log" 2>/dev/null echo echo "### 内核异常(OOM / 掉电 / 硬件)" dmesg -T 2>/dev/null | grep -i -E 'oom|killed process|under-voltage|thermal|panic|watchdog' | tail -15 echo echo "---- 八、最近一次重启 ----" echo who -b 2>/dev/null echo "运行时长:$(cut -d. -f1 /proc/uptime 2>/dev/null) 秒" echo echo "================================================================================" echo " 报告结束。这个文件可以在中转站后台的「抢救报告」分区下载。" echo "================================================================================" } > "$REPORT" 2>&1 say " 现场已记录 -> $REPORT" } # --------------------------------------------------------------------------- # 阶梯 # --------------------------------------------------------------------------- step_1_restart_tunnel() { say " [1/5] 重启隧道(不动中转,在线设备不掉线)" systemctl restart lanremote-tunnel lanremote-tunnel-h2 2>/dev/null sleep "$SETTLE_SLOW" } step_2_restart_relay() { say " [2/5] 重启中转服务" systemctl restart lanremote-relay 2>/dev/null sleep "$SETTLE_FAST" } step_3_retune_network() { say " [3/5] 重新施加网络调优(MSS / MTU / 路径探测)" # 这一步是有来历的:实测部署脚本里"先设 MTU 1400、脚本又想保持 1500" # 顺序错过一次,结果网卡停在 1400,吞吐掉到 50%。 # 重跑 net-tune.sh 让它回到脚本认为正确的值。 if [ -f "$RELAY_DIR/net-tune.sh" ]; then bash "$RELAY_DIR/net-tune.sh" >> "$LOG" 2>&1 && say " 调优脚本执行完成" else say " 找不到 net-tune.sh,跳过" fi systemctl restart lanremote-tunnel lanremote-tunnel-h2 2>/dev/null sleep "$SETTLE_SLOW" } step_4_restart_network() { say " [4/5] 重启无线网卡(4G/WiFi 卡死时用)" local iface iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}') if [ -z "$iface" ]; then say " 找不到默认路由的网卡,跳过" return fi # 用 nmcli 走管理接口,别直接把网卡 down 掉(down 了可能再也上不来) if command -v nmcli >/dev/null 2>&1; then nmcli device disconnect "$iface" >/dev/null 2>&1 sleep 4 nmcli device connect "$iface" >/dev/null 2>&1 say " nmcli 重连了 $iface" else say " 没有 nmcli,跳过(不敢直接 down 网卡:可能起不来)" fi systemctl restart lanremote-relay 2>/dev/null systemctl restart lanremote-tunnel lanremote-tunnel-h2 2>/dev/null sleep "$SETTLE_SLOW" } step_5_reboot() { if [ "$RESCUE_REBOOT" != "1" ]; then say " [5/5] 重启整机 —— **已跳过**(默认不开)" say " 要允许自动重启,把 RESCUE_REBOOT=1 写进 monitor.service 的环境变量。" say " 理由:如果问题出在供电或上游线路,重启只会让恢复更慢;" say " 而且半夜自动重启一台没人管的机器风险太大。" return 1 fi say " [5/5] 重启整机(最后的底牌)" sync systemctl reboot 2>/dev/null return 0 } # --------------------------------------------------------------------------- # 把这次成功的招数记进知识库 # --------------------------------------------------------------------------- # 用户要求:「解决后将解决问题的方法加入紧急预案」。 # 实现方式:哪一级救回来的就把它记下来。monitor 下次遇到同类症状 # 会优先试历史上奏效的那一级(见 monitor.sh 的 PREFFERED_STEP)。 remember() { local step="$1" reason="$2" { echo echo "## $(date '+%F %T') 第 $step 级奏效" echo echo "- 症状:$reason" echo "- 有效步骤:$step" echo "- 当时公网:不通;本机:$(local_health && echo 通 || echo 不通)" echo "- 详情见 rescue_reports/$(basename "$REPORT")" } >> "$KNOWLEDGE" say " 已把「第 $step 级奏效」记进 $KNOWLEDGE" } # =========================================================================== # 主流程 # =========================================================================== REASON="${1:-monitor 触发}" say "" say "================================================================================" say " 紧急预案启动:$REASON" say "================================================================================" # ⚠ 先复查一次再记录现场。 # # 为什么顺序是这样:collect() 会写一份报告文件。如果一上来就采, # 那么"网站其实已经好了、什么都不用做"的这一次也会留下一份报告 —— # 实测跑一次 e2e 就多出 2 份,全是没用的空报告, # 真正出事时的报告会被这些噪音淹掉(后台列表只能显示最近 30 份)。 # # 所以:先判健康;不健康才采现场、才开阶梯。 if is_healthy; then say " 复查:公网现在是通的(可能是偶发抖动自愈,或者启动前已经恢复)," say " 不需要动手 —— 不采现场、不留报告。" exit 0 fi # 确认不通了,这才记录现场(重启会把证据冲掉) collect "$REASON" STEP_NAMES=( "重启隧道" "重启中转服务" "重新施加网络调优" "重启无线网卡" "重启整机" ) for i in 1 2 3 4 5; do case $i in 1) step_1_restart_tunnel ;; 2) step_2_restart_relay ;; 3) step_3_retune_network ;; 4) step_4_restart_network ;; 5) step_5_reboot || continue ;; esac if is_healthy; then say "" say " ✅ 第 $i 级(${STEP_NAMES[$((i-1))]})救回来了,公网已恢复。" remember "$i" "$REASON" exit 0 fi say " 第 $i 级没解决,继续下一级" done # --------------------------------------------------------------------------- # 全部失败 —— 生成报告,等人工 # --------------------------------------------------------------------------- say "" say " ❌ 五级都没解决。再采一次现场(状态已经变了,报告要反映最新情况)" collect "$REASON(五级抢救后仍不通)" # 在报告最前面补一句给用户看的结论 { echo "⚠ 这份报告是「自动抢救全部失败」之后生成的。" echo " 机器自己试过了:重启隧道 / 重启中转 / 重施网络调优 / 重启网卡。" echo " (重启整机默认没做,见报告里的说明。)" echo " 需要人工看一眼。下面是最新的现场。" echo cat "$REPORT" } > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT" # 只保留最近 30 份,别把 460MB 的小机器塞满 ls -1t "$REPORTS_DIR"/*.txt 2>/dev/null | tail -n +31 | xargs -r rm -f say " 报告已生成:$REPORT" say " 可以在中转站后台的「抢救报告」分区下载。" exit 1