Ubuntu服务器跑久了,最怕的就是磁盘被日志撑爆。应用日志如果不加控制,动辄几十个G,排查问题的时候打开文件都费劲,更别说可能直接导致服务宕机。解决这个问题的标准工具就是logrotate,它几乎预装在每一台Ubuntu服务器上,但很多人只用默认配置,遇到复杂场景就抓瞎。下面直接讲怎么用它把应用日志管得服服帖帖。

logrotate的核心机制不是轮转,是状态追踪

很多人以为logrotate就是个定时切分日志的脚本,其实它最关键的组件是一个状态文件,通常位于

/var/lib/logrotate/status
。这个文件记录了每个日志文件上次轮转的时间、大小等信息。logrotate每次执行都会对比当前日志状态和这个文件里的记录,决定要不要触发轮转。如果你手动删了日志或者改了系统时间,状态文件没更新,轮转就会乱掉。所以排查轮转失效问题时,第一件事就是检查这个状态文件里的记录是否和实际情况一致。

主配置文件与子配置的加载顺序决定最终行为

Ubuntu上logrotate的主配置文件是

/etc/logrotate.conf
,里面通常有一行
include /etc/logrotate.d
,这意味着所有放在
/etc/logrotate.d/
目录下的应用配置都会被加载。加载顺序是先读主配置,再按字母顺序读子目录里的文件。如果两个配置里有冲突的指令,后加载的会覆盖前面的。所以如果你在子配置里设了
daily
,主配置里设了
weekly
,最终生效的是
daily
。这个细节在调试配置时非常有用。

针对应用日志的实战配置拆解

假设你有一个Node.js应用,日志写在

/var/log/myapp/app.log
,每天能产生2GB日志,你需要保留30天的历史,并且压缩旧日志。配置可以这样写:

/var/log/myapp/app.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 myapp myapp
    sharedscripts
    postrotate
        /usr/bin/systemctl reload myapp > /dev/null 2>&1 || true
    endscript
}

逐条解释一下这些指令的含义。

daily
表示每天轮转一次,注意logrotate默认每天只运行一次,由cron触发,所以如果你的日志在一天内暴涨,
daily
是不够的,后面会讲怎么处理。
rotate 30
是保留30个历史文件,超过的会被删除。
compress
让历史日志被gzip压缩,节省空间。
delaycompress
很关键,它让最近一次轮转产生的日志文件不被压缩,这样如果应用还在往那个文件写(虽然已经重命名了),不会出现压缩导致的写入错误。这个指令通常和
compress
一起用。

missingok
表示如果日志文件不存在,不报错继续执行,这在应用还没启动时很有用。
notifempty
是空文件不轮转,避免产生一堆无用的空日志。
create 644 myapp myapp
指定轮转后新创建的日志文件权限和属主,这里设成644,属主和属组都是myapp。
sharedscripts
表示
postrotate
里的脚本只执行一次,而不是每个匹配到的日志文件都执行一次。最后
postrotate
endscript
之间的命令在轮转完成后执行,这里用来通知应用重新打开日志文件,因为很多应用在日志文件被重命名后,如果不重新加载,会继续往原来的inode写,导致新日志写到轮转后的文件里,而不是新创建的空文件。

应对日志暴增:基于大小的轮转策略

如果你的应用日志量波动很大,一天之内可能就超过几个G,那

daily
就不够用了。这时要用
size
指令替代
daily
,或者两者结合。比如:

/var/log/myapp/app.log {
    size 100M
    rotate 50
    compress
    delaycompress
    missingok
    notifempty
    create 644 myapp myapp
    sharedscripts
    postrotate
        /usr/bin/systemctl reload myapp > /dev/null 2>&1 || true
    endscript
}

这里

size 100M
表示日志文件超过100MB就触发轮转。注意logrotate默认每天只运行一次,如果日志在一天内多次超过100MB,它不会立即轮转。你需要让logrotate运行得更频繁。可以把它从cron的daily目录移到hourly,或者直接用systemd timer来触发。Ubuntu 18.04之后,logrotate的定时任务已经从cron迁移到了systemd timer,配置文件在
/lib/systemd/system/logrotate.timer
,默认是每天触发。你可以复制这个文件到
/etc/systemd/system/
并修改
OnCalendar
hourly
,然后执行
systemctl daemon-reload
systemctl restart logrotate.timer
,这样logrotate每小时就会运行一次,配合
size
指令就能及时控制日志大小。

多日志文件的通配符处理与坑点

很多应用会按日期或模块生成多个日志文件,比如

/var/log/myapp/access-*.log
。logrotate支持通配符,但有一个容易踩的坑:如果通配符匹配到的文件列表在两次轮转之间发生了变化(比如新增了一个日志文件),状态文件里没有这个新文件的记录,它可能不会被轮转,或者被重复轮转。解决方法是尽量让通配符匹配的文件名模式稳定,或者在应用层面就做好日志文件的命名规范,避免动态生成新的文件名模式。配置示例:

/var/log/myapp/access-*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 644 myapp myapp
    sharedscripts
    postrotate
        /usr/bin/systemctl reload myapp > /dev/null 2>&1 || true
    endscript
}

这里

sharedscripts
的作用就很明显了,因为通配符可能匹配到多个文件,如果不加这个指令,
postrotate
脚本会对每个文件都执行一次,导致应用被多次reload。

