我的个人服务器上跑着一个博客、一个工作台和一条每天早上自动发文章的流水线。之前它们出问题,我都是自己打开网页才发现。于是给服务器加了一个小小的 watcher,有事推送到手机上。
推送本身很容易,难的是让它不吵。一个隔三差五误报、半夜响、同一件事推十遍的告警,用不了几天就会被静音,等于没有。这篇整理我在设计时定下的几条规则。
一、只推“需要我做点什么”的事
watcher 每 60 秒检查一次,但只有下面几类情况会推送:
| 情况 | 触发条件 |
|---|---|
| 站点或订阅源打不开 | 连续 2 次检查失败;恢复时再通知一次 |
| 磁盘快满 | 使用率 ≥ 85% |
| 每日文章没发出来 | 到 10:30 当天还没有 |
| 某台电脑很久没同步 | 超过设定时长没有数据进来 |
| 工作台有新建议 | 只推新产生的,汇总成一条 |
判断标准是:收到这条推送,我是不是需要去做点什么。CPU 偶尔冲高、某次请求慢了一点,这些我知道了也不会做什么,就不推。
二、连续失败才报,恢复也要报
网络偶尔抖一下,一次检查失败很正常。要求连续两次失败才推送,能挡掉绝大多数误报,代价是最多晚一分钟知道。
恢复通知同样重要。收到“站点打不开”之后,如果之后再没有消息,我不知道它是已经好了还是一直坏着,只能自己去看。有了“已恢复”,一来一回两条消息就把事情说清楚了。
三、同一件事只推一次
每 60 秒检查一次,如果磁盘一直在 86%,不做处理就会每分钟推一条。我的做法是给每条通知一个去重键,推过的键记进数据库,下次遇到同一个键就跳过:
def notify_once(key, title, body, level="warn"):
if kv_get(f"notified:{key}"):
return False
if send(title, body, level):
kv_set(f"notified:{key}", now_iso())
return True
# 键的粒度决定“多久可以再提醒一次”
notify_once(f"disk:{today}", "磁盘使用率 86%", "...") # 每天最多一次
notify_once(f"daily-missing:{today}", "今日文章未发布", "...")
notify_once(f"down:site:{incident_id}", "站点无法访问", "...") # 每次故障一次
键里放什么,决定了重复提醒的节奏:放日期就是每天最多一次,放故障编号就是每次故障一次。
还有一个容易忽略的点:第一次启动时不要推旧积压。如果 watcher 一上线就把工作台里已有的建议全推一遍,第一印象就是“这东西好吵”。所以新建议只推 watcher 启动之后产生的,而且多条合成一条。
四、晚上安静,除非真的严重
23 点到第二天 7 点是免打扰时段,这段时间只推严重级别(比如站点整个打不开),其余的等到早上再说。时段写在配置里,跨午夜的写法要注意:“23-7”的意思是 23 点之后或 7 点之前,判断条件是“或”,不是“且”。
五、推送失败不能拖垮监控
推送要走外部服务,外部服务会超时、会限流、会改接口。如果发送函数抛异常,整个 watcher 循环就停了,监控反而因为推送出错而失效。所以 send() 的约定是永远不抛异常:每个渠道单独 try,设短超时,失败只记日志,返回是否至少有一个渠道成功。
def send(title, body, level="warn") -> bool:
ok = False
for name, fn in CHANNELS: # 配置了哪些渠道就有哪些
try:
fn(title, body, level, timeout=8)
ok = True
except Exception as e: # 一个渠道失败不影响其他渠道
log.warning("notify %s failed: %s", name, e)
return ok
只有发送成功时才写去重键,这样这次没发出去,下一轮还会再试。
六、渠道:选一个手机上本来就在用的
- Bark:iPhone 上最省事,装好 App 拿到一个地址,填进配置就能用。
- 企业微信、飞书、钉钉群机器人:适合本来就天天开着这些软件的人。飞书和钉钉建议打开“加签”,密钥和地址分开存放。
地址和密钥都只写在服务器的环境文件里,不进代码仓库。工作台上留了一个“测试推送”按钮,配完渠道点一下,比等一次真故障来验证靠谱得多。
小结
个人服务器的告警,目标不是“什么都知道”,而是“该知道的时候一定知道,其他时候别打扰我”。连续失败才报、恢复也报、同一件事只推一次、晚上只推严重的、推送失败不影响监控,这五条做到了,手机上的提醒才会一直被认真看。
