【前情提要】 磁盘告警处理完了,复盘也做了。但老周说:“你昨晚是运气好,日志只是大,没把系统写死。如果磁盘 100% 了,你 SSH 都连不上。”小林问:“那怎么办?”老周说:“配 logrotate,让日志自己切割。”

一、老周的追问

早上到工位,小林把昨晚的磁盘告警处理过程汇报了一遍。

老周听完,点了点头,然后问了一个问题:

“你昨天 truncate 了日志,磁盘从 93% 降到 75%。那明天呢?”

“明天……日志继续写,继续涨?”

“对。”老周说,“你 truncate 是‘把杯子倒空’,但杯子还在接水。你今天倒一次,明天它又满了。你得让杯子自动换,而不是手动倒。”

“怎么自动换?”

“logrotate。”老周说,“Linux 自带的日志切割工具。它按时间或大小,自动把旧日志切走、压缩、保留一定数量、删除更早的。你配好之后,就不用管了。”

“那我昨天为什么没想到?”

“因为你还没学。”老周说,“今天教你。配完这个,你以后不会再因为日志满磁盘被告警吵醒了。”

二、logrotate 是什么?

“logrotate 是 Linux 的日志管理工具。”老周说,“它干四件事:”

第一,切割。

“把当前日志改名,比如 access.log 改成 access.log.1,然后让 Nginx 重新打开一个新的 access.log 继续写。”

第二,压缩。

“旧日志用 gzip 压缩,节省空间。access.log.1 变成 access.log.1.gz。”

第三,保留。

“只保留最近 N 份,比如 7 份。第 8 份自动删除。”

第四,定时。

“logrotate 由 cron 或 systemd timer 定时触发,默认每天跑一次。”

“那它怎么知道要切割哪些日志?”

“看配置文件。”老周说,“主配置在 /etc/logrotate.conf,各个服务的配置在 /etc/logrotate.d/ 下。你进去看看。”

小林敲:

ls /etc/logrotate.d/

输出:

apt
bootlog
dpkg
nginx
rsyslog
ufw
...

“看到了吗?nginx 已经有配置文件了。”老周说,“你打开看看。”

cat /etc/logrotate.d/nginx

输出:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    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
        invoke-rc.d nginx rotate >/dev/null 2>&1
    endscript
}

“这是 Ubuntu 默认的 Nginx 日志切割配置。”老周说,“它已经配好了,但你从来没检查过,对不对?”

小林脸一红:“我没看过。”

“所以你磁盘满了,不是因为没有 logrotate,而是因为你没有验证它到底有没有在跑。”老周说,“今天你把这份配置搞懂,再验证一遍。”

三、logrotate 配置详解

老周拿过小林的笔记本,逐行解释。

