Linux 日志管理:journalctl 与 logrotate 实践

通过 journald 和 logrotate 控制日志留存、轮转与压缩,避免日志占满系统磁盘。

技术文章约 16 分钟156 次阅读

#问题的本质:日志是数据,不是垃圾

日志把磁盘写满,是运维事故里最「冤」的一种:业务没挂、进程没崩,就是 / 100%,服务全瘫。这篇文章解决的核心问题是:怎么让 Linux 日志既查得到、又不占死磁盘。要回答它,先看清一个最基础的事实——日志在落盘之前就由两套程序分工:systemd-journald 把结构化日志写进自己的二进制 journal 并自管体积,nginx、Tomcat 这类"自己打开文件写日志"的程序则把纯文本文件交给 logrotate 定时轮转。后文所有配置与排坑,都从这条分工推导。

围绕这条分工,因果链是这样的:journald 因为采用字段化的二进制格式,才能按服务、按优先级、按时间做索引级过滤,并能压缩和校验,代价是文件不能直接 cat/grep,也不能像文本日志那样随意切割,只能让 journald 自己按限额管理;logrotate 面对"进程长期持有文件描述符持续追加"的文本文件,因此有了 create 与 copytruncate 两种轮转路线,以及各自的取舍。下面按机制、操作、验证、边界四层展开。

#原理层:journald 为什么用二进制 journal,而不是像 syslog 那样按文本行追加

#日志如何进入 journald

Linux 上产生日志的入口主要有四类,journald 是它们的汇聚点:

  • 内核消息:内核 printk 输出经 /dev/kmsg 交给 journald,条目带 _TRANSPORT=kernel;
  • systemd 管理的服务:服务进程的 stdout/stderr 被 systemd 捕获并以流式写入 journal,条目带 _TRANSPORT=stdout,因此 journalctl -u nginx 能按单元名过滤;
  • 传统 syslog 应用:调用 syslog(3) 写 /dev/log 套接字,而该套接字正是由 journald 监听,原始 syslog 消息被转成 journal 条目,带 _TRANSPORT=syslog,同时保留 SYSLOG_IDENTIFIER 等字段;
  • 原生协议:systemd-cat、sd_journal_send() 等直接以原生协议投递,带 _TRANSPORT=native。

_TRANSPORT、_PID、_COMM、_SYSTEMD_UNIT、_SOURCE_REALTIME_TIMESTAMP 这些下划线前缀字段是 journald 自动附加的元数据,应用只提供 MESSAGE 本体与自己的字段。理解这一点,就理解了 journalctl 过滤的底层来源。

#二进制格式带来的四种能力

当 journald 把条目落盘时,不是拼一行文本,而是按字段写入二进制 journal 文件:每条消息拆成 MESSAGE、PRIORITY、_PID、_COMM、_SYSTEMD_UNIT 等字段,字段在文件内带类型和索引。由此得到纯文本追加模型做不到的四件事:

  1. 字段查询:journalctl _PID=1234、journalctl -u nginx 是对字段做匹配,不需要全文扫描;
  2. 按条目压缩:超过一定大小的条目默认压缩(现代 systemd 用 XZ/ZSTD),日志里大量重复前缀压得很好,Compress=yes 只是确认这个行为;
  3. 完整性校验:journal 文件带校验和,journalctl --verify 能查损坏;开启 Seal=yes 还能对归档文件做前向安全密封(需要先 journalctl --setup-keys);
  4. 崩溃安全:条目定长成块追加,不会出现"读到半行"的问题,崩溃后也不会留下半个文本行。

#为什么不像 syslog 那样按文本行追加

文本追加模型(rsyslog 写 /var/log/messages)的问题是查询效率与压缩粒度:想按时间、按优先级、按服务过滤,只能把整个文件读进来逐行处理;想压缩就得整文件 gzip,轮转、查询都耦合在"行"这个粒度上。journald 选择"写时结构化、查时按字段",用格式复杂度换来了检索、压缩与校验的确定性。

代价也由此而来:journal 文件不能被普通文本工具直接读(cat/grep/tail 看不到内容),必须经过 journalctl 解码;目录也不能像文本日志那样"爱怎么切怎么切",只能交给 journald 自己管理大小。所以"想看看磁盘上有哪些日志文件"要用 journalctl --disk-usage,而不是 ls /var/log。

