Logrotate实践指南
在 Linux 长期运行的服务器中,日志文件就像一条贪吃蛇
很多新人运维/开发的习惯是写个 Crontab 脚本 rm -rf *.log,或者手动删除大文件。结果往往是:文件没了,磁盘空间依然被占用
💡 根本原因:Linux 文件系统通过 Inode 管理文件。如果一个进程(如 Java/Nginx)打开了日志文件,它就持有该 Inode 的句柄。你删除了文件名,只是删除了目录项,只要进程不释放句柄,磁盘块就永远不会标记为“空闲”。
我们需要的是 Logrotate —— Linux 的御用“垃圾分类回收工”
核心原理
Logrotate 是 Linux 系统中最标准的日志管理工具,主要用于防止日志文件无限增长导致磁盘空间耗尽,同时支持日志归档与轮转,便于后续查询和管理
触发机制
很多人误以为 Logrotate 是守护进程,其实它只是一个由 cron 或 systemd 唤醒的普通 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
配置中必须包含 size 或 maxsize 来确保未满大小时不误切(虽然设为 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进行沙箱演练