Linux 基础体系 · 第 81/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux logrotate 深入:轮转、压缩、信号、并发和磁盘保护

logrotate 是一个负责管理普通日志文件的用户态工具。它按照配置判断日志是否需要轮转,将正在增长的文件变成历史归档,创建新的活动日志,并可压缩、删除旧归档,以及通知日志生产者重新打开文件。

它解决的不是“所有日志管理问题”:

  • journald 管理的是 systemd journal 数据库及其持久化文件,不应直接用 logrotate 管理 .journal 文件。
  • rsyslogsyslog-ng、Nginx、Apache、应用程序分别负责产生日志或接收日志。
  • logrotate 主要负责文件命名、生命周期、压缩和删除。
  • 磁盘治理还需要结合 dfdulsofiostat、文件系统配额和日志采集策略。

可以把链路抽象为:

应用或守护进程
      │
      ├── 写入普通日志文件 ──> logrotate ──> 历史文件/压缩/删除
      │                              │
      │                              └── 信号或控制命令 ──> 重新打开日志
      │
      └── 写入 journald ──> journalctl / journald vacuum

本文讨论现代主流 Linux 发行版中的常见实现和生产行为。具体默认配置、压缩程序、定时器名称以及 logrotate 版本选项,仍应以目标系统上的 man logrotatelogrotate --version 和发行版包配置为准。


1. 先区分日志文件、文件名和文件描述符

理解轮转的关键不是文件名,而是文件描述符和 inode。

进程调用 open() 打开日志后,内核返回一个文件描述符。这个描述符指向一个打开文件对象,而打开文件对象最终关联某个 inode。进程后续调用 write() 时,并不会因为路径名发生变化就重新解析路径。

例如,应用打开:

/var/log/myapp/app.log

假设该路径当前指向 inode 100。

执行:

mv /var/log/myapp/app.log /var/log/myapp/app.log.1
touch /var/log/myapp/app.log

结果是:

/var/log/myapp/app.log.1  -> inode 100,旧进程仍可能继续写入
/var/log/myapp/app.log    -> 新 inode 200,路径上是空文件

如果应用没有重新打开日志文件,它继续持有的文件描述符仍然指向 inode 100。因此,路径名上的新 app.log 可能长期为空,而旧的 app.log.1 继续增长。

这解释了轮转中最重要的因果关系:

rename 只改变目录项和路径名,不会自动改变进程已经持有的文件描述符。

因此,典型的安全轮转流程是:

  1. 将活动日志重命名为历史文件。
  2. 创建同名的新活动日志。
  3. 让日志生产者重新打开日志。
  4. 旧文件不再增长,之后才能安全压缩或删除。

2. logrotate 的核心状态和数据流

一次轮转并不是简单执行 mv。它通常经历以下状态:

flowchart TD
    A[定时器或 cron 启动 logrotate] --> B[读取配置与状态文件]
    B --> C{是否满足轮转条件}
    C -- 否 --> D[更新或保持状态后退出]
    C -- 是 --> E[检查权限、目录和命名]
    E --> F[rename 或 copytruncate]
    F --> G[create 新活动日志]
    G --> H[执行 prerotate/postrotate]
    H --> I[压缩历史日志]
    I --> J[按 rotate/olddir 删除过期归档]
    J --> K[写回状态文件]

不同版本和配置下,脚本、压缩和删除的精确顺序可能受 delaycompresssharedscripts 等指令影响,但必须把握三个状态:

  • 活动文件:当前应用按固定路径写入的文件。
  • 历史文件:完成一次轮转后不再应该增长的文件。
  • 压缩归档:历史文件进一步转换出的 .gz.xz.zst 等文件。

logrotate 还维护一个状态文件,常见位置包括:

/var/lib/logrotate/status

状态文件记录某个日志上次轮转的时间,用于判断 dailyweeklymonthly 等时间条件。它不是日志内容,也不是锁文件的专用数据库;但在常见实现中,执行过程会对状态文件进行锁定,以避免多个 logrotate 实例同时更新状态。

查看当前使用的状态文件:

grep -R "logrotate" /etc/cron.* /etc/systemd/system /usr/lib/systemd/system 2>/dev/null
logrotate --version

