1. 日志只增不减,服务器是怎么悄悄被拖垮的
先说一个我早年接手的真实故障。某个业务模块跑了一周之后,突然告警说磁盘占用率到了 98%,SSH 登录都卡得不行,应用日志开始疯狂报错“No space left on device”。上去一看,/var/log/ 底下躺着几个十多个 GB 的日志文件,个别文件大到连 tail 都要卡几秒。当时第一反应是“谁没事写了死循环打日志”,查了一圈发现根本没有,就是正常的访问日志、错误日志日积月累,一旦没人管,它能把你整块磁盘都吃光。
这个场景你应该不陌生。Linux 下几乎所有服务都会写日志,Nginx 的 access.log、MySQL 的 error.log、Java 应用的 stdout 重定向文件、系统自身的 syslog,只要有流量有请求,日志文件就会一直膨胀。很多新手以为“日志嘛,存着就行,反正磁盘大”,但磁盘再大也有用完的一天,而且日志文件过大带来的问题远不止磁盘告警这么简单——排查问题时 grep 一个几十 GB 的文件,慢到你怀疑人生;日志文件占用磁盘 inode 到上限,其他进程连创建临时文件都失败;老日志没有任何清理机制,历史遗留的敏感信息长期躺在磁盘上,审计的时候都是雷。
logrotate 就是专门解决这个问题的。它的名字拆开看就是“log”(日志)+“rotate”(轮转),核心职责是:到了一定条件(比如每天、或者文件长到多大),把当前正在写的日志文件改名归档,再新建一个新文件让服务继续写,同时按照策略删除或压缩最老的归档,整个过程不需要你手动操作,也不需要重启服务。
我见过不少运维做了两三年,日志管理还是靠“写个 shell 脚本每天 crontab 跑一下”。这个思路没错,但 script 脚本要考虑的事情太多了:怎么处理服务正在占用文件描述符的问题、归档文件怎么命名、压缩时机怎么选、超过多少个归档之后要删除、如果服务挂了你还要不要继续轮转……这些边界情况 logrotate 全部已经处理好了。它不是什么新潮工具,几乎跟着 Linux 发行版捆绑安装了几十年,稳定、简单、无处不在,但真正用得好的人并不算多。
这篇文章我会从 logrotate 的执行机制讲起,逐步拆解它的配置语法,再给几组真正可以直接落地的配置模板,最后重点聊聊我踩过的那些坑——有些坑排查了好几个小时,最后发现就是一个配置项的锅。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 刨根问底:logrotate 的调度机制与执行原理
2.1 它到底靠什么被触发
很多人的误区是:我把配置写进 /etc/logrotate.d/ 了,logrotate 就会自动轮转。不对,logrotate 本身不是一个常驻后台的服务,它是一个命令行工具,必须由外部机制定时调用它才会执行。绝大多数发行版把这个定时任务放在了 cron 里。
打开 cron 配置看一眼:
code复制/etc/cron.daily/logrotate
脚本内容大致是:
bash复制#!/bin/sh
/usr/sbin/logrotate /etc/logrotate.conf
这个脚本由 cron 每天执行一次(具体时间取决于发行版,通常在凌晨 3 点到 5 点之间)。也就是说,logrotate 的默认调度粒度是“天”。你给某个日志文件配置了 daily,但它实际执行的点是在 crontab 任务运行的那一瞬间,不是凌晨零点整,不要在时间精度上较真。
另外,新一点的 systemd 发行版(比如 CentOS 7+、Ubuntu 16.04+)还会通过 systemd-timers 来触发。你执行 systemctl list-timers 可能会看到 logrotate.timer,它的触发逻辑和 cron 大同小异,不用过度纠结用哪个,反正最终都是执行 /usr/sbin/logrotate /etc/logrotate.conf。
2.2 主配置与子配置的加载关系
logrotate 执行时先读取主配置文件 /etc/logrotate.conf,然后在主配置里通过 include 指令加载 /etc/logrotate.d/ 目录下的所有子配置。用 man logrotate 里的话说,主配置负责全局默认行为,子配置负责单个软件的专属规则。
看一下默认主配置的关键几行:
text复制weekly
rotate 4
create
include /etc/logrotate.d
/var/log/wtmp {
monthly
create 0664 root utmp
missingok
rotate 1
}
第一行 weekly 是全局默认轮转周期,如果你给某个日志文件没有单独指定频率,就按每周一次处理。rotate 4 是全局保留四份归档,超出后删除最旧的。create 表示轮转后创建新日志文件。include 把 /etc/logrotate.d/ 下的文件全部加载进来,相当于把多个配置拼成一个大配置。
你在 /etc/logrotate.d/ 里写的每个配置块,可以覆盖这些全局默认值。比如 Nginx 的配置里指定了 daily,那么它的日志就是每天轮转,不受全局 weekly 影响。
2.3 状态文件和轮转判定的依据
logrotate 如何决定“我今天到底要不要转这个文件”?靠的是状态文件。默认位置在 /var/lib/logrotate.status(老版本是 /var/lib/logrotate/logrotate.status)。
状态文件的内容大致长这样:
text复制logrotate state -- version 2
"/var/log/nginx/access.log" 2025-5-20-3:0:0
"/var/log/mysql/error.log" 2025-5-16-3:25:0
一行对应一个日志文件,记录的是它最后一次轮转的时间。当 logrotate 判断 daily 规则时,它会看当前日期是否已经过了上次记录日期的一天以上。对于 size 规则,它会调 stat() 看文件大小是否超过了阈值。
这里面有一个关键逻辑我提醒你注意:不满足轮转条件时,logrotate 不会去重命名文件,但状态文件里的时间也不更新。 也就是说,如果日志文件一直很小,可能连续十几天都不会碰它一下,这是正常现象,不是配置失效了。
2.4 轮转一个正在被写入的日志文件,凭什么不丢日志
这是 logrotate 最核心的设计,也是很多人看不懂的地方。
进程写日志时,打开的是文件描述符,指向 inode。你执行 mv access.log access.log.1,仅仅是把目录项换了名字,inode 还是同一个,正在写的进程持有的是旧 inode,它会继续往这个已改名的文件里写内容。所以 logrotate 重命名完老日志之后,必须主动让进程打开一个新的日志文件,否则后续日志还是全部写进了 access.log.1。
logrotate 解决这个问题靠的是 create 指令和 postrotate 脚本的配合。
code复制create` 指令:轮转后立刻以指定权限创建一个新文件,比如 `create 0644 nginx adm`,它就帮你 `touch` 出一个新的 access.log。但此时进程还持有旧文件描述符,如果进程不重新打开文件,新文件就永远是空文件。
`postrotate` 脚本:轮转动作完成后执行的脚本。最常见的动作是给进程发信号,让它重新打开日志文件。Nginx 的做法是 `kill -USR1 $(cat /var/run/nginx.pid)`,Apache 的做法是 `kill -USR1 $(cat /run/apache2.pid)`。进程收到信号后重新打开日志文件,把文件描述符指向新创建的 access.log,从此心日志写新文件,旧文件被归档。
还有另一类情况,比如 syslog、某些守护进程不支持或者不方便发信号重开文件,这时候用 copytruncate。它的思路不一样:
code复制copytruncate` 指令:先把当前日志文件复制一份作为归档(`cp`),然后把原文件截断(`truncate -s 0`)成空文件。进程的文件描述符一直指向同一个 inode,截断之后继续写,它甚至感觉不到发生了轮转。
copytruncate 的优点是无需进程配合,缺点是在“复制”和“截断”这两个动作之间写入的那一小段日志会丢失(一般就是几毫秒到几十毫秒的窗口,但也存在)。另外大文件复制时 IO 有瞬时压力。能用 create + postrotate 解决的场景,优先不要用 copytruncate。
2.5 压缩:什么时候压最合适
归档文件默认是不压缩的,嫌占空间就开 compress,它调用的压缩工具是 gzip,生成 .gz 后缀的文件。你也可以通过 compresscmd 指定成 xz、bzip2 等工具,但一般 gzip 的压缩率和速度已经够用。
这里有个 delaycompress 选项,我要单独讲一下:它的作用是把压缩推迟到下一次轮转。为什么会有这种需求?因为很多服务(比如 Nginx)在 postrotate 信号发出之后,有一部分 worker 进程仍然持有旧文件描述符,还在往旧归档文件里写一点尾巴数据。如果轮转后立刻进行压缩,这部分数据可能还没来得及写完,压缩就把文件锁住或者读不全了。delaycompress 让新归档文件先以原始状态保留 24 小时,等下一次轮转时再压缩。它通常和 create + postrotate 搭配使用,这是 Nginx 官方推荐的标准姿势。
3. 配置语法逐条拆解:把第一份可落地的配置写出来
3.1 一个经典配置的解剖
先看一个完整的 Nginx 访问日志轮转配置:
text复制/var/log/nginx/access.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
逐行看:
-
/var/log/nginx/access.log:要轮转的日志文件路径。注意它不带大括号,路径后面直接跟左大括号。 -
daily:每天轮转一次。可选的还有weekly、monthly、以及无规律大小触发的size。 -
rotate 14:保留 14 份归档,轮转超过 14 份后删除最老的那份。“保留 14 天日志”与业务要求的日志留存期直接对应。 -
compress:归档文件压缩成 .gz。如果你同时配合delaycompress,那压缩动作延迟到下一次轮转执行。 -
missingok:如果日志文件不存在,不报错继续执行。这个选项强烈建议加上,否则某些情况下日志文件被手动删掉,logrotate会一直报错。 -
notifempty:如果日志文件是空的,跳过轮转。避免天天生成一堆 0 字节的 .1 文件。 -
create 0640 nginx adm:轮转后创建新文件,权限 0640,属主 nginx,属组 adm。注意:这里指定的属主属组必须和 Nginx worker 进程的权限匹配,否则新文件它没权限写,日志直接断更。 -
sharedscripts:这个文件列表里如果配了多个日志路径,默认每个路径轮转完都会执行一次 postrotate 脚本;加上这个选项后,所有路径统一轮转完,只执行一次 postrotate。Nginx 的 access.log 和 error.log 放在同一个配置块里时,sharedscripts非常关键,否则会发两次信号。 -
postrotate ... endscript:轮转动作之后执行的脚本块,用于通知 Nginx 重开日志文件。注意postrotate和endscript必须各自独占一行。
3.2 size 规则与 daily 规则的取舍
有些场景你希望按大小轮转,而不是按天。Java 应用如果开启了 System.out 重定向到文件,一天之内可能产生多个 GB 的日志,等不到晚上就必须切。看这个配置:
text复制/var/log/java/app.log {
size 500M
rotate 10
compress
missingok
notifempty
copytruncate
}
关键点在于 size 500M。一旦文件大小超过 500MB,就触发轮转,不关心距离上次轮转过了多久。注意:size 和 daily 可以同时写,但只有当“文件超大小 or 满足周期”任一条件满足时才会轮转,它们是“或”的关系,不是“且”的关系。如果你写:
text复制daily
size 500M
那么要么满一天、要么满 500M,谁先到转谁。想实现“必须满一天、并且超过 500M 才转”,logrotate 不支持直接表达,得靠 shell 包一层判断,实际这种需求也极少。
3.3 olddir:把所有归档挪到单独目录
默认归档文件和原日志同目录,日志一多,/var/log/ 下就散落着一堆 .1、.2、.gz 文件。想清爽一点,用 olddir 把归档挪走:
text复制/var/log/nginx/access.log {
daily
rotate 30
olddir /var/log/nginx/archive
missingok
notifempty
sharedscripts
postrotate
kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
这里有一条容易踩的坑:olddir 目录下的归档文件会沿用原文件的名字。比如 access.log 轮转后变成 /var/log/nginx/archive/access.log.1,下一次变成 access.log.2。但如果你同时开了 dateext(见下文),且日志在一天内轮转了多次,文件名会变成 access.log-20250520-13:30:00,而 olddir 目标目录必须手动创建好,logrotate 不会帮你建目录。
3.4 dateext 与 dateformat:让归档名带上日期
用 .1 .2 这种序号命名,时间一长根本看不出这份日志是哪天的。dateext 选项会让归档文件以日期作为后缀,默认格式是 -YYYYMMDD。比如:
text复制/var/log/nginx/access.log {
daily
rotate 90
dateext
compress
...
}
轮转后生成 access.log-20250520.gz,可读性好了很多。你还可以用 dateformat 自定义格式,但注意格式里的 % 必须被合法解析,而且不能带 / 否则会创建多层目录。我常用的格式是 dateformat -%Y%m%d-%s,这样按天轮转时如果某天发生多次轮转,还能用时间戳区分。
有个小细节:dateext 和 dateformat 组合时,后缀的匹配基于“原文件名 + 日期后缀”,rotate N 判断要删除哪些旧文件时,是按排序序号的,所以不要只在日期后缀上打转,保留份数依然要按业务需求卡好。
4. 实战配置模板:覆盖最常见的四类场景
4.1 Nginx:标准 HTTP 服务日志
text复制/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
几点说明:
-
用通配符
/var/log/nginx/*.log可以一次性覆盖 access.log、error.log 以及自己按域名拆分的 vhost 日志,前提是它们都在该目录下。 -
delaycompress在这里是必须的。Nginx 的信号处理是异步的,USR1信号发出后,部分 worker 可能还没有切换到新文件,立刻压缩旧文件存在截断风险。延迟到第二天压缩,安全得多。 -
sharedscripts配合通配符使用时要集中注意力:如果通配符匹配到了 10 个文件,加上这个选项,postrotate 只执行一次,Nginx 只收到一次 USR1 信号,完全够用,还避免了信号风暴。
4.2 Java 应用:日志量不可控的守护进程
Java 应用没有内置的日志切割能力,如果你用的是 Logback 或 Log4j2 自带的 RollingFileAppender,那是另一套方案;但很多人图省事,习惯把启动命令的 nohup java -jar app.jar > app.log 2>&1 & 直接写文件,那么 logrotate 接管就非常合适:
text复制/opt/myapp/logs/app.log {
size 500M
rotate 7
compress
missingok
notifempty
copytruncate
}
为什么这里用 copytruncate?因为 Java 的 stdout 重定向文件没有重开日志文件的信号机制,你给它发 USR1 信号它根本不会重新打开文件。copytruncate 不需要进程配合,直接把文件截断,Java 进程继续写入。代价是复制大文件时会有短暂的 IO 开销,忍受一下。
如果日志量极大,500M 的文件复制也会痛。你可以启用 compress 来缓解归档体积,但 copytruncate 的复制过程是不压缩的,它先复制原文件,压缩发生在归档文件上。这一步在 500M 日志的场景下一般几秒钟就完成了,IO 压力可接受。
4.3 Docker 容器日志:宿主机的 json-file 驱动
用 Docker 跑服务,日志默认位于 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log。如果不处理,这个文件会无限增长。Docker 自带 log-opts max-size 和 max-file 参数,但如果你用的是旧版本、或者 K8s 里配置没生效,那么 fallback 方案就是 logrotate:
text复制/var/lib/docker/containers/*/*-json.log {
daily
rotate 7
size 200M
compress
delaycompress
copytruncate
missingok
notifempty
}
注意:这里不要用 create + postrotate 的方式。Docker 容器内的进程持有的是容器 stdout 的文件描述符,你宿主机上发什么信号都没法让它重开宿主文件。只能 copytruncate。加 delaycompress 是因为 Docker 的文件写入和截断之间也有微小的竞态窗口,延迟压缩可以减少日志丢失概率。
另外补充一句,现代 Docker 版本还是优先用 Docker 引擎自带的日志轮转参数比较合理,logrotate 只能作为补救手段。/etc/docker/daemon.json 里配置:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "200m",
"max-file": "7"
}
}
这个方案生效于 Docker 引擎层面,不依赖宿主机 cron,比 logrotate 更优雅,能配置的情况下优先用它。
4.4 系统基础日志:wtmp、btmp、syslog
发行版自带配置已经覆盖了大部分:
text复制/var/log/wtmp {
monthly
create 0664 root utmp
missingok
rotate 1
}
/var/log/btmp {
missingok
monthly
create 0600 root utmp
rotate 1
}
wtmp 记录登录成功信息,btmp 记录登录失败信息,这些文件默认几乎不用改。/var/log/syslog 或 /var/log/messages 的轮转由 rsyslog 配套的配置管理,在 /etc/logrotate.d/syslog 里,日常不用折腾。
5. 那些年我踩过的坑:logrotate 失效排查链路
5.1 故障现场:日志文件明明很大,就是不轮转
有一回配置了 size 100M,几天后上去看,文件已经 800M 了,logrotate 像没看见一样。手动执行一次:
bash复制/usr/sbin/logrotate -v /etc/logrotate.conf
输出里清晰地显示:
text复制considering log /var/log/myapp/app.log
log does not need rotating (log size is below the 'size' threshold)
可文件明明 800M?一看,问题出在我给 size 写成了 size = 100M,而 logrotate 支持的写法是 size 100M,没有等号。它把整个 = 100M 当成了不可识别的参数,静默忽略了这条规则,回退到全局 weekly,但 weekly 判断的是日期差而不是大小,自然不触发。
这类问题在于:logrotate 解析配置时对未知参数不会直接报 fatal error,很多情况下只是忽略并继续。所以写完配置务必用下面的命令检测:
bash复制logrotate -d /etc/logrotate.conf
-d 是 debug 模式,它会输出每条日志文件被判定为“需要轮转”还是“不需要轮转”的原因,不会实际执行,不会动文件。这是排查“不生效”问题的第一工具。
5.2 故障现场:dateext 与 size 规则叠加,导致归档互相覆盖
另一个印象深刻的坑:我配置了一个按大小轮转的 Java 应用日志:
text复制/opt/myapp/logs/app.log {
size 500M
dateext
rotate 10
compress
copytruncate
}
某天访问量暴增,文件一天内触发了 4 次轮转。由于 dateext 默认日期格式是 YYYYMMDD,同一天内生成的归档文件名完全相同——app.log-20250520.1、app.log-20250520.2……在文件系统层,后续的归档直接覆盖了之前的归档。结果就是:保留的 10 份里有 6 份是同一时刻的数据,其他时间段的日志全部丢失。
这个问题的本质是 dateext 粒度太粗。解法有两种:
- 换回序列号命名:去掉
dateext,用.1.2编号,绝不冲突。 - 用精确到时间戳的自定义格式:
dateformat -%Y%m%d-%s,%s是 Unix 时间戳,每次轮转都会不同。
我后来统一采用第二种,日期可读性和唯一性都有了。
5.3 故障现场:服务没重启,新日志全部写入旧归档
有一次用 create + postrotate 配置 Apache 日志,轮转后当天的新访问日志全部写进了 access.log.1,新生成的 access.log 一直是 0 字节。
排查过程是这样的:
- 先确认轮转是否真的执行了,
logrotate -v,显示轮转成功。 - 再看
postrotate脚本是否执行了,在脚本里加一行echo "rotate executed at $(date)" >> /tmp/rotate_debug.log,重新轮转,发现脚本内容没被追加。 - 再看 Apache 的错误日志,也没报信号相关错误。
- 最终定位到:这个 Apache 实例的 pid 文件路径并非
/var/run/apache2.pid,而是/var/run/apache2/apache2.pid。脚本里找不到 pid 文件,kill -USR1没有执行,旧进程也没有收到信号。Apache 继续持有旧 inode,日志全写进了归档文件。
修复很直接,改成实际路径。这类问题的通用排查法则是:postrotate 脚本里所有文件路径、pid 路径都必须换成目标服务实际的值,不要照抄模板。 写完配置,强行轮转一次,立刻验证新日志是否进入了新文件。
5.4 故障现场:权限不足导致新日志文件无法创建
create 0640 nginx adm 看起来人畜无害,但 Nginx worker 进程是以 nginx 用户跑的,而新日志文件的属主是 nginx 且权限 0640,没问题。问题出在另一种情况:日志目录属主是 root,worker 进程对目录没有写权限。
轮转时 logrotate 会在原目录下创建新文件,创建者是谁?取决于 logrotate 进程的运行用户——通常 cron 里运行 logrotate 的是 root,所以 root 能在 root:root 的目录里创建文件。但创建出来的文件如果属主是 nginx,nginx 用户可以写;如果你没写 create,默认是根据原文件的属主属性创建,一般情况下也没问题。
真正容易出问题的是:某些服务(比如 MySQL)以 mysql 用户运行,logrotate 创建的新文件如果被 create 指定成了 root:mysql 或 mysql:root,权限不对,MySQL 重开日志文件时发现路径存在但没权限写,直接报错或者继续写旧归档。遇到这种情况,检查服务进程的用户、目录权限、文件属主三者是否匹配即可。
还有一个隐性权限坑:olddir 目标目录必须是 logrotate 可写、且目标文件最终属主合适的。 如果归档目录是 root:root 0755,而 logrotate 以 root 跑,没问题;但如果你试图把归档文件属主设置成其他用户,又要保证用户能读,酌情调整目录权限。
5.5 排查链路汇总:一页纸的故障定位流程
给你保留一份我自己常用的排查流程,遇到 logrotate 疑似失效,按顺序执行:
| 步骤 | 命令 | 目的 |
|---|---|---|
| 1 | logrotate -d /etc/logrotate.conf |
检查配置解析是否有误,看每个文件的判定结果 |
| 2 | logrotate -v /etc/logrotate.conf |
详细模式手动执行,看是否真的轮转、输出每个动作 |
| 3 | cat /var/lib/logrotate.status |
查看状态文件,判断上次轮转时间是否符合预期 |
| 4 | stat 日志文件 |
查看当前文件的 mtime、size,对照配置判断条件是否满足 |
| 5 | ps aux | grep 服务进程 |
确认服务还活着,且 pid 文件路径正确 |
| 6 | bash -x 手动执行 postrotate 脚本内容 |
单独验证脚本是否可执行、路径是否正确 |
| 7 | ls -l /var/log/ |
检查归档文件是否生成、文件名是否符合预期 |
这个流程走一遍,绝大多数问题能定位到根因。
6. 调试与验证的完整姿势:敢手动,才能放心自动化
6.1 常用参数速查
logrotate 的常用命令行参数,每个场景下都有它的意义:
-v:verbose,输出详细轮转过程,适合确认动作是否执行。-d:debug,不真正执行轮转,只打印“假如执行会发生什么”。配合-v效果最好:logrotate -dv /etc/logrotate.conf。-f:force,强制轮转。即使不满足条件,也执行一次轮转。-s:指定状态文件路径,默认是/var/lib/logrotate.status。测试时可以指定到临时文件,避免污染真实状态:logrotate -s /tmp/logrotate.status -f /etc/logrotate.conf。-m:指定压缩工具,默认是 gzip。
这里提醒一句:-f 会把未满足条件的文件也强制轮转,这在调试时非常有用,但千万不要在生产环境乱用。 你强制转一次,意味着保留的归档数量会被消耗一份,旧文件可能被提前删除。比如 rotate 14,现在有 14 份归档,你 -f 一下,多出的最老归档会被删除,日志留存期被压缩了一天。
6.2 验证轮转结果是否正确的三个维度
强制轮转完成后,别急着收工,验证三步:
-
新日志是否正常写入:关键验证方法是轮转后立即请求一次 Nginx 接口,然后
tail -f /var/log/nginx/access.log,能看到新请求行出现,就说明新文件工作正常。 -
归档文件是否完整可读:
ls -lh /var/log/nginx/,确认归档文件存在,体积合理。开启压缩的,zcat access.log-20250520.gz | tail -5看一下内容是否正常。 -
状态文件是否更新:
cat /var/lib/logrotate.status,确认对应日志文件的记录时间刷新为当前时间。
6.3 测试配置时的小技巧
我建议在测试环境里先这么干:把主配置文件复制一份做一个独立的测试配置,比如 /tmp/logrotate-test.conf,里面只放你要测试的那一条规则,然后:
bash复制logrotate -dv -s /tmp/logrotate-test.status /tmp/logrotate-test.conf
-s 指定独立状态文件,避免干扰生产环境的真实状态。-d 不真实执行,只打印逻辑。等确认无误,再扩展到真实配置里。
7. 进阶设计:从“会配”到“配得优雅”
7.1 多目录批量覆盖的两种写法对比
假设你有多个业务模块,每个模块都有自己的日志目录,你想用一份配置覆盖全部:
写法一,块内多个路径:
text复制/opt/app1/logs/*.log /opt/app2/logs/*.log {
daily
rotate 14
...
}
写法二,通配加 sharedscripts:
text复制/opt/app*/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
# 如果这些服务都支持同一个信号重开日志
find /var/run -name "*.pid" -exec kill -USR1 {} \;
endscript
}
我的建议是:能用写法一就不要用写法二里的 find + kill。不同服务的信号语义可能不同,Nginx 的 USR1 是重开日志,Java 进程收到 USR1 可能直接退出。把多个服务的日志混在一个配置块里,会让 postrotate 变得非常不可控。宁可多写几个配置块,每个服务的信号逻辑独立,出问题也好排查。
7.2 轮转状态的监控与告警
日志轮转这种事,没人盯着的话,出问题可能在几个小时后才被发现。我的经验是加一个简单的巡检脚本,每天检查归档目录里今天是否生成了新的归档文件:
bash复制#!/bin/bash
# 检查 /var/log/nginx/ 下今天是否有新的 .gz 归档
today=$(date +%Y%m%d)
count=$(ls /var/log/nginx/*${today}*.gz 2>/dev/null | wc -l)
if [ "$count" -eq 0 ]; then
echo "logrotate可能未执行" | mail -s "logrotate alert" ops@example.com
fi
这个脚本可以作为参考,实际落地时建议用现有的监控系统(Zabbix、Prometheus + Alertmanager、云监控的 shell 采集)来实现,不自己造轮子。核心就一条:轮转失败这件事一定要让系统自动发现,不要等磁盘满了才收到告警。
7.3 清理策略:不只是 rotate N,还要考虑磁盘预算
rotate 14 说的是“保留 14 份归档”,但如果你不知道每一份归档平均多大,这个数字无法直接换算成磁盘占用量。我有一次帮别人梳理日志方案,对方说保留了 30 份 Nginx 访问日志,我一看每份 2GB,30 份就是 60GB,只为了一天的访问日志。这是明显的资源浪费。
更好的设计思路是:先算磁盘预算,再反推 rotate 数量。
假设 /var/log/nginx 所在磁盘还有 20GB 余量,访问日志每天约 1.5GB,gzip 后约 300MB。你愿意为日志掏出 8GB 磁盘,那么:
- 归档数量上限 = 8GB / 300MB ≈ 26 份
- 但你还要留出压缩前的临时空间,保守一点,
rotate 20 - 再配合每天的归档巡检,如果发现连续多天日志量异常暴增,及时调小 rotate 或压缩级别
另外,daily 且 rotate 20 意味着最多保留 20 天,如果业务部门要求“日志至少留 90 天”才能满足审计需求,那 20 天就是不合格的。反过来,90 天日志如果每天 300MB 压缩后,总占用 27GB,磁盘能不能扛住也是要提前算清楚的。这种“业务需求 vs 磁盘配额”的矛盾,最后一定是要由运维和业务方一起拍板的,logrotate 只是把最终决策落地的工具。
7.4 特殊日志:按周轮转、按需执行、prerotate 的使用
最后补充几个进阶选项:
weekly:日志量小、分析频率低的场景,比如某些内部系统登录日志,一周转一次完全够。prerotate ... endscript:在轮转之前执行的脚本,比如先执行某个命令把日志里的临时内容清理掉,再转。su root root:如果logrotate以非 root 用户运行,某些文件可能没有读取权限,用su指定提权用户。平时 cron 以 root 跑的话不需要,但在某些容器环境或安全加固策略下会遇到。maxsize 100M:这是daily的补充项。如果配置了daily且日志量暴增,单独daily要等一天才能轮转,但加了maxsize 100M后,只要超过大小阈值也会触发一次轮转,之后再回到每日周期。这个选项非常适合“平时日志不多,偶尔流量尖峰”的业务。
text复制/var/log/nginx/access.log {
daily
maxsize 500M
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 $(cat /var/run/nginx.pid)
fi
endscript
}
daily 保证常态下每天切一次,maxsize 保证流量尖峰时磁盘不会瞬间被打满。
7.5 与 systemd journald 的边界
如果你用的是 systemd 系发行版,journald 的日志有自己的轮转机制,不归 logrotate 管。/etc/systemd/journald.conf 里的 SystemMaxUse、SystemMaxFileSize、MaxRetentionSec 才是控制 journal 日志大小的关键。这跟 logrotate 面向的 /var/log/ 下普通文件是两条线,别搞混。很多时候你以为“logrotate 没生效”,其实那个文件是 journald 管理的,logrotate 根本不该去碰它。
这两套体系有时候会交叉:比如 rsyslog 把 journal 导出成了 /var/log/syslog,那这个文件由谁轮转?答案是由 rsyslog 自己的 logrotate 配置管,和 journald 内部的轮转是两回事。梳理日志方案时先搞清楚日志的采集链路上每一环是谁在控制,比一上来就写 logrotate 配置更重要。
我自己在实践中慢慢形成了一个习惯:每接手一个新环境,第一件事不是急着配规则,而是先花十分钟把 logrotate 的全局配置、各应用配置、状态文件这三个东西完整看一遍。很多“日志轮转不生效”的诡异故障,其实都是前期配置积累下来的旧账。先把旧账理清楚,再谈优化和治理,你会少走很多弯路。
