给服务器矩阵配探活哨兵(watchdog)是 Agent 运维的日常动作:定时 curl 一圈站点,坏了就报警。听起来十行脚本搞定,实际做起来,误报、哑火、刷屏三种病几乎人人得一遍。这篇文章把一周内三轮真实迭代沉淀的设计准则整理成文——核心思想一句话:哨兵的价值不在”能报警”,在”该沉默时绝对沉默”。所有规则均以 Hermes cron 的 no_agent 模式(脚本 stdout 直接投递,不过 LLM)为背景,但方法论对任何调度器通用。

zxcyw-watchdog封面

一、先搞清楚投递语义:空输出 = 不说话

no_agent 型任务的语义非常直白:

  • stdout 非空 → 原文作为消息投递给值班人;
  • stdout 为空 → 什么都不发,没人知道你跑过;
  • 进程非零退出/超时 → 错误告警,防止哨兵自己死了没人管。

这三条决定了脚本的写法:只在”确实有情况”时才 echo,而不是每次打印一圈”检查完毕 ✓”。一个每 30 分钟汇报一次”一切正常”的哨兵,三天后就会被静音——然后真出事那天也没人看。

二、第一病:误报——通道故障 ≠ 目标故障

最经典的一次翻车:夜里哨兵报警”全线 522/000″,值班人爬起来查了半天,站点活得好好的。真正的故障是探测机到目标之间的那条 SSH 通道抖了一下。

识别指纹很简单:报警文案里所有探测腿返回同一个非 HTTP 值(全 ERR、全 NA),99% 是取数断了,不是站点坏了。真实故障各腿的码会分化——000、502、522、超时混杂出现。

修法三件套:

# 1) 先格式校验远端返回,不合格即走"通道断"分支
echo "$REMOTE_OUT" | grep -qE "^[0-9]+\|" || { C=ERR; }

# 2) 通道断时,本机换一条独立路径兜底复测(直连源站 + CDN 全链路)
#    兜底全绿 → 静默,只在本地记账
# 3) 通道故障计数用独立状态文件,绝不与站点 FAILS 计数混用

这里有个反直觉的点:“探测失败”本身不是报警条件。要报警的是”探测失败 且 独立兜底路径也确认坏了”。两层都坏才升级成真故障;只有通道坏,降级成一条措辞完全不同的”运维通道掉线”提示,而且连断 ≥4 轮才发。

zxcyw-watchdog-silent

三、第二病:跨境抖动——单发失败永远是常态

家庭宽带到海外机房的链路,秒级抖动是日常。一夜两次误报(单发 522/000,下一轮全 200)之后,给每条探测内置重试:

probe() {  # probe <url> <可接受码1> [更多...]
  local url="$1"; shift
  local code="ERR" i ok
  for i in 1 2 3; do
    code=$(curl -s -o /dev/null -w "%{http_code}" -m 15 -A "$UA" "$url" 2>/dev/null)
    for ok in "$@"; do [ "$code" = "$ok" ] && { echo "$code"; return; }; done
    [ "$i" -lt 3 ] && sleep 5
  done
  echo "$code"
}

三个细节:

  1. 可接受码按业务语义给。付费站根路径 403 是设计如此,403 就该算”活”;登录墙页面 302 也算活。把 200 当成唯一健康码的哨兵,一半报警都是假案。
  2. 单请求 -m 15 卡死超时,重试间隔 5s,三发才判坏。
  3. 超时预算要连带算:3×(15+5)+双程 SSH ≈ 60s+,外层任务 timeout 给 200s,别贴脸设置——贴脸的后果是哨兵把自己超时杀掉,非零退出,又是一条假报警。

四、第三病:哑火——监控自己的监控

旧版哨兵有个隐蔽缺陷:SSH 到探测节点整体失败时,变量拼出来是空串,空串既不在健康名单也不在故障分支里,脚本安静地走完一轮,什么都没发。哨兵死了,没有任何人知道哨兵死了。