手工测试时应使用独立状态文件,避免污染生产状态:

sudo logrotate -d -v -s /tmp/logrotate-test.status /etc/logrotate.d/myapp

其中:

  • -d 是 debug 模式,不实际修改文件。
  • -v 输出详细过程。
  • -s 指定状态文件。
  • -f 强制轮转,不能与 -d 混淆。

3. 轮转条件:时间、大小和检查频率

3.1 时间条件不是实时定时器

常见时间条件包括:

daily
weekly
monthly
yearly

它们表示“当 logrotate 被执行时,如果距离上次轮转已经满足该时间条件,则允许轮转”。它们并不意味着 logrotate 会在日志恰好达到某个时间点时立即运行。

例如系统每天凌晨运行一次:

00:00:00  logrotate 被执行
00:00:05  app.log 满足 daily
12:00:00  app.log 继续增长

下一次轮转通常要等到第二天凌晨。若主机上的 cron 被禁用,或者 systemd timer 失败,daily 也不会自动发生。

现代发行版常见两种调度方式:

systemctl status logrotate.timer
systemctl cat logrotate.timer

或者:

ls -l /etc/cron.daily/logrotate

不要假设所有发行版都使用同一种调度机制。轮转配置正确但调度器没有执行,表现上仍然是“日志没有轮转”。

3.2 大小条件的含义

常见大小指令:

size 100M
minsize 100M
maxsize 1G

它们的语义不同:

  • size 100M:文件达到该大小时允许轮转,但只有在 logrotate 被检查时才会发现。
  • minsize 100M:同时满足时间条件和大小条件才轮转。
  • maxsize 1G:达到最大大小时允许提前轮转,即使时间条件尚未到达。

size 与时间条件的组合容易出错。实际使用时应阅读目标版本的手册,因为同一日志块中指令的解析顺序和最后一个相关指令的优先级会影响行为。不要把:

daily
size 100M

简单理解为“每天轮转,或者超过 100 MB 就实时轮转”。更准确的理解是:

  1. 任务由外部调度器触发。
  2. logrotate 读取状态和文件大小。
  3. 根据配置判断本次是否轮转。
  4. 任务未运行时,文件可以继续无限增长。

3.3 完整算例

假设:

daily
maxsize 1G
rotate 7

logrotate 每天 00:00 执行:

时间 文件大小 是否执行检查 结果
周一 00:00 100 MB 按时间轮转
周一 12:00 1.2 GB 不会实时轮转
周二 00:00 1.2 GB 轮转一次
周二 06:00 1.1 GB 继续增长
周三 00:00 1.1 GB 再轮转一次

如果希望高流量日志不依赖每日任务间隔,必须提高检查频率,或者使用应用本身的按大小轮转能力。logrotate 本身不是常驻监控进程。


4. 轮转方式一:rename/create

默认且通常更优的方式是重命名活动文件,再创建新文件。例如:

/var/log/myapp/app.log
        │
        └── rename
             ↓
/var/log/myapp/app.log.1

然后创建:
/var/log/myapp/app.log

一个典型配置:

/var/log/myapp/app.log {
    daily
    rotate 7
    missingok
    notifempty

    create 0640 myapp adm

    compress
    delaycompress

    postrotate
        /bin/systemctl kill -s USR1 myapp.service
    endscript
}

这里的 create 0640 myapp adm 表示新建活动日志的权限、用户和组。它解决的是新 inode 的属性问题,而不是旧文件的属性问题。

4.1 为什么必须通知进程

如果应用使用标准的“打开一次、持续写入”模式,轮转后会出现:

旧文件 app.log.1 继续增长
新文件 app.log 为空或增长很慢

此时执行:

sudo lsof +L1

可能看到已经被删除、但仍被进程打开的文件;查看某个进程的文件描述符:

sudo ls -l /proc/<PID>/fd

如果显示类似:

/var/log/myapp/app.log.1 (deleted)

说明文件名已经从目录中移除,但进程仍持有 inode。磁盘空间不会因为 rm 或轮转而立即释放,直到最后一个文件描述符关闭。

4.2 信号不是统一标准

