跳转至

一次日志分区写满的完整复盘

这一篇复盘「/var/log 被写满,服务没报错却坏了」的典型故障。你的盘可能是数据盘、可能是别的目录,但诊断思路通用:服务不崩不等于服务没坏,先看盘、再找是哪个日志在暴涨、最后别被「删了没释放」骗了。

故障概述

下午起,一个核心应用开始「时好时坏」:部分接口返回 500,写入数据库报错,但进程一直在跑、CPU 占用正常。看起来像偶发故障,重启了几次都没好。最后用 df -h 一看——根分区 / 已 100%,/var/log 单独分区也 100%。所有「坏掉」的功能,背后都是写不进磁盘。

影响

  • 写日志/写临时文件/写缓冲的操作全部失败:接口 500、入库失败、定时任务报错。
  • 读操作(查库、读配置)大多正常,所以看起来「一半好一半坏」——这是最迷惑人的地方。
  • 持续约 3 小时,从首次偶发 500 到清理分区并恢复写入。
  • 因为磁盘满,连告警日志都写不进去,监控平台只看到「接口 500 增多」,没有直接「磁盘满」信号。

时间线

时间 事件
T+0 首个接口偶发 500,重启服务短暂恢复
T+45 500 增多、入库报错,怀疑应用 bug 开始查代码
T+90 确认进程没崩,转向系统层排查
T+120 df -h 发现 /var/log 100%,定位到磁盘
T+150 du -sh /var/log/* | sort -h 找出暴涨日志,确认是应用刷错误栈
T+165 清理旧日志、触发 logrotate,恢复写入
T+180 给 /var/log 加独立监控告警,复盘收尾

根因

真正的触发点是日志量暴涨:应用在故障窗口内进入了「错误重试循环」,每重试一次打一条完整错误栈,/var/log/app/error.log 以每分钟几十 MB 的速度增长,把 /var/log 分区写满。叠加两个放大器:

  1. logrotate 未覆盖该路径:轮转配置只配了通用 /var/log/*.log,应用自己的子目录 app/error.log 没被任何规则匹配,等于从不轮转,只进不出。
  2. 无磁盘使用率监控:当时只监控了「接口可用性」,没监控「磁盘剩余空间」;等 500 出现时,盘其实早就满了。

关键判据(复盘里的分水岭):df -h 显示 /var/log 100%,而 touch /var/log/test 报 No space left on device——这是写入失败,不是应用逻辑 bug。

诊断里最容易踩的几个坑

这次拖慢恢复的不是「查不到」,而是方向错了。几个真实教训:

  1. 先看盘,再看代码。我一开始怀疑是应用 bug,翻代码翻了一个多小时。冷静下来 df -h 一眼就锁定磁盘。服务「时好时坏」先跑系统层,别急着 debug 业务。
  2. 进程没崩 ≠ 服务没坏。很多服务对「写日志失败」是静默降级的——日志写不进去,它不退出,只是新请求报错。只看 ps 判断「服务正常」会漏掉整个磁盘类故障。
  3. du 加起来对不上。df 显示 100%,du -sh /var/log/* 加起来却不到分区大小——这是「删了没释放」。某个进程仍持有已删除日志的句柄,lsof +L1 才能看到它。
  4. 重启不是修复。重启服务只是让它重新打开日志文件、临时释放句柄,但如果日志还在暴涨,盘很快又会满。要治的是日志源,不是重启。
  5. 轮转配置要覆盖真实路径。只配 /var/log/*.log 会漏掉应用子目录。用 logrotate -d 干跑,逐条看哪些文件根本没被处理。

修复

一步步定位,从最容易的开始:

# 1) 先看盘——服务「时好时坏」第一件事
df -h
# 根分区 / 与 /var/log 都 100% 时,基本就是写不进去
touch /var/log/test 2>&1        # No space left on device → 确认满盘

# 2) 找出是哪个日志在暴涨
du -sh /var/log/* 2>/dev/null | sort -h | tail
# 对可疑大文件采样看增长速率
du -sh /var/log/app/error.log
sleep 30 && du -sh /var/log/app/error.log   # 涨了 → 还在刷

# 3) 找「删了没释放」的句柄
lsof +L1 2>/dev/null | grep -iE 'log|deleted' | head
# 有 deleted 条目 → 进程还持有已删文件,重启或让它 reopen

临时止血:先删旧日志释放空间(保留最近一份),再手动触发轮转:

# 保留最近 3 份,其余删掉(先确认没有进程在用)
find /var/log -name '*.log' -mtime +3 -delete
# 手动轮转(不 compress,先腾空间)
logrotate -f /etc/logrotate.conf
# 释放被删未释放的句柄:重启持有者
systemctl restart <the-app>
# 确认写入恢复
df -h && echo ok > /var/log/test && rm /var/log/test

彻底解决:治日志源 + 补轮转 + 加监控。先到应用侧修掉「错误重试循环」这个刷日志根因,再给 app/error.log 补一条轮转规则:

# /etc/logrotate.d/app
/var/log/app/*.log {
  daily
  rotate 7
  compress
  delaycompress
  missingok
  notifempty
  copytruncate
}

防再发

这次事故暴露的三件事,逐条上护栏:

  1. 磁盘使用率监控(第一护栏):不只监控接口可用性,要监控磁盘剩余空间。给每个关键分区加告警——85% 预警、90% 严重告警、95% 紧急。这是「服务没报错却坏了」的最早探测器。
  2. logrotate 规则补全:遍历所有日志路径,确认每条都有轮转规则;用 logrotate -d 定期干跑校验,防「某个路径从没被处理」。
  3. 日志量看板与限流:监控各日志增长率(MB/min),对异常增长提前告警;应用侧给 debug/错误栈输出加速率限制,防重试循环把盘刷爆。
  4. 日志单独分区:把 /var/log 或应用日志放独立分区,避免日志满拖垮根分区和数据分区。
  5. 故障演练:定期模拟「磁盘 95%」,验证监控能否在 T+5 内告警、清理流程是否真的生效。

给磁盘加一个最朴素的监控——比「等接口 500 了再报警」早一步:

# 每 5 分钟检查关键分区使用率,超阈值即告警
cat > /usr/local/bin/check-disk.sh <<'EOF'
#!/bin/bash
THRESHOLD=85
df -h | awk -v t=$THRESHOLD 'NR>1 && $5+0 >= t {print $6" "$5" used"}' | \
while read -r line; do
  echo "DISK HIGH: $line" | mail -s "Disk alert" [email protected]
done
EOF
chmod +x /usr/local/bin/check-disk.sh
# crontab:*/5 * * * * /usr/local/bin/check-disk.sh