#持久化 vs 挥发性:/run 与 /var/log/journal 的因果

journald 的存储位置由 journald.conf 的 Storage= 决定,取值有 auto(默认)、persistent、volatile、none。auto 的判定规则正是文章开头"重启就没了"的根源:

  • 如果 /var/log/journal/ 目录存在,就持久化,文件写到 /var/log/journal/<机器ID>/,机器 ID 来自 /etc/machine-id;
  • 否则写入 /run/log/journal/(tmpfs,重启清空)。

所以开启持久化的关键动作不是改配置,而是先建目录:mkdir -p /var/log/journal 后重启 systemd-journald,auto 模式的机器立刻转为持久化。反过来也能解释另一个常见现象:volatile 模式下查上次开机的内核日志 journalctl -b -1 没有数据,因为上一轮开机的内容根本没落盘。

#journald 自己怎么管体积

持久化之后,journal 不会无限增长,但机制要理解对:journald 在写入新条目、归档当前活跃文件时评估限额。相关设置:

  • SystemMaxUse=:journal 最大占用,超出后删除最旧的归档文件;
  • SystemKeepFree=:给文件系统保留的空闲下限,宁可淘汰旧日志也不让分区写满;
  • SystemMaxFileSize=:单个活跃文件写到此大小就归档、换新文件;
  • MaxRetentionSec=:条目允许保留的最大时长,超过即成为删除候选。

默认不配时,journald 也有基于所在文件系统容量的估算上限(通常约 10%,具体规则见 journald.conf(5)),所以"没配也能转"。

"满了自动删最旧"的完整因果是:活跃文件写到 SystemMaxFileSize → 归档成新文件 → 总占用超过 SystemMaxUse → 删除最旧归档,直到回到限额内。这里引出一个高频坑:--vacuum-* 只作用于已归档文件,当前活跃文件不受影响;想立刻腾空间,要先 journalctl --rotate 让活跃文件归档,再 vacuum。

#操作层:journald 持久化配置与 journalctl 查询

前置条件:systemd ≥ 249(Ubuntu 22.04 / Debian 12 / RHEL 9 均在此版本线上),systemd 作为 PID 1 运行,操作需要 root/sudo。文中命令以 Debian/Ubuntu 系路径为例,RHEL 系除 logrotate 状态文件路径外完全一致。

#开启持久化并限制体积

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage       # 现在能看到占用

编辑 /etc/systemd/journald.conf,加入或取消注释:

[Journal]
Storage=persistent
SystemMaxUse=2G
SystemKeepFree=4G
MaxRetentionSec=30d
Compress=yes

各字段的实际含义:

  • Storage=persistent:显式指定持久化(若保持 auto,建目录后同样生效);
  • SystemMaxUse=2G:journal 自身上限 2G,超出删最旧归档;
  • SystemKeepFree=4G:所在分区空闲不足 4G 时立即收紧 journal 占用;
  • MaxRetentionSec=30d:条目超过 30 天即为过期删除候选;
  • Compress=yes:保持按条目压缩,不开反而更占空间。

改完重启并确认:

sudo systemctl restart systemd-journald
journalctl --disk-usage
systemctl status systemd-journald   # 输出里会出现 Permanent journal 行

journalctl --disk-usage 的典型输出形如:

Archived and active journals take up 1.2G in the file system.

想核对"生效的配置"而不是"文件里写了什么",用:

systemd-analyze cat-config systemd/journald.conf | grep -E "SystemMaxUse|SystemKeepFree|Storage"

(systemd-analyze cat-config 是 systemd ≥ 245 的命令,输出合并所有 drop-in 后的配置并带注释。)

如果已经超限想立刻腾空间,先归档再清理:

journalctl --rotate                              # 全部活跃文件先归档
journalctl --vacuum-size=1G                      # 删到 1G 以下
journalctl --vacuum-time=14d                     # 只留 14 天内的条目
journalctl --vacuum-files=5                      # 只留 5 个归档文件

#journalctl 常用过滤

journald 全家桶是排障主力,比翻文件效率高得多:

journalctl -u myapp --since "1 hour ago"          # 某服务最近 1 小时
journalctl -u myapp -p warning..emerg             # 只看 warning 及以上
journalctl -k -b -1                               # 上次开机的内核日志(查 panic 神器)
journalctl -f -u nginx                            # 实时跟随最新输出
journalctl -n 100 --no-pager -u nginx             # 最后 100 行,脚本友好
journalctl --since "2026-08-01" --until "2026-08-02" _PID=1234

