mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
== 为什么重写 ==
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)。
154 lines
5.5 KiB
Bash
154 lines
5.5 KiB
Bash
#!/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
|