logrotate 日志轮转实战:/var/log 撑爆根分区、配置被静默跳过与 journald 限额的完整排查

前几天帮一个朋友看他那台跑了两年的小站服务器,登录进去发现一个问题:/var/log 占了 38GB,根分区 40GB,可用空间只剩 900MB。他说没装什么大东西,也不知道怎么就满了。这类问题在个人站长里极其普遍,而且危险在于——磁盘满的那一刻,MySQL 可能直接拒绝写入,网站变成了只读,甚至崩掉。

更麻烦的是,logrotate 明明在跑,为什么日志还是涨到 38GB?这篇就把从定位到根治的完整链路写清楚,包括几个很多人不知道的 logrotate 陷阱。

第一步:先搞清楚是谁在占空间,别急着删

磁盘满了最忌讳的动作就是随手 rm -rf,删错了恢复不了。先看清楚再说。

# 分区使用率,看是哪个挂载点满了
df -hT

# 找出 / 下占用最大的目录(不跨文件系统,深度2层)
du -xhd2 / 2>/dev/null | sort -rh | head -20

# 定位到具体大文件(超过 200M)
find / -xdev -type f -size +200M -exec ls -lh {} \; 2>/dev/null | awk '{print $5, $9}'

du -x 的 -x 很关键,它保证不跨文件系统,否则如果你挂了一个网络盘或者另一个数据盘,du 会爬进去,慢得像蜗牛还可能卡死。

还有一种情况是 du 和 df 对不上:df 说用了 38G,du 加起来只有 12G。这几乎必然是已删除但仍被进程打开的文件。文件被 rm 了,但某个进程还持有 fd,inode 就不会释放,空间也不会归还。

# 找出已删除但被占用的文件
lsof +L1 2>/dev/null | awk '{print $1, $2, $7/1024/1024"MB", $NF}' | sort -k3 -rn | head -10