/var/log/nginx/*.log

“匹配 /var/log/nginx/ 下所有 .log 文件。access.log 和 error.log 都会被处理。”

daily

“每天切割一次。也可以写 weekly、monthly、size 100M。size 100M 表示日志超过 100M 就切,不等一天。”

missingok

“如果日志文件不存在,不报错,继续处理下一个。避免因为某个日志没生成而中断。”

rotate 14

“保留 14 份旧日志。第 15 份自动删除。你 daily + rotate 14,就是保留 14 天。”

compress

“压缩旧日志。用 gzip,文件名变成 .gz。”

delaycompress

“延迟压缩。刚切割的日志不立刻压缩,等下一次切割时再压。为什么?因为有些程序切割后还在写旧文件,立刻压缩会报错。”

notifempty

“如果日志是空的,不切割。”

create 0640 www-data adm

“切割后,创建新日志文件,权限 0640,属主 www-data,属组 adm。如果不写 create,新日志可能权限不对,Nginx 写不了。”

sharedscripts

“多个日志文件,postrotate 只执行一次。如果不写,每个日志文件都会执行一次。”

prerotate ... endscript

“切割前执行的脚本。这里是检查 httpd-prerotate 目录,运行里面的脚本。”

postrotate ... endscript

“切割后执行的脚本。这里是通知 Nginx 重新打开日志文件。”

小林看着 postrotate 里的 invoke-rc.d nginx rotate,问:“这行是干啥的?”

“通知 Nginx:‘日志切了,你重新打开新日志文件’。”老周说,“Nginx 收到信号后,关闭旧日志句柄,打开新日志。如果你不通知它,它会继续写旧的 access.log.1,而新的 access.log 是空的。”

“那 invoke-rc.d 和 systemctl reload nginx 有啥区别?”

“invoke-rc.d 是 Debian/Ubuntu 的兼容脚本,最终也是调 systemctl。”老周说,“你可以改成 systemctl reload nginx,更直接。”

小林记下:

  • daily:每天切割
  • rotate 14:保留 14 份
  • compress:压缩旧日志
  • delaycompress:延迟压缩
  • create:切割后创建新日志,指定权限
  • postrotate:切割后通知 Nginx 重新打开日志

四、手动测试 logrotate

“配置看懂了,现在测试。”老周说,“logrotate 默认由 cron 每天跑,但你不能等到明天。用 -d 模拟运行。”

小林敲:

sudo logrotate -d /etc/logrotate.d/nginx

输出:

reading config file /etc/logrotate.d/nginx
...
Considering log /var/log/nginx/access.log
  Now: 2026-09-14 10:00
  Last rotated at 2026-09-14 00:00
  log does not need rotating (log has already been rotated)
...

“-d 是 debug 模式,只显示会做什么,不实际执行。”老周说,“你看到 log does not need rotating,说明今天已经切割过了,不用再切。”

“那我想强制切一次呢?”

“-f。”

sudo logrotate -f /etc/logrotate.d/nginx

“-f 强制切割,不管时间。”老周说,“你跑一下,然后看日志目录。”

小林执行:

sudo logrotate -f /etc/logrotate.d/nginx
sudo ls -lh /var/log/nginx/

输出:

-rw-r----- 1 www-data adm    0 Sep 14 10:05 access.log
-rw-r----- 1 www-data adm 8.3G Sep 14 02:20 access.log.1
-rw-r----- 1 www-data adm 200M Sep 14 02:20 error.log
-rw-r----- 1 www-data adm    0 Sep 14 10:05 error.log.1

“access.log 变成 0 字节,access.log.1 是旧的 8.3G。”小林说,“但 access.log.1 没压缩。”

“因为 delaycompress。”老周说,“它延迟压缩,等下一次切割时才压。你再强制切一次。”

sudo logrotate -f /etc/logrotate.d/nginx
sudo ls -lh /var/log/nginx/

输出:

-rw-r----- 1 www-data adm    0 Sep 14 10:06 access.log
-rw-r----- 1 www-data adm    0 Sep 14 10:05 access.log.1
-rw-r----- 1 www-data adm 1.2G Sep 14 10:05 access.log.2.gz

“看到了吗?access.log.2.gz 是压缩后的旧日志,1.2G。”老周说,“从 8.3G 压到 1.2G,省了 7G。”

“那 access.log.1 是 0 字节?”

“对,因为第二次切割时,第一次的 access.log.1 被压缩成 access.log.2.gz,然后新的 access.log.1 是空的。这就是 delaycompress 的行为。”

小林记下:

  • logrotate -d:模拟运行,不实际执行
  • logrotate -f:强制切割
  • delaycompress:旧日志延迟一次再压缩
  • 切割后:access.log 是新的,access.log.1 是上一次的,access.log.2.gz 是压缩的更早的

五、验证 logrotate 是否在跑

“你现在知道怎么手动切了。”老周说,“但关键是,它每天自动跑了吗?”

“怎么看?”

“看 logrotate 的状态文件。”

cat /var/lib/logrotate/status

输出:

logrotate state -- version 2
"/var/log/nginx/access.log" 2026-9-14
"/var/log/nginx/error.log" 2026-9-14
...

“2026-9-14,说明今天跑过了。”老周说,“如果日期是很久以前,说明 logrotate 没跑。”

“那它由谁触发?”

“Ubuntu 上,logrotate 由 systemd timer 触发。”

systemctl status logrotate.timer

输出:

● logrotate.timer - Daily rotation of log files
     Loaded: loaded (/lib/systemd/system/logrotate.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since ...
    Trigger: Tue 2026-09-15 00:00:00 CST; 13h left

“active (waiting),说明 timer 在等,明天 0 点触发。”老周说,“enabled,说明开机自启。”

“那如果我想改成每小时跑一次呢?”

“改 timer。”老周说,“但你不一定需要。日志量大的话,可以用 size 配置,而不是改 timer。比如 size 100M,日志超过 100M 就切,不等每天。”

“那 logrotate 自己有没有日志?”

“有。”老周说,“/var/lib/logrotate/status 是状态,/var/log/syslog 里有 logrotate 的执行记录。你 grep 一下。”

sudo grep logrotate /var/log/syslog | tail -10

输出:

Sep 14 00:00:01 ubuntu systemd[1]: Starting Daily rotation of log files...
Sep 14 00:00:02 ubuntu logrotate[1234]: rotating pattern: /var/log/nginx/*.log
Sep 14 00:00:02 ubuntu systemd[1]: Finished Daily rotation of log files.

“看到了吗?今天 0 点跑的。”老周说,“你以后排查日志切割问题,先看 /var/lib/logrotate/status,再看 /var/log/syslog。”

小林记下:

  • 状态文件:/var/lib/logrotate/status
  • 触发方式:systemd timer,logrotate.timer
  • 查看状态:systemctl status logrotate.timer
  • 执行日志:grep logrotate /var/log/syslog

六、排错挑战:日志切割后,Nginx 还在写旧文件

下午,小林想试试 copytruncate 的写法。他把 postrotate 那行注释掉,改成 copytruncate。

sudo nano /etc/logrotate.d/nginx

改成:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    copytruncate
}

保存,强制切割:

sudo logrotate -f /etc/logrotate.d/nginx

然后访问博客,观察日志:

sudo tail -f /var/log/nginx/access.log

刷新博客,终端没有新日志。

“啥?”小林懵了,“日志不写了?”

他检查 access.log.1:

sudo tail -f /var/log/nginx/access.log.1

刷新博客,日志出现在这里。

“Nginx 还在写 access.log.1。”小林说,“copytruncate 没生效?”

他喊来老周。

老周看了一眼配置:“你 copytruncate 写了,但你有没有注意到,copytruncate 和 create 是冲突的?”

“啥意思?”

“copytruncate 是‘复制日志文件,然后清空原文件’。它不创建新文件,而是复制一份,再把原文件截断。所以你不能同时写 create。”老周说,“你配置里没写 create,但问题是——copytruncate 有窗口期。 复制和截断之间,有几毫秒的延迟。如果 Nginx 在这几毫秒内写了日志,就会丢。”

“那我刚才为什么 Nginx 还在写 access.log.1?”

“因为你用 copytruncate 时,logrotate 复制了 access.log 到 access.log.1,然后清空 access.log。但 Nginx 的文件句柄还指向原来的 access.log,它不知道文件被清空了。所以它继续写 access.log,不是 access.log.1。”

“但我刚才看到日志在 access.log.1 里啊?”

“那是因为你没等。”老周说,“你 tail -f 的时候,access.log.1 是复制出来的副本,里面是旧日志。Nginx 还在写 access.log,但 access.log 被清空了。你刷新博客,新日志应该出现在 access.log 里。”

小林再试了一次:

sudo tail -f /var/log/nginx/access.log

刷新博客,日志出现了。

“回来了。”小林说。

“所以 copytruncate 的问题不是‘不写’,而是‘可能丢日志’。”老周说,“copytruncate 适合不支持重新打开日志的程序。Nginx 支持,所以用 postrotate 通知它,更可靠。”

小林改回原来的配置:

postrotate
    systemctl reload nginx
endscript

再次强制切割:

sudo logrotate -f /etc/logrotate.d/nginx

这次 Nginx 重新打开了新日志,access.log 正常写入。

排错思路总结:

  1. copytruncate 和 create 冲突,不能同时用。
  2. copytruncate 有窗口期,可能丢日志。
  3. copytruncate 不通知程序重新打开日志,程序继续写原文件。
  4. Nginx 支持 postrotate 通知,优先用 postrotate。
  5. 只有程序不支持重新打开日志时,才用 copytruncate。
  6. 验证:tail -f access.log,刷新页面,看新日志写在哪。

“记住,”老周说,“copytruncate 是备选方案,不是首选。 能用 postrotate 就用 postrotate。你以后遇到日志切割问题,先看程序支持不支持重新打开日志。”

七、加一条告警:监控日志增长速度

“日志切割配好了,但你还可以加一条告警。”老周说。

“监控什么?”

“日志增长速度。”老周说,“如果日志每秒增长超过 10M,持续 1 分钟,就告警。这样你能在日志撑爆磁盘之前发现。”

“怎么写?”

“PromQL 里,日志大小是 node_filesystem_avail_bytes 的变化率。你算 1 分钟内可用空间的下降速率。”

rate(node_filesystem_avail_bytes{mountpoint="/"}[1m]) < -10 * 1024 * 1024

“rate 是每秒变化率,-10 * 1024 * 1024 是每秒减少 10M。负数表示下降。”

“那告警规则呢?”

- alert: DiskFillingFast
  expr: rate(node_filesystem_avail_bytes{mountpoint="/"}[1m]) < -10 * 1024 * 1024
  for: 1m
  labels:
    severity: warning
  annotations:
    summary: "根分区磁盘快速下降"
    description: "根分区可用空间每秒下降超过 10M,持续 1 分钟,可能日志暴涨。"

小林把规则加到 node_rules.yml,检查,重启 Prometheus。

“这条告警比‘磁盘使用率 85%’更早。”老周说,“85% 是‘快满了’,这条是‘正在快速填满’。两条一起用,你既能知道‘快满了’,也能知道‘正在快速涨’。”

小林记下:

  • 磁盘使用率告警:> 0.85,告诉你快满了
  • 磁盘下降速率告警:< -10M/s,告诉你正在快速填满
  • 两条配合,提前发现问题

八、练习

  1. 练习一:用 logrotate -d /etc/logrotate.d/nginx 模拟运行,观察输出。然后用 logrotate -f 强制切割,确认 access.log 变成 0 字节,旧日志被重命名。
  2. 练习二:修改 /etc/logrotate.d/nginx,把 rotate 14 改成 rotate 7,daily 改成 size 100M。用 logrotate -d 验证配置,然后手动创建一个 200M 的日志文件,强制切割,确认切割生效。
  3. 练习三:在 Prometheus 中添加 DiskFillingFast 告警规则,用 promtool check rules 检查通过后重启。用 dd 快速填充磁盘,观察告警从 inactive → pending → firing 的过程,确认收到邮件。

参考答案

练习一:

sudo logrotate -d /etc/logrotate.d/nginx
sudo logrotate -f /etc/logrotate.d/nginx
sudo ls -lh /var/log/nginx/

练习二:

sudo nano /etc/logrotate.d/nginx

修改:

/var/log/nginx/*.log {
    size 100M
    missingok
    rotate 7
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        systemctl reload nginx
    endscript
}
sudo logrotate -d /etc/logrotate.d/nginx
sudo dd if=/dev/zero of=/var/log/nginx/test.log bs=1M count=200
sudo logrotate -f /etc/logrotate.d/nginx
ls -lh /var/log/nginx/

练习三:

sudo nano /etc/prometheus/rules/node_rules.yml

添加:

      - alert: DiskFillingFast
        expr: rate(node_filesystem_avail_bytes{mountpoint="/"}[1m]) < -10 * 1024 * 1024
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "根分区磁盘快速下降"
          description: "根分区可用空间每秒下降超过 10M,持续 1 分钟,可能日志暴涨。"
promtool check rules /etc/prometheus/rules/node_rules.yml
sudo systemctl restart prometheus
dd if=/dev/zero of=/tmp/fill.img bs=1M count=10000

观察 Prometheus Alerts 页面。

九、下集预告

logrotate 配好了,日志自动切割了,磁盘告警也不会再因为日志暴涨而频繁触发了。老周说:“监控告警项目做完了,但你有没有发现,你每天还是在手动查日志?”小林点头。老周说:“明天开始,第三个项目:日志分析。你要学会用 grep、awk、sed,从日志里挖出有用的信息。”

下一集:《告警太多了,老周说“你得学会看日志”》