日志排查:journalctl 与 grep 的组合拳

举报
yd_213781742 发表于 2026/11/04 09:57:36 2026/11/04
【摘要】 线上 502 排查时 grep -r /var/log 翻不动?先讲清 journal 为什么是带索引的数据库,再给一套固定打法:用 --since/-b/-p 和结构化字段把范围收死,再用 grep 命中关键词、sed 聚合分布,附六步顺序与八个常见坑。

一、现象:出事时日志"翻不动"

线上接口报 502,登录机器,第一反应是 grep -r "error" /var/log。屏幕刷了十几秒,出来几千行,大部分是健康检查和旧记录;有用的几行夹在中间,往上翻不到请求 ID,往下翻不到堆栈结尾。换 tail -f 业务日志,又发现错误只打到了标准错误,被 systemd 收进了 journal,文件里根本没有。

这类场景通常不是"日志没打",而是检索方式不对。日志有两个来源:journald 管的二进制 journal,和 rsyslog 或应用写的文本文件。前者有结构化字段和索引,后者只能线性扫描。混用 grep -r 硬扫,等于放弃索引和时间窗,只能靠运气。

下面把 journalctl 的范围收缩能力和 grep 的模式匹配能力拼成一套固定打法。

二、原理:journal 是数据库,grep 是手术刀

journal 不是文本文件。 /var/log/journal 下是一组二进制文件,journald 收到日志时会给每条记录打上一组字段:_PID、_UID、_COMM、_SYSTEMD_UNIT、PRIORITY、_HOSTNAME、__REALTIME_TIMESTAMP、MESSAGE。它按 unit、时间、优先级建索引,所以 journalctl -u nginx --since "10 min ago" 是索引查询,不需要从头扫到尾。

journalctl 负责"缩小",grep 负责"命中"。 journalctl 的过滤维度是离散的:哪个 unit、哪次启动、哪个时间段、哪个优先级、哪个字段等于什么。grep 的能力是连续的:正则、大小写、上下文、反向匹配、计数。两者职责不重叠,串起来才有效,而且顺序不能反:先 journalctl ... | grep ...,候选集通常能从百万行降到几百行;反过来先 grep -r /var/log 再人工筛时间,等于用最贵的方式做最便宜的事。

文本副本会丢字段。 很多发行版同时跑 rsyslog,把 journal 转存成 /var/log/syslog 或 messages,只剩时间、主机、进程名和消息,_PID、_SYSTEMD_UNIT 全没了,也没法按 boot 切分。所以排障优先查 journal,只有 journal 被清理或容器没接 journald 时才退回文本。

三、分步操作

3.1 先圈定 unit 和时间窗

不要一上来就全量查。先确认服务名,再给出足够小的时间窗。

systemctl list-units --type=service --state=running --no-pager | grep -E "nginx|app|api"
systemctl list-units --state=failed --no-pager
journalctl -u app.service --since "30 min ago" --no-pager | tail -n 80

--since 接受自然语言:today、yesterday、"2 hours ago"、"2026-11-04 09:00:00"。时间窗越小,后面 grep 越快。

3.2 用 -b 区分"这次启动"和"上次启动"

服务崩溃后如果重启过,当前 boot 只剩重启之后的部分,现场往往在上一次启动里。

journalctl --list-boots --no-pager
journalctl -b -1 -u app.service --no-pager | tail -n 120
journalctl -b -1 -p err --no-pager

-b 0 是本次启动,-b -1 是上一次;--list-boots 列出每次启动的时间范围和 boot ID。

3.3 用结构化字段精确过滤

知道 PID、进程名或 syslog tag 时,直接按字段查,比 grep 消息内容可靠得多。

journalctl _PID=18422 --no-pager
journalctl _COMM=java --since "1 hour ago" --no-pager | tail -n 50
journalctl -t sshd --since today --no-pager | tail -n 30
journalctl -u app.service -o json-pretty -n 1

-t 匹配 syslog identifier,适合 sshd、cron 这类服务;-o json-pretty 把一条记录的字段摊开,能用哪些一目了然。

3.4 选对输出格式,grep 才干净

默认 short 格式每行都带主机名和进程名,正则容易被前缀干扰。按目的选格式:

journalctl -u app.service --since "10 min ago" -o short-iso --no-pager | tail -n 20
journalctl -u app.service --since "10 min ago" -o cat --no-pager | grep -n "ERROR"
journalctl -u app.service --since "10 min ago" -o json --no-pager | head -n 2

short-iso 带可排序的 ISO 时间和时区偏移;cat 只留消息体,适合喂给 grep 和 awk;json 交给 jq 做字段级筛选。注意 -o cat 之后没有时间信息。

3.5 grep 的四种打法

范围定好之后,grep 负责最后一击:

journalctl -u app.service --since "1 hour ago" --no-pager | grep -iE "timeout|refused|reset by peer"
journalctl -u app.service --since today --no-pager | grep -E "HTTP/[0-9.]+ (4|5)[0-9]{2}"
journalctl -u app.service --since today --no-pager | grep -B 3 -A 15 "Traceback"
journalctl -u app.service --since today --no-pager | grep -v "healthcheck"

第一条忽略大小写做多关键词匹配;第二条用正则捞状态码;第三条带上下文,堆栈一起出来;第四条反向过滤,去掉健康检查噪音。-B/-A 的数字别开太大,否则等于又把日志全打一遍。journalctl 自带的 -g 可在内部匹配,省一次管道,默认区分大小写:

