告警通知渠道选型:从邮件到短信

告警发到哪儿,看起来是个极小的技术决定:邮件、钉钉、企业微信、Telegram、短信,随便挑一个填进去就完了。但真正跑过一段时间的人都会发现,通知渠道的选择会直接决定这套监控能不能用满一年。渠道选错的典型症状是:重要的事你漏了,不重要的事天天吵你,或者最要命的那种——机器挂了,告警也一起没了。

这篇文章按「可靠性、到达率、成本、静默能力」四个维度拆开讲通知渠道,最后给一套适合个人站长的组合方案。所有结论都按「一个人维护、没有值班室、手机可能静音」这个前提来写,企业级的双活通知中心不在这里讨论。

一、先算一件事:告警链路有多长,就有多少种断法

一条告警从产生到你看手机,中间至少经过五跳:检查脚本发现异常、脚本调用发送接口、发送方的服务接收、推送通道投递、手机系统通知。任何一跳断掉,你的告警就消失了,而且大多数断法是静默的——脚本报错但 cron 把 stderr 丢进邮件,而你根本没配本地 MTA;接口返回 200 但其实限流了;推送送到但手机把这个 App 的通知权限关了。

所以评估渠道时的第一个问题不是「快不快」,而是「这条链路里有没有单点,以及断了以后我会不会知道」。这一点上,邮件和 IM 机器人差别很大:邮件走的是外部 SMTP,链路长、延迟大、容易进垃圾箱,但好处是有完整投递日志可查;IM 机器人链路短、到达快,但一旦 token 失效或机器人被移出群,你收到的只有脚本里一个被忽略的错误码。

判断办法很朴素:每月主动发一条测试告警,然后确认自己真的收到了。这一步听起来傻,但它是唯一能保证「链路还活着」的手段。测试告警要带上明确的标记,比如标题以「测试」开头,并在日报里统计「本月测试告警是否到达」,连续两个月漏掉就该换渠道了。

二、渠道对比:邮件、机器人、短信、电话的边界

邮件是默认选项,也是最容易被高估的。它的优点是免费、无配额压力、可以附件、有历史记录可搜索。缺点是延迟不可控(一分钟到十分钟都正常)、容易被判垃圾、手机上不点开就等于没收到。对个人站来说,邮件适合承载日报和低优先级清单,不适合承载「半夜必须起来处理」的事。

IM 机器人(企业微信、钉钉、飞书、Telegram Bot)是目前个人站最实用的主力渠道。走的是 HTTPS POST,一条 curl 就能发,延迟通常在一秒内,支持 markdown 排版,手机上会响。它的主要风险有三个:一是群机器人有频率限制,短时间内连发几十条会被限流(各家阈值不同,但「同一会话一分钟 20 条」是常见的量级),而限流期间的告警是直接丢的;二是机器人可能被管理员踢出群;三是如果告警发到的是一个「通知全关」的群,等于发进了黑洞。

短信和电话是唯一能穿透「手机静音 + 勿扰模式」的渠道,但要花钱,且单价不低。合理的用法是把它留给极少数 P0:站点完全不可访问、数据库不可写。这里有一个非常实用的技巧——用分级做省钱:P0 走短信,P1 走 IM 机器人,P2 只进日报。一个人维护的站,P0 一个月能触发两次就算多了,成本完全可以接受。

还有一种常被忽略的渠道:外部探活服务。它的价值和上面几个不是同一类——它不解决「用什么发」,而解决「谁来发现问题」。自己机器上的脚本挂了、磁盘满了、机器整个失联,本地监控是不会告警的。用第三方免费监控(或者另一台小机器)定时请求一个公开 URL,是唯一能覆盖「整机失联」这个场景的手段。这个渠道必须配,且必须和主站在物理上分离。

三、发送脚本:把失败也变成一条告警

绝大多数自写的告警脚本只关心「怎么把消息发出去」,不关心「发失败了怎么办」。这是通知渠道里最常见、后果最严重的缺陷:脚本 curl 失败,退出了一个非零码,然后什么也没发生——因为没人看 cron 的 stderr。

正确写法是把发送过程包一层,失败时降级到备用渠道,并且把「发送失败」本身写进一个本地文件,让下一次的日报能看见它。