logrotate 不知道某个应用应该发送什么信号。信号的含义由具体程序定义:

  • 某些程序使用 SIGHUP 重新打开日志或重新加载配置。
  • Nginx 常见使用 USR1 重新打开日志文件。
  • rsyslog 通常可通过 HUP 重新加载或重新打开,具体行为依版本和配置。
  • 某些应用只支持管理 socket、HTTP 管理接口或专用命令。
  • 某些应用收到 HUP 会重载完整配置,甚至退出,不能凭经验发送。

因此不能写出一个通用的:

kill -HUP $(cat /run/app.pid)

就认为轮转完成。必须查看服务文档,并验证新旧文件的增长情况。

例如验证某个服务:

sudo systemctl kill -s USR1 myapp.service
sudo journalctl -u myapp.service -n 50 --no-pager
sudo lsof -p "$(systemctl show -p MainPID --value myapp.service)" | grep app.log

使用 systemctl kill 的好处是可以按 systemd 服务管理的进程组发送信号;直接读取 PID 文件可能遇到过期 PID、权限或多进程模型问题。

4.3 postrotate 的位置和风险

常见写法:

postrotate
    /bin/systemctl kill -s USR1 myapp.service
endscript

postrotate 在轮转动作后执行,用于通知服务。脚本通常由 /bin/sh 执行,因此应使用绝对路径,并明确处理失败。

可以加入错误检查:

postrotate
    if ! /bin/systemctl kill -s USR1 myapp.service; then
        echo "failed to signal myapp" >&2
        exit 1
    fi
endscript

但要注意:脚本失败并不一定能回滚已经完成的重命名。轮转不是事务,可能出现“文件已经改名,但服务没有重新打开”的中间状态。因此生产配置必须在测试环境验证信号行为,而不是只验证配置语法。


5. 轮转方式二:copytruncate

copytruncate 的工作方式是:

  1. 将活动日志内容复制到历史文件。
  2. 对原活动文件执行截断,使其大小变为零。
  3. 应用继续使用原来的文件描述符,不需要重新打开。

配置示例:

/var/log/legacy/app.log {
    daily
    rotate 5
    copytruncate
    compress
    missingok
    notifempty
}

它适合无法重新打开日志文件的旧程序,但代价是轮转期间存在并发窗口。

5.1 丢失和重复的形式化分析

设活动文件在复制开始时包含字节区间:

[0, N)

复制线程从偏移 0 开始读取。与此同时,应用追加数据:

[N, N + K)

如果新增数据在复制已经经过对应位置后才写入,它可能没有被复制到历史文件;随后 truncate 将原文件截断为零,这部分数据就丢失了。

反过来,如果追加发生在复制读取过程中,具体结果还取决于读取时序和文件系统行为,可能出现边界上的重复、缺失或记录被截断。copytruncate 的核心问题不是“速度慢”,而是:

复制和写入没有原子协调,复制完成与截断之间不存在应用级一致性协议。

因此,copytruncate 不能保证无损轮转。低价值、可重建或无法改造的旧程序可以接受该取舍;审计日志、账务事件、合规记录和高价值业务日志不应默认使用它。

5.2 copytruncate 的磁盘风险

rename/create 只需要创建一个很小的新目录项,新旧文件在逻辑上共用已有数据,额外空间主要来自历史归档和压缩过程。

copytruncate 在复制完成前同时保留:

原活动文件 + 完整副本

设活动文件大小为 S,文件系统可用空间为 F,复制过程需要的额外空间近似为 S,还要考虑压缩临时文件、其他日志和文件系统保留空间。若:

F < S + 压缩临时空间 + 安全余量

复制可能失败,或者系统在复制过程中接近满盘。

因此,不能只根据“轮转后会删除旧文件”判断安全;轮转动作本身可能先制造额外副本。


6. 压缩:节省空间,但不是零成本

启用压缩:

compress

常见默认压缩程序是 gzip,但发行版和配置可能覆盖 compresscmdcompressext 等指令。不要假定所有主机都默认使用同一个算法。

压缩的生命周期通常是:

app.log
  ↓ rename
app.log.1
  ↓ 压缩
app.log.1.gz

