logrotate 日志轮转实战:配置模板、postrotate 重读信号、state 文件与常见不生效排查

日志不管,磁盘迟早爆:logrotate 是站长的兜底工具

几乎每个 Linux 服务都会把运行日志写到文件里:Nginx 的 access.log、PHP-FPM 的慢日志、MySQL 的 error log、应用的运行日志。这些文件只增不减,一个访问量中等的站,Nginx 的 access.log 一天涨几百兆很常见。你不管它,三个月后一块 40G 的盘就被日志吃满,接着 MySQL 写不进去、PHP 无法写缓存,整站 500。而 logrotate 就是系统自带的、专门解决这件事的轮转工具:按时间或大小切割日志、压缩旧文件、删掉过期文件,全自动。

很多人对 logrotate 的理解停留在「系统默认已经配好了」,直到某天磁盘满了才发现默认配置只覆盖了系统日志,自己装的 Nginx、自编译的 MySQL、自定义应用的日志根本没纳管。本文按个人站长的真实需求,把 logrotate 的配置、常见服务的落地写法、切割后的重读信号,以及常踩的坑讲清楚,全部在 Debian 12 上实测。

logrotate 是怎么工作的

logrotate 本身不是常驻进程,它靠 cron(或 systemd timer)每天被拉起一次,读取 /etc/logrotate.conf 和 /etc/logrotate.d/ 目录下的配置,逐个检查每个日志是否到达轮转条件。理解的三个核心动作就够了:

  • 轮转(rotate):把当前 access.log 改名成 access.log.1,同时新建一个空的 access.log。服务继续往新文件写。
  • 压缩(compress):对轮转出来的旧文件做 gzip,省空间。
  • 删除(rotate N):只保留 N 个历史文件,更老的删掉。

关键点在于「服务继续往新文件写」。如果服务没有在轮转后重新打开日志文件,它会继续往已经被改名、被压缩、甚至被删除的旧 inode 写,新文件则一直是空的——这就是「日志切割后不生效」最常见的根因,后面细讲。

看现状:先搞清哪些日志已经纳管

动手前先摸底。/etc/logrotate.d/ 下每个文件管一类服务,直接列出来看:

ls -l /etc/logrotate.d/
cat /etc/logrotate.d/nginx 2>/dev/null

再看 logrotate 自己认为的配置有没有语法问题,用 debug 模式空跑一遍,它会打印「如果真跑会做什么」,但不实际执行,非常适合上线前验证:

logrotate -d /etc/logrotate.conf

输出里会逐条列出 rotating pattern、是否满足条件、会做什么动作。任何语法错误这里会直接报出来,比等到半夜 cron 跑失败才发现要强得多。

给自建服务写一份 logrotate 配置

假设你在 /www/wwwlogs/ 下跑着 Nginx,日志文件是 access.log 和 error.log,要每天切割、保留 14 天、旧文件压缩。在 /etc/logrotate.d/ 下新建一个文件,比如 mynginx:

