日志轮转没生效导致磁盘爆满:logrotate 四个静默失效点与磁盘告警脚本实战

一、先说结论:日志不是"写满了磁盘",而是"盘被吃光时你已经没机会登录了"

几乎每个个人站长都遇到过一次这样的时刻:网站突然 502,SSH 连上去敲命令卡半天,敲了 df -h 才看到根分区 100%。再查下去,凶手指向 /var/log 下面一个十几 GB 的 Nginx 访问日志,或者一个几个月没轮转的 journal 目录。更尴尬的是,等你发现问题时,日志文件已经把空间吃干,很多程序连写日志的资格都没有了,反过来让故障现场变成一片空白。

这篇文章不讲"日志要轮转"这种废话,只讲一件事:在一台真实跑着 Nginx + PHP-FPM + MySQL 的单机服务器上,日志轮转到底会在哪几个地方静默失效,以及每一处该怎么验证。这些都是我自己的服务器上踩出来的,改动都很小,但每一条都对应一次真实的深夜救火。

二、第一个静默失效点:logrotate 配置写了,但 cron 从来没跑

最常见的错觉是"我在 /etc/logrotate.d/nginx 里写了配置,所以它在轮转"。实际上 logrotate 是被定时任务驱动的,而这个定时任务本身可能根本没在运行。Debian/Ubuntu 上新版用的是 logrotate.timer(systemd timer),老版本用的是 /etc/cron.daily/logrotate。这两者你至少要确认一个活着。

验证顺序建议这样走:

# 1. 先看 timer 是否存在且在跑
systemctl list-timers --all | grep -i logrotate
systemctl status logrotate.timer

# 2. 如果没有 timer,看 cron.daily 那条还在不在
ls -l /etc/cron.daily/logrotate
grep -r logrotate /etc/cron.d/ 2>/dev/null

# 3. 关键一步:看上次到底跑没跑成功
cat /var/lib/logrotate/status | head -20