压缩会消耗:

  • CPU;
  • 磁盘读取带宽;
  • 压缩输出写入带宽;
  • 临时空间;
  • 归档目录所在文件系统的 inode。

在磁盘已经处于高延迟或高队列深度时,压缩可能与业务 I/O 竞争。可以使用:

iostat -xz 1 10

重点观察:

  • await:I/O 平均等待时间;
  • aqu-szavgqu-sz:平均队列深度,字段名随 sysstat 版本变化;
  • %util:设备忙碌时间比例,但在现代并行存储上不能单独作为“已饱和”的充分条件;
  • r/sw/srkB/swkB/s:读写请求和吞吐。

6.1 delaycompress

配置:

compress
delaycompress

表示最近一次轮转产生的归档暂时不压缩,等下一次轮转时再压缩。典型结果:

第一次轮转:
app.log       新活动文件
app.log.1     未压缩

第二次轮转:
app.log       新活动文件
app.log.1     最近一次归档,未压缩
app.log.2.gz  更早归档

它常用于某些程序在收到重开信号后,仍可能短暂访问旧文件的场景,或者运维人员希望最近归档可以直接读取。它不是解决文件描述符问题的替代品:如果进程一直写旧 inode,delaycompress 只会延迟压缩,并不会让进程自动切换。

6.2 压缩失败的处理

压缩失败可能由以下原因造成:

  • 压缩程序不存在;
  • 权限不足;
  • 磁盘空间不足;
  • I/O 错误;
  • 归档文件被其他进程修改;
  • 多个轮转任务并发操作同一文件。

应通过 debug 和详细日志检查:

sudo logrotate -d -v -s /tmp/logrotate-test.status /etc/logrotate.d/myapp
sudo logrotate -v -s /tmp/logrotate-test.status /etc/logrotate.d/myapp

测试目录必须与生产目录隔离。-f 会实际执行轮转,不应在不了解影响的生产日志上直接使用。


7. 文件命名、保留数量和删除边界

最小的归档保留配置:

/var/log/myapp/app.log {
    daily
    rotate 7
}

rotate 7 通常表示保留 7 个轮转后的归档,而不是“保留包括当前活动文件在内的 7 个文件”。实际文件可能是:

app.log
app.log.1
app.log.2
...
app.log.7

当新一轮归档产生时,更旧的归档会向更大的编号移动,超出保留数量的文件被删除。不同命名指令会改变具体后缀,但“当前文件不等同于一个历史归档”的概念不能混淆。

使用日期后缀:

dateext
dateformat -%Y%m%d

可能得到:

app.log-20250308

日期命名更适合按日期检索,但若同一天内发生多次轮转,还需要考虑 datehouragodateyesterday 或版本支持的其他指令,避免命名冲突。不要仅凭文档片段复制日期指令,应在目标发行版上用 debug 模式验证。

olddir 可以把历史归档移动到其他目录:

/var/log/myapp/app.log {
    daily
    rotate 14
    olddir /var/log/myapp/archive
}

常见实现要求 olddir 与原目录位于同一文件系统,因为轮转主要使用 rename();跨文件系统的 rename() 会失败。若要跨文件系统迁移,应使用日志采集器、归档程序或明确的复制/校验流程,而不是假设 olddir 会自动完成跨设备复制。


8. 一个可运行的生产型配置示例

先准备一个测试目录和测试文件:

sudo install -d -o root -g adm -m 0750 /var/log/myapp
printf 'first line\n' | sudo tee -a /var/log/myapp/app.log >/dev/null
sudo chmod 0640 /var/log/myapp/app.log

配置文件:

/var/log/myapp/app.log {
    daily
    maxsize 500M
    rotate 14

    missingok
    notifempty

    create 0640 myapp adm

    compress
    delaycompress

    sharedscripts
    postrotate
        /bin/systemctl kill -s USR1 myapp.service
    endscript
}