journalctl -u app.service --since today -g "timeout|refused" --no-pager

3.6 聚合统计:从"看到"到"看懂"

几千行错误里往往只有三五种真问题,把数字抹掉再计数就能看出分布。

journalctl -u app.service --since today -o cat --no-pager \
  | grep -i "error" \
  | sed -E 's/[0-9]+/N/g' \
  | sort | uniq -c | sort -rn | head -n 20

sed 把数字替换成 N,避免每条日志因耗时、端口不同被当成不同模式。按小时看趋势可以换成 short-iso 再 cut -c1-13,同样交给 sort | uniq -c。

3.7 追一条请求:跨 unit 串联

一个请求经过网关、应用、数据库代理,日志分散在不同 unit。只要业务日志带了 trace id,就能去掉 unit 限制做全局搜索。

journalctl --since "15 min ago" -o cat --no-pager | grep -F "trace_id=7f3a9c"
journalctl --since "15 min ago" -o short-iso --no-pager | grep -F "trace_id=7f3a9c" | sort

-F 表示固定字符串匹配,trace id 里带 . 或 [] 时不会被当成正则元字符。第二句加 sort,多 unit 混合输出时顺序不严格,按 ISO 时间排序才能还原调用链。

3.8 实时盯盘

复现问题时要边操作边看日志,但直接 -f 会被无关输出淹没。

journalctl -u app.service -f
journalctl -u app.service -f | grep --line-buffered -iE "error|panic|oom"

--line-buffered 是关键:grep 默认按块缓冲,输出到管道时可能攒够 4KB 才吐一次,表现为"报错了却半天不显示"。

3.9 日志不在 journal 里怎么办

容器默认用 json-file 驱动,日志在 /var/lib/docker/containers/ 下,只有配置 --log-driver=journald 才进 journal;传统应用也常自己写文件。这时先压缩范围再匹配:

docker ps --format '{{.Names}}\t{{.ID}}' | grep api
docker logs --since 30m --tail 500 api-gateway 2>&1 | grep -iE "error|panic"
tail -n 5000 /var/log/nginx/error.log | grep -n "upstream timed out"
zgrep -h "panic" /var/log/syslog*.gz | tail -n 20
grep -rn --include="*.log" -E "OutOfMemory|Killed process" /var/log/ 2>/dev/null | head -n 30

tail -n 5000 先截断再 grep,比直接扫大文件快一个数量级;zgrep 直接读轮转后的 .gz;--include 避免扫到二进制。

3.10 固化成脚本

同样的组合每天用,就该写成脚本,免得现场漏参数。

#!/usr/bin/env bash
set -euo pipefail
UNIT="${1:?usage: $0 <unit> [minutes]}"
MIN="${2:-30}"
journalctl -u "$UNIT" --since "$MIN min ago" -o short-iso --no-pager \
  | grep -Ei "error|fail|timeout|refused|panic|oom" \
  | tail -n 100

set -euo pipefail 不能省。grep 没匹配到会返回 1,在 pipefail 下会让整个管道失败,后面还有逻辑时要写成 || true。想验证日志链路,可以先 echo "test" | systemd-cat -t demo -p err,再 journalctl -t demo -n 5 查一下。

四、常见坑

忘了 --no-pager。 在脚本或非交互环境里,分页器可能吞掉输出或让命令挂住,表现为"命令跑了却没返回"。

时区没对齐。 journalctl 按本机时区显示,服务器常是 UTC,业务日志打的是 +08:00,对着看会差 8 小时,误判成"日志缺失"。用 -o short-iso 看偏移,或先 timedatectl 确认时区。

grep 区分大小写。 Error、ERROR、error 是三种写法,漏一个就少一批,不确定就加 -i。

-f 配合 grep 看不到实时输出。 十有八九是缓冲,加 --line-buffered。

-p err 不是"只有 err"。 它表示 err 及更严重的级别(err、crit、alert、emerg),想看单一级别得自己按 PRIORITY 字段筛。

unit 名写错。 模板服务和实例名(如 app@1)最容易拼错,先 systemctl list-units | grep app 确认全名再查。

多行堆栈被拆散。 journald 按行接收,Java 异常的 at ... 每行都是独立记录,grep 只命中首行。用 -A 带上下文,或 -o cat 后交给 awk 合并。

journal 被 vacuum 掉才发现没配保留策略。 关键机器应显式设置 SystemMaxUse 和 MaxRetentionSec,并把重要日志转发到远端。

日志里有敏感信息。 token、手机号经常直接进日志,贴到工单或文章前先脱敏。

五、小结

排查顺序可以固定成六步:确认 unit 全名 → 用 --since 圈时间窗 → 用 -b 选对启动 → 用 -p 或字段过滤降噪 → 用 grep -iE 命中关键词 → 用 sed + sort + uniq 聚合看分布。跨服务的问题再加一步 trace id 全局搜索。

日常最常用的三条命令值得背下来:

journalctl -u <unit> --since "30 min ago" -p warning --no-pager | tail -n 100
journalctl -u <unit> --since today -o cat --no-pager | grep -iE "error|timeout|refused"
journalctl --since "15 min ago" -o short-iso --no-pager | grep -F "<trace_id>" | sort

journalctl 的价值在于"知道去哪找",grep 的价值在于"从找到的东西里挑出要的"。范围收缩在前、模式匹配在后,日志排查就从体力活变成按图索骥。要避免的只有一件事:不加时间窗和 unit 限制,直接对整个 /var/log 递归 grep。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。