修法是把它变成显式分支:[ -z "$C" ] && C=ERR,空值计入坏腿。设计准则:哨兵必须监控自己的可用性,取数失败要显式建模成一个状态,而不是靠 if/else 的默认路径兜住。

zxcyw-watchdog-retry

五、第四病:刷屏与闭环——报警要有始有终

另一类投诉来自”重复播报”:某个余额条件写的是 >0 就报,首单成交后每 30 分钟报一次同一笔老订单。规则:

  • 增量制而非状态制。报”比上次多了什么”,不报”现在有什么”。状态文件记下上次已报的基线(订单 ID、累计值),只在增量出现时说话。
  • 节流防风暴:连续失败奇数轮才 echo(第 1、3、5 轮),避免每轮重复轰炸。
  • 恢复必须自报:故障恢复时如果 FAILS≠0,主动播一条”✅ 已恢复(此前报警 N 轮)”再清零。值班人要的是闭环,不是一个悄悄消失的报警——你不知道它恢复了,就会一直担心。

六、进阶坑:HTTP 200 ≠ 你监控的服务在应答

一周内最硬核的一课。部署一个 systemd 托管的 API 服务,测试流程是 curl 健康端点——全绿,汇报”上线成功”。

真相:早就手动后台起过一个测试进程占着同端口,systemd 服务绑不上端口进入崩溃循环(重启计数 343 次)。所有 curl 的 200 应答者是那个僵尸测试进程。

从此服务上线验收必须核”应答者身份”四件套:

ss -tlnp | grep :9100                      # ① 端口持有 pid
systemctl show svc -p MainPID --value     #    == systemd MainPID?
ps -o ppid= -p <pid>                       # ② PPID 必须 = 1(被 systemd 收养)
systemctl show svc -p NRestarts            # ③ 连续采样两次,数字冻结
systemctl show svc -p SubState             # ④ running,不是 auto-restart

activating/auto-restart + NRestarts 持续增长 = 崩溃循环。期间公网探测可能间歇 200(残留进程或 CDN 缓存在应答),拿探测码单独判健康,等于把僵尸进程当成分身。标准动作:先精确匹配杀掉所有同端口手动测试进程(注意 pgrep -f 模式串会匹配到自己这条 shell,用 pgrep -x 或 [r]sync 式括号 trick 防自伤),enable 服务,立刻核四件套,再谈功能验证。

zxcyw-watchdog-inject

七、CDN 站点的探活:必加源站直连腿

探一个挂在 CDN 后面的站点,只走 CDN 路径会碰上这种情况:探针机到 CDN 边缘的出口劣化,TLS 能握手但请求挂死,全腿 000——看起来像全站瘫痪,实际上家里和源站全绿。

判据:CDN 腿坏 且 源站腿也坏,才允许报警。源站腿 = 直连源站 IP + Host 头 + -k:

curl -sk -m 15 --resolve example.com:443:SRC_IP https://example.com/ -o /dev/null -w "%{http_code}"

报警文案里附带源站腿读数,值班人一眼就能定位坏在哪一层(边缘/回源/源站/DB)。

八、交付标准:故障注入三态验收

哨兵写完不许直接上线,必须手工注入三种状态各跑一遍:

注入场景 期望行为
一切正常 零输出(静默)
通道断 + 站点活 零输出(只记账)
真故障(改坏目标) 报警文案,含分层读数

三态齐了才算交付。用环境变量注入(INJ_R=1 强制返回坏值)可以在不动生产的情况下测完第三态。

小结:五条铁律

  1. 探 CDN 站点必加源站直连复核腿;
  2. 空 stdout = 静默,有输出 = 投递;
  3. 每条探测内置重试再判坏,可接受码按业务语义定;
  4. 显式建模”取数失败”,哨兵要监控自己;
  5. 增量制报警 + 节流 + 恢复自报,报警有始有终;
  6. HTTP 码必须配”应答者身份”验证,僵尸进程会替死服务应答。

Agent 做运维,写探测逻辑是五分钟的事,设计”什么情况下闭嘴”才是全部工作量。