要理解语义的几点:

  • 优先级从高到低是 emerg(0)、alert(1)、crit(2)、err(3)、warning(4)、notice(5)、info(6)、debug(7);-p warning..emerg 是范围写法,等价于 0–4 级;只写 -p warning 表示"warning 及以上";
  • --since/--until 接受 "1 hour ago"、"yesterday"、ISO 时间等自然写法;
  • -b -1 依赖持久化:volatile 模式下上一次开机的内容没落盘,结果为空或提示找不到对应 journal 文件;
  • journalctl 默认走 less 分页,进脚本、管道时加 --no-pager。

配合 -o json-pretty 可以导出结构化日志接监控:

journalctl -u myapp --since "10 min ago" -o json-pretty

每条输出是一个 JSON 对象,字段就是 MESSAGE、PRIORITY、_PID、_SYSTEMD_UNIT 等,示意如下(实际值随环境变化):

{
  "PRIORITY" : "6",
  "MESSAGE" : "GET /index.html 200",
  "_PID" : "1234",
  "_SYSTEMD_UNIT" : "nginx.service",
  "_SOURCE_REALTIME_TIMESTAMP" : "2026-08-21 09:36:12.782Z"
}

再用 jq 处理即可写告警管道。

#操作层:logrotate 管住"自己写文件"的日志

journald 只负责 journal。nginx、Tomcat 这类自己打开文件写日志、自己持有文件描述符的程序,它们的文本文件 journald 不碰——这是第二条因果线:谁写文件,谁决定文件生命周期;logrotate 是替"不会轮转的程序"做轮转的外部管家。

#logrotate 由什么驱动

logrotate 本身只是命令行工具,靠周期任务触发:Debian/Ubuntu 由 /etc/cron.daily/logrotate(anacron 带随机延迟)每天跑一次;Fedora、RHEL 系常见独立的 logrotate.timer(systemd 定时器)驱动。两者都是"每天最多一次、带随机延迟",所以不要指望 0 点整准点轮转。它的判断依据记录在状态文件里(Debian 系 /var/lib/logrotate/logrotate.status,RHEL 系 /var/lib/logrotate.status),用来计算"距上次轮转是否够一个周期"。

#nginx 的完整配置

配置放在 /etc/logrotate.d/ 下,每个服务一个文件。nginx 的典型配置:

/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    dateext
    missingok
    notifempty
    create 0640 nginx adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
    endscript
}

逐条对应默认 create 流程说明:

  • daily:轮转周期为天(另有 weekly、monthly,以及按体积的 size/maxsize);
  • rotate 14:保留 14 份归档,加上当前文件共 15 份;
  • compress:轮转后 gzip 归档;delaycompress 让刚轮转的那一份先不压,方便立刻查昨天的日志;
  • dateext:归档文件名带日期(如 access.log-20260821),避免同名覆盖;
  • missingok:文件不存在时不报错;
  • notifempty:日志为空不轮转;
  • create 0640 nginx adm:轮转后创建新文件,属主 nginx、属组 adm(RHEL 系模板常见 create 0644 nginx adm,按发行版调整,核心是属主必须与进程运行用户一致);
  • sharedscripts:多个匹配文件共用一个 postrotate,只执行一次;
  • postrotate/endscript:轮转完成后通知进程重开文件句柄。nginx 用 kill -USR1(USR1 信号即"重新打开日志文件");Java 应用如果自己持有句柄不释放,就得换 copytruncate 路线。

#create 与 copytruncate:两种取舍

这是 logrotate 最核心的工程判断。默认 create 流程是:

  1. 把 access.log 改名为 access.log.1;
  2. 原地创建新的空 access.log;
  3. 执行 postrotate 发信号,让进程重开新文件句柄(nginx 的 -USR1 就是干这个)。

它的前提是程序听得懂信号、愿意重开句柄:nginx、apache、rsyslog 和多数用标准日志库的程序都可以。

copytruncate 则完全不依赖程序配合:

  1. 把 access.log 复制一份作为归档;
  2. 把原文件原地 truncate 清空;
  3. 程序继续写同一个文件描述符,不需要任何信号。

