Logrotate实践指南

10 阅读8分钟

Logrotate实践指南

在 Linux 长期运行的服务器中,日志文件就像一条贪吃蛇

很多新人运维/开发的习惯是写个 Crontab 脚本 rm -rf *.log,或者手动删除大文件。结果往往是:文件没了,磁盘空间依然被占用

💡 根本原因:Linux 文件系统通过 Inode 管理文件。如果一个进程(如 Java/Nginx)打开了日志文件,它就持有该 Inode 的句柄。你删除了文件名,只是删除了目录项,只要进程不释放句柄,磁盘块就永远不会标记为“空闲”。

我们需要的是 Logrotate —— Linux 的御用“垃圾分类回收工”

核心原理

Logrotate 是 Linux 系统中最标准的日志管理工具,主要用于防止日志文件无限增长导致磁盘空间耗尽,同时支持日志归档与轮转,便于后续查询和管理

触发机制

很多人误以为 Logrotate 是守护进程,其实它只是一个由 cronsystemd 唤醒的普通 CLI 工具。具体触发方式因发行版和配置而异,主要有以下两种:

  • 通过 systemd 定时器

某些系统使用 logrotate.timer 来定期触发 logrotate.service

systemctl status logrotate.timer
  • 通过传统 cron 任务

在其他系统中,logrotate 通常由 cron 或 anacron 调度,其脚本位于 /etc/cron.daily/logrotate,由 crond 服务每日执行一次

ls /etc/cron.daily/logrotate    # 查看脚本是否存在
systemctl status crond          # 检查 cron 服务是否运行

无论采用哪种方式,logrotate 都是“按需运行”,执行完毕即退出,不会长期驻留内存

模式A:Create + Signal

适用:Nginx, Apache, PHP-FPM 等支持重载信号(Reload)的服务

原理:利用 Linux mv (rename) 的原子性。重命名文件不改变 Inode,进程继续写“旧”文件,直到收到信号切换到新文件

sequenceDiagram
    participant Logrotate
    participant FileSystem
    participant Nginx
    
    Note over Nginx: 正在写入 access.log (Inode: 101)
    Logrotate->>FileSystem: mv access.log access.log.1
    Note over FileSystem: Inode 101 没变<br/>Nginx 仍在写 access.log.1
    Logrotate->>FileSystem: create new access.log (Inode: 102)
    Logrotate->>Nginx: 发送 kill -USR1 信号
    Nginx->>Nginx: 收到信号,重新打开日志句柄
    Note over Nginx: 切换写入 access.log (Inode: 102)
    Note over FileSystem: 此时 Logrotate 可安全压缩 access.log.1

模式B:Copytruncate

适用:Java (Spring Boot),Python, Go 等将日志重定向到文件且无法响应重载信号的应用

原理:先把内容拷出来,再把原文件“清零”

⚠️风险:在“复制完”到“清空”的几毫秒时间窗口内,如果应用写入新数据,这些数据会永久丢失

graph LR
    A[应用持续写入 log] --> B{Logrotate 介入}
    B -- Step 1: cp log log.1 --> C[生成归档]
    B -- Step 2: truncate -s 0 log --> D[原文件大小归零]
    D -- 句柄不变 --> A
    style B fill:#f9f,stroke:#333
    style D fill:#ff5555,stroke:#333

两种模式对比

理解这两种模式的区别是避免“日志丢失”和“进程写错文件”的关键

维度Create + Signal(推荐)Copytruncate
定位标准原子级方案仅作为兜底方案
适用对象支持重载信号的服务(Nginx、Apache、PHP、Rsyslog)无法重载/感知信号的进程(Java、Python、Go 重定向日志)
底层原理Inode 机制:重命名文件不改变 Inode,进程持有的句柄依然有效,数据持续写入旧文件直到被信号中断I/O 读写:将日志内容物理复制一份,然后利用系统调用将原文件大小强行归零
执行步骤1. 重命名日志
2. 创建新的空白日志文件
3. 发送信号通知应用(kill -USR1)应用切换到新句柄
1. 将当前日志内容复制出来(cp access.log access.log.1)
2. 瞬间清空原日志文件(truncate -s 0 access.log)
数据完整性安全 (0丢失):全过程无时间窗口,原子操作有风险 (存在丢失窗口):在“复制完”到“清空”的几毫秒内,如果应用写入新数据,这些数据会随清空动作彻底丢失
性能损耗极低:重命名只是元数据修改,瞬间完成较高:若日志文件高达数GB,复制过程会产生显著磁盘 I/O 峰值
配置关键字create 0640 user group
postrotate (发送信号)
copytruncate
无需 create 或 postrotate

配置文件

不要直接改 /etc/logrotate.conf,请在 /etc/logrotate.d/ 下创建独立文件!

全参数详解

