#!/bin/bash # ============================================================================ # 网站监测单元(Lightsail 版) # ============================================================================ # # 每 60 秒查一次网站,连续 3 次不通就叫紧急预案。 # # ── 为什么查两层 ────────────────────────────────────────────────────── # # PRIMARY https://yk.xxmxxs.top/v1/health # 用户实际走的路:Cloudflare -> nginx -> 中转,全程 https # # FALLBACK http://127.0.0.1:13245/v1/health # 中转本身,绕开 Cloudflare 和 nginx # # 两个都查才能**分清是哪一层坏了**,从而选对抢救动作: # # PRIMARY 挂 + FALLBACK 好 -> 问题在 CF / DNS / nginx 层(edge) # 两个都挂 -> 中转服务本身有问题(core) # # 只查一个的话就没法区分 —— 不管是哪层坏了都只能"重启中转", # 而 edge 层的问题重启中转根本没用。 # # ── 为什么用 timer 而不是常驻进程 ────────────────────────────────────── # # 崩了下一分钟自己就回来,不会有"跑久了状态变脏"的问题。 # # ── 踩过的两个坑(都写在这里免得再犯)──────────────────────────────── # # 1. 改监听地址之后忘了改探测地址 # 把中转改成只绑 127.0.0.1 之后,探测还在查公网 :13245, # 于是连续报"网站挂了" -> 启动预案 -> 重启中转 -> 再查还是失败, # 差一点就是无限重启循环。 # # 2. 忘了 curl 默认不跟跳转 # certbot 加了 http -> https 的 301 之后,探测拿到 301 而不是 200, # 又被判成"挂了"。所以下面用 -L。 # # 共同教训:**改了入口的行为,必须回头看所有探测入口的地方。** # ============================================================================ set -u RELAY_DIR="${RELAY_DIR:-/opt/ykxmz-relay}" PRIMARY_URL="${PRIMARY_URL:-https://yk.xxmxxs.top}" FALLBACK_URL="${FALLBACK_URL:-http://127.0.0.1:13245}" FAIL_THRESHOLD="${FAIL_THRESHOLD:-3}" COOLDOWN="${COOLDOWN:-600}" CHECK_TIMEOUT="${CHECK_TIMEOUT:-10}" STATE="$RELAY_DIR/monitor_state.txt" LOG="$RELAY_DIR/monitor.log" LOCKFILE="/tmp/ykxmz-monitor.lock" 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) # ---- 探两层 ---- # -L 跟跳转:nginx 那边 http 会 301 到 https。 # 不跟跳转的话拿到 301 会被误判成故障(这个坑踩过一次)。 PUB_CODE=$(curl -sL -o /dev/null -w '%{http_code}' \ --max-time "$CHECK_TIMEOUT" "$PRIMARY_URL/v1/health" 2>/dev/null) PUB_DUR=$(curl -sL -o /dev/null -w '%{time_total}' \ --max-time "$CHECK_TIMEOUT" "$PRIMARY_URL/v1/health" 2>/dev/null) LOC_CODE=$(curl -s -o /dev/null -w '%{http_code}' \ --max-time "$CHECK_TIMEOUT" "$FALLBACK_URL/v1/health" 2>/dev/null) PUB_CODE=${PUB_CODE:-000} LOC_CODE=${LOC_CODE:-000} PUB_DUR=${PUB_DUR:-0} # 健康 = 用户那层通。这层的通才算"网站是好的"。 CODE="$PUB_CODE" # 哪一层坏了?给预案用的提示 LAYER="" if [ "$PUB_CODE" != "200" ] && [ "$LOC_CODE" = "200" ]; then LAYER="edge" elif [ "$PUB_CODE" != "200" ] && [ "$LOC_CODE" != "200" ]; then LAYER="core" fi write_state() { { echo "fails=$1" echo "last_rescue=$2" echo "last_code=$PUB_CODE" echo "local_code=$LOC_CODE" echo "layer=$LAYER" echo "last_check=$NOW" echo "last_dur=$PUB_DUR" } > "$STATE" } if [ "$CODE" = "200" ]; then PREV=$(grep -m1 '^last_code=' "$STATE" 2>/dev/null | cut -d= -f2) if [ "$PREV" != "200" ] && [ -n "$PREV" ]; then log "网站恢复(HTTP 200,${PUB_DUR}s)—— 之前连续失败 $FAILS 次" fi write_state 0 "$LAST_RESCUE" exit 0 fi FAILS=$((FAILS + 1)) log "网站异常 第 $FAILS 次(用户层 HTTP $PUB_CODE,中转层 HTTP $LOC_CODE${LAYER:+,层次=$LAYER},${PUB_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 次,启动紧急预案(层次:${LAYER:-未知})${HINT:+,知识库提示第 $HINT 级常奏效}" write_state "$FAILS" "$NOW" timeout 300 bash "$RELAY_DIR/rescue.sh" \ "监测单元:网站连续 $FAILS 次不通(层次 ${LAYER:-未知})" >> "$LOG" 2>&1 RC=$? if [ "$RC" = "0" ]; then log " 预案完成:已恢复" else log " 预案完成:仍未恢复(退出码 $RC),报告在 rescue_reports/" fi exit 0