两个代价必须知道:

  • 丢日志窗口:复制与清空之间写入的条目会丢(毫秒到秒级),高并发下更明显,所以它不该用于高可靠性审计日志;
  • 复制成本与状态变化:每天整文件复制一遍,文件大时 IO 翻倍;截断后原文件归零,若配置了按 size 判断轮转,文件大小再次超标会立刻触发下一轮,周期语义与 create 路线不同。

取舍结论:能发信号的用 create;实在无法重开句柄的(某些 Java 应用、老守护进程),才用 copytruncate 并接受丢窗口。

#手动触发与验证

手动轮转验证(不依赖 cron/timer):

logrotate -d /etc/logrotate.d/nginx        # dry-run:只看计划,不实际轮转
logrotate -v /etc/logrotate.d/nginx        # 实际执行并打印详情
logrotate -f /etc/logrotate.d/nginx        # 强制轮转,无视周期判断

-d 是 dry-run,适合先确认配置语法与将要发生的动作;-f 用来"现在就转一次"验证结果。执行后检查:

ls -l /var/log/nginx/
grep nginx /var/lib/logrotate/logrotate.status   # RHEL 系用 /var/lib/logrotate.status

预期看到 access.log.1 已生成(delaycompress 生效时是未压缩的 .1 而非 .1.gz),access.log 是新的空文件,nginx 日志继续写入新文件而不中断。确认 postrotate 是否真正生效,可以对比 inode:

ls -i /var/log/nginx/access.log            # create 模式下轮转前后 inode 应变化

inode 没变说明 nginx 还在写旧句柄:要么 -USR1 没发出去,要么 pid 文件路径不对(如 nginx.pid 不在 /var/run)。

#操作层:Docker 是日志占磁盘的重灾区

容器 stdout 默认由 dockerd 写到宿主机 /var/lib/docker/containers/<容器ID>/<容器ID>-json.log,一行一个 JSON。跑几天就是几十 G——它不受 journald 管,也不在 logrotate 默认覆盖范围里,是典型的"没人管"的洞。别只靠 logrotate 去搜这个目录,直接在 docker daemon 配好轮转:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "20m", "max-file": "5" }
}

写入 /etc/docker/daemon.json 后重启 docker:

sudo systemctl restart docker

两个注意点:

  • daemon.json 只影响之后创建的容器,存量容器保持旧配置;
  • 轮转文件由 docker 自己删除,超过 max-file 的旧文件直接丢弃,docker logs 只能看到保留范围内的日志;需要长期留存的请接日志采集/集中日志,json-file 驱动本身不保留旧档。

已经吃掉的可以立刻清:

du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail
truncate -s 0 /var/lib/docker/containers/<id>/<id>-json.log

docker 根目录不一定在 /var/lib/docker,先用下面的命令确认实际路径再替换进上面的命令:

docker info --format '{{.DockerRootDir}}'

#验证层:怎么确认我做对了

#验证 journald 持久化与限额

ls -d /var/log/journal/$(cat /etc/machine-id)          # 持久化目录存在
journalctl --disk-usage                                 # 显示当前占用
systemctl status systemd-journald                       # 出现 Permanent journal 行
journalctl --verify                                     # 校验全部 journal 文件完整性

--verify 逐文件校验,出现 PASS 且无 FAIL 即为健康;若报 "File corruption detected" 之类信息,说明文件损坏,需要考虑恢复或清理。

#验证查询语义

journalctl -u nginx -b 0 -n 20 --no-pager
journalctl --since "1 hour ago" -p warning..emerg --no-pager | tail -20
journalctl -k -b -1 --no-pager | head -20          # 有持久化才能查上次开机

预期:-u 只出现该单元的消息;-p 只出现 0–4 级条目;-b -1 能返回上次开机的内核日志而不是空结果。

#验证 logrotate

  • dry-run:logrotate -d /etc/logrotate.d/nginx 打印每步动作但不实际轮转;
  • 强制轮转 + 查结果:按"手动触发与验证"一节执行;
  • 确认周期驱动在跑:Debian 系 ls /etc/cron.daily/logrotate,RHEL 系 systemctl status logrotate.timer;
  • 用状态文件核对上次轮转时间。

#模拟一次打满的验收

按原稿的验收场景:给日志所在分区留 20G、业务正常、磁盘占用约 30%,压一次磁盘水位:

dd if=/dev/zero of=/var/log/stress.file bs=1M count=2048   # 灌一个 2G 文件
df -h /var/log
rm -f /var/log/stress.file