# 全局或指定日志文件的通用配置块
/var/log/application/*.log {

    # 周期与触发条件
    daily                     # [推荐] 按天轮替 (最常用)
    # weekly                  # [可选] 按周轮替
    # monthly                 # [可选] 按月轮替
    # hourly                  # [特殊] 按小时 (需系统 cron.hourly 支持或配合 Systemd Timer)
    
    maxsize 100M              # [推荐] 优先满足周期,但若文件超过 100M 则提前切分
    # size 100M               # [可选] 忽略周期限制,只要超过 100M 立即切分
    # minsize 10M             # [可选] 周期到了后,必须同时满足大于 10M 才切分


    # 保留与清理
    rotate 30                 # [必选] 保留最近 30 份归档日志
    maxage 90                 # [可选] 清理超过 90 天的旧归档 (兜底策略,清理 rotate 漏掉的)
    # mail admin@example.com  # [可选] 轮替时将日志发送邮件 (现代运维较少使用)


    # 压缩策略
    compress                  # [推荐] 开启 gzip 压缩 (access.log-20230101.gz)
    # nocompress              # [可选] 不压缩

    delaycompress             # [核心] 延迟一天压缩。切分出的最新归档暂时保持明文
                              # 作用:防止应用在切分后还有极短时间在写旧文件,避免数据写入损坏的压缩包
    
    # 文件名与格式
    dateext                   # [推荐] 使用日期作为后缀 (如 access.log-20231001)
    # nodateext               # [可选] 使用数字递增后缀 (如 access.log.1, access.log.2)
    
    dateformat -%Y-%m-%d      # [可选] 自定义日期格式
    # dateyesterday           # [技巧] 在后缀中使用昨天的日期 (便于 T+1 日志分析)


    # 容错与权限
    missingok                 # [推荐] 日志源文件不存在时不报错
    # nomissingok             # [默认] 文件不存在则报错中断
    
    notifempty                # [推荐] 如果日志为空 (0KB),不进行轮替
    # ifempty                 # [默认] 即使是空文件也强制轮替


    # 核心运作模式 (二选一,切勿同时开启)
    # --- 模式 A:Create + Signal (推荐:Nginx, Apache) ---
    # create 0640 nginx root  # 切分后立即创建新文件,并赋予指定权限
    # sharedscripts           # 多个日志文件只执行一次 postrotate 脚本
    # postrotate              # 脚本:通知应用重新打开日志句柄
    #     /bin/kill -USR1 `cat /var/run/nginx.pid`
    # endscript

    # --- 模式 B:Copytruncate (妥协:Java, Python Stdout) ---
    copytruncate              # 复制内容到归档后,直接清空原文件 (不推荐但实用)
    # nocopytruncate          # [默认] 不使用此模式
    
    # --- 权限修正 (特殊场景) ---
    # su appuser appgroup     # [特殊] 如果日志目录属于非 root 用户,必须显式指定切换用户
}

生产环境配置模板

  • 场景一:Nginx(Signal 重载模式),路径 /etc/logrotate.d/nginx-custom
/var/log/nginx/*.log {
    daily                       # 每天切割
    dateext                     # 使用日期作为后缀
    dateformat -%Y-%m-%d        # 显式指定日期格式
    rotate 30                   # 保留30天
    compress                    # 开启压缩
    delaycompress               # 延迟一天压缩,给Nginx缓冲时间
    missingok
    notifempty
    create 0640 nginx root      # 切割后创建新文件的权限
    sharedscripts               # 切割所有log后只 reload 一次 nginx
    
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 `cat /var/run/nginx.pid`
        fi
    endscript
}
  • 场景二:Java 应用(Copytruncate 模式),路径 /etc/logrotate.d/app-java

对于 java -jar app.jar > app.log & 这种无法接收信号的应用,我们只能妥协,但要做足安全措施

/home/app/logs/app.log {
    daily
    dateext
    rotate 60
    missingok
    compress
    delaycompress
    notifempty
    copytruncate        # 关键!复制并清空,应用不需要重启
    
    # 强制风控:防止单日日志也撑爆磁盘
    # 即使 crontab 是每天一次,加上这个size参数可以在手工执行时优先生效
    maxsize 500M 
    
    # 权限修复:解决“父目录权限不足”问题
    su appuser appgroup
}

进阶:如何处理 TB 级流量

默认的 daily 无法满足 TB 级流量需求,我们需要 Logrotate 每小时执行。同时,为了防止某一时段流量极小产生大量空日志,或者流量极大撑爆磁盘,我们需要结合 crond 和 size 策略

  • 自定义 Logrotate 配置文件

不要放在 /etc/logrotate.d/(系统默认的 daily 任务),而是创建一个独立目录:

mkdir -p /etc/logrotate.hourly.d/

vi /etc/logrotate.hourly.d/fast-app

配置中必须包含 sizemaxsize 来确保未满大小时不误切(虽然设为 hourly,但双重保险更好):

/var/log/fast-app.log {
    rotate 24                   # 保留24小时
    maxsize 100M                # 双重保险:超100M也切
    missingok
    notifempty
    compress
    delaycompress
    create 0644 root root
    
    dateext
    # 必须精确到小时,否则第二次切割会失败
    dateformat -%Y%m%d-%H 
    
    postrotate
        /bin/kill -USR1 `cat /var/run/nginx.pid 2>/dev/null` 2> /dev/null || true
    endscript
}
  • 由于这不是标准的系统任务,我们需要手动在 crontab 中指定调度,crontab -e
# 每小时的第59分钟,调用 logrotate 读取刚才的独立配置
# 解释:这里不需要加 -f (强制),因为我们在配置文件里写了 maxsize 或 size
# 只要文件大小或时间满足条件,logrotate 自然会工作
59 * * * * /usr/sbin/logrotate /etc/logrotate.hourly.d/fast-app

调试与验证

部署配置后,直接干等是不合格的运维。必须掌握以下三板斧:

  • Debug 模式(演习)

不会产生任何文件变动,只会输出解析过程。用来检查配置文件有没有语法错误

# -d 参数 = Debug
logrotate -d /etc/logrotate.d/nginx-custom
  • Verbose 模式(实操但带回显)

执行实际切割,并打印细节

logrotate -v /etc/logrotate.d/nginx-custom
  • Force 模式(强制立刻切割)

即使未满足切割时间/大小条件,也强制进行一次轮替。用于上线后立即验证权限和 postrotate 脚本是否正确

logrotate -f /etc/logrotate.d/nginx-custom

结语:Logrotate 虽小,却是 Linux 稳定性建设的基石。对于生产环境,始终建议开启 delaycompress 保护数据,优先使用 create 模式而非 copytruncate,并一定要在上线前使用 logrotate -d 进行沙箱演练