逐项解释:

  • daily:至少按每日检查周期判断。
  • maxsize 500M:在 logrotate 被执行时,若超过 500 MB,可提前轮转。
  • rotate 14:保留 14 个历史归档。
  • missingok:文件不存在时不报错退出。
  • notifempty:空文件不轮转。
  • create 0640 myapp adm:创建新活动文件,并设置权限和属主。
  • compress:压缩历史归档。
  • delaycompress:最近一份归档延迟到下一轮再压缩。
  • sharedscripts:一个日志块中匹配多个文件时,脚本只执行一次,而不是每个文件执行一次。
  • postrotate:轮转后通知应用重新打开日志。

但该配置成立有前置条件:

  1. 系统中确实存在 myapp.service
  2. 服务文档确认 USR1 的语义是重新打开日志。
  3. 用户 myapp 和组 adm 存在。
  4. logrotate 以足够权限运行。
  5. 服务允许 logrotate 创建的新文件被其写入。

先做语法和动作预览:

sudo logrotate -d -v -s /tmp/myapp.status /etc/logrotate.d/myapp

预期可以看到类似:

considering log /var/log/myapp/app.log
Creating new state
  Now: ...
  Last rotated at ...
log does not need rotating

如果需要在隔离测试文件上强制执行:

sudo cp /etc/logrotate.d/myapp /tmp/myapp-logrotate.conf
sudo sed -i \
  -e 's#/var/log/myapp/app.log#/tmp/myapp/app.log#' \
  -e '/systemctl kill/d' \
  /tmp/myapp-logrotate.conf

sudo install -d -m 0750 /tmp/myapp
printf 'test\n' | sudo tee /tmp/myapp/app.log >/dev/null
sudo logrotate -f -v -s /tmp/myapp.status /tmp/myapp-logrotate.conf
find /tmp/myapp -maxdepth 1 -type f -printf '%f %s bytes\n' | sort

不应直接对真实生产日志使用 -f 来验证配置,因为它会跳过时间判断,实际重命名、创建、压缩和删除文件。


9. 脚本、共享脚本和失败路径

logrotate 支持几类脚本:

prerotate
postrotate
firstaction
lastaction

常见用途是:

  • prerotate:轮转前做检查或通知;
  • postrotate:轮转后通知服务重新打开日志;
  • firstaction:一组日志开始处理前执行一次;
  • lastaction:一组日志全部处理后执行一次。

sharedscripts 的差异很重要。假设一个配置块匹配:

/var/log/myapp/*.log {
    postrotate
        systemctl kill -s USR1 myapp.service
    endscript
}

没有 sharedscripts 时,脚本可能针对每个匹配文件执行;有 sharedscripts 时,则针对整个日志块执行一次。这会影响:

  • 信号发送次数;
  • 脚本耗时;
  • 失败状态;
  • 是否会重复触发应用重载。

脚本应避免以下风险:

postrotate
    kill -HUP $(cat /var/run/app.pid)
endscript

问题包括:

  • PID 文件可能不存在;
  • PID 可能已经复用;
  • cat 失败后命令替换为空;
  • 发送给错误进程;
  • 多进程服务只通知了一个进程;
  • HUP 语义可能不是重新打开日志。

更安全的方向是使用服务管理器或应用官方控制接口,并在测试中观察:

sudo systemctl status myapp.service
sudo journalctl -u myapp.service --since "5 minutes ago"
sudo lsof /var/log/myapp/app.log /var/log/myapp/app.log.1

10. 并发:logrotate 自身、应用和其他轮转器

10.1 多个 logrotate 实例

典型风险是同时存在:

cron.daily/logrotate
systemd logrotate.timer
手工执行的 logrotate
第三方运维脚本

如果它们使用相同配置和状态文件,常见实现会通过状态文件锁避免并发。若锁已被占用,命令可能直接退出并返回非零状态,或者在支持该选项的版本中等待锁。

检查调度来源:

systemctl list-timers --all | grep -i logrotate
grep -R "logrotate" /etc/cron* /usr/lib/systemd /etc/systemd 2>/dev/null

不要为了“让任务一定执行”而随意使用跳过状态锁的选项。--skip-state-lock 只适合已经由外部机制保证互斥的场景;否则两个实例可能同时重命名、压缩或删除同一组归档。

10.2 不同状态文件造成的竞态

即使两个实例不共享状态文件,也不代表安全:

logrotate -s /tmp/a.status /etc/logrotate.d/myapp &
logrotate -s /tmp/b.status /etc/logrotate.d/myapp &
wait

它们会分别认为“尚未轮转”,然后竞争同一个日志目录。可能出现:

  • 一个实例先生成 .1,另一个实例又将其移动;
  • 压缩同一个归档;
  • 编号跳跃;
  • 状态文件互不知情;
  • 脚本被重复执行。

锁保护的对象通常是某个状态文件,不是所有日志路径的全局锁。因此生产环境应保证同一日志集合只有一个轮转调度源。

10.3 应用写入与轮转并发

即使使用 rename/create,应用仍可能在以下时间点写入旧文件:

T1 logrotate rename
T2 应用 write 旧 fd
T3 logrotate create 新文件
T4 应用收到信号
T5 应用 reopen

T2 的数据会进入旧文件,这是正常的;只要应用最终重新打开,新文件会继续接收后续日志。真正危险的是应用永不重开,或者信号处理失败。

若应用采用多进程模型,还需确认:

  • 主进程收到信号后是否通知 worker;
  • worker 是否各自持有日志文件描述符;
  • copytruncate 是否会影响所有写入者;
  • 是否存在异步日志线程继续持有旧 fd。

11. 权限、属主和安全边界

日志往往包含用户输入、请求头、令牌、路径、数据库错误和内部地址。轮转配置本身也是安全边界的一部分。

11.1 新文件权限

create 决定新日志的模式和属主:

create 0640 root adm

若应用以非 root 用户运行,应使用能够写入的属主或组。例如:

create 0640 myapp adm

但不能机械地把日志改成 0666。过宽权限可能让普通用户读取密码、会话令牌或个人数据。

某些版本支持:

su myapp adm

让轮转操作以指定用户和组处理日志。这对 /var/log 下由非 root 服务写入的日志有帮助,但也会改变重命名、创建、压缩和删除的权限边界。应结合目录权限、父目录粘滞位和发行版默认安全策略测试。

11.2 符号链接和路径替换

日志路径所在目录若可被不可信用户写入,轮转脚本和创建动作可能受到符号链接、路径替换或权限竞态影响。生产环境应保证:

namei -l /var/log/myapp/app.log
stat -c '%A %U:%G %n' /var/log/myapp /var/log/myapp/app.log

检查路径中每一级目录的属主和写权限。不要让普通应用用户拥有整个日志目录的任意重命名权限,同时又让 root 轮转脚本无条件执行复杂命令。

11.3 删除不是安全擦除

rotate 0、过期归档删除和 rm 只是在文件系统目录中移除引用,并不保证数据无法恢复。对于含敏感数据的日志,还要考虑:

  • 归档是否应在采集后立即删除;
  • 存储是否加密;
  • 备份和对象存储的生命周期;
  • 压缩包权限;
  • 合规要求;
  • SSD、写时复制文件系统和快照对“安全擦除”的影响。

不要把 shred 当作通用合规解决方案。现代存储设备和快照系统可能使其无法提供预期保证。


12. 磁盘保护:保留策略必须用容量推导

仅设置:

rotate 7
compress

不能证明磁盘安全。需要估算日志产生速率和归档容量。

设:

  • r:日志产生速率,单位为字节/小时;
  • T:轮转周期,单位为小时;
  • N:保留归档数量;
  • c:压缩后平均大小占原始大小的比例;
  • B:其他日志和系统数据占用;
  • F:文件系统总容量;
  • R:必须保留的安全余量。

单个未压缩归档大小近似:

S=rTS = rT

压缩归档总量近似:

A=N×S×cA = N \times S \times c

若考虑当前活动文件、最近一次未压缩归档、压缩临时空间和其他数据,粗略上界可写为:

UB+S+S+A+ΔU \approx B + S + S + A + \Delta

其中:

  • 第一个 S 是活动文件轮转前可能达到的大小;
  • 第二个 S 是轮转或 copytruncate 阶段的额外副本;
  • A 是历史压缩归档;
  • Δ 是临时文件、文件系统元数据、突发流量和误差余量。

安全条件是:

UFRU \le F - R

完整算例

假设:

日志速率 r = 20 MB/h
轮转周期 T = 24 h
保留数量 N = 14
压缩比例 c = 0.25
其他占用 B = 30 GB
安全余量 R = 10 GB
文件系统总容量 F = 100 GB

每天原始日志:

S=20×24=480 MBS = 20 \times 24 = 480\text{ MB}

14 份压缩归档:

A=14×480×0.25=1680 MBA = 14 \times 480 \times 0.25 = 1680\text{ MB}

仅按长期占用估算:

U长期30+0.48+1.68=32.16 GBU_{\text{长期}} \approx 30 + 0.48 + 1.68 = 32.16\text{ GB}

看起来安全。但若使用 copytruncate,复制过程中可能暂时需要接近额外的 480 MB;若日志突增到平时的 10 倍,单日文件可能达到 4.8 GB;若压缩发生在同一文件系统,还需为输出和临时文件预留空间。真正的安全配置还必须考虑峰值,而不是只看平均速率。

12.1 dfdu 和已删除文件

磁盘空间异常时,先区分三个概念:

df -h /var
du -xhd1 /var/log
sudo lsof +L1
  • df 反映文件系统块和 inode 使用情况;
  • du 统计目录树中仍可访问的文件;
  • lsof +L1 查找链接数为零但仍被进程打开的文件。

如果 df 显示使用率很高,但 du /var/log 统计不出来,常见原因是进程仍打开已删除日志。恢复空间的方法通常不是再次删除,而是让持有 fd 的进程关闭或重新打开文件,例如重启服务或通过其官方 reopen 接口处理。

12.2 inode 耗尽

大量小日志或大量未压缩归档可能耗尽 inode,即使字节空间还有剩余:

df -ih /var

因此磁盘保护需要同时关注:

块空间、inode、目录项数量、写入延迟和设备队列

13. 与 journald、rsyslog 的边界

13.1 journald

journald 使用自己的 journal 文件格式,并由自身负责:

  • 持久化或内存存储;
  • 按大小、时间和空闲空间限制;
  • 索引;
  • journalctl 查询;
  • vacuum 清理。

常见配置在:

/etc/systemd/journald.conf

查看当前限制:

journalctl --disk-usage
systemctl status systemd-journald

清理 journal 应使用 journald 的接口,例如:

sudo journalctl --vacuum-time=14d
sudo journalctl --vacuum-size=2G

这与 logrotaterotate 14 不是同一种策略。前者按 journal 数据库和其文件管理机制清理,后者按普通路径匹配文件。

13.2 rsyslog

rsyslog 常将消息写入:

/var/log/syslog
/var/log/messages
/var/log/auth.log

不同发行版路径和配置不同。使用 logrotate 管理 rsyslog 文件时,必须确保轮转后 rsyslog 重新打开文件。发行版通常会提供类似:

postrotate
    /usr/lib/rsyslog/rsyslog-rotate
endscript

或通过服务信号完成。不要直接复制某发行版的脚本到另一发行版;先检查:

ls -l /etc/logrotate.d/
grep -R "rsyslog\|syslog" /etc/logrotate.d /etc/rsyslog* 2>/dev/null

如果 rsyslog 同时接收 systemd journal 转发消息,还要确认链路是否发生重复采集,避免以为轮转失败,实际是同一条日志通过两条路径写入。


14. 常见误解与失败表现

误解一:改名后应用自然会写新文件

错误。应用持有的是文件描述符,不是每次写入都根据路径重新 open()。验证方法:

sudo lsof /var/log/myapp/app.log /var/log/myapp/app.log.1

若旧文件继续增长,应检查 reopen 信号和应用日志。

误解二:daily 表示每 24 小时精确轮转

错误。它依赖外部调度器执行。检查 timer、cron、系统日志和状态文件。

误解三:copytruncate 没有信号问题,所以一定更安全

错误。它避免了 reopen 要求,但牺牲了复制期间的一致性,存在丢日志风险,并且需要额外磁盘空间。

误解四:compress 会立即压缩刚产生的所有归档

通常不一定。delaycompress 会保留最近归档未压缩;脚本和版本行为还可能影响观察到的顺序。应使用:

sudo logrotate -d -v -s /tmp/test.status /path/to/config

确认实际动作。

误解五:删除文件后空间一定释放

错误。只要进程仍持有文件描述符,inode 和数据块仍然被占用。使用:

sudo lsof +L1

定位。

误解六:手工执行 logrotate -f 只是预览

错误。-f 是强制实际轮转。预览应使用 -d,并配合独立状态文件。

误解七:配置文件权限正确,日志就不会泄露

错误。还要检查:

  • 新旧日志的属主和模式;
  • 归档目录权限;
  • 压缩包权限;
  • 备份和采集系统;
  • 脚本执行权限;
  • 普通用户是否能替换日志路径中的目录项。

15. 生产诊断路径

15.1 日志没有轮转

按以下顺序检查:

sudo logrotate -d -v /etc/logrotate.conf
systemctl status logrotate.timer
systemctl list-timers --all | grep -i logrotate
ls -l /var/lib/logrotate/status

重点判断:

  1. 配置是否被 /etc/logrotate.confinclude 引入;
  2. 匹配路径是否正确;
  3. 文件是否不存在、为空或未达到条件;
  4. 状态文件是否记录了最近轮转时间;
  5. 调度器是否真正运行;
  6. 是否因锁被其他实例占用;
  7. 脚本或权限错误是否导致任务失败。

15.2 轮转后新日志为空

检查旧文件是否继续增长:

watch -n 2 'ls -lh /var/log/myapp/app.log /var/log/myapp/app.log.1'

同时查看进程打开的文件:

sudo lsof | grep '/var/log/myapp/app.log'

如果进程持有 .1(deleted),说明 reopen 没有成功。解决方向是:

  • 核对正确的信号;
  • 使用服务管理器发送信号;
  • 检查应用是否真的支持动态 reopen;
  • 检查 create 的属主和权限;
  • 检查应用是否写入了其他路径。

15.3 轮转导致磁盘或 I/O 告警

执行:

df -hT /var
df -ih /var
sudo du -xhd1 /var/log | sort -h
sudo lsof +L1
iostat -xz 1 10

然后区分:

  • 归档保留过多;
  • 压缩任务与业务竞争 I/O;
  • copytruncate 制造了临时副本;
  • 已删除文件仍被打开;
  • inode 耗尽;
  • olddir 或归档目录位于错误的文件系统;
  • 日志速率已超过原有容量模型。

如果系统接近满盘,不要只执行一次删除命令。应先确认哪些日志可删除、是否需要保留审计证据、是否有远端采集副本,并在必要时停止或降级非关键日志源,避免删除活跃日志造成新的数据问题。


16. 选择策略时的实际取舍

对于支持 reopen 的现代守护进程,通常优先采用:

rename/create + 正确的 reopen 信号 + compress + 明确保留策略

对于无法重新打开文件的旧程序,才考虑:

copytruncate + 接受轮转窗口的数据丢失风险 + 预留临时空间

对于高价值日志,还应考虑让应用或日志代理直接提供可靠的轮转机制,例如:

  • 应用按大小轮转并使用原子重命名;
  • 通过日志代理统一采集和限流;
  • 发送到远程存储后执行本地生命周期清理;
  • 使用 journald 的容量和 vacuum 策略;
  • 将业务事件和调试日志分开设置保留周期。

logrotate 的正确性依赖三个外部条件:

可靠轮转=正确的文件操作+生产者重新打开+可验证的生命周期与容量策略\text{可靠轮转} = \text{正确的文件操作} + \text{生产者重新打开} + \text{可验证的生命周期与容量策略}

缺少任何一项,都可能出现不同故障:

  • 没有正确文件操作:新文件权限错误、重命名失败、归档覆盖;
  • 没有重新打开:旧 inode 继续增长、dfdu 不一致;
  • 没有容量策略:压缩失败、满盘、服务写入失败;
  • 没有并发控制:重复轮转、状态错乱、脚本重复执行;
  • 没有验证:配置看似正确,但调度器、信号或权限实际不成立。

因此,logrotate 不应被看成一组“按日期删除文件”的配置,而应被看成一个围绕 inode、文件描述符、进程控制、压缩成本、并发锁和容量上界运行的生命周期系统。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。