这个脚本的价值不在「复杂」,而在把磁盘剩余空间本身变成监控对象。它抓到的是「盘快满了」这一层,而这层往往比应用报错早几个小时暴露问题。进阶版可以接 Prometheus node_exporter 的 node_filesystem_avail_bytes,直接进告警规则。

复盘结论

「服务没报错却坏了」先看盘——进程在跑、CPU 正常,不代表服务没坏。诊断顺序永远是:df -h 看盘 → du -sh 找暴涨日志 → lsof +L1 查删了没释放 → 治日志源而非重启。最容易被忽略的是 「删了没释放」:df 满但 du 对不上,说明有进程还握着已删文件的句柄。

复盘到最后,最值钱的一句话是:监控的对象错了,反应就永远慢半拍。 我们以前「监控接口可用性」,它只在业务已经报错时才报警;这次之后我们「监控磁盘剩余空间」,它能在盘满前几个小时就把问题暴露出来。把监控前移一层,往往比多写几段告警规则更能缩短 MTTR——这也是这次事故给我最大的收获:不是技术多难,而是把「该看的地方」看对。


关联阅读:[Linux 磁盘 IO 监控../../monitor/linux-disk-io-monitor.md) · [Loki 日志聚合../../monitor/loki-log-aggregation.md) · [Prometheus TSDB 运维../../monitor/prometheus-tsdb-operations.md)