1. 为什么日志必须轮转磁盘、inode 与文件句柄的三重压力1.1 日志只增不减的三个后果任何一个都能让你半夜爬起来先讲个真实场景。上周我接到一个告警线上应用磁盘使用率 100%ssh 上去一看/var/log/nginx/access.log已经 87G还在疯长。这种问题在 Linux 系统管理里太常见了而且不只是磁盘满了这么简单它通常是三重压力叠加磁盘空间被占满。这是最直接的表现。应用还在正常写日志但系统其它组件开始报错连 ssh 登录都可能变得异常缓慢。inode 被耗尽。日志文件数量暴增时文件系统 inode 也会被吃光。inode 耗尽后整个分区连一个空文件都创建不出来那时候连/tmp都写不进去排障手段直接少一半。日志本身失去排障价值。一个 87G 的日志文件grep一次要等几分钟tail都卡想从里面捞一条几小时前的错误信息基本等于大海捞针。所以日志轮转不是锦上添花是 Linux 系统管理里必须做的日常操作。logrotate 就是干这件事的标准工具按你定义的规则定期把日志文件改名、压缩、清理同时让应用能继续往新文件里写。1.2 logrotate 的运转机制一句话讲清楚logrotate 本身不常驻内存也不自己做定时任务它是被 cron 或 systemd timer 周期性调用的。每次执行时按配置文件里的规则检查每个日志文件的当前状态判断该不该轮转周期到了大小超了两者都满足该轮转的话按照rotate N的设定把旧文件依次改名或压缩保留 N 份更新状态文件记录每个文件最近一次轮转的时间执行postrotate脚本让应用重新打开新的日志文件。整套流程看起来简单真正容易出问题的地方全在细节配置文件的读取顺序、size和周期这两个触发条件的优先级、轮转后新文件的属主属组、还有那些看似正常其实没轮到的隐蔽故障。这些我都会在后面展开。1.3 为什么不能直接 rm 日志文件这个坑必须先排掉很多人图省事磁盘满了直接rm access.log。在 Linux 下这有个非常隐蔽的坑如果某个进程正打开着这个文件rm 只是删掉了目录项文件本身还在占用的磁盘空间不会释放直到进程关闭这个文件描述符。那进程什么时候会关闭可能永远不关。Nginx、Tomcat、Java 进程都非常典型它们长期持有日志文件的 fd。你rm之后df -h一看磁盘还是满的就因为那个文件被进程钉住了。你甚至能找到它lsof | grep deleted能看到一堆(deleted)的文件还在被进程持有。logrotate 解决这个问题的方式是先改名、再通知应用重新打开或者用copytruncate让原文件描述符继续有效。理解了这个背景后面看 Nginx 配置里的postrotate脚本你就能明白为什么它要那样写。2. 配置文件读取逻辑主配置、子配置与指令覆盖关系2.1 /etc/logrotate.conf 里到底写了什么几乎所有主流 Linux 发行版都默认装好了 logrotate主配置在/etc/logrotate.conf。先打开看一眼Ubuntu 系的默认内容大致是这个样子weekly rotate 4 create dateext compress include /etc/logrotate.d /var/log/wtmp { monthly create 0664 root utmp minsize 1M rotate 1 }这段配置信息量很大逐行解释weekly是全局默认轮转周期所有没单独指定周期的日志都按每周轮转rotate 4保留 4 份旧日志create表示轮转后自动创建一个新的空日志文件dateext用日期作为旧日志文件的后缀compress轮转后的旧日志用 gzip 压缩include /etc/logrotate.d把子配置目录合并进来最后那个wtmp配置块是一个完整的独立规则。2.2 include 与 /etc/logrotate.d 子配置的合并顺序include /etc/logrotate.d是核心机制。装 Nginx 时包管理器会自动放一个/etc/logrotate.d/nginx装 Docker 也有自己的syslog、unattended-upgrades 都有。每个应用维护自己的轮转规则互不干扰这是 logrotate 设计得最好的地方。理解 include 机制要注意两点。第一子配置里的指令会覆盖主配置里之前的全局默认值。比如主配置写了weekly但/etc/logrotate.d/nginx里写了daily那 nginx 日志就按每天轮转。第二include 之后出现的配置块同样生效像上面那段默认配置里的 wtmp 块它是在 include 之后才声明的依然有效。实际操作中我基本不在主配置里改太多东西只保留全局默认值所有业务日志的规则都单独写在/etc/logrotate.d/下的独立文件里一台机器上部署了什么应用目录里一眼能看到排查问题也方便。2.3 高频指令语义速查表我整理了一份生产环境最常用的指令对照表每一条都标注了适用场景。这张表值得存下来写配置的时候随手查比翻 man page 快得多。指令作用典型用法注意事项daily/weekly/monthly轮转周期daily周期由 cron 执行频率决定最终精度rotate N保留 N 份旧日志rotate 14超过 N 份后最旧的会被删除compress旧日志 gzip 压缩compress压缩的是轮转出来的历史文件delaycompress延迟一天压缩delaycompress配合 postrotate 信号重开场景使用dateext用日期作后缀dateext可与dateformat自定义格式create mode owner group轮转后创建新文件create 0640 www-data adm属主必须匹配实际写日志的用户copytruncate复制后清空原文件copytruncate应用无需感知但会丢极小窗口日志missingok文件不存在不报错missingok所有配置都建议加notifempty空文件不轮转notifempty防止产生一堆空的历史文件sharedscripts所有文件轮转完只执行一次脚本sharedscripts避免 postrotate 脚本对每个文件重复执行postrotate / endscript轮转完成后执行脚本见 Nginx 模板通常用于通知进程重开日志size 100M超过大小就轮转size 500M独立于周期每天 cron 最多检查一次maxsize 100M周期优先、大小兜底maxsize 100M周期未到但超了大小时也会轮转minsize 100M周期到了且超大小才轮转minsize 1M周期没到就不动即使超过大小su user group以指定用户执行轮转su root syslog解决目录权限不足的报错此外还有olddir、maxage、extension这些我实际用得较少等你把上面的核心指令吃透再按需查阅即可。2.4 一眼看懂的默认配置逐行解析把前面那段默认配置翻译成人话就是所有没单独配置的日志每周转一次保留 4 份轮转后新建空文件旧文件加日期后缀并压缩。而wtmp这个登录记录文件因为一年也长不了多大所以每月转一次保留 1 份。注意wtmp块里的minsize 1Mwtmp 只有超过 1M 且到了月度周期才轮转。这种配置很典型——低频小文件用minsize而不是size避免一个月不到就转出好几个空壳文件。理解了这个逻辑你就能根据每个日志的增长速度定制不同策略而不是所有日志套一个模板。3. 生产环境可直接抄的三份配置模板Nginx、Tomcat 与 Docker 容器日志3.1 Nginx信号重开日志文件的标准写法Nginx 的日志是 worker 进程直接打开的轮转后必须让 Nginx 重新打开新的日志文件。标准做法是给它发送USR1信号。我用的模板长这样保存在/etc/logrotate.d/nginx/var/log/nginx/*.log { daily rotate 14 compress delaycompress missingok notifempty create 0640 www-data adm sharedscripts postrotate if [ -f /run/nginx.pid ]; then kill -USR1 $(cat /run/nginx.pid) fi endscript }几个关键点create 0640 www-data adm是为了保证轮转后新文件属主是www-data否则 Nginx worker 进程没有写权限日志文件会被跳过新日志直接丢光。postrotate里的脚本是检查 pid 文件存在后给主进程发USR1。Nginx 收到这个信号会优雅地重新打开日志文件不会中断请求。delaycompress配compress是有讲究的USR1发出去后Nginx 可能还有一个极短的窗口在写旧文件。如果立即压缩可能把还在写入的内容截断。延迟一天压缩能确保旧日志完整。sharedscripts必须加。不加的话/var/log/nginx/*.log匹配到几个文件就执行几次postrotateNginx 会被反复通知重开日志虽然一般不致命但完全没必要。3.2 Tomcat catalina.outcopytruncate 为什么是唯一可靠方案Java 系的 Tomcat、Spring Boot 应用日志里最让人头疼的是catalina.out。它是 Tomcat 的标准输出和标准错误重定向Tomcat 进程从头到尾只持有这一个文件描述符永远不会主动重新打开。你就算用create把新文件建好Tomcat 也还是往旧 fd 上写旧文件又没删干净的话日志就飘到了奇怪的地方。对这种场景copytruncate是唯一省心的方案。原理是先copy一份当前文件作为历史日志然后立刻把原文件truncate清空。关键是原文件的 inode 和 fd 都没变应用进程完全感知不到还在往同一个 fd 上写只是位置回到了文件开头。我的 Tomcat 配置/opt/tomcat/logs/catalina.out { copytruncate daily rotate 30 compress missingok notifempty size 500M }这里我特意加了size 500M。catalina.out平时没多大但一旦线上出问题疯狂打堆栈几个小时内就能长到几个 G。光是daily不够加上size条件就能在磁盘爆掉之前提前轮转。3.3 Docker 容器 JSON 日志logrotate 兜底 daemon 层限流双保险Docker 容器的标准输出日志存在宿主机上路径固定是/var/lib/docker/containers/容器ID/*-json.log。这类文件的典型特点是只增不减容器进程不会重新打开日志文件。所以这里也必须上copytruncate/var/lib/docker/containers/*/*-json.log { copytruncate daily rotate 7 compress missingok notifempty delaycompress }但是我要特别提醒这只是一道兜底保险真正应该做的是在 Docker daemon 层面限制单个日志文件大小。修改/etc/docker/daemon.json{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 5 } }配置生效后每个容器日志超过 100M 就自动轮转最多保留 5 份。这比 logrotate 每天检查一次及时得多能真正防止日志在两次 cron 之间把磁盘撑爆。新配置只对之后创建的容器生效存量容器需要重建才用它。3.4 自定义 Python / Golang 应用信号重开 vs copytruncate 的取舍自己写的应用决定权在你手里这是我建议的取舍标准如果你的应用能处理信号比如 Python 的logging模块配置好Golang 用lumberjack优先用 Nginx 那套方案轮转后发信号让应用重新打开日志文件。日志零丢失还能正常压缩。如果应用代码改不动或者根本不会处理信号那只能copytruncate但要接受它有一个极小的写丢失窗口。我在实际项目里见过不少人给 Python 应用配了个daily create的规则结果应用还在往旧 fd 写新文件一直是空的。排查了一下午最后才意识到是 fd 没换血的问题。这类配置看起来正常日志就是不对劲的故障本质往往就是这里。4. 五处容易翻车但文档不会高亮的配置细节4.1 size、minsize、maxsize 的语义差别容易把周期和大小混为一谈这是翻车率最高的一处。很多教程只告诉你size 100M表示超过 100M 就轮转但当你把它和daily混用时三个指令的优先级完全不同配置组合轮转条件典型场景size 100M文件超过 100M 就转不管周期文件增长极快必须按大小控制daily size 100M周期和作用叠加超过 100M 且今天未转就会转相当于每天最多一次超了兜底daily maxsize 100M周期到了就转周期没到但超 100M 也转阿里等大流量场景常用防爆盘daily minsize 100M周期到了且超过 100M 才转缺一不可低频小文件防止转出空壳记不住的话就记一句话size是超了就转maxsize是周期优先大小兜底minsize是周期到了还得够大才转。我自己踩过minsize的坑配了weekly minsize 1M结果某周日志文件只有 800Klogrotate 一声不吭地跳过状态文件里那个文件上周没轮转。后来看 debug 输出才明白minsize是不仅要到期还得过线。4.2 dateext 改名规则一次修改导致旧备份清理失灵dateext默认会生成access.log-20250101这种格式。如果你之前没用dateext轮转文件是access.log.1、access.log.2后来突然加了dateext轮转文件名变成了access.log-20250101。问题在于logrotate 的rotate N清理逻辑只认它自己生成的那套命名模式。你没改配置之前留下的access.log.1、access.log.2它不会帮你清理会一直躺在那里直到你手动删掉。另外dateformat可以自定义后缀格式dateext dateformat -%Y-%m-%d注意dateformat必须配合dateext使用而且有些版本要求格式里带%以外的字符比如横线。我习惯用-%Y-%m-%d因为按天排序时字符串序和自然时间序一致。但同样提醒不要频繁改命名规则。每次改动都会造成新旧命名并存排查时非常容易看晕。4.3 copytruncate 的间隙丢日志审计日志千万别这么配copytruncate的机制是先复制、再清空。问题出在复制和清空之间的那个极短窗口如果有日志正好在这个窗口里写入那这部分数据就丢了。普通业务日志丢几十 KB 问题不大但审计日志、金融交易日志这类要求不丢数据的场景绝对不能用copytruncate。我曾经给一个支付系统的审计日志配过copytruncate上线第二天就被对方 DBA 看日志发现了断档。后来改成了create postrotate信号重开的方案日志零丢失。所以原则很简单能发信号重开日志的应用一律别用copytruncate实在不行才用它且必须接受它有丢失窗口。4.4 缺 sharedscripts 导致 postrotate 脚本跑 N 次如果配置里写的是/var/log/nginx/*.log这种通配符同时又没有声明sharedscripts那么 logrotate 会对每一个匹配的文件单独执行一次完整的轮回包括单独执行一次postrotate。假设匹配了 3 个日志文件Nginx 的USR1信号就被发了 3 次。虽然 Nginx 不会因此出大事但如果你在postrotate里写的是重启整个应用那问题就大了服务会被反复重启。sharedscripts的含义是所有匹配的文件轮转完毕之后才执行一次脚本。所以只要是通配符配置且postrotate里的动作不是必须针对单个文件就应该加上它。这是低成本、高收益的一行。4.5 权限不匹配目录 insecure 与 create 属主错误权限相关的坑分两种我一一说明。一是指纹目录权限导致的 security 拒绝。logrotate 出于安全考虑如果日志文件所在的父目录是world writable或group writable 但属组不是 root它会直接跳过并输出一条类似下面的错误error: skipping /var/log/business/app.log because parent directory has insecure permissions生产环境如果业务把日志目录的属组改成了业务账号组权限还带写极容易触发这个保护机制。解决办法是调整目录属主属组去掉多余的写权限或者在配置块里加su root 业务组。二是create的属主没对齐。轮转后新建的日志文件归属如果不正确应用就会写不进去。比如 Nginx worker 是www-data用户但你create 0644 root root那新文件对www-data是只读的Nginx 只好把错误写到 error.log 里access.log 文件大小永远是 0。排查时第一件事就是ls -l看新文件属主和实际写日志的进程用户是否一致。5. 日志不轮转的完整排查链路从状态文件倒推问题根因5.1 先确认调度与执行cron 或 systemd timer 到底有没有跑日志不轮转是我在群里被问得最多的问题。先说结论90% 的情况轮转其实跑了但没轮到你想的那个文件。不过不管怎样第一步都应该确认 logrotate 本身是否被调度执行。不同发行版调度方式不一样。Ubuntu 18.04 之后默认有logrotate.timer可以用systemctl status logrotate.timer查看RHEL/CentOS 系列通常放在/etc/cron.daily/logrotate里由 cron 或 anacron 在凌晨执行。先确认调度存在systemctl status logrotate.timer cat /etc/cron.daily/logrotate如果调度正常再手动触发一次看输出/usr/sbin/logrotate -v /etc/logrotate.conf注意别一上来就logrotate -f。-f是强制轮转不管条件是否满足会干扰你判断为什么它不转。5.2 读状态文件last rotated 时间戳暴露一切logrotate 会把每次轮转记录写进状态文件默认路径/var/lib/logrotate/logrotate.status。它长这样logrotate state -- version 2 /var/log/nginx/access.log 2024-12-20-3:0:0 /var/log/nginx/error.log 2024-12-19-3:0:0 /var/log/business/app.log 2024-11-30-3:0:0看到没第三行那个文件名最后轮转时间停在 11 月 30 日而今天是 12 月 20 日——问题就锁定了。这时候要思考的是为什么 20 天没轮转在这个阶段我通常还会顺手看一眼日志文件的大小和修改时间ls -lh /var/log/business/app.log*如果只有一个文件且 mtime 一直在更新说明应用写日志正常但 logrotate 根本在考虑或者跳过它。5.3 debug 模式回放决策-d 输出里藏着 skipped 的真实原因接下来是最关键的一步用 debug 模式回放一次决策过程。/usr/sbin/logrotate -d /etc/logrotate.d/business-d是 dry-run只打印会执行的操作不真正动文件。注意它默认会重新读取状态文件所以能看到这个文件上次轮转是什么时候、现在时间是否满足周期、大小是否触发、最终决定是 rotate 还是 skip。输出里我最常关注几个关键字considering log /var/log/business/app.loglogrotate 在考虑这个文件了log does not need rotating周期和大小条件都没满足log needs rotating触发了轮转条件skipping /var/log/business/app.log because parent directory has insecure permissions权限问题见 4.5 节。有一次我排查了半天-d输出里赫然写着log does not need rotating——因为配置文件里写的是monthly而不是我以为的daily。很多时候问题就是这么朴素别一开始就往复杂了想。5.4 高频报错信息速查看到这些别慌把最常见的报错放到一张表里遇到直接对号入座报错信息节选含义处理办法parent directory has insecure permissions父目录权限存在安全风险调整目录属主去掉多余组写权限或加su指令unknown option xxx配置关键字拼写错误对照 2.3 节速查表逐字检查ALERT exit status 1至少一条配置执行失败往前翻具体 error 行通常前面已直接提示failed to rename ... No such file or directory目标文件路径不对或目录不存在检查配置路径、磁盘是否已满、目录是否被误删log does not need rotating条件不满足检查周期和minsize是否把条件卡死了用-v看判断依据状态文件里文件很久没更新该文件被某条配置屏蔽了结合-d输出看是否被skip是否是通配符冲突5.5 修复后如何验证从 -d 到 -f 的完整流程修复后别急着收工按这个顺序验证一遍# 1. 先 dry-run确认配置语法和判断结果都正确 /usr/sbin/logrotate -d /etc/logrotate.d/business # 2. 确实没问题了再指定该配置文件强制轮转一次 /usr/sbin/logrotate -vf /etc/logrotate.d/business之后检查三样东西# 目录下是不是生成了一份新历史日志新文件是否正常 ls -lh /var/log/business/ # 新文件的属主属组是否和应用写入用户一致 ls -l /var/log/business/app.log # 状态文件里的时间戳是否更新为现在 tail -n 5 /var/lib/logrotate/logrotate.status如果新日志文件在应用持续写入下大小正常增长旧历史文件被正确压缩状态文件时间戳已更新那这个轮转规则才算真正闭环了。6. 上线前的演练与日常巡检让 logrotate 的状态透明可见6.1 隔离演练在 /tmp 里把轮转流程跑一遍新写好的配置我从来不直接扔到生产环境就完事。先在/tmp里搭一个迷你测试环境把所有变量都验证过再上线。mkdir -p /tmp/logtest/logs # 造一个稍大一点的日志文件 dd if/dev/zero of/tmp/logtest/logs/app.log bs1M count20 cat /tmp/logtest/logrotate.conf EOF /tmp/logtest/logs/app.log { daily rotate 3 compress missingok notifempty create 0644 $(whoami) $(id -g -n) size 1M } EOF # debug 模式预演 /usr/sbin/logrotate -d /tmp/logtest/logrotate.conf # 确认没问题后强制轮转 /usr/sbin/logrotate -vf /tmp/logtest/logrotate.conf # 看结果 ls -lh /tmp/logtest/logs/这一步能帮你提前发现配置路径写错、权限设置不对、压缩指令不兼容等低级问题而且是在不影响生产环境的情况下。6.2 演练输出怎么读considering、rotating 与 postrotate跑logrotate -vf时你会看到类似下面的输出reading config file /tmp/logtest/logrotate.conf Considering log /tmp/logtest/logs/app.log log needs rotating rotating log /tmp/logtest/logs/app.log, log-rotateCount is 3 renaming /tmp/logtest/logs/app.log to /tmp/logtest/logs/app.log.1 running postrotate script compressing log with: /usr/bin/gzip看懂这几行就掌握了 logrotate 的行为模型先读配置、逐个考虑文件、判定需要轮转、改名、执行 postrotate、最后压缩。如果postrotate里写的脚本有问题这时会直接报错你就能当场修正。每台新上线的机器我都建议跑一遍这个流程把配置能加载和配置真能跑通区分开。6.3 日常巡检的小脚本思路状态文件 磁盘水位轮转配置上完线不代表万事大吉。日志增长模式会变应用升级可能改日志路径磁盘剩余也会变化所以日常巡检要有。我最简单的做法是看状态文件里有没有明显过老的时间戳。写一个小脚本每周检查一次#!/bin/bash # 找出状态文件里超过 10 天没轮转的日志 while IFS read -r line; do file$(echo $line | awk -F {print $2}) date_str$(echo $line | awk -F {print $3} | awk -F- {print $1}) last$(date -d $date_str %s 2/dev/null) now$(date %s) diff_days$(( (now - last) / 86400 )) if [ $diff_days -gt 10 ]; then echo WARN: $file not rotated for $diff_days days fi done /var/lib/logrotate/logrotate.status再配合磁盘水位监控比如df -h /超过 80% 就告警。这两道关卡配合基本不会让日志爆炸这种事再打得我措手不及。最后再补一句个人体会。logrotate 的资料到处都是但真正值钱的不是那几条指令而是每一个配置决定背后的权衡周期和大小怎么组合、create和copytruncate怎么选、信号重开还是硬着头皮容忍丢日志窗口。我每次接手一台新机器第一件事就是打开/etc/logrotate.conf和/etc/logrotate.d/挨个看把每份配置的意图还原出来。这个习惯帮我避开了很多次线上事故也让我在写新规则的时候更有把握。你也试着从看懂配置开始慢慢建立自己的判断标准比背一百条命令有用得多。