要说清机制:dd 灌的是普通文件,不会触发 journald 的 SystemMaxUse 清理——journald 只在自己写入新条目时评估限额。要确定性验证自动清理与回收,需要先制造大量日志条目再观察:

for i in $(seq 1 20000); do systemd-cat -t loadtest echo "load test $i"; done
journalctl --rotate
journalctl --disk-usage

配合 SystemMaxUse=2G,条目量足够大时,rotate 后总占用会回到限额内;条目量不够也可直接用 --vacuum-size 观察回收。整体压力测试的验收标准只有一个:磁盘水位线恒定,服务可用性不受日志影响——journal 涨不破 2G,nginx 轮转后服务不断流,docker 日志被 max-size 兜住。

#边界与坑

#journal 涨满 /var/log

  • journal 文件位于 /var/log/journal/<machine-id>/,du -sh /var/log/journal 能看到真实占用;
  • "满了自动删"发生在写入/归档时,不是实时监控;活跃文件可能先涨到 SystemMaxFileSize 再被归档删除,瞬时占用超过 SystemMaxUse 是设计而非 bug;
  • 磁盘真的 100% 时 journald 写不进去,journalctl 报错,系统日志出现 No space left on device。此时先 journalctl --rotate 再 --vacuum-size=512M;若连 vacuum 都异常,可 systemctl stop systemd-journald 后手动删除 /var/log/journal/<machine-id>/ 下的归档文件再启动;
  • SystemKeepFree=4G 的意义在于:宁可淘汰旧日志,也不让分区被日志填满拖死系统。这与"日志是证据"存在冲突——有审计/合规要求的机器,应把 /var/log 单独分区,或接远程日志。

#rsyslog 与 journald 并存:双份日志

传统发行版同时跑 rsyslog 和 journald:应用调 syslog(3) 写 /dev/log,journald 收一份;journald 又按 ForwardToSyslog= 把消息转发给 rsyslog,rsyslog 写 /var/log/messages。配上持久化 journal 后,同一行日志在磁盘上出现两份,口径还经常不一致。工程上二选一:

  • 以 journald 为主:ForwardToSyslog=no,/var/log/messages 不再增长,查询全走 journalctl;
  • 以 rsyslog 为主:journald Storage=volatile 只做中转与查询缓存,长期归档交给 rsyslog(imjournal 从 journal 读取,不影响应用侧)。

原则一句话:一个日志只在一处持久化;转发到远程日志系统或采集 Agent 属于另一层,不算重复。

#copytruncate 的丢日志窗口

机制前面已讲。再加一条排查经验:如果程序明明持有句柄却用了 copytruncate,轮转后的归档可能"缺最后几秒"的内容——别拿它当事故现场的证据。要证据级日志,走 create + 信号,或让应用自己轮转(log4j 的 RollingFile、nginx 官方日志模板都是 create 路线)。

#其他高频坑

  • --vacuum-* 不动活跃文件:先 journalctl --rotate 再清理;
  • docker logs 与 truncate:部分 Docker 版本对 *-json.log 的文件偏移有缓存,truncate 后 docker logs 可能短暂看不到新日志;要稳妥就 docker restart <容器>,或从源头设 max-size;
  • logrotate 的周期判断:daily 是"距上次轮转至少一天",配合随机延迟可能上午才转;排障先 -d 再 -f;
  • 权限:create 0640 nginx adm 若属主不对,nginx 重开文件时可能因无权限写而报错;新文件属主必须与进程运行用户一致;
  • journald.conf 改动要重启:改完不重启 journald 不生效;systemctl restart systemd-journald 本身不丢日志,正在写的文件会继续。

#收尾

一句话总结分工:journald 负责"结构化、可查询、有上限"的 journal,logrotate 负责"程序自写文件"的轮转,docker 日志在守护进程层封顶;三者各管各的份额,运维只盯一个指标——磁盘水位线恒定,服务可用性不受日志影响。下一步建议:把手动触发的 vacuum 与 logrotate 脚本化(cron 或 systemd timer),把 journalctl --verify 与 df -h /var/log 接进监控告警;而当你的日志有法规留存或审计要求时,不要依赖本地 journal 的"自动删旧",尽快接集中日志系统。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

Linux 日志管理:journalctl 与 logrotate 实践 · LXH·BLOG