/www/wwwlogs/*.log {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    dateext
    dateformat -%Y%m%d
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

逐条解释几个关键指令:

  • daily / weekly / size:轮转频率。也可以用 size 100M 表示「涨到 100M 就切」,对流量不稳的站更实用。
  • rotate 14:保留 14 个历史文件,第 15 个自动删。配合 daily 就是保留两周。
  • missingok:日志不存在时不报错。notifempty:文件为空时不轮转。
  • delaycompress:最新一份轮转文件先不压缩(因为可能还有服务在写它),下一次轮转时才压。配合 compress 用。
  • dateext:用日期命名(access.log-20261010)而不是 .1、.2,人看得懂,也不会因为序号滚动而混淆。
  • create 0640 www-data adm:轮转后新建的日志文件权限和属主,一定要设对,否则服务可能没权限写。

postrotate 与重读信号:切割后必须让服务重新打开文件

前面提过,服务一直持有旧日志的文件句柄,轮转改名后它还在写旧文件。postrotate ... endscript 就是用来在轮转后通知服务「重新打开日志」。不同服务的信号不同,记错会导致日志写到空文件里去:

  • Nginx:kill -USR1 主进程,重新打开日志。这是 Nginx 官方文档的标准做法。
  • Apache:kill -USR1 或 graceful 重启。
  • MySQL:不能发信号,要用 mysqladmin flush-logs,或 mysql 里执行 FLUSH LOGS。
  • PHP-FPM:kill -USR1 主进程重开日志。

sharedscripts 表示:当通配符匹配到多个日志文件(access + error)时,postrotate 脚本只执行一次,而不是每个文件执行一次。对一个 PID 发一次信号就够了,不加 sharedscripts 会重复发信号甚至出错。写 MySQL 的 postrotate 要注意,命令里带 mysql 认证,最好用 /root/.my.cnf 存凭据,避免把密码写在 logrotate 配置里:

/var/log/mysql/*.log {
    daily
    rotate 7
    compress
    delaycompress
    missingok
    notifempty
    create 0640 mysql adm
    sharedscripts
    postrotate
        mysqladmin --defaults-file=/root/.my.cnf flush-logs 2>/dev/null || true
    endscript
}

copytruncate:给不支持重开日志的程序兜底

有些程序(很多自研脚本、第三方商业软件)不支持「收到信号重开日志」。这时候 postrotate 发信号它也不理,日志照旧写向旧句柄。对这类程序,logrotate 提供了 copytruncate 选项:先复制一份日志内容出来,然后再把原文件清空(truncate 到 0),服务手里的句柄不变,继续往同一个文件追加,无缝衔接。

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

但 copytruncate 有代价:复制和清空之间有一个极短的窗口,这个窗口内写入的日志可能丢失。所以它只是兜底方案——只要程序支持 USR1 之类信号,就优先用信号重开,而不是 copytruncate。另外 copytruncate 和 create 不能同时用,因为文件句柄要保持原样,不能新建。

用 size 和 maxsize 应对流量突增

纯按天切割有个隐患:某天突然来了大量访问,access.log 可能在几小时内就涨到几个 G,撑爆磁盘,而当天还没到轮转时间。解决办法是给配置加一个体积上限,配合 daily 一起用——达到任一条件就切:

/www/wwwlogs/*.log {
    daily
    maxsize 512M
    rotate 14
    compress
    ...
}

maxsize 表示「即使没到 daily 时刻,涨到 512M 也立即轮转」。这对做 SEO 引流、偶尔被爬虫或攻击刷流量的站特别有用,相当于给日志体积上了个硬保险。

用 state 文件看清 logrotate 到底跑没跑

logrotate 能按天轮转,靠的是记住「上次轮转发生在什么时候」。这个状态存在 /var/lib/logrotate/logrotate.status(发行版不同路径略有差异),里面每个日志文件对应一行最后轮转时间。如果某天发现该切的没切,第一件事就是看这个文件——它会告诉你 logrotate 认为上次是什么时候跑的:

cat /var/lib/logrotate/logrotate.status
grep mynginx /var/lib/logrotate/logrotate.status

常见误区是「手动改了配置但状态文件里时间对不上」,导致 logrotate 觉得「今天已经轮转过了」而不动作。调试期想强制立即轮转,用 -f 忽略状态和周期判断,直接跑一次:

logrotate -f /etc/logrotate.d/mynginx
ls -l /www/wwwlogs/

跑完立刻 ls 看目录:应该出现带日期的旧文件(access.log-20261010)和新的空 access.log。这一步是判断「配置到底生效没有」最直接的证据,比读半天文档都强。

用独立 timer 精确控制轮转时刻

系统默认的 logrotate 由 /etc/cron.daily/logrotate 触发,而 cron.daily 的执行时刻由 anacron/cron 决定,通常是凌晨随机偏移,不利于和备份、报表等任务错开。想让轮转固定在自己的时间窗口,可以禁掉默认调度,改用 systemd timer 精确控制。先写一个 oneshot service,再写一个 timer 在每天固定时刻触发它:

# /etc/systemd/system/logrotate-custom.timer
[Unit]
Description=Daily logrotate at fixed time

[Timer]
OnCalendar=*-*-* 04:30:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true 很关键:如果服务器在预定时刻恰好关机,开机后 systemd 会补跑一次漏掉的轮转,不会因为「错过了 04:30」而整天不切日志。启用后 systemctl enable --now logrotate-custom.timer,用 systemctl list-timers 就能看到下次触发时间。把轮转排在备份任务之前,能让日志切割和备份解耦,避免一个大日志同时拖累两边。

日志集中化:轮转之后的下一步

轮转解决的是「本地磁盘不被撑爆」,但对多台机器的站长,还有更进一步的玩法——把轮转后的日志集中收集。最轻量的是 rsyslog 的网络转发,让各台机器把日志同时发到一台日志服务器;也可以在本机轮转后用 postrotate 钩子把压缩包 rsync/scp 到集中存储:

postrotate
    rsync -az /www/wwwlogs/*.log-* backup@loghost:/archive/$(hostname)/ 2>/dev/null || true
endscript

注意这里用 || true 兜底,即使网络不通、rsync 失败,也不会让整个 logrotate 报错中断本地轮转——本地清理永远是第一优先级,异地同步是锦上添花。集中化之后,排查问题时不用再一台台 ssh 上去翻日志,本地留近期、远端存历史,兼顾了性能和可追溯性。

常见坑与收尾清单

  • 切割后新日志为空、旧文件还在被写:漏了 postrotate 重读信号,或信号发错了。Nginx 用 USR1,MySQL 用 flush-logs。
  • 通配符匹配了不该管的文件:比如 *.log 把 access.log 和 access.log.1 全匹配了,导致轮转错乱。用具体文件名或限定模式。
  • create 的属主权限不对:轮转后服务写不了新日志,403 或静默丢弃,属主要和原文件保持一致。
  • 和 cron 里手写的清理脚本冲突:既 logrotate 又自己 rm,容易误删正在被引用的文件,统一交给 logrotate 管。
  • 改完不验证:永远先 logrotate -d 空跑,再 logrotate -f 强制跑一次看结果,别等半夜 cron 出问题。
  • debug 后忘了 -f 真跑:-d 只打印不执行;想让刚写的配置立刻生效做一次真轮转,用 logrotate -f /etc/logrotate.d/mynginx。

logrotate 是那种「配好一次、省心几年」的基础设施。花二十分钟把 Nginx、MySQL、PHP 和你自己应用的日志都纳管起来,等于给服务器请了个不知疲倦的管家,再也不用担心哪天被日志悄悄撑爆磁盘。

Last modification:October 10th, 2026 at 08:26 pm

Leave a Comment