logrotate 日志切割实战:Nginx 信号重开、copytruncate 取舍与「切了但磁盘没变小」的真相

日志不切割,是在给服务器埋雷

很多站长对日志的态度是"能记就行",直到某天磁盘被一个几十 GB 的 access.log 撑满,服务全线报错,才回头去查这个文件是什么时候长成这样的。日志本身不复杂,难的是让它"可控地增长、可控地轮转、可控地删除"。Linux 世界里干这件事的标准工具是 logrotate,它几乎是所有发行版默认自带的,但默认配置只覆盖了系统日志,你自己的 Nginx、MySQL、应用日志基本没被管起来——这才是隐患的根源。

这篇文章不堆手册,而是从"一个真实站长会遇到什么"出发:日志怎么切才不丢数据、切完怎么让 Nginx 重新打开文件、压缩保留策略怎么定、以及为什么"切割成功但磁盘没变小"这种最气人的情况会发生。

先理解 logrotate 的运行机制

logrotate 不是常驻进程,而是被 cron 或 systemd timer 每天唤起一次的批处理工具。Debian/Ubuntu 上它由 /etc/cron.daily/logrotate 触发,CentOS/RHEL 上由 logrotate.timer 触发。每次运行时,它读取 /etc/logrotate.conf 以及 /etc/logrotate.d/ 目录下的所有片段,逐个判断每个日志是否满足轮转条件(按天、按周、按大小)。

所以排查 logrotate 问题的第一句话永远是:它到底跑了没有。查看运行记录:

cat /var/lib/logrotate/status
# 或
cat /var/lib/logrotate.status
systemctl status logrotate.timer
journalctl -u logrotate --since today

status 文件记录了每个日志"上次轮转的日期",这是判断"为什么没切"的直接证据。如果某个日志的 last rotated 一直没更新,说明它根本没被任何配置匹配到。

一份给 Nginx 用的 logrotate 配置

Nginx 默认会在 /var/log/nginx/ 下写 access.log 和 error.log。给它配一份靠谱的轮转规则,放在 /etc/logrotate.d/nginx:

/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)
    endscript
}

逐项解释这些指令为什么重要。daily 表示每天一次;rotate 14 表示保留 14 份历史;compress 开启 gzip 压缩,日志压缩比通常能到 10:1 以上,对磁盘是实打实的节省;delaycompress 是"延迟一轮再压缩",因为刚切出来的那份可能还有进程正在写;notifempty 表示空文件不轮转,避免产生一堆 0 字节的垃圾;create 0640 www-data adm 保证新文件权限正确,否则 Nginx 可能因权限问题写不进去。

最关键的两条是 sharedscripts 和 postrotate。Nginx 是"文件句柄"模型:进程打开 access.log 后一直持有这个 fd,你就算把文件改名,它依然往旧 inode 写。所以切割后必须通知 Nginx 重新打开日志文件——kill -USR1 就是干这个的。sharedscripts 保证这段脚本所有匹配的日志只执行一次,而不是每个文件跑一遍。

切割了但磁盘没变小?inode 与 fd 的锅

这是最经典、也最让人抓狂的现象:明明 logrotate 执行了,日志也改名压缩了,df 一看磁盘占用纹丝不动。原因基本有两个。

第一个是"进程仍持有旧文件句柄"。切割时把 access.log 重命名成 access.log.1,但 Nginx 没收到信号,继续往已经改名的旧 inode 里写。你在目录里看不到这个文件在增长(因为名字变了),但它占用的空间一点没释放。用 lsof 一看就现形:

lsof +L1 | grep -i deleted
lsof | grep nginx | grep -i log

第二类原因是分布式文件系统的延迟回收,或者被 du 的统计口径误导——du 默认按硬链接只算一次,而 df 按块统计。这种情况下用 du --apparent-size 和 du -sh 对比一下,往往能看出差异。

解决第一个问题的通用手法是"复制截断法"(copytruncate)。它先复制一份日志,再把原文件清零,进程的 fd 依然指向同一个文件,因此不需要发信号:

