#!/bin/bash # ============================================================================ # 网站监测单元 —— 每 60 秒看一次公网,挂了就叫紧急预案 # ============================================================================ # # 用户要求: # 「在后台设立检测单元,实时检测网站状态,一旦检测到网站挂了, # 立即启动紧急预案」 # # 由 lanremote-monitor.timer 每 60 秒起来跑一次(不是常驻进程 —— # 小机器内存紧,定时任务比常驻守护省得多,而且崩了下一分钟自己就回来了)。 # # --------------------------------------------------------------------------- # 为什么一定要**从公网测**,不能测 127.0.0.1 # --------------------------------------------------------------------------- # # 本机 200 只说明中转进程活着。用户真正关心的是"打开 # https://relay.xxmxxs.top 能不能出页面" —— 那要经过 # Cloudflare 边缘 + 隧道 + 本机三段。 # # 实测过的场景:隧道两条实例都掉了,本机 curl 127.0.0.1 照样 200, # 但公网 502。只看本机的话,监测单元永远不会报警。 # # --------------------------------------------------------------------------- # 防抖与冷却 # --------------------------------------------------------------------------- # # * 连续失败 FAIL_THRESHOLD 次才动手(默认 3 次 ≈ 3 分钟)。 # 实测这条 4G 线路本来就有 2~4% 的偶发失败,一次就救会天天瞎折腾。 # # * 救过一次之后 COOLDOWN 秒内不再救(默认 600 = 10 分钟)。 # 否则预案刚重启完隧道、还没连上,下一次检查又判失败、又救一遍, # 来回重启反而永远连不上。 # # --------------------------------------------------------------------------- # 知识库 # --------------------------------------------------------------------------- # # rescue.sh 每次救成功都会把"第几级奏效"写进 rescue_knowledge.md。 # 这里读出来,如果历史上某一级反复奏效,就把它记下来传给预案, # 预案可以据此跳过前面没用的级别(见 rescue.sh 的 PREFER_STEP)。 # # --------------------------------------------------------------------------- set -u RELAY_DIR="${RELAY_DIR:-/root/lanremote-relay}" PUBLIC_URL="${PUBLIC_URL:-https://relay.xxmxxs.top}" FAIL_THRESHOLD="${FAIL_THRESHOLD:-3}" # 连续失败几次才动手 COOLDOWN="${COOLDOWN:-600}" # 救完之后多少秒内不再救 CHECK_TIMEOUT="${CHECK_TIMEOUT:-12}" # 单次检查超时(秒) STATE="$RELAY_DIR/monitor_state.txt" LOG="$RELAY_DIR/monitor.log" LOCKFILE="/tmp/lanremote-monitor.lock" # 同时只跑一个(timer 到点时上一次可能还没跑完) if ! mkdir "$LOCKFILE" 2>/dev/null; then exit 0 fi trap 'rmdir "$LOCKFILE" 2>/dev/null' EXIT log() { echo "$(date '+%F %T') $*" >> "$LOG"; } # ---- 读状态(连续失败次数 / 上次抢救时间)---- FAILS=0 LAST_RESCUE=0 if [ -f "$STATE" ]; then FAILS=$(grep -m1 '^fails=' "$STATE" 2>/dev/null | cut -d= -f2) LAST_RESCUE=$(grep -m1 '^last_rescue=' "$STATE" 2>/dev/null | cut -d= -f2) FAILS=${FAILS:-0} LAST_RESCUE=${LAST_RESCUE:-0} fi NOW=$(date +%s) # ---- 检查公网 ---- CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time "$CHECK_TIMEOUT" \ "$PUBLIC_URL/v1/health" 2>/dev/null) DUR=$(curl -s -o /dev/null -w '%{time_total}' --max-time "$CHECK_TIMEOUT" \ "$PUBLIC_URL/v1/health" 2>/dev/null) DUR=${DUR:-0} write_state() { { echo "fails=$1" echo "last_rescue=$2" echo "last_code=$CODE" echo "last_check=$NOW" echo "last_dur=$DUR" } > "$STATE" } if [ "$CODE" = "200" ]; then if [ "$FAILS" != "0" ]; then log "公网恢复(HTTP 200,${DUR}s)—— 之前连续失败 $FAILS 次" fi # 只在状态变化时记一行,避免日志被 60 秒一条刷爆(460MB 机器) PREV=$(grep -m1 '^last_code=' "$STATE" 2>/dev/null | cut -d= -f2) [ "$PREV" != "200" ] && log "公网正常(HTTP 200,${DUR}s)" write_state 0 "$LAST_RESCUE" exit 0 fi # ---- 失败:累加计数 ---- FAILS=$((FAILS + 1)) log "公网异常 第 $FAILS 次(HTTP ${CODE:-无响应},${DUR}s)" write_state "$FAILS" "$LAST_RESCUE" if [ "$FAILS" -lt "$FAIL_THRESHOLD" ]; then exit 0 fi # ---- 到阈值了,看冷却 ---- SINCE=$((NOW - LAST_RESCUE)) if [ "$LAST_RESCUE" != "0" ] && [ "$SINCE" -lt "$COOLDOWN" ]; then log " 已经连续失败 $FAILS 次,但距上次抢救只过了 ${SINCE}s(冷却 ${COOLDOWN}s),先不动手" exit 0 fi # ---- 读知识库,看历史上哪一级最常奏效 ---- HINT="" if [ -f "$RELAY_DIR/rescue_knowledge.md" ]; then HINT=$(grep -oE '第 [0-9] 级奏效' "$RELAY_DIR/rescue_knowledge.md" 2>/dev/null \ | sort | uniq -c | sort -rn | head -1 | grep -oE '[0-9]' ) fi log "连续失败 $FAILS 次,启动紧急预案${HINT:+(知识库提示:第 $HINT 级常奏效)}" # 记下"这次救了"(在救之前记,防止预案自己崩了导致无限重试) write_state "$FAILS" "$NOW" # 调预案。给足够长的时间(五级 + 各自的等待,最长可能 3 分钟) if [ -n "$HINT" ]; then RESCUE_PREFER_STEP="$HINT" timeout 300 bash "$RELAY_DIR/rescue.sh" \ "监测单元:公网连续 $FAILS 次不通" >> "$LOG" 2>&1 else timeout 300 bash "$RELAY_DIR/rescue.sh" \ "监测单元:公网连续 $FAILS 次不通" >> "$LOG" 2>&1 fi RC=$? if [ "$RC" = "0" ]; then log " 预案完成:已恢复" else log " 预案完成:仍未恢复(退出码 $RC),报告在 rescue_reports/" fi exit 0