应用不配合时的强制处理:copytruncate

有些应用不支持reload后重新打开日志文件,或者你根本没权限去reload应用。这时候可以用

copytruncate
指令。它的原理是先把日志文件内容复制一份,然后清空原文件,而不是重命名。这样应用不需要重新打开文件,因为inode没变。但这个方法有风险:在复制和清空之间产生的日志可能会丢失。对于日志量极大的应用,这个窗口期可能丢失的数据会比较多。所以能用
create
postrotate
通知应用重载的方案,就不要用
copytruncate
。配置示例:

/var/log/myapp/app.log {
    size 100M
    rotate 10
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

注意这里去掉了

create
postrotate
,因为
copytruncate
不需要这些。

日志文件权限和属主问题导致轮转失败

这是一个非常常见的问题。logrotate默认以root权限运行,但如果日志文件所在的目录权限设置不当,比如属主是myapp且权限是700,root当然能访问,但轮转后新创建的日志文件属主如果设错了,应用就写不进去了。所以

create
指令里的用户和组必须和应用进程的用户一致。另外,如果日志目录的权限是700且属主是myapp,root创建的日志文件虽然能写进去,但后续如果应用以myapp身份运行,可能因为目录权限问题无法写入新日志。最佳实践是日志目录权限设为755,属主为root,属组为应用组,这样root可以管理日志文件,应用组也有写入权限。

使用dateext让历史日志文件名包含日期

默认情况下,轮转后的历史日志文件名是

app.log.1
app.log.2.gz
这种格式。如果你想按日期来命名,方便查找某一天的日志,可以加上
dateext
指令。这样文件名会变成
app.log-20250115.gz
。还可以配合
dateformat
自定义日期格式,比如
dateformat -%Y%m%d
。配置示例:

/var/log/myapp/app.log {
    daily
    rotate 30
    compress
    delaycompress
    dateext
    dateformat -%Y%m%d
    missingok
    notifempty
    create 644 myapp myapp
}

这样你就能得到

app.log-20250115.gz
这样的文件,按日期查找非常直观。

调试配置:先dry-run再正式运行

改完配置不要直接等定时任务触发,先用debug模式跑一遍看看效果。命令是

logrotate -d /etc/logrotate.d/myapp
,这个
-d
参数表示dry-run,不会真正执行轮转,但会输出详细的执行计划,包括它会怎么处理每个日志文件,会执行哪些脚本。如果输出里有错误或者不符合预期的地方,马上就能发现。确认无误后,可以用
logrotate -f /etc/logrotate.d/myapp
强制执行一次,
-f
会忽略状态文件里的记录,强制轮转所有匹配的日志文件。这在首次部署配置时很有用,可以立即看到效果。

集中管理多个应用的日志轮转

当服务器上跑着多个应用时,建议为每个应用单独建一个配置文件放在

/etc/logrotate.d/
目录下,文件名就用应用名,比如
myapp
nginx
mysql
。这样管理起来清晰,也方便后续用配置管理工具批量部署。如果多个应用有相同的轮转策略,可以创建一个公共配置文件,然后在各个应用配置里用
include
指令引用,但要注意include的路径必须写绝对路径,而且被引用的文件里不能包含通配符的日志路径,否则会导致重复匹配。

监控logrotate的执行状态

logrotate执行后不会主动通知你结果,如果轮转失败,你可能几天后才发现磁盘满了。建议在

postrotate
脚本里加一行,把轮转结果记录到系统日志,或者发送到监控系统。比如:

postrotate
    /usr/bin/systemctl reload myapp > /dev/null 2>&1 || true
    echo "$(date): myapp logs rotated" >> /var/log/logrotate-notify.log
endscript

更完善的做法是用

logger
命令写入syslog,这样可以被集中日志系统收集:

postrotate
    /usr/bin/systemctl reload myapp > /dev/null 2>&1 || true
    logger -t logrotate "myapp logs rotated successfully"
endscript

然后配合logwatch或其他监控工具,定期检查syslog里有没有logrotate的错误信息。

处理应用日志写入不标准的情况

有些应用不写标准日志文件,而是通过管道输出到stdout/stderr,由systemd-journald或supervisor收集。这种情况logrotate管不到,需要配置journald的日志持久化和清理策略,或者用supervisor的日志轮转功能。但如果你把journald的日志转存为文件,比如通过

journalctl -u myapp > /var/log/myapp/journal.log
,那这个文件就可以交给logrotate管理了。注意这种方式下,每次转存都是全量覆盖,所以logrotate的
copytruncate
模式比较合适。

总结几个关键原则

第一,优先用

create
postrotate
通知应用重载的方案,避免用
copytruncate
。第二,日志目录权限要规划好,让root能管理,应用能写入。第三,
delaycompress
几乎应该成为标配,避免压缩导致的写入问题。第四,部署前先用
logrotate -d
测试,部署后用
logrotate -f
强制运行一次验证。第五,把轮转结果记入系统日志,方便排查问题。第六,如果日志增长速度超过轮转频率,要么提高轮转频率(改systemd timer),要么用
size
指令配合高频触发。把这些原则落实到每个应用的配置里,Ubuntu服务器的磁盘空间基本就不会被日志撑爆了。