# 不装 lsof 的替代方案
ls -l /proc/*/fd/* 2>/dev/null | grep deleted | head -20

常见的主角是:被 rm 掉的 Nginx access.log、还在往旧文件写的 MySQL binlog、某个 daemon 的 stdout 重定向文件。处理办法不是删文件(已经删了),而是让进程重新打开:kill -HUP <pid> 对 Nginx 有效,MySQL 就得重启或者 flush logs。

第二步:判断日志该不该这么大

回到正题。38GB 的 /var/log 里,我一般按下面这张表判断正常与否:

文件正常量级异常信号
nginx/access.log每天几十到几百 MB一天几个 GB:被爬虫扫、被 CC、或日志格式里塞了太多字段
nginx/error.log几 MB 到几十 MB持续增长:某个 upstream 一直报错、fastcgi 超时刷屏
mysql/slow.log几十 MBGB 级:慢查询阈值太小或没开 log_queries_not_using_indexes
journal/受 SystemMaxUse 限制没有配置限额时会无限涨
syslog / messages几十 MB持续增长:内核在刷某条错误、硬件有问题
btmp / wtmpKB 到 MBbtmp 变大:正在被暴力破解 SSH

这里有个信号特别值得注意:如果 error.log 在持续增长,别急着压缩,先 tail -f 看几秒。我见过一个站 error.log 一天 4GB,原因是 fastcgi 的 connect timeout 太短,PHP-FPM 队列一满就成批报错,每秒上千条。日志只是症状,根因在容量。

第三步:logrotate 为什么没兜住

这是本文的核心。logrotate 确实在跑,但没生效,原因通常是下面五类之一。

原因一:配置文件的属主或权限不对(最常见)

logrotate 会静默跳过它认为"不安全"的配置文件。/etc/logrotate.d/ 下的文件必须 root 所有,且不能是 group/world writable。如果你用宝塔或者某个面板上传过配置,权限可能是 644 但属主变成了 www,那么整份配置会被无声忽略。

# 检查配置安全性
ls -l /etc/logrotate.d/

# 日志里会明确说明被跳过
grep -i skipping /var/lib/logrotate/status /var/log/syslog 2>/dev/null | tail
journalctl -u logrotate --no-pager -n 50

判定标准:-rw-r--r-- 1 root root。只要属主不是 root,或者有 group/other 的写权限,logrotate 就不认。修法:chown root:root /etc/logrotate.d/xxx && chmod 644 /etc/logrotate.d/xxx。

原因二:copytruncate 用错,导致 Nginx 还在写旧 inode

很多人配置里写了 create 但没写 postrotate 里的 reload,或者反过来。Nginx 的日志句柄在 master 进程里持有,rotate 之后必须发信号让它重开:

/www/wwwlogs/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

注意 sharedscripts:如果不加,有多个日志文件时 postrotate 会被执行多次,每次都 reload 一次 Nginx,虽然无害但是浪费。delaycompress 的作用是让最新的那个 .1 文件不压缩,因为可能还有进程在写,压缩后写进去就损坏了。

还有一种配法是 copytruncate,它不需要 reload,直接复制后清空原文件。缺点是在复制和清空之间有一个极短的时间窗口,这几毫秒内写入的日志会丢。对 Nginx 这种支持信号的进程,用 USR1 更干净。

原因三:size 和 daily 同时写,语义是"或"

这是最容易被误解的一条。配置里同时写 daily 和 size 100M,很多人以为是"每天一次,但超过 100M 也切"。实际上 logrotate 的行为是:只要满足其中任一条件就轮转,而且 size 会覆盖时间条件——文件不到 100M 时,即使过了一天也不切。

如果你的日志增长慢,配了 size 100M,结果可能十几天都不轮转。更糟的是配了 maxsize 和 daily 之外的组合,容易产生意料之外的行为。建议:按天切就只写 daily,需要防止单日爆炸就加 maxsize 500M(maxsize 的语义是"超过这个量就切,但也保证按周期切",比 size 更合理)。

原因四:只 rotate 不删除,rotate 14 变成了 14 天但每个 2GB

rotate 14 留 14 份,但如果每天日志 2GB,那就是 28GB。压缩能降到 1/10 左右,所以 compress 一定要开。但压缩本身也吃 CPU,建议放在低峰期,用 su 指定用户避免权限问题。

原因五:手动跑 logrotate 和 cron 跑的不是一回事

很多人用 logrotate -f /etc/logrotate.conf 手动测试成功,就以为好了。实际上 cron 里跑的是 systemd timer 或者 /etc/cron.daily/logrotate,两者的环境变量、PATH 完全不同。手动能跑通不代表定时能跑通。

# 确认定时器状态
systemctl list-timers --all | grep logrotate
systemctl status logrotate.timer

# 看它上次实际执行的结果
grep logrotate /var/log/syslog | tail -20

# 调试模式:不实际执行,只输出它会做什么
logrotate -d /etc/logrotate.d/nginx 2>&1 | head -40

logrotate -d 的调试输出里,如果某个配置被跳过,会明确告诉你原因,这是最快定位手段。

第四步:journald 是另一个常被忽略的黑洞

systemd 的日志默认存在 /var/log/journal/ 的二进制文件里,logrotate 完全管不到它。它有自己的限额机制:

# 当前占用
journalctl --disk-usage

# 永久设置上限(写入配置)
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
SystemMaxUse=500M
SystemMaxFileSize=50M
MaxRetentionSec=30day
EOF

systemctl restart systemd-journald

# 立即回收
journalctl --vacuum-size=500M
journalctl --vacuum-time=30d

没有这些配置时,journald 默认按文件系统大小的 10% 来限制,40GB 的盘就是 4GB,而且这个 10% 是算在 /var/log/journal 所在文件系统上的。对于小盘 VPS,这个默认值可能比你预期的大得多。

第五步:真正的根治——限制源头

清空间是止血,不是治病。要让 /var/log 长期稳定,得从产生端控制。

Nginx 日志:如果访问量大,考虑用 buffer=32k flush=5s 减少 IO,或者干脆关掉 access_log 只留 error_log(静态资源站可以这么干)。如果被爬虫刷日志,用 map 把已知爬虫单独写到另一个文件,或者在 Nginx 层直接返回 444。

map $http_user_agent $loggable {
    default 1;
    ~*(semrush|ahrefs|mj12bot|dotbot) 0;
}
access_log /www/wwwlogs/access.log combined if=$loggable;

MySQL 慢日志:把 long_query_time 从默认 10 调到 1 或 2,反而会让慢日志变小,因为很多"10 秒以上"的查询在 1 秒阈值下也能被抓到,但不会像设置了 log_queries_not_using_indexes 那样把所有未走索引的查询全写进去——后者才是慢日志爆炸的头号原因,小表全表扫描也会被记录。

btmp 暴涨:说明 SSH 正在被爆破。改端口、禁用密码登录、上 fail2ban,比清日志重要得多。btmp 每月都在涨是一个很明确的安全信号。

# 看爆破来源
lastb | awk '{print $3}' | sort | uniq -c | sort -rn | head

# 清空 btmp(用 : 而不是 rm,避免丢 inode)
: > /var/log/btmp
chmod 600 /var/log/btmp

注意这里用了 : > file 而不是 rm file。前者清空内容保留 inode,正在写这个文件的进程不会出错;后者会让进程继续写一个已删除的 inode,空间不释放。

加一个磁盘水位告警,别等满了才知道

最后一步是监控。别等用户告诉你网站 500 了才发现磁盘满。一个十行的脚本 + cron 就够:

#!/bin/bash
# /usr/local/bin/disk-check.sh
THRESHOLD=85
df -h --output=pcent,target -x tmpfs -x devtmpfs | tail -n +2 | while read pct mnt; do
    n=${pct%\%}
    if [ "$n" -ge "$THRESHOLD" ]; then
        echo "磁盘告警: $mnt 已用 $pct" | mail -s "[$(hostname)] 磁盘告警" you@example.com
    fi
done

# /var/log 单独也检查一次增长速率
SIZE=$(du -sm /var/log 2>/dev/null | cut -f1)
[ "$SIZE" -gt 2048 ] && echo "var/log 超过 2GB: ${SIZE}MB" | mail -s "日志告警" you@example.com

阈值我建议设 85%,不要设 95%。因为从 85% 到 100% 往往只需要一次日志暴涨的时间,而你需要时间响应。另外把"增长率"也纳入监控:一个 /var/log 从 2GB 涨到 6GB,比它一直是 6GB 更值得警惕。

收尾:一套定期体检清单

把这套做成月度例行检查,五分钟就能跑完:

  1. df -h 看水位,任何一个分区超 80% 就处理
  2. du -xhd2 /var/log | sort -rh | head 看日志排名
  3. logrotate -d /etc/logrotate.conf 看有没有被跳过的配置
  4. lsof +L1 看有没有已删除未释放文件
  5. journalctl --disk-usage 看 journal 是否超限
  6. lastb | wc -l 看爆破规模
  7. 确认 MySQL slow.log、Nginx error.log 过去七天的增长曲线

磁盘满这件事,本质上从来不是"空间不够",而是"没人管"。一个 40GB 盘跑一个日 PV 几千的个人站,绰绰有余。它满,是因为日志没人轮转、journal 没设限额、慢日志阈值配错,三个小疏忽叠加了两年。把上面这几处配置一次改对,之后你几乎可以忘掉这件事——这才是运维应该追求的状态:不是天天救火,而是让火没机会烧起来。

Last modification:October 18th, 2026 at 07:23 pm

Leave a Comment