前几天半夜被同事一个电话叫起来,说某台线上服务器的磁盘满了,应用写不进去数据。我登上去一看,好家伙,一个 Nginx 的 access.log 已经撑到了 30 多 GB,再往前翻,这个文件从部署那天起就没动过。那台机器数据盘一共才 40 GB,一个日志文件就占了四分之三。
这种问题相信不少人都遇到过。日志文件如果不加控制,它会理所当然地把你的磁盘吃干抹净,而 logrotate 就是解决这个问题的标准工具。它是 Linux 系统里默认自带的一个日志切割、轮转工具,几乎每个发行版都有。Ubuntu 20.04 上也内置了它,只是很多人从来没有主动去看过它到底在干什么。
这篇文章我就从 logrotate 的核心机制讲起,把配置文件语法掰开揉碎,再给几套可以直接抄的实战配置,最后聊一聊我踩过的那些坑。不管你是刚接触服务器的新手,还是已经在生产环境摸爬滚打了一段时间的运维,这篇文章应该都能给你一些有用的参考。
1. 日志持续增长背后:必须切割的三个原因
先说结论:日志切割不是一个附加的"锦上添花"功能,而是线上服务稳定运行的基础设施之一。很多人觉得日志嘛,写就写了,反正硬盘那么大,直到某一天磁盘 IO 和空间双双告警,才意识到问题有多严重。
1.1 磁盘空间被拖垮的速度比你想象中快
一个最普通的业务接口,假设 QPS 只有 50,每行 access log 平均 200 字节,你算一下一天会产生多少日志: 50 × 86400 × 200 / 1024 / 1024 ≈ 823 MB。也就是说,一个日均请求量并不算高的服务,一天就能产生接近 1 GB 的日志。
这还只是一个接口。现实生产环境里,一个应用往往有 access log、error log、业务流水、慢查询日志、调试日志好几路输出,叠加起来就是几 GB 甚至几十 GB 一天。如果一个月不去处理,磁盘被写满是确定性事件,只是时间早晚的问题。
1.2 单个超大文件带来的连锁反应
日志文件大了以后,不只是占空间这么简单。第一个受害者是排查问题的效率:用 tail 看一眼还好,但如果你想 grep 某个时段的关键字,一个 30 GB 的文件能把整个磁盘的 IO 拖垮。因为 grep 要顺序扫描整个文件,哪怕只是找一个关键词,也得等它从头扫到尾。
第二个受害者是日志采集和归档。现在稍微正规一点的环境都会接日志采集,像 Filebeat、Logstash 这类 agent 读取超大文件时,要维护文件读取位置(offset),文件原地继续写没问题,但如果因为磁盘满了导致写入中断,位置就错乱了,采集端可能重复读或者漏读一大段。
第三个受害者是文本编辑器、less、vim 这类日常排查工具,打开大文件要么巨慢,要么直接内存溢出。
1.3 切割的最终目的:把巨型文件变成可管理的小文件
所谓日志切割,核心就两件事:一是按时间或按大小把日志文件切分成一段一段的小文件;二是设置保留策略,只保留最近 N 份,更老的日志自动删除或压缩归档。
这样磁盘空间始终处于可控状态,排查问题时可以只针对某一天的日志文件进行精准检索,采集端也能稳定地读取完整文件。logrotate 这个工具就是专门干这个的,它不会面向业务逻辑做任何事,只关心文件本身。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. logrotate 的执行机制:谁在背后调用它
很多初学者有一个误区,以为 logrotate 是一个常驻后台的守护进程。其实不是。它本质上是一个命令行工具,需要别人定期触发它执行。在 Ubuntu 20.04 上,这个"别人"是 cron 和 systemd timer 两套机制配合完成的。
2.1 系统默认的触发链路
Ubuntu 20.04 安装 logrotate 之后,会生成两个关键文件:
/etc/cron.daily/logrotate:cron 的每日任务脚本/lib/systemd/system/logrotate.timer和/lib/systemd/system/logrotate.service:systemd 的定时器单元
其中 /etc/cron.daily/logrotate 这个脚本的内容核心只有一行:
bash复制/usr/sbin/logrotate /etc/logrotate.conf
也就是说,系统每天会在 cron 的 daily 时段执行一次 logrotate,读取主配置文件 /etc/logrotate.conf。而 /etc/logrotate.conf 里有一行关键配置:
code复制include /etc/logrotate.d
它的作用是把 /etc/logrotate.d/ 目录下所有配置文件都加载进来。所以你在 /etc/logrotate.d/ 里放的任何配置文件,都会被每天自动执行一遍。
同时,Ubuntu 20.04 默认启用了 logrotate.timer,它也会定时触发 logrotate 的执行。cron 和 systemd timer 两套机制同时存在,正常情况下不会冲突,因为 systemd timer 设计了随机延迟和幂等机制。你可以用下面的命令确认 timer 状态:
bash复制systemctl status logrotate.timer
systemctl list-timers logrotate.timer
输出会显示 timer 的下一次触发时间。如果 cron 先跑了,logrotate 已经处理完毕,timer 再触发时,因为发现没有需要轮转的文件,就会直接跳过。
2.2 读取顺序与配置生效逻辑
logrotate 的执行顺序是先读主配置 /etc/logrotate.conf,再按字母顺序读取 /etc/logrotate.d/ 下的所有配置文件。如果同一个日志文件在多个配置块里被重复定义,logrotate 的处理策略是:后面的配置会重置前面配置的状态。注意,不是"合并",是"整个配置块被重置"。
这个特性非常容易踩坑。比如你在 /etc/logrotate.d/nginx 里定义了对 /var/log/nginx/*.log 的轮转规则,又在 /etc/logrotate.d/myapp 里也写了一个针对 /var/log/nginx/access.log 的规则,那么后读取的那个配置会完全覆盖前一个,前一个配置对 access.log 的所有参数都会失效。
2.3 手动执行与强制轮转
既然是命令行工具,logrotate 当然支持手动触发,这在调试配置时极其重要。常用参数如下:
bash复制logrotate -d /etc/logrotate.conf # debug 模式,只输出执行计划,不实际执行
logrotate -f /etc/logrotate.conf # 强制轮转,即使还没到轮转条件
logrotate -v /etc/logrotate.conf # 详细输出
logrotate -s /var/lib/logrotate/status /etc/logrotate.conf # 指定状态文件
其中 -d 是我最常用的调试参数。它会模拟一遍执行流程,打印出每一步会做什么,但不会真正移动或创建文件。强烈建议每次修改完配置后都用它跑一遍,比你直接 -f 强制轮转安全得多。
logrotate 还有一个状态文件,默认在 /var/lib/logrotate/status。它记录了两个关键信息:每个日志文件上次轮转的日期,以及当前已轮转的次数。logrotate 判断"今天是否需要轮转"就是靠这个状态文件里的日期,而不是看文件修改时间。所以如果你手动改了系统时间,或者恢复了快照,有可能导致 logrotate 误判或漏判。
3. 配置文件语法与参数细节:读不懂就配不对
logrotate 的配置文件看起来不大友好,尤其第一次接触时,满眼都是 daily、rotate 7、compress 这种短词。但实际上它的语法很简单,一个配置块由一个路径加一堆参数组成,只要把关键参数吃透,就能应对绝大多数场景。
3.1 配置文件的基本结构
一个完整的配置块长这样:
bash复制/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
/usr/bin/kill -USR1 `cat /var/run/myapp.pid`
endscript
}
第一行是日志文件的路径,支持通配符;大括号里是参数。有几个通用的全局参数放在 /etc/logrotate.conf 中,但没有特殊说明的话,放在具体配置块里的参数优先级更高。
3.2 轮转周期与大小判断
轮转周期的判断是 logrotate 最容易让人迷糊的地方。它支持两种维度:
- 时间维度:
daily(每天)、weekly(每周)、monthly(每月) - 大小维度:
size 100M、maxsize 100M、minsize 100M
其中 size 的语义比较特殊,它不是"达到 100M 才轮转"的简单判断,而是"周期到了或者大小到了"两个条件只要满足一个就轮转。举个例子,如果你配置了 daily + size 100M,那么即使还没到一天,只要文件到了 100M 也会触发轮转;反之,如果到了一天但文件只有 10M,也会轮转。
如果你想实现"必须同时满足两个条件才轮转",要用 maxsize 和 minsize 组合。maxsize 100M 表示:到了周期(比如 daily)时,如果文件小于 100M 就不轮转;minsize 100M 表示:只要到了 100M,就不管周期是否到达,立刻轮转。实际使用中,我建议线上服务统一用 daily + maxsize 100M 的组合,既保证每天切一次,又避免日志量异常暴涨时磁盘被瞬间打满。
3.3 文件命名与保留数量
文件轮转后新文件叫什么名字,由 dateext 参数控制。不加 dateext 时,轮转后的文件叫 access.log.1、access.log.2,数字越大越旧;加了 dateext 后,变成 access.log-20250120,用日期做后缀。
dateext 的好处是文件一眼就能看出是哪天的日志,归档和排查都方便;缺点是如果某天产生了多个轮转文件(比如因为 size 触发了多次),后生成的文件会覆盖同名文件。要解决这个问题,可以用 dateformat 参数加上时间戳精度,比如:
bash复制dateext
dateformat -%Y%m%d%H%M%S
rotate 14 表示最多保留 14 份轮转后的文件,第 15 次轮转时会删除最老的那份。如果想永远不删,可以写 rotate 0,但这往往会造成磁盘无限增长,正常场景不推荐。
3.4 压缩策略:compress 与 delaycompress 的微妙配合
compress 参数指定轮转后压缩旧日志,默认用 gzip,可以通过 compresscmd 改成 xz、bzip2 或 zstd。压缩能大幅减小磁盘占用,但要注意压缩本身消耗 CPU 和内存。对于日志量巨大的服务,压缩瞬间会产生 CPU 尖峰。
delaycompress 是一个容易被忽略但实际很有用的参数。它的作用是:本轮轮转产生的那个文件不压缩,等到下一轮轮转时才压缩。为什么需要延迟压缩?因为很多服务在日志被轮转后,文件句柄还指向旧文件,进程需要收到信号才会重新打开新文件。如果轮转后立即压缩,可能压缩的是一个还在被写入的文件,导致日志内容丢失或产生压缩错误。延迟一天再压缩,给应用留出了关闭旧文件句柄的窗口。所以使用 postrotate 发送信号的应用场景,我基本都会配 delaycompress。
3.5 用户权限与文件属性
create 参数决定轮转后新建日志文件的权限和归属。默认值是 create 0640 root utmp,但更常见的写法是:
bash复制create 0640 www-data www-data
这意味着轮转后生成的新 access.log 文件,属主和属组都是 www-data,权限 640。如果不写 create,logrotate 默认不会创建新文件,应用在轮转后如果尝试写入不存在的文件,就会报错。
还有一个 su 参数。如果被轮转的日志文件属主不是 root,而 logrotate 进程以 root 身份运行时有时会遇到权限问题,这时候可以指定:
bash复制su www-data www-data
它的作用是让 logrotate 以指定用户身份执行轮转操作。这个参数在某些目录权限受限的场景非常管用,比如应用日志所在的目录只有应用自己的用户能写。
3.6 异常场景参数补齐
missingok:日志文件不存在时跳过,不报错。建议所有配置都加上,否则 logrotate 会向 root 发邮件告警。notifempty:文件为空时不轮转。日志为空说明应用可能没在写,频繁轮转空文件没有任何意义。copytruncate:先复制文件内容到新文件,再把原文件清空。这个方案适合那些无法通过信号让进程重新打开日志文件的应用,因为复制之后原文件的文件句柄依然有效,进程不必感知轮转。但缺点是复制期间可能会丢失少量日志。sharedscripts:当配置块匹配了多个日志文件(如/var/log/nginx/*.log)时,默认每个文件轮转后都会执行一次postrotate脚本。加上sharedscripts后,所有文件轮转完毕才执行一次脚本。Nginx 这类日志文件多、信号影响全局的场景,必须加这个参数,否则每次轮转都会触发一次平滑重载,资源白白浪费。olddir:把轮转后的文件移动到指定目录,适合日志与归档目录分离的场景。tabooext:禁止匹配某些扩展名的文件,默认禁止.rpmnew、.rpmsave、v、.swp、~等,可以按需追加。
4. 实战配置一:Nginx 日志轮转的典型写法
Nginx 应该是生产环境里最常见需要配置 logrotate 的软件。它的日志文件多、增长快,而且父进程会继承旧的文件句柄,轮转后必须让它重新打开新文件。
4.1 标准配置与逐行解读
Ubuntu 20.04 上通过 apt 安装的 Nginx,其实自带了 /etc/logrotate.d/nginx 这个配置。但如果你是自己编译的 Nginx,或者日志路径改成自定义了,就得自己写。我的常用模板如下:
bash复制/var/log/nginx/*.log {
daily
maxsize 200M
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
这段配置的逻辑是:每天执行一次轮转,但如果某天日志量特别大、超过了 200M,也立即触发;最多保留 14 份,压缩旧文件但延迟一天;如果文件不存在或为空就跳过;轮转后新建文件归属 www-data 用户;所有日志文件都处理完毕后,向 Nginx 主进程发送 USR1 信号,让它重新打开日志文件。
4.2 为什么必须发送 USR1 信号
Nginx 的日志写入方式决定了它必须配合信号才能正确轮转。Nginx 的主进程和 worker 进程在启动时打开日志文件,之后一直持有这个文件描述符。logrotate 把 access.log 重命名为 access.log-20250120 之后,Nginx 仍然在往那个被重命名的文件里写数据,新生成的 access.log 反而没人写。
kill -USR1 就是告诉 Nginx:"兄弟,日志文件已经被切走了,你重新打开一个吧。"Nginx 收到 USR1 后会优雅地关闭旧的文件句柄,重新打开日志路径对应的新文件。
如果漏了 postrotate 里的信号处理,你会看到一种典型的故障现象:磁盘上的 access.log 文件没有被切割,或者切割后新文件的大小一直为 0,而旧的带日期后缀的文件还在不断变大。
4.3 sharedscripts 的陷阱
我第一次写 Nginx 的 logrotate 配置时没有加 sharedscripts,结果每次轮转 access.log 和 error.log 时,都各触发了一次 kill -USR1。虽然 Nginx 对这种信号是有幂等保障的,不会出大问题,但每次轮转要重开两次日志句柄,浪费不说,在极端情况下还可能导致瞬间丢日志。
加上 sharedscripts 之后,logrotate 会等这一批文件全部处理完,才执行一次 postrotate 脚本。这是 Nginx 场景的标准做法,不要省略。
5. 实战配置二:Docker 容器与无法重开日志句柄的应用
玩 Docker 之后,日志轮转的写法又不一样了。容器里的 Nginx、Java 应用通常把日志写到 stdout 或固定路径,容器内的 logrotate 往往没有配置,而且容器本身也不建议在里面跑 cron。这时候要在宿主机层面处理。
5.1 Docker json-file 驱动的日志轮转
Docker 默认的日志驱动是 json-file,容器打印到 stdout 的内容会写到 /var/lib/docker/containers/<container-id>/<container-id>-json.log。这个文件如果不控制,同样会无限增长。最简单的控制方法不是在宿主机上写 logrotate 配置,而是直接给 Docker daemon 配日志轮转参数:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
配置好后重启 Docker daemon。这样 Docker 自己会在 -json.log 达到 20M 时切一个新文件,最多保留 5 个文件。这是容器日志轮转的首选方式,不需要额外依赖宿主机 cron。
但这里有个坑:Docker 的 max-size 轮转是"写满就切",它不会等待你配置的轮转周期,而且轮转后的文件命名是 -json.log.1、-json.log.2 这种。如果你想把容器日志纳入统一的 logrotate 管理,比如想按天切、压缩、归档到特定目录,就得换思路了。
5.2 宿主机 logrotate 处理容器日志文件
对于直接用 docker run 管理、且日志驱动是 json-file 的容器,我一般会在宿主机 /etc/logrotate.d/docker-container 里加一段配置:
bash复制/var/lib/docker/containers/*/*-json.log {
daily
maxsize 100M
rotate 7
compress
missingok
copytruncate
}
注意这里用的是 copytruncate 而不是 create + 信号。因为容器里的进程不会收到宿主机上发送的信号,你也没法方便地让它重新打开日志文件。copytruncate 会先复制原文件内容到轮转文件,然后把原文件截断成空文件。容器里的进程因为文件句柄没变,完全无感知,日志继续往原文件写。
但你要接受 copytruncate 的两个代价:一是复制和截断之间存在极短的时间窗口,可能丢几条日志;二是日志量大时,复制操作本身会占用磁盘 IO。对于容器日志这种本身就不要求 100% 不丢的场景,这两点都是可以接受的。
5.3 应用自身支持重新打开日志场景的轮转
如果你的应用是 Java 系的日志框架(Logback、Log4j2),或者任何支持信号重开日志文件的进程,应该优先用 create + postrotate 信号的方式,而不是 copytruncate。比如 PHP-FPM:
bash复制/var/log/php7.4-fpm.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
if [ -f /var/run/php7.4-fpm.pid ]; then
kill -USR1 `cat /var/run/php7.4-fpm.pid`
fi
endscript
}
判断依据很简单:只要进程支持通过信号重新加载、重开日志文件,就用 create 方案,日志完整性更高;不支持就退而求其次用 copytruncate。
6. 配置测试、状态查询与常见故障排查
logrotate 配置写完之后,不能直接等明天 cron 跑,一定要先手动验证。这个过程虽然很简单,但能帮你避开 90% 的配置低级错误。
6.1 用 debug 模式检查配置
在改完配置之后,第一件事是执行:
bash复制logrotate -d /etc/logrotate.conf
注意看输出中对你刚写的配置块的描述。如果配置语法有问题,比如少了右括号、路径写错,logrotate 会在这里直接报错。-d 模式不会真正执行轮转,所以可以放心反复跑。
如果 debug 通过,再用 -f 强制跑一次:
bash复制logrotate -f -v /etc/logrotate.conf
这时候去查看实际的日志文件,确认轮转后的文件名、压缩状态、权限都符合预期。但要注意,-f 会强制轮转所有日志文件,包括状态文件里记录着"今天已经轮转过"的文件。在一个繁忙的生产环境里,大批量强制轮转可能带来额外的磁盘 IO 压力,建议低峰期操作,或者只针对单配置文件执行:
bash复制logrotate -f /etc/logrotate.d/myapp
这样只强制轮转 myapp 相关的日志,不影响其他服务。
6.2 状态文件:logrotate 的记忆库
状态文件 /var/lib/logrotate/status 值得时不时看两眼。它的内容大概是这样的:
code复制logrotate state -- version 2
"/var/log/myapp/access.log" 2025-1-20-0:0:0
"/var/log/nginx/error.log" 2025-1-19-0:0:0
第一行是固定格式,不用管;后面的每一行记录一个日志文件的最近轮转日期。这里有个隐藏的坑:如果你手动删除了某个日志文件,但没同步更新状态文件,logrotate 会以为这个日志文件还存在,并且"上次轮转"还是很久以前,从而可能触发一次意想不到的轮转。
版本 2 的状态文件还可能包含完整路径和哈希信息,格式稍微复杂一些,但核心含义不变。排查问题时,如果你发现某个日志一直没被轮转,先打开状态文件确认 logrotate 是否真的认为它该轮转了。
6.3 常见故障现象与排查思路
我把实际运维中最常遇到的 logrotate 故障整理成了一个表格,方便对照排查:
| 故障现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 日志文件一直不轮转 | 状态文件里的日期还没到期;daily 任务没触发;配置块里路径写错 | 用 logrotate -d 看实际判断逻辑;用 -f 强制验证配置 |
| 轮转后新日志文件为空,旧文件还在涨 | 应用没收到信号,还在往旧文件句柄写 | 检查 postrotate 脚本,确认信号正确发送给主进程 |
| 轮转报权限报错 | 日志目录属主与运行 logrotate 的用户不一致 | 用 su 参数指定正确的用户,或调整目录权限 |
| 压缩后的日志文件属主变成 root | 压缩过程默认以 root 运行 | 检查 compresscmd 脚本是否用了 --no-name 或指定属主 |
| 重复轮转或轮转后文件名冲突 | 多个配置块匹配了同一个日志文件 | 检查 /etc/logrotate.d/ 下是否有重复路径,删掉后写的那个配置 |
| cron 执行了但 logrotate 没跑 | /etc/cron.daily 脚本权限不对,或 cron 服务被禁用 |
检查 /etc/cron.daily/logrotate 是否有执行权限,systemctl status cron |
| 配置了 daily 但实际两天才轮转一次 | systemd timer 与 cron 都执行但互斥锁竞争;服务器关机时间跨天 | 检查 timer 日志 journalctl -u logrotate.timer |
6.4 一个让人印象深刻的权限坑
我在生产环境里遇到过一次很诡异的故障:应用日志轮转后,新的日志文件属主变成了 root,导致应用进程(非 root 用户)无法写入,直接停止打日志。排查到最后发现原因是我在配置文件里漏写了 create 参数。logrotate 默认会以 root 身份创建新文件,所以新文件属主就是 root。
当时应用的配置是:
bash复制/var/log/myapp/*.log {
daily
rotate 30
compress
}
没有任何 create、su、copytruncate 参数。因为应用是普通用户启动的,轮转前日志文件是应用用户写的,轮转后新文件却归 root 所有,应用自然写不进去。解决办法是在配置块里显式指定:
bash复制create 0644 myapp myapp
或者用 su myapp myapp 让 logrotate 以应用用户身份执行整个轮转流程。这个场景非常典型,因为人往往会默认"logrotate 应该像 ls 一样保留原文件权限",但实际上它默认的行为是新建 root 属主的文件。
7. 进阶配置思路:更贴近真实业务需求的轮转方案
基础配置会了之后,很多场景还需要针对性地调整。这里分享几个我实际用过并且值得参考的进阶配置模式。
7.1 多日保留与按周归档的折中方案
有些业务要求日志保留 60 天以上,但又不想每天都产生一个新文件导致目录里文件数量爆炸。一个折中方案是:每天轮转,但 rotate 数量设置足够大;同时用 olddir 把旧日志归档到独立目录,再配合外部脚本做定期归档压缩。
bash复制/var/log/myapp/*.log {
daily
rotate 180
compress
delaycompress
missingok
notifempty
olddir /var/log/myapp/archive
create 0644 myapp myapp
}
这样日志轮转后会进入 archive 目录,目录里按 compress 参数自动压缩。配合外部脚本按月份归档到冷存储即可。180 份对应约半年的保留期,如果你需要精确到天数,可以自己算好 rotate 数量。
7.2 日志量波动大:size 和 maxsize 怎么选
如果业务有典型的流量波峰波谷,比如白天 QPS 高、晚上几乎没流量,用纯 daily 会导致白天的日志文件很大,晚上的日志文件很小。用纯 size 又无法形成按天的归档语义。
最合理的配置是 daily + maxsize 组合:
bash复制daily
maxsize 500M
这个组合的意思是:正常情况下每天切一次,但如果某天日志量异常大、没到夜里 12 点就已经 500M 了,那也先切一次,防止单个文件过大。它和 daily + size 500M 的区别在于,size 条件下只要文件到了 500M 就切,即使这个周期才刚开始;maxsize 条件下则要求"至少到周期"才切。你细品一下这个差异,就能理解日常巡检中看到有人把 size 和 maxsize 混用时的后果了。
7.3 使用 zstd 替代 gzip 压缩
默认情况下 logrotate 使用 gzip 压缩,压缩率尚可,但速度一般,大文件压缩时 CPU 占用偏高。如果是新一点的系统,可以换成 zstd,它在压缩速度和压缩率上的综合表现更出色。
bash复制compress
compresscmd /usr/bin/zstd
compressext .zst
compressoptions -3
注意修改压缩命令后,compressext 也要同步改,否则 logrotate 检查压缩后缀时会搞错。另外要确认系统里确实装了 zstd。
7.4 对正在写入的日志文件做 copytruncate 时的性能考量
copytruncate 大文件时开销不小。假设一个文件有 4 GB,logrotate 执行时它会先完整复制这 4 GB,再执行 truncate。复制期间磁盘 IO 会明显升高,而且应用还在继续写日志,truncate 那一瞬间会短暂阻塞写入。
针对这个问题,我通常的做法是把 maxsize 设小一点,比如 100M~200M,让轮转动作更频繁但每次复制量更小。这样虽然轮转次数变多,但每次操作的性能尖峰都大幅降低,总体磁盘压力反而更平滑。对于日志量特别大的容器场景,这是更稳的策略。
8. logrotate 与日志采集系统的协作
现代运维环境里,日志切割不只是为了省磁盘空间,还要跟采集系统配合。Filebeat、Fluentd、Logstash 这些工具读取日志时,需要根据文件名变化来判断文件切换。如果 logrotate 的命名规则和采集系统的 pattern 对不上,就会出现重复采集或漏采集。
8.1 日志文件名变化对采集的影响
如果你用 dateext,轮转后的文件名是 access.log-20250120;如果不加,是 access.log.1。两种命名规则对采集系统来说区别很大。
Filebeat 默认根据文件路径 + 文件 inode 来识别日志文件的连续性。我们假设 Filebeat 配置的 paths 是 /var/log/nginx/*.log。在 logrotate 轮转时,access.log 变成 access.log.1,同时新生成 access.log,这个新文件对 Filebeat 来说是全新的 inode,会重新开始采集。老文件 access.log.1 虽然也匹配了 pattern,但 Filebeat 会根据状态文件判断,只读取之前没读完的部分,不会整个重读。
这里有个关键点:如果 logrotate 用的是 copytruncate,文件 inode 没变,Filebeat 会认为还是同一个文件,只是在原文件上继续读。而 logrotate 已经把文件截断了,Filebeat 的 offset 还停留在很大值,就会直接跳过新写入的内容,直到下次轮转才可能对齐。这就是为什么 copytruncate 场景容易出现"日志采集暂停"的故障。
解决思路有两个:一是尽量避免对采集中的日志使用 copytruncate,改用 create + 信号;二是如果必须用,可以考虑在采集端配置一次完整的文件重新扫描。
8.2 用 sharedscripts 配合采集端的 reload
当采集端长时间监听某个日志文件时,logrotate 轮转会带来一个隐蔽问题:采集端持有的文件句柄在轮转后可能依然指向旧文件。即使 logrotate 创建了新文件,采集端也不会自动切换,除非它检测到文件 inode 变化。
这时可以在 postrotate 里增加一条命令,让采集端重新加载文件列表。比如 Filebeat 支持发送 SIGHUP 信号触发重读配置:
bash复制postrotate
kill -HUP $(cat /var/run/filebeat.pid)
endscript
但要注意频率。如果日志量特别大,每天轮转多次,频繁给采集端发信号会造成它不断重启采集状态,反而影响吞吐。实际项目中我会评估轮转频率和采集端状态持久化机制,低频轮转时直接发信号,高频轮转时依赖文件 pattern 自动感知。
8.3 按应用维度隔离日志目录的好处
前面讲的配置都是基于系统日志目录 /var/log 下的固定路径。如果你有权限做应用层规划,我建议把日志目录设计成按应用隔离的结构,比如:
text复制/var/log/myapp/
access.log
error.log
business.log
然后 logrotate 配置里针对不同日志文件写不同规则。这样既方便采集端配置 pattern,也能针对不同日志类型分别设置保留周期。比如 access 日志保留 7 天,business 日志因为合规要求保留 180 天,如果放在同一个通配符匹配下就做不到精细控制,拆开就能各自定义。
9. 实测验证:一套自检命令组合
配置完成之后,我一般会执行下面这套命令组合,确保 logrotate 行为符合预期。这套命令组合基本覆盖了从语法到实际效果的完整验证链路。
9.1 语法和调试检查
bash复制# 全局 debug
logrotate -d /etc/logrotate.conf
# 只看某个应用的配置块
logrotate -d /etc/logrotate.d/myapp
重点观察 debug 输出里"considering log ..."的字样,它会列出 logrotate 认为需要处理的文件以及判断依据(周期、大小、日期),这对排查"为什么还没轮转"非常有帮助。
9.2 强制轮转验证
bash复制# 强制轮转某个应用
logrotate -f -v /etc/logrotate.d/myapp
执行后立即检查:
- 新文件是否生成,属主和权限是否正确
- 旧文件是否被正确重命名或压缩
- postrotate 脚本是否执行,应用是否正常写入新文件
- 采集端状态是否正常,有没有重复或漏采集
强制轮转完成之后,建议立刻看一眼应用日志,确认进程是否还在正常打日志。很多情况下你感觉配置没问题,但实际轮转后应用日志就断了,这一步能尽早暴露问题。
9.3 定时执行的验证
如果怀疑 cron 或 systemd timer 没触发,可以用下面的方式验证:
bash复制# 查看 timer 状态
systemctl list-timers logrotate.timer
# 手动触发一次 systemd 服务
systemctl start logrotate.service
# 查看 cron 日志
grep logrotate /var/log/syslog
Ubuntu 20.04 的 cron 日志默认打到 /var/log/syslog。如果看到 CRON 条目里调用 logrotate 的记录,说明定时链路是通的。
10. 从故障中学到的几个经验总结
做运维这些年,跟 logrotate 打交道的时间不算少,大多数"日志把磁盘打满"的事故,根因都是配置和实际场景不匹配。几点经验供大家参考。
第一,不要迷信发行版自带的默认配置。Ubuntu 20.04 上的 Nginx 包确实自带了 logrotate 配置,但它用的是很保守的规则,rotate 数量、压缩开关都不一定满足你的业务需求。自己项目里的应用日志,更要单独写配置,并且写完之后必须手动验证。
第二,postrotate 脚本里信号一定不能省。很多人知道 Nginx 轮转要发 USR1,但还是会漏,尤其是自己编译的 Nginx 时 pid 文件位置对不上,脚本就静默失败了。建议写完后手动跑一次 -f,观察日志文件是否真的切成功。
第三,copytruncate 能用,但能不用就不用。它对日志完整性有天然缺陷,只适合那些实在不配合的进程。现在绝大多数主流服务都支持通过信号重开日志,多花五分钟查一下 pid 文件,值得。
第四,状态文件有时候会是排查问题的最佳线索。很多 Logrotate 疑难杂症最终都能在 /var/lib/logrotate/status 里找到答案——是文件没记入状态、日期不对,还是记录被意外重置了。
第五,压不压缩、保留多少天,建议跟业务方一起定,不要纯按运维习惯来。有些业务有合规要求需要保留 180 天,有些业务日志涉及用户隐私,轮转后还涉及删除策略,这些背景信息比技术配置本身更重要。
最后再分享一个我自己一直在用的小习惯:每次改完 logrotate 配置,都会在配置文件的注释里写明"预期保留多少天、为什么这么保留"。一个月后再看这个文件,你会感谢当时那个写注释的自己。
