【前情提要】Postfix 配好了,Alertmanager 能发邮件了。小林第一次收到告警邮件,兴奋得不行。但老周说:“光收邮件不够,你得知道告警来了之后该干什么。”当天凌晨,小林的手机响了——磁盘告警。
一、凌晨两点的邮件
手机震动把小林从睡梦里拽了出来。他迷迷糊糊摸过手机,屏幕亮得刺眼:
发件人:xiaolin@qq.com
主题:[FIRING:1] HighDiskUsage (warning)
时间:2026-09-14 02:13:47
内容:
根分区使用率超过 85%
根分区使用率已持续 5 分钟超过 85%,当前值 87.3%
小林盯着屏幕愣了五秒,脑子才慢慢转起来。
“磁盘满了?”
他翻了个身,想继续睡:“明天再说吧……”
然后他想起老周上周说的话:
“告警响了,你不处理,它就会一直响。而且明天可能就不是 87% 了,是 97%。到时候你连 SSH 都连不上,因为系统写不了日志、建不了临时文件。”
小林一个激灵坐起来,打开电脑,连上 SSH。
二、先看磁盘:df -h
“第一件事,看哪个分区满了。”小林自言自语,回忆老周教的命令。
df -h
输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 44G 3.5G 93% /
/dev/sda2 1.0G 100M 900M 10% /boot
tmpfs 2.0G 0 2.0G 0% /dev/shm
“根分区 93% 了。”小林说,“刚才告警是 87%,现在已经 93% 了。涨得这么快?”
他看了一眼时间:凌晨 2 点 20 分。距离告警触发才 7 分钟。
“什么东西涨这么快?”
三、找大目录:du -sh
“老周说过,df 看整体,du 看细节。”小林回忆。
sudo du -sh /* 2>/dev/null | sort-rh | head -10
输出:
12G /var
8.5G /usr
3.2G /home
1.1G /opt
...
“/var 最大,12G。”小林说,“进去看看。”
sudo du -sh /var/* 2>/dev/null | sort-rh | head -10
输出:
10G /var/log
1.2G /var/lib
...
“/var/log 10G。”小林说,“日志怎么这么大?”
sudo du -sh /var/log/* 2>/dev/null | sort-rh | head -10
输出:
8.5G /var/log/nginx
800M /var/log/syslog
300M /var/log/journal
...
“Nginx 日志 8.5G。”小林惊了,“我博客才上线两周,哪来 8.5G 日志?”
sudo ls -lh /var/log/nginx/
输出:
-rw-r----- 1 www-data adm 8.3G Sep 14 02:20 access.log
-rw-r----- 1 www-data adm 200M Sep 14 02:20 error.log
“access.log 8.3G。”小林说,“而且还在涨。”
他看了一眼 error.log,200M,也不小,但跟 access.log 比,小巫见大巫。
四、看日志内容:是什么在刷
“先看看日志里是什么。”小林说。
sudo tail -20 /var/log/nginx/access.log
输出:
192.168.1.100 - - [14/Sep/2026:02:20:01 +0800] "GET /api/health HTTP/1.1" 200 2 "-" "curl/7.81.0"
192.168.1.100 - - [14/Sep/2026:02:20:01 +0800] "GET /api/health HTTP/1.1" 200 2 "-" "curl/7.81.0"
192.168.1.100 - - [14/Sep/2026:02:20:01 +0800] "GET /api/health HTTP/1.1" 200 2 "-" "curl/7.81.0"
...
“/api/health?”小林皱眉,“我博客是静态站点,没有 /api/health 这个路径啊。”
他又往下看:
192.168.1.100 - - [14/Sep/2026:02:20:02 +0800] "GET /api/health HTTP/1.1" 404 153 "-" "curl/7.81.0"
192.168.1.100 - - [14/Sep/2026:02:20:02 +0800] "GET /api/health HTTP/1.1" 404 153 "-" "curl/7.81.0"
...
“404 了。”小林说,“有人在用 curl 疯狂请求 /api/health。每秒好几个。”
他统计了一下:
sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort-rn | head -5
输出:
500000 192.168.1.100
1200 192.168.1.101
800 192.168.1.102
“50 万次请求,全是 192.168.1.100。”小林说,“这是我 Mac 的 IP。”
他愣住了。
“我 Mac 上有什么在跑?”
五、找到源头:ps 和 lsof
“先看 Mac 上有没有什么进程在发请求。”小林回到 Mac,打开终端。
ps aux | grep curl
输出:
xiaolin 12345 0.5 0.1 ... curl -s http://192.168.1.x/api/health
“有个 curl 进程。”小林说,“哪来的?”
他仔细看:
ps -p 12345 -o pid,ppid,command
输出:
PID PPID COMMAND
12345 12300 curl -s http://192.168.1.x/api/health
“父进程是 12300。”小林继续查:
ps -p 12300 -o pid,ppid,command
输出:
PID PPID COMMAND
12300 12200 /bin/bash /Users/xiaolin/scripts/health_check.sh
“health_check.sh?”小林说,“我什么时候写过这个脚本?”
他打开脚本:
cat ~/scripts/health_check.sh
内容:
#!/bin/bash
while true; do
curl -s http://192.168.1.x/api/health
sleep 1
done
“这……”小林想起来了。
上周他在练习写健康检查脚本,写了一个“每秒检查一次”的循环,测试完忘了关。终端关了,但脚本还在后台跑,一直请求 Ubuntu 的 /api/health。
“我写的脚本,把我自己的博客搞挂了。”小林哭笑不得。
六、应急处理:先止血
“先停掉脚本。”小林在 Mac 上执行:
kill12300
kill12345
再次检查:
ps aux | grep health_check
没有输出。
“停了。”小林说,“Ubuntu 上的日志应该不再涨了。”
他回到 Ubuntu,观察日志:
sudo tail -f /var/log/nginx/access.log
终端不再刷新。
“不涨了。”小林松了口气。
七、清理日志:truncate 而不是 rm
“现在清理日志。”小林说。
他想起老周说过:“清理正在被进程写入的日志,不要用 rm,用 truncate。”
“为什么?”
“rm 只是删掉目录项,文件本身还在,进程还在往里写,磁盘空间不会释放。truncate 把文件大小清零,进程继续写,但空间释放了。”
小林执行:
sudo truncate -s0 /var/log/nginx/access.log
sudo truncate -s0 /var/log/nginx/error.log
再次检查:
df -h
输出:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 36G 12G 75% /
“从 93% 降到 75%。”小林说,“释放了 8G。”
“但 truncate 是临时的。”小林自言自语,“明天日志还会涨。得配日志切割。”
他想起老周提过 logrotate,准备明天问。
八、排错挑战:rm 了日志,磁盘还是满的
第二天到工位,小林跟老周汇报:“周哥,昨晚磁盘告警,我处理了。”
“怎么处理的?”
“停了脚本,truncate 了日志。”
“不错。”老周点头,“但如果你当时用的是 rm 呢?”
小林想了想:“rm 之后,磁盘应该也释放了吧?”
“不一定。”老周说,“你试试。”
小林在 Ubuntu 上创建一个测试文件:
dd if=/dev/zero of=/tmp/test.log bs=1M count=100
“100M 的文件。”老周说,“现在用 tail -f 持续读它。”
tail -f /tmp/test.log &
“然后 rm 掉。”
rm /tmp/test.log
“现在看磁盘。”
df -h /tmp
小林看了一眼:“还是 100M 没释放。”
“对。”老周说,“因为 tail -f 还开着这个文件,文件句柄还在。rm 只是删了目录项,文件本身没删。你 rm 了日志,但 Nginx 还在写,空间不会释放。”
“那怎么找这种文件?”
“lsof +L1。”老周说,“列出所有被删除但还被进程占用的文件。”
sudo lsof +L1
输出:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
tail 12345 xiaolin 3r REG 8,1 104857600 0 123456 /tmp/test.log (deleted)
“看到了吗?”老周说,“(deleted) 标记,NLINK 是 0,说明文件被删了但还被占用。”
“那怎么释放?”
“重启进程,或者 truncate 这个文件。”
sudo truncate -s0 /proc/12345/fd/3
“或者直接杀掉进程。”
kill 12345
小林试了,磁盘释放了。
排错思路总结:
rm 了日志,df 显示空间没释放,说明有进程还占着文件句柄。
- 用
sudo lsof +L1 找出被删除但被占用的文件。
- 输出中
(deleted) 和 NLINK=0 是标志。
- 解决:重启占用进程,或
truncate -s0 /proc/PID/fd/FD。
- 清理正在写入的日志,用
truncate,不用 rm。
- 更根本的解决:配
logrotate,让日志自动切割。
“记住,”老周说,“rm 删文件,df 不释放,先 lsof +L1。这个坑,每个运维都踩过。”
九、告警恢复:[RESOLVED] 邮件
处理完磁盘,小林等了几分钟,QQ 邮箱收到一封邮件:
主题:[RESOLVED] HighDiskUsage
内容:
根分区使用率超过 85%(已恢复)
当前值 75%
“恢复了。”小林说。
“对。”老周说,“你处理完,Prometheus 下次评估规则时,条件不满足,告警自动恢复,Alertmanager 发恢复邮件。”
“那如果我不处理呢?”
“它会一直发。”老周说,“repeat_interval 默认 1 小时,每小时提醒你一次,直到你处理。”
“那如果我处理了,但忘了关静默呢?”
“静默过期自动失效。”老周说,“但如果你静默了没处理,告警恢复后,静默还在,那就不会有恢复邮件。静默不是处理,只是暂时闭嘴。”
小林记下:
- 告警恢复后,
send_resolved: true 会发 [RESOLVED] 邮件
- 未处理告警,按
repeat_interval 重复提醒
- 静默只是暂时屏蔽,不是处理
十、复盘:这次故障的根因
“现在复盘。”老周说,“这次故障,根因是什么?”
小林想了想:“我写了个死循环脚本,一直在请求博客。”
“对,这是直接原因。但根本原因呢?”
“根本原因……我没配日志切割?”
“对了一半。”老周说,“还有两个。”
“哪两个?”
“第一,你没有监控日志增长速度。你监控了磁盘使用率,但没监控‘日志每秒增长多少’。如果配了,你会在日志涨到 1G 的时候就发现,而不是等到 8.5G。”
“第二,你的脚本没有超时和退出机制。你写了个 while true,没有 timeout,没有 max_retries。这种脚本,一旦忘了关,就是灾难。”
小林在笔记本上写下:
- 直接原因:死循环脚本疯狂请求
- 根本原因一:日志没有切割
- 根本原因二:没有监控日志增长速度
- 根本原因三:脚本没有超时和退出机制
“那怎么改?”
“三个改进。”老周说,“第一,配 logrotate,明天教你。第二,加一条告警:‘日志每秒增长超过 10M 持续 1 分钟’。第三,你以后写脚本,while 循环必须加 timeout 和退出条件。”
小林记下。
十一、练习
- 练习一:用
dd 创建一个 200M 的测试文件,用 tail -f 持续读它,然后 rm 掉。用 df -h 确认磁盘空间没释放,用 sudo lsof +L1 找到被占用的文件,用 truncate 释放空间。
- 练习二:在 Ubuntu 上模拟一次磁盘告警:用
dd 填充根分区到 85%,观察 Prometheus 告警从 inactive → pending → firing 的过程。收到邮件后,用 truncate 清理文件,观察告警恢复。
- 练习三:写一个脚本
~/log_growth_check.sh,检查 /var/log/nginx/access.log 的大小,如果超过 1G,输出警告并记录到 ~/log_growth_warning.txt。用 cron 配置为每 5 分钟运行一次。
参考答案
练习一:
dd if=/dev/zero of=/tmp/test.log bs=1M count=200
tail -f /tmp/test.log &
rm /tmp/test.log
df -h /tmp
sudo lsof +L1 | grep test.log
sudo truncate -s 0 /proc/$(pgrep tail)/fd/3
kill $(pgrep tail)
df -h /tmp
练习二:
# 填充磁盘
dd if=/dev/zero of=/tmp/fill.img bs=1M count=10000
# 观察 Prometheus Alerts 页面
# 收到告警后清理
rm /tmp/fill.img
df -h
# 等待告警恢复
练习三:
nano ~/log_growth_check.sh
内容:
#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
THRESHOLD=$((1024 * 1024 * 1024)) # 1G
if [ -f "$LOG_FILE" ]; then
SIZE=$(stat -c %s "$LOG_FILE")
if [ "$SIZE" -gt "$THRESHOLD" ]; then
echo "$(date): $LOG_FILE 大小超过 1G,当前 $((SIZE / 1024 / 1024))M" >> ~/log_growth_warning.txt
fi
fi
chmod+x ~/log_growth_check.sh
crontab -e
添加:
*/5 * * * * /home/devops/log_growth_check.sh
十二、下集预告
磁盘告警处理完了,复盘也做了。但老周说:“你昨晚是运气好,日志只是大,没把系统写死。如果磁盘 100% 了,你 SSH 都连不上。”小林问:“那怎么办?”老周说:“配 logrotate,让日志自己切割。”小林打开 logrotate 文档,准备配置。但他不知道,daily、rotate、postrotate、copytruncate,每一个都有讲究。
下一集:《日志切割:别让日志把磁盘撑爆》