Commit 4e55a207 by luoqi

feat(ops): 探针加每日心跳(dead-man's switch)—— 没收到本身就是告警

外部探针解决了「pac-service 死了没人知道」,但它自己也会死:测试机宕、cron 被删、
脚本改坏、/etc/pac-health-watch.env 丢失 —— 这些情况下探针**静静地停掉**,
而所有人以为它还看着。这是 09-04 的同款失败换个位置又出现一次。

每天固定时刻(默认 9 点,WATCH_HEARTBEAT_HOUR=-1 关)推一条「我还活着 +
过去一个周期探了几次/失败几次」。刻意排在生产每日健康报告(09:07)旁边:
两条都到 = 两侧都活;只到一条 = 立刻知道断的是哪一侧。

两个实现细节:
· 心跳判据是「今天还没推过 且 已到点」,不是「分钟正好相等」——
  cron 可能因负载/重启错过某一格,按分钟判会整天不推,而漏推 = 误报"探针死了"。
· 状态文件的每个数值字段都做正则校验。它是被 source 的,写坏一次(磁盘满/并发)
  会让后面的算术直接语法错退出 → 探针静默死亡,正是本脚本要防的那类失败。
  已用"塞垃圾进状态文件"的用例验证能自愈。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 7cea75b2
...@@ -30,6 +30,14 @@ ...@@ -30,6 +30,14 @@
# 防「探针机自己断网 → 误报生产挂了」。默认空=不启用 # 防「探针机自己断网 → 误报生产挂了」。默认空=不启用
# (连续 3 次 × 5 分钟 = 15 分钟已能滤掉绝大多数网络抖动) # (连续 3 次 × 5 分钟 = 15 分钟已能滤掉绝大多数网络抖动)
# WATCH_STATE_DIR 状态文件目录(默认 /var/lib/pac-health-watch) # WATCH_STATE_DIR 状态文件目录(默认 /var/lib/pac-health-watch)
# WATCH_HEARTBEAT_HOUR 每日心跳的小时(0-23,默认 9;设 -1 关闭)。见下方「为什么要心跳」
#
# 【为什么要心跳(dead-man's switch)】探针本身也会死:测试机宕、cron 被删、脚本被改坏、
# /etc/pac-health-watch.env 丢了 —— 这些情况下探针**静静地停掉**,而所有人以为它还看着。
# 这正是 09-04 的同款失败:告警器坏了没人告诉你。
# 所以每天固定时刻推一条「我还活着 + 过去 24h 探了多少次/失败几次」。
# ⇒ **没收到心跳本身就是告警**。它刻意排在生产每日健康报告(09:07)旁边:
# 两条都到 = 两侧都活;只到一条 = 立刻知道是哪侧断了。
# #
# crontab 示例(测试机 root,每 5 分钟): # crontab 示例(测试机 root,每 5 分钟):
# */5 * * * * ALERT_WEBHOOK_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' \ # */5 * * * * ALERT_WEBHOOK_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' \
...@@ -49,6 +57,7 @@ WATCH_REPEAT_EVERY="${WATCH_REPEAT_EVERY:-12}" ...@@ -49,6 +57,7 @@ WATCH_REPEAT_EVERY="${WATCH_REPEAT_EVERY:-12}"
WATCH_TIMEOUT="${WATCH_TIMEOUT:-10}" WATCH_TIMEOUT="${WATCH_TIMEOUT:-10}"
WATCH_CONTROL_URL="${WATCH_CONTROL_URL:-}" WATCH_CONTROL_URL="${WATCH_CONTROL_URL:-}"
WATCH_STATE_DIR="${WATCH_STATE_DIR:-/var/lib/pac-health-watch}" WATCH_STATE_DIR="${WATCH_STATE_DIR:-/var/lib/pac-health-watch}"
WATCH_HEARTBEAT_HOUR="${WATCH_HEARTBEAT_HOUR:-9}"
WATCH_DRY_RUN="${WATCH_DRY_RUN:-0}" WATCH_DRY_RUN="${WATCH_DRY_RUN:-0}"
case "${1:-}" in case "${1:-}" in
...@@ -86,15 +95,19 @@ probe() { ...@@ -86,15 +95,19 @@ probe() {
} }
# ── 状态读写 ─────────────────────────────────────────────────────────── # ── 状态读写 ───────────────────────────────────────────────────────────
fails=0; alerted=0; since="" fails=0; alerted=0; since=""; cnt_ok=0; cnt_fail=0; hb_date=""
# shellcheck disable=SC1090 # shellcheck disable=SC1090
[[ -f "$STATE" ]] && . "$STATE" [[ -f "$STATE" ]] && . "$STATE"
[[ "$fails" =~ ^[0-9]+$ ]] || fails=0 # ⛔ 每个字段都要校验:状态文件是被 source 的,写坏一次(磁盘满/并发)会让后面的算术
[[ "$alerted" =~ ^[01]$ ]] || alerted=0 # 直接语法错退出 —— 探针从此静默死亡,而这正是本脚本要防的那类失败。
[[ "$fails" =~ ^[0-9]+$ ]] || fails=0
[[ "$alerted" =~ ^[01]$ ]] || alerted=0
[[ "$cnt_ok" =~ ^[0-9]+$ ]] || cnt_ok=0
[[ "$cnt_fail" =~ ^[0-9]+$ ]] || cnt_fail=0
save_state() { save_state() {
printf "fails=%s\nalerted=%s\nsince='%s'\nlast='%s'\n" \ printf "fails=%s\nalerted=%s\nsince='%s'\ncnt_ok=%s\ncnt_fail=%s\nhb_date='%s'\nlast='%s'\n" \
"$fails" "$alerted" "$since" "$(ts)" > "$STATE" "$fails" "$alerted" "$since" "$cnt_ok" "$cnt_fail" "$hb_date" "$(ts)" > "$STATE"
} }
# ── 推送 ─────────────────────────────────────────────────────────────── # ── 推送 ───────────────────────────────────────────────────────────────
...@@ -137,6 +150,21 @@ if diag=$(probe "$WATCH_URL"); then ...@@ -137,6 +150,21 @@ if diag=$(probe "$WATCH_URL"); then
fi fi
[[ "$fails" -gt 0 ]] && say "恢复正常(此前连续失败 $fails 次)" [[ "$fails" -gt 0 ]] && say "恢复正常(此前连续失败 $fails 次)"
fails=0; alerted=0; since="" fails=0; alerted=0; since=""
cnt_ok=$((cnt_ok + 1))
# 每日心跳。⚠️ 判据是「今天还没推过 且 已到点」而不是「正好等于某分钟」——
# cron 可能因为负载/机器重启错过某一格,按"分钟相等"判会整天不推,
# 而心跳漏推 = 误报"探针死了"。所以用日期去重 + 到点即可。
today="$(date +%F)"
if [[ "$WATCH_HEARTBEAT_HOUR" != "-1" && "$hb_date" != "$today" ]] \
&& [[ "$(date +%-H)" -ge "$WATCH_HEARTBEAT_HOUR" ]]; then
hb_date="$today"
push "🟢" "info" "$WATCH_NAME 探针存活(每日心跳)" \
"$(printf '目标正常。\n上一个周期:探测 %s 次成功 / %s 次失败。\n\n⚠️ **收不到这条 = 探针本身死了**(测试机宕/cron 被删/配置丢失),\n这时生产是死是活没有任何人在看,请手工确认。' \
"$cnt_ok" "$cnt_fail")"
cnt_ok=0; cnt_fail=0
fi
save_state save_state
say "OK $diag" say "OK $diag"
exit 0 exit 0
...@@ -152,6 +180,7 @@ if [[ -n "$WATCH_CONTROL_URL" ]]; then ...@@ -152,6 +180,7 @@ if [[ -n "$WATCH_CONTROL_URL" ]]; then
fi fi
fails=$((fails + 1)) fails=$((fails + 1))
cnt_fail=$((cnt_fail + 1))
[[ -z "$since" ]] && since="$(ts)" [[ -z "$since" ]] && since="$(ts)"
say "失败 #$fails $diag" say "失败 #$fails $diag"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment