做个人站久了,最典型的一种状态是:服务器上装了三四个监控脚本,每天收几十封告警邮件,然后慢慢地、很自然地,把它们全部丢进一个自动归档的文件夹里。等到某天数据库真的挂了,你翻回那个文件夹,发现它三天前就报过 CPU 异常、两天前报过磁盘快满了、一天前报过 MySQL 连接数打满——每一条都报过,但一条都没起作用。
这篇讲的不是怎么装监控工具,而是告警本身该长什么样。因为绝大多数个人站的监控不是「没有」,而是「报了等于没报」。要么太吵被忽略,要么太迟钝事后才响,要么响了你也不知道该怎么办。这三个问题,比「用不用 Prometheus」重要得多。
一、先分清楚三类信号,别用一套规则对待
很多站长写监控脚本时的思路是「超过某个值就发邮件」,但服务器上的异常其实分成性质完全不同的三类,混在一起处理必然失控。
第一类是瞬时尖峰。比如凌晨三点的一个备份任务把 CPU 顶到 95%,持续了两分钟;比如爬虫集中抓取让带宽短时间翻三倍。这类事件绝大多数是预期的、无害的,用「单点采样超过阈值就告警」的规则去套,等于给自己制造噪声。正确的处理是看持续性而不是瞬时值,比如「连续 5 分钟超过 90%」才报。
第二类是趋势性劣化。磁盘从 60% 涨到 70% 用了两个月,又用一个月涨到 80%。这类问题单看某一天的数值完全正常,永远触发不了阈值,但它才是真正会把你搞崩的那一类。它的处理方式是看增长速率:按当前速率算,还有多少天写满。
第三类是状态翻转。服务从 running 变成 failed、证书从有效变成过期、备份任务从成功变成失败。这类是布尔量,无所谓阈值和持续时间,变了就该立刻响,且必须响到有人处理为止。
三类信号对应三种判定逻辑:持续超阈值、速率外推、状态翻转。如果你现在所有的告警都是第一类,那另一半问题你根本没在监测。
二、磁盘要报「还能撑几天」,不是「用了百分之多少」
这是我认为个人站监控里最高性价比的一个改动。绝大多数人写的是「磁盘使用率超过 85% 就告警」,问题是:一个写入量稳定的小站,到 85% 可能还有半年;一台日志暴涨的机器,从 85% 到写满可能只有两天。同一个阈值,意义完全不同。
改成按天数算,一条 awk 就能做到。思路是:取磁盘总量、已用量、以及最近一天的增长量,外推剩余天数。
TOTAL=$(df -k / | awk 'NR==2 {print $2}')
USED=$(df -k / | awk 'NR==2 {print $3}')
# 用 24 小时前后两次快照的差值算日增长,快照存 /var/lib/monitor/df.prev
PREV=$(cat /var/lib/monitor/df.prev 2>/dev/null || echo "$USED")
GROWTH=$(( (USED - PREV) ))
echo "$USED" > /var/lib/monitor/df.prev
awk -v t="$TOTAL" -v u="$USED" -v g="$GROWTH" 'BEGIN {
if (g <= 1024) { print "stable"; exit }
left = (t - u) / g;
printf "days_left=%.1f growth_mb_per_day=%.0f |", left, g/1024;
if (left < 7) exit 2;
if (left < 21) exit 1;
}'退出码 1 是提醒、2 是警告,配合 cron 的邮件或者在脚本末尾接一段通知逻辑。这里面有个坑:df.prev 必须每天同一时间点采,如果 cron 在凌晨三点跑,但你手动在下午两点跑了一次,那一次算出来的增长是整个业务高峰的量,会虚高好几倍。稳妥的做法是让脚本自己记时间戳,两次采样间隔不足 20 小时就直接跳过并退出,别算。
还有一层:df 看的是块使用率,但真正把站点搞挂的经常是 inode 耗尽。一个缓存目录里堆了几百万个小文件,磁盘还剩 40%,但 inode 已经 100%,新文件一个也写不进去,报错是「No space left on device」但 df -h 看起来一切正常。所以磁盘类检查至少要三条:块使用率、inode 使用率、以及上面这个增长速率。
三、把「静默」做成机制,而不是靠自觉
值班时最危险的状态不是「告警太多」,而是「告警没有来」。而人在没有告警的时候,默认会认为「一切正常」,这个假设经常是错的——可能监控脚本自己崩了、可能 crontab 被覆盖了、可能机器整台掉线了。
标准做法是心跳反转,也叫 dead man's switch:让监控端每天固定时间主动推一条「我还活着」的消息,由接收方判断有没有按时收到,而不是让接收方等着「出问题」的那条消息。
# 每天 09:00 推一条心跳:把过去 24 小时的检查结果摘要发出去
#!/bin/bash
SUMMARY=$(cat /var/lib/monitor/last_run.log 2>/dev/null | tail -20)
MSG="heartbeat host=$(hostname) time=$(date '+%F %T')"
curl -s -m 10 -X POST "https://your-endpoint.example/heartbeat" \
--data-urlencode "text=$MSG" --data-urlencode "detail=$SUMMARY"
如果接收方(可以是你自己写的另一个小服务,也可以是任何一个支持「超时未收到即报警」的通知平台)连续 26 小时没收到心跳,就说明发信号的这一侧出问题了。这个逻辑的价值在于:它把「监控失效」本身也纳入了监控。个人站最大的盲区恰恰在这里——你监控了服务器,但没人监控监控。
心跳还能顺手携带状态摘要。比如把最近一轮检查的结论(磁盘还剩几天、证书还剩几天、最近一次备份的时间)一起塞进去,那么每天收到的那条心跳本身就是一份日报,你不用再去翻别的页面。日常巡检的成本从「主动去看」变成「扫一眼邮件标题」,这是能长期坚持下去的关键。
四、分级:不是所有告警都值得半夜叫醒你
个人站没人三班倒,所以分级不只是「重要程度」,还得考虑响应时间。我自己的分法是三档,判定标准很粗暴:如果这条告警响的时候我不处理,多久会影响到用户?
- P0(立刻处理):站点 500、主域名打不开、数据库连不上、磁盘剩余不足 1GB。这类走即时推送,不管几点。
- P1(当天处理):证书剩余不足 10 天、备份连续失败 2 次、磁盘剩余不足 7 天、磁盘 IO 持续打满。这类每天固定时间汇总一次,不打扰睡眠。
- P2(有空处理):磁盘剩余不足 21 天、某个非核心服务重启过、SSH 登录失败次数上升。这类进周报。
关键不是分几档,而是每一档的处置动作必须写死在告警内容里。一条只说「磁盘 92%」的告警,和你凌晨被叫醒后还要花二十分钟登录服务器、查、判断、决定,是完全不同的事。写成「磁盘 92%,日增长 1.2GB,预计 2.3 天写满,先执行 /root/bin/log-clean.sh 清 30 天前日志,再看 /var/log/nginx 是否异常增长」,你的处理时间能从二十分钟压到两分钟。
具体的实现方式是在检查脚本里给每类告警配一个「建议动作」字段。这不费什么力气,但它是告警从「通知」变成「可用信息」的分水岭。
五、阈值别拍脑袋,用自己站点的历史数据定
「CPU 超过 80% 告警」这类阈值几乎都是从别处抄来的,而每个站点的正常水位完全不同:一个纯静态站平时的 CPU 可能只有 2%,突然到 30% 就很不正常;一个跑着图片处理的站,CPU 常态 60%,到 80% 毫无意义。
务实的做法是把过去两周的数据落盘,算出分位数,然后按分位数定阈值。比如每五分钟采一次 CPU 和内存,存成一行 CSV,两周后算一下 P95,把告警线设在 P95 之上留一点余量。这个方法的好处是你不需要理解业务,只需要数据。
# 每 5 分钟采一行,写入 /var/lib/monitor/metrics.csv
ts=$(date '+%F %T')
cpu=$(awk '{print $1+$2+$3}' /proc/stat > /dev/null; echo 0) # 占位,实际用 vmstat 更准
load1=$(awk '{print $1}' /proc/loadavg)
memfree=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
conn=$(ss -s | awk '/^TCP:/ {print $2}' | tr -d ',')
echo "$ts,$load1,$memfree,$conn" >> /var/lib/monitor/metrics.csv
# 两周后看分位数:sort 后取第 95% 位置
awk -F, 'NR>1 {print $2}' /var/lib/monitor/metrics.csv | sort -n | \
awk '{a[NR]=$1} END {print "load1_p95=" a[int(NR*0.95)]}'一台机器一个指标跑两周就知道它的正常长什么样了。这个过程不需要任何监控系统,几十行 shell 加一个 CSV 就够。等数据攒起来了,你会发现最该告警的往往不是你以为的那个指标——比如很多个人站真正先出问题的是 TCP 连接数而不是 CPU,因为 PHP-FPM 的进程堆积会先把连接占满。
六、告警疲劳本质上是个设计问题
很多人把「告警太多所以我不看了」归咎于自己不够自律,其实不是。人在面对重复的、无行动价值的信号时,忽略它是正确的本能反应。所以治告警疲劳的方法是把无行动价值的告警删掉,而不是要求自己更有毅力。
一条实用的判据:收到这条告警后,如果你过去十次的处理动作都是「确认一下,然后什么也没做」,那它就该被删掉或者降级。反过来,如果一条告警每次响你都得动手,那它响多少都不算多。
具体可以定期做一次「告警审计」:把过去一个月所有告警列出来,每条后面标一个「处理动作」。动作是「无」的,要么改成更精确的阈值、要么合并进日报、要么直接删。我自己每季度做一次,通常能砍掉三分之一。
还有一个容易忽略的点:重复告警要合并。同一个问题在没解决之前,每五分钟报一次,一个晚上能攒两百条,第二天你只会觉得吵。正确的做法是同一个检查项在未恢复前只报一次,恢复时报一条「已恢复」,中间的变化写进日报。这个状态需要落盘(比如在 /var/lib/monitor/state/ 下按检查项名存当前状态),实现不难,但对使用体验的改善极大。
七、从零开始的上线顺序
如果你现在的监控还停留在「想起来才看一眼」,不要一上来就上完整方案。按下面的顺序做,每一步都是一个可用的状态,而不是半成品:
第一步,先做三件最基本的:站点首页能不能打开(HTTP 200 且内容含一个固定字符串)、磁盘剩余天数、最近一次备份的时间。这三条覆盖了个人站 80% 的致命故障。
第二步,加心跳机制。在第一步的基础上,让机器每天主动报到一次。这一步做完,你才能相信「没消息就是好消息」。
第三步,把告警按 P0/P1/P2 分档,P0 走即时通知、其余走日报。同时给每条告警补上建议动作。
第四步,开始采指标落盘,两周后用分位数把阈值校准一遍。到这一步,噪声会自然降下来。
第五步,每季度做一次告警审计,砍掉没有行动价值的条目。
八、小结
监控告警这件事,难点从来不在工具,而在判定逻辑:什么该报、报多快、报给谁、报的时候带不带处置建议。把瞬时尖峰和趋势劣化分开、把磁盘改成算天数、给「监控自己挂了」也留一条心跳、按响应时间分级并把动作写进告警正文、用自己站点的历史分位数定阈值——这五件事做完,你会发现告警数量下来了,但真正有效的告警一条没丢。
对一个人维护的站点来说,监控的目标不是「全面」,而是「每一条响起来你都会动手」。能长期坚持下去的监控,一定是足够安静的监控。