#!/bin/bash # ============================================================================ # 紧急预案(Lightsail 版)—— 网站打不开时按阶梯自动抢救 # ============================================================================ # # 和随身 WiFi 那版的区别: # # 那版是**围着 Cloudflare 隧道**转的(第 1 级就是重启隧道、 # 第 3 级重施 MSS/MTU 调优)。现在这台新机器**有公网 IP**, # 中转直接对外监听 13245,没有任何隧道 —— 所以: # # * 第 1 级从"重启隧道"改成"重启中转服务" # * 去掉 MSS/MTU 那两级(那是 4G 线路的问题,这里没有) # * 加一级"看端口有没有被占/进程死没死" # # 保留下来的核心设计没变: # # 1. **先记录现场,再动手**(重启会把证据冲掉) # 2. **阶梯式,每级都验**:从代价最小到最大,好了就停 # 3. **全失败要留证据**:生成 txt 报告,后台可下载 # 4. **成功的招数记进知识库**:下次同类问题优先试 # # 谁调用:monitor.sh(连续 N 次不通就调这里) # 也可以人工跑:bash /opt/ykxmz-relay/rescue.sh # ============================================================================ set -u RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}" PORT="${RELAY_PORT:-13245}" # 公网健康检查地址。没有域名就填 IP。 PUBLIC_URL="${PUBLIC_URL:-http://13.54.30.196:$PORT}" REPORTS_DIR="$RELAY_DIR/rescue_reports" KNOWLEDGE="$RELAY_DIR/rescue_knowledge.md" LOG="$RELAY_DIR/rescue.log" LOCKFILE="/tmp/ykxmz-rescue.lock" SETTLE_FAST=6 SETTLE_SLOW=15 # 是否允许"重启整机"这张底牌。默认关。 RESCUE_REBOOT="${RESCUE_REBOOT:-0}" mkdir -p "$REPORTS_DIR" 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" say() { echo "$*" echo "$(date '+%F %T') $*" >> "$LOG" } # 公网健康检查 —— **看公网,不看本机** 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 \ "http://127.0.0.1:$PORT/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 " 本机 http://127.0.0.1:$PORT/v1/health -> $(local_health && echo 200-正常 || echo 不通)" echo if local_health && ! is_healthy; then echo " 判断:**本机是好的,问题在公网那一侧**(防火墙规则、安全组、上游线路)。" elif ! local_health; then echo " 判断:**本机的中转服务就没起来**,先看第二、三节。" else echo " 判断:两边都通(可能是刚才已经自愈,或者只是偶发抖动)。" fi echo echo "---- 二、服务状态 ----" echo printf ' %-24s %s\n' "ykxmz-relay" "$(systemctl is-active ykxmz-relay 2>&1)" printf ' %-24s %s\n' "ykxmz-monitor.timer" "$(systemctl is-active ykxmz-monitor.timer 2>&1)" echo echo " --- 最近 20 行服务日志 ---" journalctl -u ykxmz-relay --no-pager -n 20 2>/dev/null | sed 's/^/ /' echo echo "---- 三、进程与端口 ----" echo echo " 监听 $PORT 的进程:" ss -tlnp 2>/dev/null | grep ":$PORT " | sed 's/^/ /' \ || echo " **没有任何进程在监听 $PORT**" echo echo " 相关进程:" ps -eo pid,rss,etime,cmd --sort=-rss 2>/dev/null \ | grep -E 'relay\.py|python3' | grep -v grep | head -6 | sed 's/^/ /' echo echo "---- 四、系统资源 ----" echo free -m 2>/dev/null | sed 's/^/ /' echo uptime | sed 's/^/ /' echo df -h / /opt 2>/dev/null | sed 's/^/ /' echo echo "---- 五、网络 ----" echo ip -brief addr show 2>/dev/null | sed 's/^/ /' echo echo " 默认路由:" ip route show default 2>/dev/null | sed 's/^/ /' echo echo " 本机自测各端口:" for p in "$PORT" 80 443 22; do printf ' %-6s ' "$p" timeout 3 bash -c "/dev/null \ && echo "通" || echo "不通" done echo echo "---- 六、日志尾部 ----" echo echo "### 中转服务 relay.log" tail -30 "$RELAY_DIR/relay.log" 2>/dev/null | sed 's/^/ /' echo echo "### 监测单元 monitor.log" tail -20 "$RELAY_DIR/monitor.log" 2>/dev/null | sed 's/^/ /' echo echo "### 内核异常(OOM / 硬件)" dmesg -T 2>/dev/null \ | grep -i -E 'oom|killed process|error|panic' | tail -15 | sed 's/^/ /' echo echo "---- 七、最近一次启动 ----" echo who -b 2>/dev/null | sed 's/^/ /' echo " 运行时长:$(cut -d. -f1 /proc/uptime 2>/dev/null) 秒" echo echo "================================================================================" echo " 报告结束。可以在中转站后台的「抢救报告」分区下载。" echo "================================================================================" } > "$REPORT" 2>&1 say " 现场已记录 -> $REPORT" } # --------------------------------------------------------------------------- # 阶梯 # --------------------------------------------------------------------------- step_1_restart_relay() { say " [1/4] 重启中转服务(最轻的一招,不动系统其他部分)" systemctl restart ykxmz-relay 2>/dev/null sleep "$SETTLE_FAST" } step_2_kill_and_start() { say " [2/4] 强杀残留进程再拉起(进程卡死/端口被占时用)" # 有时候 systemd restart 会因为老进程没退干净而失败, # 表现为"服务说 active 但端口没人听"。 pkill -f "$RELAY_DIR/relay.py" 2>/dev/null sleep 3 systemctl reset-failed ykxmz-relay 2>/dev/null systemctl start ykxmz-relay 2>/dev/null sleep "$SETTLE_FAST" } step_3_check_firewall() { say " [3/4] 检查本机防火墙有没有挡住 $PORT" # Lightsail 有两层:网页上的防火墙规则(实例级)+ 系统内的 ufw/iptables。 # 网页那层我们改不了,但系统内这层能看。 if command -v ufw >/dev/null 2>&1; then echo " ufw 状态:$(ufw status 2>/dev/null | head -1)" >> "$LOG" ufw status 2>/dev/null | grep -q "$PORT" \ || say " ⚠ ufw 里没有放行 $PORT 的规则" fi local n n=$(iptables -L INPUT -n 2>/dev/null | grep -c "$PORT") say " iptables INPUT 里提到 $PORT 的规则:$n 条" # 重新绑一次端口(有时是监听 socket 坏了) systemctl restart ykxmz-relay 2>/dev/null sleep "$SETTLE_SLOW" } step_4_reboot() { if [ "$RESCUE_REBOOT" != "1" ]; then say " [4/4] 重启整机 —— **已跳过**(默认不开)" say " 要允许自动重启,把 RESCUE_REBOOT=1 写进 ykxmz-monitor.service。" return 1 fi say " [4/4] 重启整机(最后的底牌)" sync systemctl reboot 2>/dev/null return 0 } remember() { local step="$1" reason="$2" { echo echo "## $(date '+%F %T') 第 $step 级奏效" echo echo "- 症状:$reason" echo "- 有效步骤:$step" echo "- 当时公网:$(is_healthy && echo 通 || echo 不通);本机:$(local_health && echo 通 || echo 不通)" echo "- 详情见 rescue_reports/$(basename "$REPORT")" } >> "$KNOWLEDGE" say " 已把「第 $step 级奏效」记进 $KNOWLEDGE" } # =========================================================================== # 主流程 # =========================================================================== REASON="${1:-monitor 触发}" say "" say "================================================================================" say " 紧急预案启动:$REASON" say "================================================================================" # ⚠ 先复查再记录现场。 # collect() 会写一份报告文件;如果网站其实已经好了, # 那一次也会留下报告,把真正出事时的报告淹掉(后台只显示最近 30 份)。 if is_healthy; then say " 复查:公网现在是通的,不需要动手 —— 不采现场、不留报告。" exit 0 fi collect "$REASON" STEP_NAMES=("重启中转服务" "强杀残留再拉起" "查防火墙并重绑" "重启整机") for i in 1 2 3 4; do case $i in 1) step_1_restart_relay ;; 2) step_2_kill_and_start ;; 3) step_3_check_firewall ;; 4) step_4_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 " 需要人工看一眼 —— 特别是 Lightsail **网页上的防火墙规则**," echo " 那一层系统内部改不了。" echo cat "$REPORT" } > "$REPORT.tmp" && mv "$REPORT.tmp" "$REPORT" ls -1t "$REPORTS_DIR"/*.txt 2>/dev/null | tail -n +31 | xargs -r rm -f say " 报告已生成:$REPORT" exit 1