
遇到线上问题我一般先看三样东西端口通不通、CPU 负载高不高、最近的日志里到底发生了什么。前两样用ss -tnlp和top就能快速搞定第三样就绕不开journalctl -xe背后就是常被误解的 systemd-journald。很多从传统 /var/log/messages 时代过来的运维总觉得 journald 是个不能直接 cat 的怪东西而新接触 Linux 的同学又容易被它的一堆字段和参数劝退。这篇文章我想把 journald 从头到尾摊开讲一遍它跟 syslog 到底差在哪、日常怎么查、怎么让它持久化、怎么控制容量、怎么把日志送到集中采集平台还有我在生产环境里踩过的几个坑。适合两类人看一是想彻底搞懂 journald 而不是只会journalctl -xe的人二是正在搭建日志链路、需要决定journald 日志怎么对接外部系统的人。1. 先搞清楚 journald 到底在存什么二进制日志与传统文本日志的分水岭1.1 传统 syslog 日志的麻烦我用 syslog 用了很多年说实话它的问题在日志量小的时候不明显一旦服务器上跑的服务多起来就非常难受。首先是格式混乱。你看 /var/log/messages 里有的程序规规矩矩按 syslog 协议打一行有的程序往 stderr 一扔被 crond 或 systemd 拾走后格式千奇百怪。Java 程序抛一个多行异常能被拆散成七八行零散的文本grep 一个关键字出来的上下文根本连不起来。其次是检索效率低。文本日志本质上只能靠 grep 一遍遍扫日志量大了以后想查某个 PID 的全部日志得先 grep 出 PID再去另一个大文件里 grep来回折腾。字段没有索引查询全靠肉眼看。第三是权限和管理割裂。messages、secure、cron、maillog 各管一摊想看一个用户的操作轨迹要在好几个文件之间跳来跳去。而且随便一个能写日志的进程都能往 syslog 里写入内容日志的可信度其实不高。1.2 journald 的存储模型结构化字段、索引与追加写journald 最大的变化是把日志从一行文本变成了一条记录。每条记录由一堆键值字段组成比如内核字段_PID、_UID、_COMM、_EXE、_CMDLINE还有 systemd 自己附加的_SYSTEMD_UNIT、_BOOT_ID、_MACHINE_ID以及业务程序自己写的MESSAGE、PRIORITY、SYSLOG_IDENTIFIER。千万别觉得这是 systemd 搞的私有格式。journal 文件格式是公开的端到端可解析官方文档里有完整的格式说明。它内部用追加写的方式记录日志每个 journal 文件后面还有哈希索引所以按字段查比如_PID1234基本是索引命中比 grep 大文件快几个数量级。还有一个容易忽略的好处多行日志在 journald 里可以作为一条完整记录存下来。应用通过 syslog 接口或原生 journal 接口写入时可以把一个多行堆栈作为一个 message检索和展示都是完整的不会像文本日志那样被截断成碎片。1.3 日志文件去哪了/var/log/journal 与 /run/log/journal 的秘密很多人第一次用 journald 会问日志文件在哪为什么 /var/log 下面找不到这就要说到 Storage 的取值了。默认情况下 Storageauto它的逻辑是如果 /var/log/journal 目录存在且可写就持久化写入如果不存在就退回到 /run/log/journal。而 /run 是内存文件系统重启机器日志全部消失。问题就出在这大多数发行版安装完后/var/log/journal 根本不存在。所以很多服务器装好 systemd 后直接就是日志只在内存里的状态一旦重启之前的日志都成了历史。想要持久化老实的做法是mkdir -p /var/log/journal systemctl restart systemd-journald或者更直接在 journald.conf 里把 Storage 改成 persistent。我自己的习惯是显式改 persistent而不是依赖 auto 的目录探测因为 auto 的行为容易被误判。还要注意权限/var/log/journal 属主应该是 root:systemd-journal权限 2755。systemd-journal 组内的用户可以只读访问日志这个组就是给运维账号和采集程序用的。别图省事把目录设成 777日志里有的是敏感信息权限还是要管好。2. journalctl 实战筛选、跟随、格式输出与 cron 日志追踪2.1 最常用的查询组合单位、时间、优先级journalctl 的命令逻辑其实很清晰先选看谁的日志再选看哪个时间段的最后选过滤到什么严重级别。按服务查是最常用的journalctl -u nginx journalctl -u nginx -u php-fpm journalctl -u nginx --since 1 hour ago journalctl -u nginx --since 2025-01-01 00:00:00 --until 2025-01-02 00:00:00 journalctl -u nginx -p warning-p可以指定优先级区间比如-p err只看 error 及以上-p err..warning看 error 到 warning 之间的。--since和--until支持两种时间写法一种是绝对时间一种是相对时间-30min、-1h、-7d排障的时候--since -30min这种写法最方便。还有一个很实用的参数是-b看某个启动周期的日志journalctl -b # 本次启动 journalctl -b -1 # 上一次启动 journalctl -b -2 -u nginx # 上上次启动时 nginx 的日志系统刚开机那段时间出问题或者怀疑是上次重启前发生了什么用这个能精准定位。2.2 用字段精确过滤_SYSTEMD_UNIT、_COMM、_PID-u虽然好用但它的匹配范围是 unit 名称。更底层的玩法是直接按 journal 字段过滤journalctl _PID1234 journalctl _COMMsshd journalctl _SYSTEMD_UNITnginx.service journalctl _UID1000字段过滤走的是索引日志量大时比journalctl | grep快得多。我排障时经常先拿-o json-pretty把一条日志的完整字段打出来看看到底有哪些 key 可以用再回去写精确查询journalctl -u myapp --since -10min -o json-pretty | head -n 80比如发现字段里有_EXE/usr/bin/python3下次就可以用journalctl _EXE/usr/bin/python3把所有 python 进程的日志一把捞出来比按 unit 更精准。有个小坑提醒一下journalctl 默认是分页输出的less一旦接管道或重定向到文件它反而会一次性全量输出。在脚本里最好显式加--no-pager避免碰到某些环境下 PAGER 变量导致脚本卡住。2.3 格式化输出的选择json-pretty、cat、short-iso默认输出格式那串带着时间、主机名、进程名的前缀人看还行脚本处理就难受了。journalctl 的-o参数有很多选项我常用的三个格式用途-o short-iso时间戳是 ISO 8601 格式排序和解析都方便-o cat只输出日志正文适合把日志流直接喂给 grep 或重定向到文件-o json-pretty展开全部字段调试查询条件时必备配合 jq 处理 JSON 输出能写出非常顺滑的查询journalctl -u myapp -o json --since -30min | jq -r select(.PRIORITY 3) | .MESSAGE这条命令把 error 级别的 message 全部提取出来一行一条比 grep 靠谱得多因为 PRIORITY 是结构化字段。2.4 顺着热搜聊一下怎么用 journald 查 cron 执行日志查看 crontab 执行日志是搜索热词也是我见过被坑得最多的地方。很多人的认知是crontab 执行的任务肯定有日志但实际上cron 默认不会记录任务脚本内部输出。脚本往 stdout 打的字、报错的堆栈只有当你配了 MAILTO 并且本机有邮件服务时才会发到邮箱而现在绝大多数服务器根本没装邮件服务于是脚本输出就彻底消失了。排查 crontab 问题正确姿势分两步。第一步看 cron 服务自己跑没跑journalctl -u crond --since -7d不同发行版 unit 名可能叫 crond 或 cron看具体环境。第二步要想确认某个任务的执行结果最好在脚本里主动打日志#!/usr/bin/env bash echo $(date %F %T) starting backup /var/log/mybackup.log # 具体任务逻辑 echo $(date %F %T) backup done, code$? /var/log/mybackup.log如果你不想动脚本也可以用logger把输出送进 journald然后用-t按 tag 过滤# 在脚本里 logger -t myjob backup failed # 查询 journalctl -t myjob --since today记住一个原则写 crontab 前先想好日志去哪。默认路径就是哪儿都不去这是设计如此不是 bug。3. 日志持久化、容量上限与清理让 journald 不再一重启就失忆也不把磁盘塞满3.1 为什么默认配置下重启会丢日志第 1 章讲过 Storageauto 的退回逻辑。这里补充一个很多人没注意到的细节即使你创建了 /var/log/journaljournald 也不是无限保留所有日志。它有自己的容量管理策略超过上限后会自动丢弃最老的日志这是它和 syslog 截然不同的地方。syslog 是有多少写多少写满为止journald 是用一个环状缓冲的思路永远控制总量。所以别指望有了持久化就能查到几个月前的所有日志容量上限不设好旧日志会自动被清理。3.2 journald.conf 关键参数速查与推荐初值配置文件在 /etc/systemd/journald.conf修改后重启 systemd-journald 生效。几个核心参数我用表列一下参数作用说明Storage日志存储模式persistent强制写 /var/log/journalauto按目录存在与否决定volatile只写内存none不落盘SystemMaxUse日志总共占用的最大磁盘空间默认是文件系统的 15%对纯日志来说可能偏高我生产上一般设 2G 或 4GSystemKeepFree给系统其他部分预留的磁盘空间默认 15%避免日志把磁盘写满SystemMaxFileSize单个 journal 文件大小上限默认是 SystemMaxUse 的 1/8到上限后轮转新文件MaxRetentionSec按时间保留日志比如 30d超过 30 天的日志删除Compress是否压缩默认 yes压缩能省一半左右空间Seal是否给日志加防篡改签名审计场景用默认 no一个常见的反面配置是SystemMaxUse 设太小比如 100M而业务日志量很大结果你查昨天的日志发现没了。日志本身就是排查问题的依据削日志容量省下的磁盘空间和出问题时找不到日志的代价完全不成正比。我建议生产环境至少给日志留 2G 以上的空间具体要看业务量宁可多留不要抠门。设置完以后重启服务systemctl restart systemd-journald3.3 vacuum 系列命令三种清理姿势手动清理 journald 日志官方支持的命令是 vacuum 系列比 rm 删文件安全得多# 清理到总大小不超过 500M journalctl --vacuum-size500M # 只保留最近 30 天的日志 journalctl --vacuum-time30d # 只保留最近 10 个日志文件 journalctl --vacuum-files10为什么推荐 vacuum 而不是直接 rmjournald 有正在写入的活跃文件直接删文件有可能让 journald 句柄悬空日志写入异常。vacuum 会先做轮转把老文件归档再按条件安全删除这个过程是 journald 自己控制的不会伤及正在运行的服务。如果你要清空所有历史日志不要直接截图里的老教程说删 /var/log/journal 下所有文件正确做法是先轮转再清理journalctl --rotate journalctl --vacuum-time1s--rotate把所有活跃日志切成归档文件然后 vacuum 按 1 秒的保留时间把所有归档全删掉这样才算干干净净。3.4 日志文件权限与多用户读取journald 日志里可能有敏感信息权限控制要注意。默认 /var/log/journal 下文件权限是 0640属主 root:systemd-journal。要让你自己的运维账号也能读日志而不需要 sudo执行usermod -aG systemd-journal username重新登录后就能直接journalctl查看日志。如果是采集程序要读也可以用同样的方式把它加入组。这里有个细节journald 的 Seal 选项如果启用会给日志文件加上加密签名用于检测日志是否被篡改。这在等保、审计场景很有用但开了之后日志文件就不能手工修改了vacuum 也不受影响。常规场景不一定要开审计场景建议开。4. 把 journald 日志导出到集中采集rsyslog、syslog-ng、filebeat 的选择4.1 什么时候需要外送日志journald 本身确实能存日志但它不是日志分析平台。多台机器的日志要统一查询需要送到 Elasticsearch 或云日志服务告警平台要消费日志流需要日志从本机出来合规审计要求日志长期归档本机磁盘也扛不住这些场景都需要把 journald 日志外送。外送之前灵魂三问下游系统要结构化字段吗日志量多大下游是 ELK 还是传统的 syslog 归档答案不同选型完全不同。4.2 journald 的四个转发开关journald.conf 里有四个转发开关ForwardToSyslog、ForwardToSocket、ForwardToWall、ForwardToConsole。默认 ForwardToSyslog 是 yes意思是日志同时会转发给本机的 syslog 服务通常是 rsyslog。如果你的服务器上根本没有 syslog 服务这个开关开着也没什么影响如果装了 rsyslog注意别让它重复消费。这里要强调一个关键问题把 journald 日志转成 syslog 文本格式结构化字段就丢了。你在 journald 里存的_PID、_SYSTEMD_UNIT、PRIORITY这些字段到 syslog 变成一串文本下游再想按字段查就难了。所以如果下游是日志分析平台优先走原生 journal 读取而不是先转 syslog 再倒一手。4.3 filebeat 直接消费 journal 文件如果你的日志下游是 Elasticsearch最省事的方案是 filebeat 的 journald input。它直接读取 /var/log/journal 或 /run/log/journal 下的日志文件不需要 syslog 中转结构化字段原样保留。配置示例filebeat.inputs: - type: journald seek: cursor max_bytes: 4096 output.elasticsearch: hosts: [http://192.168.1.100:9200] index: linux-journal-%{yyyy.MM.dd}seek 的取值有三种tail 表示只从 filebeat 启动后的新日志开始head 表示从最早的日志开始扫cursor 表示从上次读取的位置继续。生产环境我强烈建议用 cursor配合 filebeat 的 registry 文件能做到断点续传filebeat 重启不会丢日志也不会重复读。有个点要注意filebeat 官方文档建议每台机器只跑一个消费 journald 的 filebeat 实例。如果你起了多个实例同时消费同一个 journal会出现位置竞争和重复读取。机器上已经跑了其他 filebeat 采集普通文件日志的话把 journald input 加进同一个实例就好别各跑各的。4.4 syslog-ng 和 rsyslog 的取舍如果公司日志架构还是传统的 syslog 体系rsyslog 和 syslog-ng 也能接 journald。rsyslog 用 imjournal 模块module(loadimjournal StateFileimjournal.state) *.* /var/log/from-journal.log这个 StateFile 是 rsyslog 记录 journal 读取位置的不能删删了会从头重读一遍。syslog-ng 则可以直接用 journald 作为 sourcesource s_journald { journald(); };选型我总结一下采集方式适合场景注意点filebeat journald input下游是 ELK、日志量大、需要字段单实例消费seek 设 cursorrsyslog imjournal想继续用文本日志归档结构化字段丢失StateFile 别乱动syslog-ng journald()复杂路由、多源汇聚、需要过滤配置复杂学习成本高实际经验绝大多数系统日志集成的需求都可以优先考虑 filebeat 直读 journald。只有当下游明确要 syslog 协议比如对接老旧的 SIEM 平台才走 rsyslog 或 syslog-ng 中转。倒腾一次你就知道字段化的日志在分析平台上有多好用转成文本再解析回来是很痛苦的一件事。5. systemd 服务标准输出捕获机制以及我踩过的几个日志坑5.1 StandardOutput 与 StandardError 的四种模式systemd 服务默认会把进程的 stdout 和 stderr 接到 journald所以你写一个服务代码里 print 到屏幕的内容都会出现在journalctl -u unit里不需要自己写日志文件。这是 systemd 时代和 sysvinit 时代体验差别最大的地方之一。如果想自定义可以在 service 文件的 [Service] 段配置StandardOutputjournal StandardErrorjournal注意这里没有缩进是段内的键值对。可选的值包括值行为journal写入 journald默认syslog写入 syslog 服务console写到终端如果有 ttyjournalconsole同时写 journal 和终端file:/path写到普通文件file:/path这个选项要慎用。它把 stdout 重定向到普通文件但这样一来程序的结构化上下文就没了而且文件句柄在服务启动时由 systemd 打开文件管理方式和 journald 完全不一样我在生产环境只用过调试场景能不用就不用。还有一个概念叫 SYSLOG_IDENTIFIER程序调用 syslog() 时传入的 tag。journalctl 里用-t按这个 tag 过滤比如journalctl -t CRON看 cron 的日志。如果你的程序用 syslog 打日志这个字段会自动带上。5.2 日志截断与多行合并问题有一次查一个 Java 服务的问题程序 stdout 打了一长串 JSON结果 journalctl 里看到这条日志被截断了中间直接断成两半。原因很简单journald 对单条消息有大小限制超过限制的会被截断。而且对于 stdout 流journald 是按行拆的意思是一行 stdout 对应一条日志记录行太长就可能出问题。解决思路有两个方向。第一个方向是程序侧改造成单行结构化日志把多行堆栈打成一行 JSON这是最稳的方案。第二个方向如果程序必须打多行文本建议走 syslog 接口因为 syslog 接口允许把多行文本作为一个 message 提交journald 会原样存下来。我做日志规范时定的规矩是业务日志一律单行 JSON日志里禁止换行异常堆栈用转义字符拼成一行。虽然人看 JSON 不如看堆栈舒服但配合 jq 和采集链路价值大得多。5.3 journald 丢日志的两个场景限流与内存压力日志丢了是最难排查的问题之一因为日志丢了你根本不知道它存在过。journald 有两个丢日志的机制要注意。第一个是限流。journald 默认有个速率限制单位时间内的日志条数超过阈值就会被丢弃满足条件时会在日志里出现类似 Suppressed 的提示。如果你的应用是高频打印日志的类型比如每秒几百条 debug很可能触发限流。调优参数在 journald.conf 里RateLimitIntervalSec5s RateLimitBurst10000这两个参数表示每 5 秒最多接收 10000 条日志。生产环境要根据业务量调大或者干脆确保日志级别不滥用。第二个是内存压力。如果 journald 运行在 volatile 模式日志存在 /run/log/journal占的是 tmpfs内存紧张时 journald 会拒绝写入或丢失日志。这个问题在容器场景尤其常见。想解决最好的办法就是持久化到 /var/log/journal让日志占用的是磁盘而不是内存。还有一个容易被忽视的场景程序崩溃时 stdout 缓冲未刷新。C 语言程序如果异常退出printf 的内容可能还在缓冲区里systemd 没来得及抓到日志就丢了。这种情况只能在程序侧做处理比如关键日志实时 flush或者用 syslog 接口发送——syslog 是同步走的不容易丢。5.4 时间同步问题导致日志时间线错乱journald 每条记录的时间戳是用系统时钟在写入时生成的。如果服务器没配时间同步系统时间会漂移甚至开机初期 RTC 时间不准日志时间可能是过去的、未来的或者干脆是 1970 年。这时候你用--since查询结果完全对不上。我处理过一台服务器开机日志显示 1970 年修好时间后新日志又跳到正确时间中间的时间线乱得一塌糊涂。从那以后我坚持所有服务器都启用 systemd-timesyncd 或 chrony并且 jounal 查询时报错定位时先确认时间同步状态不然排障方向都会被带偏。另外容器场景下多个容器共用宿主机的 journald日志的时间戳是宿主机时钟容器内看到的系统时间和宿主机不一致也会造成困惑。排查时先确认date输出再看 journal 时间两个对不上就得先解决时钟。6. 用 journald 做一点进阶的事审计追踪与轻量监控6.1 审计谁动了我的服务journald 记录了服务的启动、停止、重载事件这些在journalctl -u里其实都能看到。比如你想知道 nginx 服务昨晚是不是被重启过journalctl -u nginx.service --since yesterday --no-pager | grep -E Starting|Started|Stopped|Reloading能看到对应的时间点。如果再配合 sudo 日志基本能还原出谁在什么时间执行了什么操作导致服务重启。对于更严格的审计需求journald.conf 里可以开 Sealyes给日志加防篡改签名。这个功能平时用不上但等保测评或者需要提供我的日志是完整的、没改过的证据时它很有用。注意开了 Seal 之后日志文件不能手工修改否则签名校验失败。6.2 服务启动慢的定量分析systemd-analyze blame 能看每个服务的启动耗时这是整体视角。如果想看某个服务启动过程中的具体日志时间线用 journald 配合 short-iso 格式journalctl -u myservice.service -o short-iso --no-pager | head -n 50时间戳精确到微秒能看出服务在哪一步耗时最长。前一阵我排查一个数据库服务启动慢就是靠这个命令发现它在等网络挂载超时日志时间线里有一大段空白问题立刻明朗。6.3 基于 journal 的轻量监控脚本不需要上完整监控平台时journald 也能做简单的错误巡检。我的习惯是写个定时脚本扫最近几分钟某个服务的 error 日志有异常就告警#!/usr/bin/env bash UNITmyapp.service ERROR_COUNT$(journalctl -u $UNIT --since -5m -p err --no-pager | wc -l) if [ $ERROR_COUNT -gt 0 ]; then journalctl -u $UNIT --since -5m -p err --no-pager | tail -n 30 | mail -s [ALERT] $UNIT 有 $ERROR_COUNT 条错误 opsexample.com fi这个脚本逻辑很简单但要注意每次运行都调 journalctl 全量扫描日志量大时性能一般。更讲究的做法是用 journalctl 的--after-cursor配合游标位置只读取新增日志把状态存到文件里。不过这是后话小规模场景上面的脚本够用了。我个人的体会是journald 不是一个版本升级版的 syslog它是一套完全不同的日志处理思路。花一个小时把它的存储模型、字段体系和周边参数搞清楚之后排查问题、对接采集系统的效率会高很多。如果你还没有把日志持久化打开现在就可以创建 /var/log/journal 目录并重启 journald这个动作花不了一分钟但下次重启服务器后你会感谢自己做了这一步。