监测单元和预案重写成"分层"版(来源改为本地整份文件)

== 为什么重写 ==

certbot 签证书时加了 http -> https 的 301,而监测单元原来判断
"curl 拿到 200 才算健康"、且 curl **默认不跟跳转** -> 拿到 301
-> 判成"网站挂了" -> 又要启动预案 -> 重启中转 -> 再查还是 301。
这是**第二次**踩同一类坑了(上一次是改监听地址忘了改探测地址)。

共同教训:**改了入口的行为,必须回头看所有探测入口的地方。**

== 新设计:分两层探测 ==

    PRIMARY   https://yk.xxmxxs.top/v1/health     用户实际走的路
    FALLBACK  http://127.0.0.1:13245/v1/health    中转本身

两个都查才能分清哪一层坏了,进而选对抢救动作:

    PRIMARY 挂 + FALLBACK 好  -> edge 层(CF / DNS / nginx)
   两个都挂                  -> core 层(中转服务本身)

预案据此走不同的阶梯:

    edge: 重载 nginx -> 重启 nginx -> 查 443 与证书
    core: 重启中转 -> 强杀重拉 -> 中转+nginx 都重启

不区分层次的话只能瞎重启,而 edge 层的问题重启中转一万次也没用。

== 做法上的改进:不再远端打补丁 ==

前几轮一直是"SSH 过去用嵌套 heredoc 现场改 monitor.sh",
连续踩三次:PowerShell 在本机求值 \、引号被吃、
脚本里的 %{http_code} 和 Python 的 %% 格式化打架。

现在改成:relay/monitor-lightsail.sh 和 relay/rescue-lightsail.sh
是**唯一事实来源**,本地编辑 -> 整份上传 -> 重装,
改了什么一目了然,也能进 git。

== 顺带 ==

因为 Lightsail 防火墙还没放行 443,PRIMARY 一直是 000,
监测会把 fails 一直加上去、白生成抢救报告。
所以暂时把 monitor.timer 停掉,等 443 开了再启用
(脚本 _build/pause_monitor.py,恢复是一条 systemctl start)。
This commit is contained in:
xmz7891
2026-10-08 22:53:44 +08:00
parent e63b5f08d7
commit 47e08af151
6 changed files with 780 additions and 106 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/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