logrotate 日志轮转实战:Nginx、MySQL 与 Docker 日志防爆盘指南

个人站长的服务器上,最容易被忽视的"磁盘杀手"就是日志文件。Nginx 的 access.log 一天能长几十上百兆,访问量稍大一点,一个月就是好几个 G;PHP-FPM 的错误日志、MySQL 的慢查询日志、系统自带的 syslog 也在悄悄膨胀。等到发现磁盘满了,往往已经是数据库写不进去、网站 500 报错的时候,而日志文件占掉的空间少则几个 G,多则几十 G。

解决这个问题不需要复杂的脚本,Linux 自带的 logrotate(日志轮转)就是专门干这个的:按时间或大小自动切割日志、压缩归档、删除过期文件,全程无人值守。本文把 logrotate 的原理、配置写法、常见场景和排错方法讲透,让你彻底告别"日志撑爆磁盘"。

一、logrotate 的工作原理

logrotate 本身是个命令,通常由 cron 每天定时触发一次(系统默认装在 /etc/cron.daily/logrotate)。它读取主配置文件 /etc/logrotate.conf 和目录 /etc/logrotate.d/ 下的所有子配置,逐个处理日志文件:该切割的切割、该压缩的压缩、该删除的删除。

主配置里有一行 include /etc/logrotate.d,意思是加载这个目录下的所有配置,所以我们自己管理的服务日志,建议在 /etc/logrotate.d/ 下新建一个配置文件,而不是去改主配置。运行状态记录在 /var/lib/logrotate/status,logrotate 靠它判断"上次处理到什么时候",避免重复切割。

二、基础配置:每个指令都讲清楚

先看一个最典型的配置,以 Nginx 日志为例,新建 /etc/logrotate.d/nginx

/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    create 644 www-data www-data
    sharedscripts
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

逐条解释一下关键指令的含义:

  • daily / weekly / monthly:切割周期,按天、周、月。访问量大的站建议 daily,日志量小的站 weekly 就够了。
  • rotate 14:保留 14 份归档,超过自动删除最老的。保留数量 × 切割周期就是日志保留时长,比如 daily + rotate 14 就是保留 14 天。
  • compress:切割后把旧日志用 gzip 压缩,通常能压缩到原来的十分之一。
  • delaycompress:延迟压缩,即本次切割出来的那份日志先不压缩,下次切割时才压缩。配合 postrotate 重开日志文件使用,避免压缩还在写入的文件。
  • missingok:日志文件不存在时跳过,不报错。强烈建议加上,避免服务还没启动就报一堆错误邮件。
  • notifempty:日志文件为空时不切割,避免产生一堆空归档。
  • dateext:归档文件名带日期(如 access.log-20260811),比默认的递增序号(access.log.1)更容易看懂。
  • create 644 www-data www-data:切割后按指定权限和属主重建原日志文件。也可以不加 create,让服务自己重新打开日志文件。
  • sharedscripts + postrotate/endscript:如果配置里匹配了多个日志文件(如 access.log 和 error.log),默认每个文件都执行一次脚本;加上 sharedscripts 后只执行一次。脚本里 kill -USR1 是给 Nginx 发信号让它重新打开日志文件,这样新的日志继续写新文件,老文件才能安全归档。

三、Nginx 与 PHP-FPM 日志轮转实战

Nginx 的配置如上面所示,重点是 postrotate 里的 kill -USR1。这里要特别提醒:不要用 copytruncate 处理 Nginx 日志。copytruncate 是"复制一份再清空原文件",很多教程推荐它,因为不需要发信号,但复制大文件消耗 IO,而且复制和清空之间可能丢日志。Nginx 官方的做法就是信号重开,用 sharedscripts 方案最稳妥。

PHP-FPM 的错误日志同理,新建 /etc/logrotate.d/php-fpm

/var/log/php-fpm.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    create 644 root root
    sharedscripts
    postrotate
        [ -f /var/run/php-fpm.pid ] && kill -USR1 $(cat /var/run/php-fpm.pid)
    endscript
}

MySQL 的日志比较特殊。错误日志直接轮转即可,但慢查询日志和 general log 用 logrotate 的 create 方式会有权限问题(MySQL 启动时以 mysql 用户打开文件,轮转后新建的文件属主不对会导致写不进去),最稳妥的姿势是轮转后用 mysqladmin flush-logs 通知 MySQL 重开日志:

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

四、copytruncate 什么时候用

前面说 Nginx 别用 copytruncate,但有一种场景必须用它:程序不监听信号、无法重新打开日志文件的场景,比如某些老旧的 Java 应用或者用 nohup app >> app.log 方式跑的程序。此时只能先复制再截断:

/home/wwwlogs/app.log {
    daily
    rotate 30
    compress
    copytruncate
    missingok
    dateext
}

注意 copytruncate 和 create 不能同时用,截断瞬间可能会有极少量日志丢失,但这是无法重开日志文件时的唯一选择。