/var/log/myapp/*.log {
    daily
    rotate 7
    copytruncate
    compress
    delaycompress
    missingok
    notifempty
}

copytruncate 的代价是有极短的时间窗,窗口内新写入的日志可能丢失。所以它适合"不支持重新打开文件"的应用,而 Nginx、Apache 这类支持 USR1 信号的,优先用 postrotate 方案,更严谨。

按大小切割:搞定"一天跑出 10G"的日志

daily 只保证一天一次,但如果你的站点被打或爬虫狂刷,一天就能跑出 10GB,等到午夜才切,磁盘早就报警了。这时应该用 size 触发,或者 daily 与 size 并用(logrotate 里同时写两者时,只要满足任一条件就触发——注意这一点和很多人直觉相反):

/var/log/nginx/access.log {
    size 500M
    rotate 20
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

但 size 有个已知限制:logrotate 是由每日定时任务唤起的,如果它一天只跑一次,size 也只在那一刻被检查。想做到"接近实时"的按大小切割,得额外加一个更高频的触发。可以单独起一个每小时运行的任务:

# /etc/cron.hourly/logrotate-size
#!/bin/sh
/usr/sbin/logrotate /etc/logrotate.d/nginx-highfreq
# 或者强制以 size 判断,忽略时间
/usr/sbin/logrotate -f -s /var/lib/logrotate/status /etc/logrotate.d/nginx-highfreq

注意 -s 指定独立的 status 文件,避免和高频和每日任务互相干扰"上次轮转时间"的判断。

测试与调试:-d 和 -f 怎么用

logrotate 配置里有个拼写错误,可能几个月都不会报错——因为它只在真正轮到该文件时才解析那一行。所以每次改完配置,务必用 debug 模式预演:

logrotate -d /etc/logrotate.d/nginx
# -d 是 dry-run,只打印"打算做什么",不实际执行

确认逻辑无误后,再强制跑一次验证真实效果:

logrotate -f -v /etc/logrotate.d/nginx
ls -lh /var/log/nginx/

-v 会输出详细过程,包括每个文件为什么被切或没被切。如果看到 "log does not need rotating",说明轮转条件(时间或大小)没满足;如果是 "skipping ... because parent directory has insecure permissions",那就要去修目录权限——这是 logrotate 一个安全特性,父目录不能被组或其他用户写。

保留策略与合规:不是越久越好

日志保留两周是常见默认,但实际该保留多久,取决于三个现实约束:磁盘容量、故障复盘的追溯窗口、以及合规要求。给几条经验值。

访问日志(access.log)通常保留 14~30 天就够,爬虫和攻击的追溯用不到更久,而且压缩后占用很小。错误日志(error.log)建议保留 30~90 天,因为它记录了 5xx、超时、上游异常这些需要复盘的线索。审计类日志(如 SSH 登录、sudo 操作)在部分场景下有合规要求,可能需要保留 6 个月到 1 年,这类应该单独配置并考虑异地归档,而不是堆在本机磁盘上。数据库慢查询日志保留 30 天通常足够支撑性能调优。

特殊保留需求可以用 olddir 把历史日志挪到另一个盘,或用 maxage 按天数而非份数控制:

/var/log/myapp/*.log {
    daily
    rotate 30
    maxage 90
    olddir /data/log-archive/myapp
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

olddir 目录必须和日志同分区,否则 logrotate 无法用 rename 挪动文件,会退化成复制,效率骤降还可能失败。

和 journald 的分工,别让日志写两遍

现代发行版上,systemd 的 journald 也在收集日志。很多人踩过一个坑:应用同时把日志写进 /var/log/(被 logrotate 管)和 journald(被 journald 自己的容量上限管),结果磁盘被两份副本占着。先看 journald 实际占了多少:

journalctl --disk-usage
# /etc/systemd/journald.conf 中限制
# SystemMaxUse=500M
# MaxRetentionSec=2week

如果你只打算用文件日志加 logrotate,就把应用的 stdout 日志输入关掉或降低 journald 的上限;反之若依赖 journalctl 检索,就确保 SystemMaxUse 有个明确封顶,别让它无限增长。两条路选一条走通,比"两套都开着、谁都没管"稳得多。

日志管理听起来琐碎,却是服务器稳定性里"回报率最高"的一环:一份到位的 logrotate 配置,可能就帮你避免了一次磁盘写满导致的整站宕机。花十分钟把你的应用日志纳入 logrotate,值得。

Last modification:October 7th, 2026 at 01:24 pm

Leave a Comment