第三步是重点。/var/lib/logrotate/status(部分发行版在 /var/lib/logrotate.status)记录了每个日志文件"上一次被轮转的日期"。如果里面某个文件的日期停在两三个月前,那说明 logrotate 对它根本没生效——不是配置语法问题,往往是"这个路径压根没被任何配置文件覆盖到"。很多人手工把 Nginx 日志改到 /data/logs/nginx/,却忘了 logrotate 的配置里还写着 /var/log/nginx/*.log,结果新路径下的日志永远不轮转。

还有一个细节:cron.daily 是通过 anacron 或 systemd timer 触发的,如果服务器长期处于异常状态(比如时钟漂移、容器里没有 init 系统)导致 cron 服务停摆,轮转也就一起停了。这种时候 systemctl status cron 是必查项。

三、第二个静默失效点:copytruncate 与 Nginx 的原子性打架

轮转 Nginx 日志时你会看到一个分歧很大的选项:create 模式(默认)先把日志改名,再给 Nginx 发 USR1 信号让它重新打开文件句柄;copytruncate 模式则是复制一份再把原文件截断。

很多人为了避免"忘记发信号导致日志写进已改名的旧文件",直接选了 copytruncate。这个选择的代价是:时间窗口内的日志会丢。copytruncate 的机制是先 copy 再 truncate,在 copy 完成到 truncate 之间的这段时间里,Nginx 仍然在往原文件尾部追加,truncate 一执行,这段追加的内容就凭空消失了。访问量越大,丢得越多。如果这个日志是你做 SEO 流量分析、排查入侵的唯一依据,丢日志就是丢证据。

正确的 Nginx 写法是用 create 加 postrotate:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid) 2>/dev/null || true
    endscript
}

这里每一行都有用处,逐条说明:

delaycompress 是必须的。如果不加它,logrotate 会在改名的同时压缩,而 Nginx 在这个瞬间可能还没收到信号,仍在往那个文件名写数据;压缩工具把这个文件封死后,Nginx 的写入就流向了压缩流,日志内容直接损坏。延迟一轮压缩可以彻底避开这个窗口。

sharedscripts 也是必须的。Nginx 的日志配置是一个通配符匹配多个文件,不加 sharedscripts 的话,logrotate 会为每一个匹配到的文件各执行一次 postrotate,等于发了十几次 USR1,反复让 Nginx 重开文件句柄。虽然不致命,但纯属浪费,而且在高负载时会放大抖动。

create 0640 www-data adm 里的属主属组要跟你的实际运行用户一致。如果写错,会出现"日志文件建出来了但 Nginx 无权写入",然后你在 /var/log/nginx/error.log 里看到权限拒绝,而错误日志本身也可能因为同样原因写不了,陷入死循环。

四、第三个静默失效点:systemd journal 吃掉的空间,logrotate 管不到

这一点几乎每个人都漏。/var/log/journal/ 归 systemd-journald 自己管理,logrotate 完全不碰它。默认配置下 journal 的容量上限是文件系统的 10%,听起来不多,但如果你的根分区只有 40 GB,那就是 4 GB;如果服务器跑了很久而且日志写入频繁,这 4 GB 会在某一天悄悄占满。

容量由 /etc/systemd/journald.conf 里的三个参数决定:

SystemMaxUse=500M
SystemMaxFileSize=50M
MaxRetentionSec=2week

改完 systemctl restart systemd-journald 生效。注意 SystemMaxUse 是硬上限,journald 在超限时会自动删最旧的归档,这个行为比 logrotate 更主动,反而省心。真正危险的是那些从来不配这个值的服务器——它走默认策略,磁盘越紧张越容易被 journal 补上最后一刀。

另外一个常被忽略的开销是 journalctl --vacuum-size 这类手工清理。有人发现磁盘满了,直接 journalctl --vacuum-time=1d 清了一波,看着空间回来了,但没改配置文件,下周同样的事情再来一遍。清理是应急,改配置才是根治。顺手可以把 journal 的持久化关掉(Storage=volatile),日志只留在内存里,重启即失,对单机小站完全够用,还能省掉磁盘写入的抖动。

五、第四个静默失效点:轮转成功了,但旧日志没被删,只是换了个名字躺在那

这是一个视觉上的陷阱。你在日志目录里 ls 一下,看到 access.log、access.log.1、access.log.2.gz……看起来很正常。但如果你用的通配符或保留策略写错了,旧文件会以一种几乎不可见的方式堆积。

典型错误是 rotate 14 配 compress 但没配 delaycompress,前面已经说过。另一个典型是路径用了软链接。比如 /var/log/nginx 本身是一个指向 /data/logs/nginx 的软链接,而 logrotate 的 create 会在软链接解析后的真实目录里建文件,但删除旧文件时可能按软链接路径去数,导致计数逻辑混乱,旧的压缩包永远达不到 rotate 上限。遇到这种情况,建议直接把配置里的路径写成真实路径,不要依赖软链接。

用这条命令一眼看清日志目录的真实占用分布:

du -sh /var/log/* 2>/dev/null | sort -rh | head -15
find /var/log -type f -size +500M -exec ls -lh {} \; 2>/dev/null

第二条命令比第一条更有用——它绕过目录结构,直接找"哪个文件凭一己之力吃掉了半个盘"。日志轮转出问题时,你几乎总能在它的输出里看到罪魁祸首。

六、把日志轮转变成可监控的指标,而不是等它爆炸

上面所有问题都有一个共同特征:它们在发生时完全安静,只有磁盘满了才一起爆发。所以真正的工程解法不是在 logrotate.conf 里反复检查语法,而是把"磁盘与日志增长"变成一个每天会被看见的指标。

最省事的方式是写一个十行的检查脚本,塞进 cron,超阈值就发一封邮件或推送到你的通知渠道:

#!/bin/bash
# /root/check_log_disk.sh —— 放进 /etc/cron.daily/ 或每分钟 cron
THRESHOLD=85
USAGE=$(df --output=pcent / | tail -1 | tr -d ' %')
LOGSIZE=$(du -sm /var/log 2>/dev/null | cut -f1)

if [ "$USAGE" -ge "$THRESHOLD" ]; then
    echo "磁盘使用率 ${USAGE}%,/var/log 占用 ${LOGSIZE} MB" \
      | mail -s "[$(hostname)] 磁盘告警" you@example.com
    du -sh /var/log/* 2>/dev/null | sort -rh | head -10
fi

如果想要更细的粒度,可以把"日志文件的最后修改时间"也纳入检查。一个健康的、每天在轮转的日志目录,最新日志文件的时间必然是当天的。如果某个日志文件的 mtime 停在几天前,说明写入它的进程已经死了,或者轮转后它被换掉了而新文件落在了别的地方——这两种情况都值得看一眼。

最后给一条经验值:对于个人站长级别的站点(日 PV 几万以内),daily 轮转 + rotate 14 + compress + journal 限 500M,一套下来日志占用通常稳定在几百 MB,永远不会成为问题。真正让日志失控的从来不是"策略不够激进",而是"策略没生效却没人知道"。所以请把这篇文章里第四条命令(find /var/log -type f -size +500M)和上面的检查脚本存下来,它们比任何精美的监控面板都更早救你一次。

Last modification:September 25th, 2026 at 09:24 pm

Leave a Comment