logrotate:copytruncate 与 create 怎么选,以及「轮转了但磁盘不释放」的排查顺序
logrotate 的两种轮转方式,本质区别在于进程持有的 fd 指向哪个 inode。选错模式,出现的问题不是日志没有轮转,而是磁盘空间一直不释放。
参考文档:logrotate(8),以及 Nginx 文档中 signals 一节。
create:重命名旧文件,进程必须重开
create 是默认行为:把 app.log 重命名为 app.log-20240501,再新建一个空白的 app.log。进程手里的 fd 仍然指向被重命名后的旧 inode,只要不 reopen,后续写入全部落在旧文件里。表现出来就是:
df -h显示这块分区还是满的;- 新的
app.log一直是 0 字节; du -sh统计目录时对不上df的数字。
所以 create 模式必须配 postrotate 让进程重新打开文件:
- Nginx:
kill -USR1,或nginx -s reopen; - PHP-FPM:
kill -USR1 $(cat /run/php-fpm.pid); - httpd:
graceful。
PHP-FPM 的配置示例:
/var/log/php/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
/usr/bin/systemctl reload php-fpm >/dev/null 2>&1 || true
endscript
}
sharedscripts 保证一组通配匹配到多个文件时只执行一次 reload,避免每个日志文件都触发一次进程重载。
create 还有两个容易忽略的点:新建文件的属主、权限由 create 0640 www-data www-data 决定,写错会导致进程 reopen 之后直接写不进去;如果日志目录的父路径进程没有权限,则要加 su www-data www-data,让 logrotate 以对应身份完成轮转。
copytruncate:不重开文件,但有代价
copytruncate 先把 app.log 复制成 app.log-20240501,然后原地 truncate 原文件。inode 不变,fd 不变,进程完全不需要感知,继续往同一个 inode 写就行。这是给那些「不会重开日志文件」的进程用的兜底方案:部分第三方 PHP 应用、自己写的常驻 daemon。
代价有两个:
一是丢日志的窗口。 从 copy 完成到 truncate 执行之间,进程写进来的内容既不在副本里(已经复制完了),也会被随后的 truncate 抹掉。日志量大、文件大时这个窗口更明显。这不是理论风险,是每次轮转都可能发生。
二是 IO 峰值和瞬时双倍占用。 复制一个几 GB 的日志会在轮转瞬间打出一次明显的磁盘读+写,磁盘剩余空间不够翻倍时,轮转本身就会失败。
/var/log/mydaemon/app.log {
daily
rotate 7
missingok
notifempty
compress
delaycompress
copytruncate
}
compress 配 delaycompress 是为了不在轮转当场压缩:当场压缩会叠加一次 CPU 和 IO,把峰值推得更高。
排查顺序:日志轮转了,磁盘没释放
按这个顺序走,基本能定位:
- 确认 df 和 du 的差异。
df -h说满了,du -sh /var/log却很小,说明有文件被删掉或改名了但 fd 还开着。 - 找到持有已删除文件的进程。
lsof +L1
# 或逐个进程看
ls -l /proc/
/fd | grep deleted
- 确认进程类型,选择处理方式。 Nginx、PHP-FPM 这类支持 reopen 的,直接发信号:
nginx -s reopen
kill -USR1 $(cat /run/php-fpm.pid)
不能 reopen 的进程只能重启,或者事先改用 copytruncate。
- 如果是 copytruncate 模式还出现空间不降,检查是否有别的进程也打开了这个文件(比如 tail、日志采集 agent),truncate 后它们仍持有旧的偏移。
一个相关的细节:文件以 O_APPEND 打开时,进程只有在 reopen 之后才会写到新文件;PHP 的 error_log 走 syslog 的情况不受这个问题影响,因为写入方是 syslogd。
触发时机与调试
size 100M 只在 cron 调度 logrotate 的那一刻判断,两次调度之间的突发流量可以轻松冲过阈值。要控制单文件上限,用 maxsize 配合 daily:
daily
maxsize 100M
常用参数:
logrotate -d /etc/logrotate.conf:dry run,看它打算做什么,不动文件;logrotate -f:强制立即轮转,验证 postrotate 是否生效;logrotate -v:输出详细过程;- 状态文件在
/var/lib/logrotate/status,记录每个文件上次轮转时间,调试「为什么没轮转」时先看这里; dateext用日期后缀替代序号后缀,便于归档检索,也避免序号滚动时覆盖旧文件。
容器里不要用 logrotate
容器里的应用应该把日志写到 stdout/stderr,由运行时负责轮转——Docker 的 json-file driver 用 max-size 和 max-file 控制:
{
"log-driver": "json-file",
"log-opts": { "max-size": "50m", "max-file": "3" }
}
在容器内跑 logrotate 去处理挂载进来的宿主机目录,会和宿主机上的 logrotate 抢同一批文件,出现重复轮转、状态文件互相覆盖、truncate 竞争。日志落盘的进程在宿主机,轮转就交给宿主机。