#!/bin/sh
# notify.sh:主渠道失败自动降级,并把失败记进本地账本
LEVEL="$1"; TITLE="$2"; BODY="$3"
LOG=/var/log/notify_fail.log
send_im() {
  curl -s -m 5 -X POST "$IM_WEBHOOK" \
    -H 'Content-Type: application/json' \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[$LEVEL] $TITLE: $BODY\"}}" \
    | grep -q '"errcode":0'
}
send_mail() {
  { printf 'To: ops@example.com'; echo; printf 'From: monitor@example.com'; echo;
    printf 'Subject: [%s] %s' "$LEVEL" "$TITLE"; echo; echo; printf '%s' "$BODY";
    echo; } | sendmail -t
}
if ! send_im; then
  echo "$(date '+%F %T') im-fail $TITLE" >> "$LOG"
  send_mail || echo "$(date '+%F %T') mail-fail $TITLE" >> "$LOG"
fi

这段脚本有三个细节值得注意。用 grep -q '"errcode":0' 判断成败,是因为很多 IM 机器人在 HTTP 层永远返回 200,真正的错误码在响应体里——只看 curl 的退出码会漏掉鉴权失败、被踢出群、被限流这三类。用 -m 5 限制超时,是因为告警脚本必须比它监控的服务更快失败,否则一个卡住的告警会把 cron 槽位占满。失败记账写到本地文件,是因为「告警系统自己坏了」这件事必须留下痕迹,而痕迹要落在你在日报里一定会看的位置。

还有一个必做的检查:在脚本里加上消息去重的前缀。同一个检查项在未恢复前只报一次,恢复时补报一条「已恢复」。这个状态用一个小的状态目录实现,每个检查项一个文件,内容为空或「报警中」。没有这一步,一个持续两小时的故障会给你发两百条消息,然后你会把整个群静音——这是通知渠道失效最常见的原因。

四、静默:必须有,但必须自动结束

再好的渠道也需要静默功能。半夜重启数据库、做一次迁移、故意把某台机器下线维护,这些时候不想被吵是合理的。问题在于,手工静默是一个人自己给自己挖的坑:设了「静默两小时」,结果迁移出问题拖到第二天,告警一直被静默,站点挂了没人知道。

可行的做法是让静默自带过期时间并且写进一个可查的文件。silence 命令接收「检查项名 + 分钟数 + 原因」,写入 /var/lib/monitor/silence/检查项名,文件内容就是过期时间戳和原因。检查脚本每次运行先看一眼这个文件,过期了自动删掉。这样静默永远不会变成永久,而且事后能查到「那次为什么静默了」。原因字段同样是给未来的自己看的,不要写「维护中」,要写「2026-11-08 迁移 typecho_contents 到新表,预计 30 分钟」。

静默的粒度也要注意:静默「磁盘检查」可以,静默「全站探活」要非常谨慎。我的规矩是 P0 级别的检查项不允许写入静默文件,脚本里硬编码拒绝。规则越硬越好,因为静默的诱惑在出事的那一刻是最大的。

五、通知内容:带上下文比快更重要

很多人以为告警的价值在「及时」,其实在「省下第一个电话」。一条只有「磁盘使用率超过 90%」的告警,收到之后你还是要登录、还是要查是哪块盘、还是要算还能撑多久。一条合格的告警应该自带这些信息,让你在不登录的情况下就能决定该不该现在动手。

我固定的模板是五行:现象(哪个检查项、当前值)、时间(首次发现时刻,不是发送时刻)、影响面(涉及哪个站点或哪个服务)、建议动作(一句话)、以及一个可点的链接。建议动作这一项是整条告警里最有价值的一行,它把「设计这套监控的人当时的思考」保存下来了。写到第六个月的时候,你自己也会感谢当初写这一行的人。

最后是一个经常被忘的细节:告警正文里不要塞密码、token、内网完整路径。IM 和邮件都是会转发的东西,一条带数据库密码的告警可能三个月后出现在别人的聊天记录里。脱敏规则很简单:任何看起来像凭据的东西一律替换成 [masked]。

六、小结

通知渠道的选择,本质是给不同级别的信号分配不同的到达强度。邮件承担日报和清单,IM 机器人当主力,短信电话只留给极少数 P0,外部探活单独负责「整机失联」。发送脚本必须自带降级与失败记账,因为告警链路里大多数断法是静默的。静默要有、要带原因、要自动过期,且 P0 不允许静默。最后,告警正文要自带现象、时间、影响面、建议动作和链接——让收到通知的人不用登录就能做出第一个决定。

Last modification:November 8th, 2026 at 10:26 pm

Leave a Comment