在Linux服务器上排查问题时,我打开最多的命令不是top,也不是ps,而是journalctl。这个命令是systemd日志系统的查询入口,几乎所有用户态服务、内核日志、启动信息都会汇总到这里。无论是服务起不来、磁盘被写满、还是某个进程半夜崩溃,先跑一条journalctl,往往比翻遍/var/log/目录更快定位到问题。这篇内容我会从原理、常用操作、存储机制、实战排查几个方面展开,把我实际踩过的坑和积累的经验一起整理出来。
1. journalctl 到底是什么,它解决的问题又是什么
1.1 日志碎片化,是运维中最头疼的事
接触systemd之前的Linux,日志系统是分散的。syslog/rsyslog负责接收一部分系统日志,写入/var/log/messages或/var/log/syslog;有些应用会自己往/var/log目录里写文件,比如nginx的access.log、error.log;还有一些服务干脆把日志输出到stdout,如果没有额外重定向,重启之后什么都没有。
这就带来一个很现实的问题:当某个服务出问题,你首先得想它把日志写到哪里去了。也许是/var/log/nginx/error.log,也许是/var/log/audit/audit.log,也许它根本没写文件只是把日志打到了系统日志里,甚至只是输出到了终端。排查时间一大半都耗在“找日志”上。
systemd设计的目标之一,就是统一管理所有服务的生命周期和日志。由systemd拉起的服务,只要往标准输出和标准错误输出写内容,systemd-journald这个守护进程就会自动收集,并打上来源服务名、PID、时间戳等元数据,集中存储。journalctl就是用来查询这些集中存储日志的命令行工具。
1.2 journald 和 journalctl 的分工
两者很容易混淆,我说个简单的比喻。systemd-journald是前台的服务员,负责接收、分类、登记所有日志信息,按规矩存到仓库里;journalctl是你手里的查询窗口,你告诉它要看哪一个时间段、哪一个服务、什么级别的日志,它就能快速帮你翻出来。
默认情况下,journald会把日志先写进内存目录/run/log/journal/。这意味着系统重启后日志就没了。很多刚接触的人第一次遇到“重启之前还能看到的日志,重启后查不到”就是这个原因。要解决持久化,需要手动创建/var/log/journal目录,这个后面详细讲。
journalctl还有一个很顺手的能力,就是能直接把内核日志一起查出来。以前要看内核日志得用dmesg命令,现在通过journalctl -k就能看到,还能和普通服务日志放在同一条时间线上,排查启动慢、硬件异常时特别方便。
1.3 日志里那些下划线字段是什么来头
第一次跑journalctl,很多人会看到类似下面这样的输出:
bash复制Jan 11 10:24:16 hostname nginx[12345]: [error] client 192.168.1.10 connected ...
这是默认输出格式,看起来和传统syslog差不多。但是带上-o verbose选项后,你会发现每条日志背后还跟着一堆字段:
bash复制__CURSOR=...
__REALTIME_TIMESTAMP=...
_SYSTEMD_UNIT=nginx.service
_PID=12345
_COMM=nginx
_EXE=/usr/sbin/nginx
_HOSTNAME=hostname
MESSAGE=...
这些字段是journald自动为每条日志打上的“身份证”。比如_SYSTEMD_UNIT表示这条日志来自哪个systemd服务单元,_PID是进程号,_HOSTNAME是主机名,SOURCE_REALTIME_TIMESTAMP是应用打日志的时间。正是因为有这些结构化字段,journalctl才能做到按服务过滤、按PID过滤、按可执行文件名过滤。
这些字段是排查问题时的利器。比如你想看某个进程在某个时间段内做了什么,不需要翻完整份日志,直接用字段过滤,效率能提升好几倍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. journalctl 最常用的几个操作
2.1 查看服务日志:-u 参数是核心
排查某个服务的问题时,最常用的就是按unit过滤。unit可以理解为systemd管理的一个服务单元,比如nginx.service、docker.service、ssh.service。
bash复制journalctl -u nginx.service
这条命令会把nginx.service的所有日志都列出来,从服务启动到现在的完整记录。我排查问题时第一步基本就是这条命令,然后配合-n指定行数:
bash复制journalctl -u nginx.service -n 50 --no-pager
-n 50表示只看最近50行,--no-pager表示不让输出进分页器,直接全部滚回终端。这在脚本里也很常用,避免被less卡住。
有时候一个服务的日志里混着很多无关信息,比如健康检查、静态文件访问日志,这时候我会再叠加一个-p参数按级别过滤,只留下warning以上的记录:
bash复制journalctl -u nginx.service -p warning -n 50 --no-pager
2.2 实时跟踪日志输出:-f 参数
tail -f是跟踪日志文件最常用的命令,journalctl同样支持-f参数,效果一样,会持续输出新产生的日志。
bash复制journalctl -u docker.service -f
我在两个场景下特别喜欢用这个。第一是调试service启动脚本,修改配置后重启服务,实时看第一行报错;第二是新服务上线后先观察一段时间,确认没有异常日志刷出来再离开终端。
-f可以和-u、-p、--since组合使用。比如只看最近10分钟的警告以上日志并持续跟踪:
bash复制journalctl -u myapp.service -p warning --since "10 minutes ago" -f
2.3 按时间范围过滤:--since 和 --until
日志多了以后,最直接的需求是“找出某个时间段内发生了什么”。journalctl的时间过滤写法很灵活,我常用的几种都列在下面。
bash复制journalctl --since "2024-01-01 00:00:00"
journalctl --since "1 hour ago"
journalctl --since "yesterday" --until "today"
journalctl --since "2024-01-01 00:00:00" --until "2024-01-01 12:00:00"
时间写法在man手册里有完整列表,支持英语自然表达,比如yesterday、today、tomorrow、now等。我一般写时间戳都会精确到秒,同时加上--utc参数避免时区干扰,尤其在看跨机房设备日志的时候,时区不一致会导致排错错位:
bash复制journalctl --since "2024-01-01 00:00:00+08:00" --until "2024-01-01 02:00:00+08:00"
2.4 日志优先级过滤:-p 参数
日志级别对应Linux syslog的8个等级,从严重到轻微依次是:
| 级别 | 数字值 | 说明 |
|---|---|---|
| emerg | 0 | 系统不可用 |
| alert | 1 | 必须立即处理 |
| crit | 2 | 严重问题 |
| err | 3 | 错误 |
| warning | 4 | 警告 |
| notice | 5 | 普通但重要的信息 |
| info | 6 | 一般信息 |
| debug | 7 | 调试信息 |
用-p err过滤时,会显示err及更严重的级别,也就是数字值0到3。注意这里的方向:-p err等价于“err和比err更严重的级别”,不是只显示err本身。想要只看一个级别,需要用-p err..err这种区间写法。
我在日志特别多、想看系统“到底有没有大事”的时候,会用:
bash复制journalctl -p crit --since "24 hours ago"
如果这条命令输出很多,说明系统在过去24小时内并不是很健康。
2.5 内核日志与启动日志:-k 和 -b
内核日志曾经要靠dmesg查。journalctl同样支持:
bash复制journalctl -k
配合-b参数可以指定查看哪一次启动的记录。-b 0是当前这次启动,-b -1是上一次启动,-b -2是上上次。
服务器半夜自动重启了,想看重启前内核发生了什么,用:
bash复制journalctl -k -b -1 -p err
这个组合我现在用得很多。以前排查重启原因要翻dmesg、看时间戳、对日志,现在一条命令就能拉出上次启动时内核报的错误,效率高了不少。
3. 日志存储与持久化:为什么重启后日志没了
3.1 内存日志和磁盘日志的区别
journald默认优先把日志写入/run/log/journal/,这个目录是tmpfs,也就是内存文件系统。好处是速度快、减轻磁盘IO压力,代价是系统重启后数据全部消失。对于生产环境来说,重启后日志直接消失是很要命的,尤其是排查崩溃原因的时候,重启恰恰会丢掉最关键的崩溃前后记录。
解决办法是创建持久化日志目录:
bash复制mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
systemctl restart systemd-journald
创建目录并确认权限正确后,journald会自动把日志写入/var/log/journal/,此后重启不丢日志。具体路径一般是/var/log/journal/机器ID/,可以用journalctl --header查看当前使用的文件路径。
3.2 journald.conf 里值得改的几个参数
journald的配置在/etc/systemd/journald.conf。我用表格整理一下主要参数,方便参考。
| 参数 | 默认值 | 作用 |
|---|---|---|
| Storage | auto | auto表示有/var/log/journal就用磁盘,否则用内存 |
| Compress | yes | 是否压缩归档日志 |
| SystemMaxUse | 分区大小10% | 磁盘日志最大占用空间 |
| SystemKeepFree | 分区大小15% | 为其他用途保留的磁盘空间 |
| SystemMaxFileSize | 无 | 单个日志文件上限 |
| MaxRetentionSec | 无 | 日志文件最长保留时间 |
| RuntimeMaxUse | 内存大小10% | 内存日志最大占用空间 |
有几个参数是生产环境我会主动改的。
第一是SystemMaxUse。默认值是文件系统大小的10%,如果/var/log所在分区是500G,日志就能吃到50G。大部分场景不需要留这么多日志,我一般会设成2G到5G,防止日志膨胀把磁盘撑破。
ini复制[Journal]
SystemMaxUse=2G
SystemKeepFree=1G
第二是MaxRetentionSec。如果公司有合规要求,日志需要保留30天或90天,可以设置这个参数。但要注意,MaxRetentionSec是“最长保留时间”,journald会在轮转时判断是否存在超过该时间的旧文件并清理,并不是到点精确删除。
配置修改后重启服务生效:
bash复制systemctl restart systemd-journald
3.3 手动清理日志:vacuum 三兄弟
有时候磁盘告警已经亮起来了,来不及细调配置,需要立即释放空间。journalctl提供了三个手动清理命令。
bash复制journalctl --vacuum-size=200M
journalctl --vacuum-time=7d
journalctl --vacuum-files=5
--vacuum-size=200M表示把日志总量压缩到200M以内,超出部分删除;--vacuum-time=7d删除7天前的日志;--vacuum-files=5只保留最近的5个日志文件。
这三个命令我建议先记在脑子里,线上遇到“/var/log满了”的第一反应就是跑一句vacuum-size,比慢慢找哪个文件在吃空间要快得多。
3.4 Rotation 的触发机制
journald的日志轮转不是按固定时间执行的,而是根据条件触发。常见的触发条件包括文件大小达到SystemMaxFileSize上限、目录总大小超过SystemMaxUse、手动执行vacuum命令。另外,使用journalctl --rotate可以强制触发一次轮转,把当前日志文件切换到新文件。
排查问题的时候如果你发现日志文件没有按天切割,不用意外,journald不是logrotate那种按固定周期工作的机制。它是“满了就转”,只要容量没超,一个文件能用很久。
4. 实战排查:journalctl 帮我解决的几个真实问题
4.1 服务启动失败,30秒内定位原因
有一次服务更新后启动失败,表现是systemctl status显示active (exited),进程起来了立刻退出。我没有在服务脚本里漫无目的地找原因,直接查journald里这个unit的最近日志:
bash复制journalctl -u myapp.service -n 30 --no-pager
日志里显示:
bash复制myapp.service: Failed to bind to 0.0.0.0:8080, address already in use
问题明摆着,8080端口被另一个进程占用了。排查到确认原因全程不到一分钟。如果没有journald,我可能还得先找到日志文件路径,再看是不是写到了别的地方。
这里有个经验:很多服务写启动脚本时,会把报错信息输出到stderr,如果服务没有把stderr重定向到文件,这些关键报错在传统日志体系下是保存不下来的。但systemd会收集stderr,所以即便服务自己没有日志文件,只要是被systemd拉起的,journalctl里基本都有记录。
4.2 WSL 环境中 systemd 用户会话启动失败
现在越来越多人在Windows上用WSL跑Linux环境。新版WSL支持systemd之后,偶尔会遇到一个提示,大意是“wsl: failed to start the systemd user session for 'root'. see journalctl for details”。这个报错刚出现时很让人摸不着头脑,其实排查方式和普通Linux完全一样。
先确认当前systemd是否在运行:
bash复制systemctl is-system-running
如果看到类似degraded或failed的状态,再通过journalctl查用户会话相关日志:
bash复制journalctl --user -b --no-pager | grep -i session
或者查systemd-logind的日志:
bash复制journalctl -u systemd-logind -b --no-pager
一般这类问题在WSL里常见原因有两个。一个是WSL的配置没有开启systemd支持,需要在/etc/wsl.conf里加上[boot]和systemd=true,然后退出WSL重新执行wsl --shutdown再进来。另一个是用户会话依赖的dbus或者特定服务没起来,journald里能看到具体的失败原因。
这里有个值得说的点:同样是systemd环境,无论你是跑在物理机、虚拟机还是WSL里,排查思路完全一致。先看systemd是否正常,再看具体服务或会话日志,最后根据错误信息去解决,不要一看到WSL就觉得复杂度翻倍,本质上它就是一套systemd。
4.3 日志占满磁盘,先止损再根治
有一台主机跑了很多容器,某天监控报警磁盘使用率超过90%。运维同事先去找docker日志目录,用du查了一圈,最后发现真正的大头在/var/log/journal,占了十几个G。
我当时直接做了两步。先止损,把日志空间立刻压缩:
bash复制journalctl --vacuum-size=500M
再查为什么journald会有这么多日志。排查方向是通过日志内容确认是哪个服务在疯狂输出:
bash复制journalctl --since "1 hour ago" | awk '{print $5}' | sort | uniq -c | sort -nr | head
结果发现是一个容器的stdout日志在刷屏,因为容器跑了日志采集脚本,每秒钟输出一条心跳信息。根本解决办法是关掉这个容器不必要的日志输出,或者在应用层调整日志级别。这个问题的教训是:journald默认的10%磁盘用量其实很保守,但如果某个服务一直刷屏,这10%也会变成灾难,尤其是根分区只有几十G的小机器。
我后来给这类机器统一配置了SystemMaxUse=500M,并设置日志级别过滤,磁盘告警再也没因为日志出现过。
4.4 时间跳变导致日志错乱
有一段时间我在排查一个定时任务的运行情况,发现journalctl的输出里时间顺序是乱的,一条两小时前的日志排在当前日志后面。后来确认是系统时间被NTP校正过,时钟往前拨导致了时间戳倒挂。
这种情况在虚拟机、云主机上偶尔会出现,尤其是时间同步配置不完善、宿主机时间偏差大的时候。journald对时间很敏感,它会用系统时间作为日志时间戳,时间跳变会直接影响日志排序和查询。
排查时可以用_REALTIME_TIMESTAMP字段看真实采集时间,用_SOURCE_REALTIME_TIMESTAMP看应用上报时间,区分是应用打日志时间有问题还是journald采集时间有问题:
bash复制journalctl -o verbose | grep -E "_REALTIME_TIMESTAMP|_SOURCE_REALTIME_TIMESTAMP|MESSAGE" | head -20
如果确实是NTP问题,优先保证chrony或ntpd服务正常,同时确认时区配置一致。日志时间错乱这类问题,发现越早越容易解决,拖久了复盘根因会很痛苦。
5. 进阶技巧与避坑清单
5.1 字段过滤与组合过滤
除了常规的-u、-p、--since过滤,journalctl最强大的功能是直接按字段过滤。前面提到的下划线字段都可以作为过滤条件。
比如查某个进程的所有日志:
bash复制journalctl _PID=12345
查某个可执行文件的所有日志:
bash复制journalctl _COMM=nginx
查某个用户在特定时间段内的操作日志:
bash复制journalctl _UID=1000 --since "yesterday" --until "today"
多个条件组合也支持:
bash复制journalctl _SYSTEMD_UNIT=docker.service _PID=23456 -p err --since "1 hour ago"
组合条件越多,定位越精确。我排查跨服务问题时,经常先把所有相关服务的日志导出来合并看时间线:
bash复制journalctl -u nginx.service -u backend.service --since "today" | less
多个-u会把两个unit的日志按时间合并输出,看它们之间的先后关系特别方便。
5.2 输出格式与数据导出
journalctl默认输出接近传统syslog风格。但如果要分析数据,比如喂给脚本处理,可以用结构化格式。
bash复制journalctl -o json-pretty
journalctl -o json --since "1 hour ago" > /tmp/log.json
-o json输出每个字段的原始键值对,适合用jq或者其他日志分析工具处理。调试格式问题的时候,我常用的是-o short-iso,时间戳带ISO标准格式,和日志平台对接时不容易因为格式产生歧义。
日志导出方面,journald支持远程转发,把日志发送到中央日志服务器。有两种思路:一种是通过journald自身的转发协议,配合systemd-journal-remote接收;另一种是导出为文本流,喂给rsyslog或filebeat,再汇聚到Elasticsearch这类平台。中小规模环境用文本流对接比较省事,大规模日志采集再考虑原生协议。
5.3 需要避开的几个坑
第一,不要在脚本里直接journalctl不分页。输出量大时可能把内存打满,或者被管道阻塞。脚本里统一加上--no-pager,必要时加-n限制行数。
第二,老系统没有journalctl。journalctl属于systemd工具集,只有使用systemd作为init的系统才能用。CentOS 6及更早、某些精简容器镜像里没有这个命令,那时候还是得回退到/var/log/messages或者/var/log/syslog。
第三,容器内查看宿主日志需要特殊挂载。如果你在docker容器里直接跑journalctl,大概率会提示无法连接journald,因为容器默认没有挂载宿主机的日志套接字。要监控宿主机日志,应该通过日志采集agent从宿主机读取,不要在容器里折腾journactl。
第四,journalctl --verify如果你遇到journal文件损坏的问题,可以试一下。它会检查日志文件的一致性,并提示哪些文件有问题。注意这个命令在日志量大的时候可能跑很久,适合在维护窗口里执行。
5.4 man 手册是最后的靠山
journalctl的选项非常多,我平时常用的也就是其中一半左右。遇到记不清的写法,直接查man手册比搜索引擎靠谱,因为不同发行版的journald版本有差异,选项细节也可能不同。
bash复制man journalctl
查完选项再配合--help快速确认,一般不会出什么问题。
6. 一些实际使用体会
journalctl用了几年下来,最大的感受是它把日志排查这件事变得更简单了。以前看日志要面对一堆分散的文件,现在所有系统组件和服务的日志都能在一个地方查,而且可以按时间、按服务、按级别、按字段组合过滤,快速拼出问题发生的完整经过。
我个人的习惯是:每到一个不熟悉的系统,第一件事就是跑一遍journalctl -p warning --since "1 hour ago",看看有没有隐藏的异常;再跑一遍journalctl --disk-usage确认日志占用是否健康。这几条命令不费事,但能在问题变大之前给出信号。
如果你刚开始接触systemd和journalctl,不用急着记全所有参数。先把-u、-f、-p、--since、--until这几个用熟,日常排查就已经能覆盖绝大多数场景了。等遇到更复杂的问题,再回来翻man手册,或者从这篇笔记里找找思路,很快就能上手。
