服务器磁盘莫名其妙就满了,df -h 一看 /var/log 占了几十 G —— 这种情况十有八九是日志没轮转。
Linux 上管这件事的工具叫 logrotate。它不是常驻进程,而是被系统的定时任务每天调一次,按配置把到期的日志改名归档、压缩、删掉最老的。听起来简单,但配置里几个选项的组合方式很容易搞错,而且配错的后果往往是"日志越转越多"或者"刚重启的服务日志不见了"。
这篇讲清楚配置怎么写、那些容易误解的选项到底什么行为、以及 Docker 和 systemd 的日志为什么不归它管。
# 系统自带的配置(不要改这些,它们是发行版维护的)
ls /etc/logrotate.d/
# 全局默认值
cat /etc/logrotate.conf
# 查看某个配置的最终生效结果(最有用的命令)
logrotate -d /etc/logrotate.d/nginx
-d 是 dry-run(调试模式),它会把每条规则的判断过程打印出来,但不实际执行。改完配置一定要先跑这个 —— 你能直接看到"这条日志会不会被转、转到哪个文件名、哪些旧文件会被删"。
# 强制执行一次(测试用,会真的转)
sudo logrotate -f /etc/logrotate.d/myapp
# 只看某个文件的详细信息
sudo logrotate -v /etc/logrotate.d/myapp
/etc/logrotate.d/ 下通常已经躺着一堆配置(apt、dpkg、nginx、rsyslog 等),这些是发行版和软件包装的,别去改 —— 软件升级时会被覆盖。你自己的服务另建文件。
/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
systemctl reload myapp > /dev/null 2>&1 || true
endscript
}
逐行解释:
| 指令 | 作用 |
|---|---|
/var/log/myapp/*.log | 要管理的日志文件(可以用通配符) |
daily | 每天轮转一次 |
rotate 14 | 保留 14 份归档 |
compress | 用 gzip 压缩归档 |
delaycompress | 延迟一轮再压缩 |
missingok | 文件不存在时不报错 |
notifempty | 文件为空时跳过 |
create 0640 www-data adm | 轮转后重建文件并设权限 |
postrotate...endscript | 轮转后执行的命令 |
rotate N 保留几份,不是几天。 daily + rotate 14 才是"保留 14 天"。如果换成 weekly + rotate 14,那就是保留 14 周(约三个月)。搞混这两个是配置里最常见的错误 —— 你以为留了半个月,实际留了小半年。
compress 配 delaycompress 是标准组合。 因为有些程序在轮转后还往旧文件里写(尤其是 copytruncate 模式下),立刻压缩会导致写入失败。延迟一轮再压,等于给程序一个缓冲期。
notifempty 建议加上。 默认行为是 ifempty —— 空文件也转。对一个偶尔才有输出的日志,这会让你攒一堆 0 字节的归档。
missingok 也建议加。 默认是 nomissingok:文件不存在会报错。应用还没启动、日志还没生成时,logrotate 每天报一次错很烦。
create 的权限别写错。 不写的话会继承原文件权限,但很多时候你需要显式指定 —— 比如让 www-data 能写但其他用户读不了(0640)。
默认的轮转方式是改名(rename):把 app.log 改成 app.log.1,然后通过 postrotate 里的信号让程序重新打开一个新文件。
但有些程序不支持重新打开日志文件(没实现相应的信号处理),这时候 rename 之后它会继续往已经被改名的旧文件里写,新文件一直是空的。
copytruncate 就是为这种情况准备的:先复制一份,再把原文件清空。程序的文件句柄没变,继续往同一个 inode 写,看起来"无缝"。
代价是有一个丢数据的窗口期。 官方手册明确说了:在"复制完成"到"清空"之间,程序写入的日志会丢失。对审计类日志这是不可接受的,对普通应用日志通常可以忍。
优先用 rename + 信号的方式。 大多数正经服务都支持:
| 服务 | 重开日志的方式 |
|---|---|
| Nginx | nginx -s reopen 或 kill -USR1 $(cat /run/nginx.pid) |
| Apache | apachectl graceful |
| PHP-FPM | kill -USR1 $(cat /run/php/php8.3-fpm.pid) |
| 自定义 systemd 服务 | 自己实现 SIGHUP 处理,或直接 systemctl reload |
只有当程序确实不支持时才用 copytruncate,并在配置里加个注释说明原因。
这是 logrotate 里最绕的部分,官方手册的规则是:
| 写法 | 行为 |
|---|---|
size 100M | 只按大小,且与时间选项互斥 |
daily + maxsize 100M | 满足其一就转 —— 到了第二天或超过 100M |
daily + minsize 100M | 必须同时满足 —— 过了一天且超过 100M |
实际用法:
- 日志量稳定、想按时归档 → 只用
daily/weekly - 日志量波动大、怕撑爆磁盘 →
daily+maxsize 500M,这是最实用的组合 - 日志很少,不想攒一堆空文件 →
weekly+minsize 1M
还有一个容易误判的规则:普通情况下 logrotate 一天最多处理一个日志文件一次(因为它是被每日的定时任务调用的)。所以写了 size 10M 但日志半天就涨到 50M,它也只会在下一次运行时转一次。
日志增长很快的场景,光靠 logrotate 不够 —— 得同时把应用的日志级别调低,或者用 systemd 的 RateLimit / Docker 的 max-size 直接限制。先定位为什么日志涨这么快,思路见 VPS 日志怎么看。
默认归档文件是 app.log.1、app.log.2.gz 这样编号的。编号的缺点是看不出是什么时候的 —— 出了故障要翻"三天前的日志",得先算清楚 .3.gz 是不是三天前。
加一行 dateext 换成日期后缀:
/var/log/myapp/*.log {
daily
dateext
dateformat -%Y%m%d
rotate 30
}
归档文件变成 app.log-20260929、app.log-20260928,一眼就知道是哪天。排查问题时这个改变非常值。
dateformat 里常用的占位符:%Y(四位年)、%m(月)、%d(日)、%s(时间戳)。注意 dateformat 里不要用 : —— 文件名带冒号在某些场景下会出问题。
还有 dateyesterday,让归档文件用前一天的日期命名。因为 logrotate 通常在凌晨跑,转的是"昨天"的日志,用 dateyesterday 命名更符合直觉。
这是配置里最容易被忽略、但决定了日志能不能继续正常写入的一步。
如果只轮转不通知程序,程序会继续往旧文件(已被改名)里写 —— 结果就是新日志文件永远是空的,磁盘占用还在涨。
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
sharedscripts 别漏。 因为路径用了通配符(*.log),不加这个的话 postrotate 会对每个匹配的文件各执行一次 —— Nginx 一天被 reload 十几次,纯属浪费。
加了 sharedscripts,整组文件处理完后才执行一次脚本。
那行 [ -f /run/nginx.pid ] && 是防御性写法 —— Nginx 没在运行时,kill 会报错,导致 logrotate 记一次失败。
endscript 必须单独一行,前面不能有缩进以外的东西,这里写错会导致整个配置解析失败。
改完配置,永远先跑 dry-run:
sudo logrotate -d /etc/logrotate.d/myapp
输出里关注这几行:
considering log /var/log/myapp/app.log
Now: 2026-09-29 03:00
Last rotated at 2026-09-28 03:00
log does not need rotating (log has already been rotated)
或者:
rotating log /var/log/myapp/app.log, log->rotateCount is 14
renaming /var/log/myapp/app.log.14.gz to /var/log/myapp/app.log.15.gz
...
看到 renaming 那一段就说明它真的会转,而且你能确认归档的命名方式对不对。
想知道整个系统上哪些日志会被转、哪些不会:
# 一次检查所有配置(dry-run,不改动任何东西)
sudo logrotate -d /etc/logrotate.conf 2>&1 | less
logrotate 自己出错时的日志在 /var/log/syslog 里:
grep logrotate /var/log/syslog | tail -20
常见错误是配置语法错误、postrotate 脚本返回非零、或者权限不足。
这是最容易搞混的部分。
journalctl 看到的系统日志不由 logrotate 管理,它有自己的轮转机制。
# 看当前占用
journalctl --disk-usage
# 手动清理到 500M
sudo journalctl --vacuum-size=500M
# 按时间清理
sudo journalctl --vacuum-time=7d
要永久限制,改配置文件:
sudo sed -i 's/^#\?SystemMaxUse=.*/SystemMaxUse=500M/' /etc/systemd/journald.conf
sudo systemctl restart systemd-journald
配置项说明:
| 项 | 作用 |
|---|---|
SystemMaxUse= | journal 总占用上限 |
SystemKeepFree= | 至少给磁盘留多少空闲 |
MaxRetentionSec= | 最长保留时间(如 1month) |
SystemMaxFileSize= | 单个 journal 文件上限 |
SystemMaxUse 是唯一必须设的 —— 不设的话 journal 默认可以用掉文件系统 10% 的空间。
docker logs 看到的日志也不归 logrotate 管,而且它是最容易撑爆磁盘的 —— 一个疯狂输出的容器,日志能一天涨几十 G。
# 看哪个容器占得最多
sudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail -10
根治办法是配 Docker 的日志驱动限制(/etc/docker/daemon.json):
{
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}
sudo systemctl restart docker
max-size + max-file 组合起来就是"单个文件最大 20M,最多留 5 个",即每个容器最多占 100M 日志。
注意:这个配置只对之后新建的容器生效,已有的容器要重建(docker compose up -d --force-recreate)才会应用。
也可以在 compose 里按服务单独配:
services:
app:
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
清理已有的日志文件、以及 Docker 占满磁盘的完整方案见 Docker 占满磁盘怎么办。
有些应用(比如某些 Java 服务和按天切分的日志框架)自己就会轮转,logrotate 再去管反而会冲突。先看应用有没有自带轮转,有的话就别重复配。
/var/log/nginx/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi
endscript
postrotate
[ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
endscript
}
大多数情况下 Ubuntu 装的 Nginx 已经自带这份配置,不需要自己写。检查一下:ls /etc/logrotate.d/ | grep nginx。
/srv/app/shared/logs/*.log {
daily
rotate 30
dateext
dateformat -%Y%m%d
compress
delaycompress
missingok
notifempty
create 0640 appuser appuser
sharedscripts
postrotate
systemctl reload myapp > /dev/null 2>&1 || true
endscript
}
postrotate 末尾的 || true 是必要的 —— 如果应用此刻没在运行,systemctl reload 返回非零,logrotate 会记为失败。加 || true 让它静默通过。
/var/log/bigapp/*.log {
daily
maxsize 500M
rotate 7
compress
copytruncate
notifempty
}
daily + maxsize 500M:正常每天转一次,但涨到 500M 就立刻转,不等第二天。
# 强制转一次(不管条件是否满足)
sudo logrotate -f /etc/logrotate.d/myapp
# 只看会不会转,不实际执行
sudo logrotate -d /etc/logrotate.d/myapp
# 详细模式,实际执行
sudo logrotate -v /etc/logrotate.d/myapp
-f 用在改了配置之后想立刻验证效果,以及"磁盘快满了,现在就想清一下"的时候。
但别养成天天 -f 的习惯 —— 它会让保留份数快速消耗,而且 daily 的语义就失效了。
日志被转了但新文件没生成
少了 create。不加它的话 logrotate 只做改名,不会建新文件 —— 有些程序会自己建,有些不会。
转完之后程序还在往旧文件写
postrotate 没通知程序重新打开文件。按上面"让程序重新打开日志"那一节配。
归档文件越来越多,磁盘没降下来
rotate 设太大了,或者根本没设。默认值是 0,意味着不保留任何归档(听起来挺好),但很多教程会写个 rotate 52 之类的值,那就等于保留一年。
还有可能是 compress 没加 —— 不压缩的话归档占用和原文件差不多。
logrotate: error: stat of /path failed: No such file or directory
日志文件还不存在。加 missingok。
error: skipping "/path" because parent directory has insecure permissions
父目录权限太开放(比如 777)。logrotate 会拒绝处理,这是保护机制。把目录权限收紧到 755 或更严。
配了但完全不执行
先确认 logrotate 的定时任务在跑:
systemctl list-timers | grep logrotate
cat /etc/cron.daily/logrotate
Debian/Ubuntu 上它由 /etc/cron.daily/logrotate 触发(实际是 systemd timer 包装的)。如果连 cron 本身都不执行,看 VPS 定时任务怎么配。
改了配置要重启什么吗
不用。logrotate 不是常驻进程,下次被调用时读的就是新配置。
/var/log 已经被占满了,现在怎么办
1. 先清出空间(不然什么操作都可能失败)
sudo journalctl --vacuum-size=200M
sudo find /var/log -name "*.gz" -mtime +30 -delete
2. 再配好 logrotate 防止复发
如果日志把根分区撑满导致服务起不来,处理顺序见 VPS 硬盘满了怎么办。
删了日志文件但空间没释放
有进程还持有那个文件句柄(进程不知道文件被删了,继续往里写,空间就不释放)。
# 找出持有已删除文件的进程
sudo lsof +L1 | head -20
# 正确的做法是重启那个服务,让它重新打开日志
sudo systemctl restart myapp
所以永远不要直接 rm 正在写的日志文件 —— 用 truncate -s 0 或者走 logrotate。
要不要给日志单独分区
生产环境值得考虑 —— /var/log 撑满时不会拖垮整个系统。个人 VPS 上必要性不大,配好轮转和 SystemMaxUse 就够。
- 自己的服务有独立的
/etc/logrotate.d/配置(没混在系统自带的里面) -
rotate N的数值和轮转周期匹配(别把周期搞混成天数) -
postrotate里通知了程序重新打开日志 - 通配符路径配了
sharedscripts - 加了
missingok和notifempty -
create的权限和属主正确 - 跑过
logrotate -d确认行为符合预期 - journald 的
SystemMaxUse设了 - Docker 的
max-size/max-file配了 - 磁盘告警接上了(流量和磁盘监控)
最后一条容易被忽略:日志轮转是"防止变坏",不是"及时发现变坏"。真要兜底,还是得有个磁盘用量告警 —— logrotate 配置错了,你不会知道,直到磁盘满了。
- VPS 硬盘满了怎么办 — 已经满了怎么救急
- Docker 占满磁盘怎么办 — 容器日志的根治方案
- VPS 磁盘还有空间却写不了文件 — 小文件太多导致的另一种"满"
- VPS 日志怎么看 — journalctl 和其他日志的定位方法
- VPS 定时任务怎么配 — logrotate 本身也是靠定时任务触发的
