“/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 上执行:
kill 12300
kill 12345
再次检查:
ps aux | grep health_check
没有输出。
“停了。”小林说,“Ubuntu 上的日志应该不再涨了。”
他回到 Ubuntu,观察日志:
sudo tail -f /var/log/nginx/access.log
终端不再刷新。
“不涨了。”小林松了口气。
七、清理日志:truncate 而不是 rm
“现在清理日志。”小林说。
他想起老周说过:“清理正在被进程写入的日志,不要用 rm,用 truncate。”
“为什么?”
“rm 只是删掉目录项,文件本身还在,进程还在往里写,磁盘空间不会释放。truncate 把文件大小清零,进程继续写,但空间释放了。”
小林执行:
sudo truncate -s 0 /var/log/nginx/access.log
sudo truncate -s 0 /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 -s 0 /proc/12345/fd/3
“或者直接杀掉进程。”
kill 12345
小林试了,磁盘释放了。
排错思路总结:
rm了日志,df显示空间没释放,说明有进程还占着文件句柄。- 用
sudo lsof +L1找出被删除但被占用的文件。 - 输出中
(deleted)和NLINK=0是标志。 - 解决:重启占用进程,或
truncate -s 0 /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,每一个都有讲究。
下一集:《日志切割:别让日志把磁盘撑爆》