五、加 maxsize:按大小切割

有的日志平时没多少量,但某个时段突然暴涨(比如被刷流量),daily 周期可能一天就写几个 G。用 maxsize 指令可以"时间或大小先到先切":

/var/log/nginx/*.log {
    daily
    maxsize 200M
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    dateext
    sharedscripts
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 $(cat /var/run/nginx.pid)
        fi
    endscript
}

这样日志单份超过 200M 会立即切割,即使当天还没到轮转时间。

六、手动测试与排错

改完配置不要干等 cron 跑,先手动验证。logrotate 的三个常用参数:-d 调试模式(只显示会做什么,不真正执行)、-f 强制执行(无视状态文件,立即切割)、-v 显示详细过程:

# 调试:检查配置语法和将要执行的动作
logrotate -d /etc/logrotate.d/nginx

# 强制执行并看输出
logrotate -vf /etc/logrotate.d/nginx

常见问题排查:

  • 配置没生效:检查文件权限是否 644、属主是否 root,logrotate 对配置文件权限敏感;再确认是否被 include /etc/logrotate.d 正确加载(logrotate -d 会显示读取了哪些文件)。
  • 报错 "duplicate log entry":同一个日志路径在多个配置文件里重复定义,删掉一份即可。
  • 没有按时切割:检查 cron 是否正常运行,ls -l /etc/cron.daily/ 确认 logrotate 脚本存在且可执行,也可以 run-parts /etc/cron.daily 手动触发整套每日任务。
  • 压缩失败:确认系统装了 gzip(一般都有),查看 /var/log 下 logrotate 相关的报错输出。

七、Docker 容器日志的轮转

用 Docker 跑站的站长越来越多,容器日志是个容易被忽略的重灾区。Docker 默认的日志驱动是 json-file,所有 stdout/stderr 输出都写到宿主机 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log,这个文件会无限增长,不处理的话一个长期跑的应用几个月就能写满磁盘。

方法一:在 Docker daemon 配置里直接限制,新建或修改 /etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "5"
  }
}

重启 Docker 后,每个容器单份日志超过 50M 自动滚动,最多保留 5 份。这个方案最省心,但只对重启后新启动的容器生效,存量容器需要 recreate 一次。单个容器也可以在 docker run 或 docker-compose 里单独指定:

# docker run
docker run -d --log-opt max-size=50m --log-opt max-file=5 nginx

# docker-compose.yml
services:
  web:
    image: nginx
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"

方法二:用 logrotate 处理现有容器日志文件。容器内进程不会响应 USR1 信号,所以这里只能用 copytruncate,新建 /etc/logrotate.d/docker

/var/lib/docker/containers/*/*.log {
    daily
    rotate 7
    compress
    copytruncate
    missingok
    dateext
}

需要注意:copytruncate 截断后,docker logs 只能看到截断之后的新日志,排障时会感觉"日志变少了",这是正常现象。如果日常依赖 docker logs 查日志,优先用 daemon.json 的 max-size/max-file 方案,或者改用 docker 官方日志驱动配合日志收集系统。

八、更多排错与细节补充

最后补充几个实操中容易踩的细节:

  • dateext 与 rotate 的关系:dateext 模式下归档名带日期,rotate 数量依然有效——超过数量的最老归档照样被删,只是名字按日期排而不是按序号排。
  • 权限问题:logrotate 必须用 root 执行;如果日志目录的属主是别的用户(比如 www-data),create 指令要写对属主,否则服务写不进新日志文件,表现为"切割后网站出现 500"。
  • 切割后服务没写日志:多半是 postrotate 的脚本没生效。先用 logrotate -d 确认脚本会被执行,再检查脚本里的 pid 文件路径是否真实存在。
  • 不想压缩某类日志:可以单独给某个配置块加 nocompress 覆盖全局设置,比如需要经常 grep 的调试日志。

关于保留时长,给一个实用建议:日志主要是排障用的,一般保留 7 到 30 天就足够。Nginx access 日志如果想做长期统计,建议保留一份完整的压缩归档(rotate 数量可以大一些),或者在切割脚本里顺手把当天的 access.log 同步到对象存储,既省本地磁盘又不丢数据。另外注意服务器时区:如果系统时区不是北京时间,daily 切割的"一天"会按系统时区算,对不上日志里的时间戳是正常的,不用奇怪。

总结

日志轮转是服务器运维的必修课,配置一次一劳永逸。核心要点就三个:切割周期按日志量定、rotate 数量按保留需求定、服务能重开日志文件就用信号方案而不是 copytruncate;Docker 场景优先用 daemon 自带的 max-size/max-file。改完配置记得 logrotate -d 检查、-vf 实测一次,确认归档文件正常生成后再放心交给 cron。从此磁盘告警里,再也不会有"日志文件占满"这一项。

Last modification:August 11th, 2026 at 08:37 am

Leave a Comment