☰
日志排查四件套:grep、tail、sed、awk管道组合拳
2026/10/5 3:20:53 网站建设 项目流程

上周一我正盯着监控面板看指标,隔壁工位的同事已经对着日志刷了十几分钟,眉头越皱越紧。他用的方案是全文编辑器直接打开日志文件,文件刚加载完就卡了一下,然后拖到最底部,一边滚屏一边找报错。旁边还挂着三四个tail -f窗口,满屏日志不断往上翻,他偶尔眼尖瞄到一行红色报错,就复制出来再去别处对比。

我实在看不过去,凑过去问了一句:你这查的是哪个问题?他说接口偶发超时,想看 10 点 15 分前后那一段日志里到底发生了什么。我说你先停一下,awk、tail、grep、sed四件套组合起来,三分钟能给你拉到那个时间窗,顺手把错误类型和出现频次都统计出来。

这一篇就把当时教他的思路完整写出来。适合那些日志查得慢、每个命令都会一点但始终串不起来的同学。核心不是背参数,而是理解这几个工具在日志分析里各管哪一环,再把它们用管道串成流水线。

1. 那次现场:同事到底慢在哪里

先说清楚他为什么慢。一个中等级别的业务服务,一天的日志量轻松到几十万行,接口调用频繁时单小时日志就是好几万行。他那天的日志文件大约 400MB,文本编辑器打开要好几秒,滚动定位基本靠眼睛搜,肉眼在满屏日志里找一条偶发异常,效率约等于在一个房间里找一根针,还是没开灯的那种。

他也不是没用命令。中间他试过grep ERROR app.log,结果一次性拖出几万行,直接刷屏刷到文件末尾,反而更没法看。他也试过tail -f盯实时输出,但偶发问题半小时才出现一次,盯得眼睛都快花了。这些动作听起来都“用了命令”,实际上全踩在一个共同的坑上:没有缩小范围,就急着去看内容。

日志排查不应该是一上来就“读日志”,而应该是一层层“切日志”。切的意思,是先确定三个问题:

  • 这个问题出现在哪个时间窗,精确到分钟甚至秒。
  • 这个问题有什么特征关键字,比如报错码、traceId、接口名。
  • 这个特征在整体日志里占多大比例,是偶发还是持续。

明确了这三件事,再选择对应的命令去执行。他那天的场景是“10 点 15 分前后接口偶发超时”,那第一刀就应该切时间窗,第二刀按超时关键字过滤,第三刀看上下文,根本不需要全文滚动。

这种思路上的转变,比记住十条命令参数都管用。命令只是工具,思路才是组合拳的灵魂。下面我把每一件工具的“本职”先讲清楚,再讲怎么串。

2. 先拆单兵武器:grep、tail、sed、awk 各自最该会的用法

工具箱里的四件套,各有明确分工。我用一句话概括:grep 负责筛、tail 负责跟、sed 负责切、awk 负责算。理解这个分工,后面组合时就不容易乱。

2.1 grep:按条件筛行,而不是刷屏

grep是日志排查里用得最多的命令。它的核心能力是对输入逐行匹配,只输出符合条件的行。但很多人只用了最基础的grep 关键字,这远远不够。

常用的几个参数我整理了如下,全部用日志场景举例:

# 基础搜索,输出匹配行 grep "ERROR" app.log # 正则扩展匹配,适合多关键字或复杂模式,-E 强烈建议养成习惯 grep -E "ERROR|Exception|timeout" app.log # 反向过滤,排除噪音行 grep -v "healthcheck" app.log # 只输出匹配到的部分,而不是整行,配合排序统计非常好用 grep -oE "耗时[0-9]+ms" app.log # 输出匹配行数量,不刷屏 grep -c "ERROR" app.log # 忽略大小写 grep -i "error" app.log # 带上上下文,还原现场 grep -B 5 -A 10 "NullPointerException" app.log

这里重点说两个容易被忽略但实战价值极高的参数。

一个是-o,只输出匹配的部分。比如日志里每行末尾都有“耗时 230ms”,你想看耗时分布,用grep -oE "耗时[0-9]+ms" app.log | sort | uniq -c | sort -rn就能列出不同耗时档位的出现次数。如果不用-o,整行刷出来,同样没法做聚合分析。

另一个是-A/-B/-C,显示匹配行的上下文。排查异常时,报错本身往往只是一行,真正的原因在报错之前的调用链或之后的堆栈里。grep -B 5 -A 10直接把案发现场的前因后果一起拉出来,省掉一次手动来回翻。

还要注意一个细节:grep不是只能查文件,它还能接收其他命令的管道输出。比如排查端口问题,ss -tulnp | grep ':3690'比先记 PID 再到处翻方便得多。这种“命令输出二次过滤”的组合,是组合拳的基础形态。

2.2 tail:读尾部加跟踪,而不是全文打开

日志文件有个特点:报错总是产生在最新的尾部。tail的存在意义就是把“看最新内容”这件事做到极致。

# 查看最后 100 行 tail -n 100 app.log # 跟随文件新增内容,实时滚动 tail -f app.log # 跟踪文件,且文件被轮转或重建后能自动重新打开 tail -F app.log # 管道组合,实时过滤 tail -F app.log | grep --line-buffered "ERROR"

绝大部分人会混淆-f和-F。-f是跟随当前打开的文件描述符,-F则会检测文件是否被重命名或重建。日志系统基本都有轮转策略,比如日志到了 500MB 就自动切分成app.log.1、重新生成新的app.log。如果用的是tail -f,轮转发生之后你看到的还是旧文件,新日志全部错过,现场排查反而查了个寂寞。生产环境我基本只会用tail -F。

2.3 sed:按位置或模式切窗,精准截取日志段

sed是流编辑器,能对文本做按行处理。在日志分析里,它最锋利的用法不是替换文本,而是按行号或者按模式范围截取片段。

# 打印第 100 到 200 行 sed -n '100,200p' app.log # 打印从“开始时间”到“结束时间”之间的所有行 sed -n '/2025-03-01 10:15:00/,/2025-03-01 10:16:00/p' app.log

第二行命令就是时间窗切片的经典写法。日志如果每行带了标准时间前缀,sed能够按起止模式精准抽出一个时间段内的所有记录。这比用grep先搜开始时间、再手工数到结束时间高效得多。

sed的-n参数配合p是打印模式,默认不会输出全部内容,只打印匹配范围。如果不加-n,sed会把每一行都过一遍,匹配到的行重复打印,输出会乱套。

2.4 awk:对字段做计算,让日志开口说话

当日志量到了“人眼看不完”的级别,就得让数据自己汇报。awk的强项是按列拆分、条件过滤、分组聚合。它的基本结构是模式 { 动作 },逐行读入,默认按空白分隔字段,$1是第一列,$NF是最后一列。

# 打印每一行的第一列 awk '{print $1}' app.log # 把最后一列(比如耗时)拼在整行前面,方便排序 awk '{print $NF, $0}' app.log | sort -rn | head # 统计日志级别出现次数 awk '{print $3}' app.log | sort | uniq -c | sort -rn

注意awk默认按空格或 Tab 分隔,如果日志字段不是空白分隔,要用-F指定,比如awk -F '|' '{print $2}'。后面第四章专门讲它的统计用法,这里先记住“awk 是计算器”这个定位。

3. 组合拳第一层:时间窗、关键字、上下文三板斧

工具分工清楚了,接下来就是怎么串。组合拳的第一层,解决“偶发异常不好定位”的问题,核心是三个动作:先切时间窗、再筛关键字、最后看上下文。这三个动作分别对应sed、grep、grep -B/-A,用管道连接起来就是一条一次性出结果的命令。

我当时给同事演示的场景是:10 点 15 分前后,接口偶发超时。我让他先别急着看内容,按下面的步骤打了一套。

第一步,先估算问题量和分布。不要上来就拖全部错误,先看统计:

grep -c "timeout" app.log

这一步回答“超时问题出现了多少次”。如果只有个位数,那是偶发中的偶发,排查方式要围绕单条 traceId 展开;如果成千上万,那是大规模故障,可能要直接看硬件或依赖服务的健康状态,而不是逐行看日志。

第二步,用时间窗截取 10 点 15 分前后那一段。假设业务日志每一行都带了标准时间前缀:

sed -n '/2025-03-01 10:14:30/,/2025-03-01 10:16:30/p' app.log > window.log

把两分钟时间窗的内容落成一个临时文件,后面的所有操作都基于这个窗口文件,而不是整个 400MB 大文件。这一步直接决定了后面命令跑得快不快。

第三步,在窗口文件里按关键字过滤,并顺带看上下文:

grep -B 5 -A 10 "timeout" window.log | head -n 100

-B 5是显示匹配行之前 5 行,-A 10是之后 10 行,head -n 100是防止输出太多又一次刷屏。这样出来的内容,既限定在时间窗内,又限定在超时相关的调用链上,基本能看清一次超时前后发生了什么。

我还额外给他演示了一个更彻底的组合命令,一步到位:

sed -n '/2025-03-01 10:14:30/,/2025-03-01 10:16:30/p' app.log | grep -B 3 -A 8 "timeout" | head -n 200

这条命令的含义是:切出两分钟日志 → 筛出超时相关行 → 带出前后上下文 → 只取前 200 行。每一层管道都在减少噪音,最终输出的内容量级从几十万行降到几十行,人眼完全能看懂。

同事当时看到这条命令的执行结果,愣了一下,说“这就出来了?”我说对,这还只是第一层,下一层用awk把数据变成统计,你能获得更多信息。管道设计的核心原则是:每进一层管道,噪音就少一点;最后剩下的内容应该是能直接回答你问题的。如果管道不断加长输出却越来越多,那就是设计有问题。

还有一招实时场景下特别管用。线上正在出问题时,不要干巴巴盯tail -F,加上过滤:

tail -F app.log | grep --line-buffered -E "ERROR|timeout"

这里--line-buffered很关键。grep默认会等缓冲区满了再输出,配合tail -f实时流时,日志会一卡一卡地蹦出来,像延迟很高一样。加上--line-buffered后,每匹配到一行就立刻输出,实时性才有保证。这个参数不在man手册的显眼位置,但实战差它一个天上一个地下。

4. 组合拳第二层:awk 把日志从“看”变成“算”

第一层三板斧解决“找到异常现场”的问题,第二层解决“搞清楚异常长什么样”的问题。一个典型的场景是:拿到一坨日志,你不能只判断“有报错”,还得知道 5xx 状态占了多少、响应时间最慢的是哪几个接口、错误集中在哪个 IP 或哪个时间分钟。这些都是awk的活。

4.1 先看日志结构,再定字段策略

在没有日志格式文档的情况下,我拿到日志的第一个动作永远是head -n 3看一眼结构,确认时间在哪一列、关键字段在哪一列。很多初学者跳过这一步,直接猜$1、$2,结果输出的全是错的东西。

head -n 3 app.log

比如我常处理的访问日志格式大概是:

2025-03-01 10:15:32 [INFO] GET /api/order 200 356ms user_id=1001 2025-03-01 10:15:33 [WARN] POST /api/pay 502 1023ms user_id=1002 2025-03-01 10:15:35 [ERROR] GET /api/refund timeout 4025ms user_id=1003

按默认空白分隔,$1是日期,$2是时间,$3是级别,$4是方法,$5是路径,$6是状态码,$7是耗时,$8是关键字。如果分隔符不是空白,比如用逗号或竖线,就加-F指定。

4.2 统计状态码和日志级别分布

awk最舒服的用途是分组计数。统计状态码分布:

awk '{count[$6]++} END {for (s in count) print count[s], s}' app.log | sort -rn

这条命令把第六列按状态码分组,计数,最后在END块里遍历输出。比awk '{print $6}' | sort | uniq -c少了一次管道,而且文件越大,单进程内部聚合的优势越明显。对大日志做多次管道遍历,每一次都要重新读磁盘,慢得很。能用一次awk算完的,坚决不搞两条管道。

统计日志级别分布同理:

awk '{count[$3]++} END {for (s in count) print count[s], s}' app.log | sort -rn

4.3 找最慢的请求:注意排序的字典序陷阱

找最慢的请求需要把耗时字段提取出来排序,但这里有个坑:sort默认按字典序排序,不是按数值。比如1000ms会排在200ms前面,因为字符串比较时1小于2。很多人第一次用就中招。

正确做法是加-n或者-h。-n按数值比较,-h能识别人类可读的单位(比如 1KB、2MB)。日志里耗时通常只有毫秒数字,用-n就够了:

awk '{print $7, $0}' app.log | sort -rn | head -n 10

这条命令把耗时拼在整行前面,sort -rn按数值倒序,head -n 10取最慢的前 10 行。输出的每一行仍然带着完整日志内容,一眼能看到最慢的请求是哪个接口、什么状态、什么时间。

4.4 按时间聚合,看错误是否集中

偶发问题最容易出现的情况是:错误集中出现在某一小段时间,而不是均匀分布。用awk按分钟聚合,马上能看出规律。方法是将时间字段截取到分钟级,再分组计数。

awk '{minute=substr($2,1,5); count[minute]++} END {for (t in count) print count[t], t}' app.log | sort -k2

substr($2,1,5)取时间字段的前 5 个字符,比如10:15:32变成10:15,再按这个分钟值分组计数。输出结果类似:

23 10:14 156 10:15 42 10:16

看到 10:15 这一分钟异常数量暴涨,跟问题时间窗完全重合,基本能确定问题就在这一分钟内爆发。接下来就可以回到第三章的时间窗切段命令,把 10:15 那一分钟单独切出来,逐行分析。

4.5 条件组合:按级别和状态双重过滤

awk也可以像grep一样做条件过滤,而且更精确。比如只看 10 点到 11 点之间的 5xx 错误:

awk '$2 ~ /^10:/ && $6 ~ /^5/ {print $2, $5, $6, $7}' app.log | head

条件里用到了两个模式匹配。$2 ~ /^10:/表示第二列以10:开头,$6 ~ /^5/表示第六列以 5 开头,也就是 5xx 状态码。两个条件AND连接,过滤完成后只输出需要的列。这样日志瞬间变成了一张结构化的小表格,比看原始日志行效率高一个量级。

再说一个进阶小技巧。有时候需要的不是单条数据,而是平均响应时间。同样是awk一行搞完:

awk '/timeout/ {sum+=$7; count++} END {print "avg:", sum/count}' app.log

求和再求平均值,直接在END块里输出。这类“统计一步到位”的写法,是awk相对其他命令组合的最大优势。

5. 最容易被反咬一口的坑:日志轮转、缓冲区与编码

组合拳用熟了之后,真正影响成败的往往不是主体命令,而是细节。这里把实战中踩过、也看到别人踩过的几个坑集中列一下。

第一个坑是tail -f和tail -F的区别。前面在第二章提过,但要单独拿出来说一遍,因为几乎所有初学的人都在生产环境被它坑过。日志轮转的常见动作是:把当前app.log重命名为app.log.1,再新建/app.log。此时tail -f app.log追踪的还是旧文件描述符,文件实际还在写app.log.1,新日志进入app.log但你看不到。用tail -F后,命令会周期性地检查文件是否被替换,发现新文件后自动重新打开继续跟踪。在排查夜间日志问题时,如果用了-f,天亮一看屏幕还停在昨天下午的日志上,心态直接崩掉。

第二个坑是管道缓冲区。tail -F app.log | grep ERROR这个组合如果不加--line-buffered,grep会按块缓冲输出的内容。什么意思?就是grep要把匹配结果攒到一定量才吐出来。配上tail -f的实时流场景,你看到的报错会延迟好几秒钟才出现,误以为系统没在写日志,实际上是grep憋着没放。实时查问题务必加上--line-buffered。如果管道里再接awk,同理也可以加fflush()强制刷新,不过实操中更简单的是把awk放在grep之后做统计,而不是做一个一个输出的动作。

第三个坑是编码问题。大部分服务日志是 UTF-8,grep直接匹配没问题。但有些遗留系统的日志是 GBK 编码,grep "中文错误"匹配不到是小事,更麻烦的是乱码会让awk的字段切分错位。遇到这种情况,先用file app.log确认编码,必要时用iconv -f GBK -t UTF-8 app.log转码后再进管道。注意大文件转码会消耗内存和时间,建议先切时间窗再转码,别一上来全套转。

第四个坑是权限和重定向。使用sudo查日志时,管道往右传是没问题的,但如果你想把结果保存下来,直接sudo grep xxx app.log > result.txt时,result.txt的写入操作是以你当前用户身份执行的,不是 root。如果当前目录没有写权限,命令会报错。这时候用sudo tee接收输出:

sudo grep -E "ERROR|timeout" app.log | tee result.txt | wc -l

tee把结果同时输出到文件和屏幕,再往后接其他统计命令,是排查现场保存证据的常用姿势。

第五个坑是cat的无意义使用。cat app.log | grep xxx是典型的画蛇添足,grep本身就能直接读文件,不需要cat先输出一遍。特别是在大日志场景,cat会完整读一遍文件再交给grep读第二遍,白白增加一次磁盘 IO 和时间。直接grep xxx app.log才是标准动作。想看文件内容同样不要cat,用less可以分页且不把整个文件吐进终端,终端渲染大量文本比磁盘 IO 还慢。

6. 从五千行毛刺到三行根因:一次完整排错链路复盘

前面几章是方法论,这一章用一次完整的排错过程把组合拳串联起来。当时的情况是:线上接口偶发超时,监控面板有告警,但没有任何错误堆栈信息。同事手里有三天日志,压缩包解开后单文件约 800MB,里面有问题的关键字只判定了“timeout”。

我带着他按下面的链路一步步缩小范围。

第一步,全量估算。直接对 800MB 做一次计数,看问题严重性:

grep -c "timeout" app.log

结果 4862。不算特别多,但没有小到可以直接逐行看。

第二步,切时间窗。监控显示问题集中在 10 点到 11 点之间,先把这个小时切出来:

sed -n '/2025-03-01 10:00:00/,/2025-03-01 10:59:59/p' app.log > window-hour.log

window-hour.log大约 120MB,可操作性好很多。

第三步,按分钟聚合,看错误分布是否均匀。这一步用的是第四章的substr聚合:

awk '{minute=substr($2,1,5); count[minute]++} END {for (t in count) print count[t], t}' window-hour.log | sort -k2

结果出来之后,发现 10:15 到 10:16 这一分钟里 timeout 数量达到 3000 多条,其余分钟只有零星几条 200。问题不是均匀分布,而是集中在 10:15 这一分钟爆发。这就把排查范围从一小时缩小到一分钟。

第四步,切出一分钟的日志,按耗时排序取最重的请求:

sed -n '/2025-03-01 10:15:00/,/2025-03-01 10:15:59/p' window-hour.log | grep "timeout" > window-min.log awk '{print $7, $0}' window-min.log | sort -rn | head -n 20

head -n 20出来后,发现超时请求的路径全部指向同一个下游服务地址,且全部卡在等待响应的阶段,没有进入业务逻辑。看到这里,基本可以判断问题不在这台机器内部,而在下游依赖。

第五步,用上下文还原关键一行前后的细节,确认是否有异常堆栈或重试标记:

grep -B 3 -A 10 "timeout" window-min.log | grep -E "DB|redis|connection|socket" | head -n 50

过滤出来的内容显示大量“connection pool exhausted”和“waiting for connection”。到这里,根因已经很明确:数据库连接池被打满,请求在获取连接时排队超时。从 800MB 原始日志到这一结论,全程用到的命令不超过五条,耗时两分多钟。

复盘一下这套链路:先计数 → 再切窗口 → 再聚合 → 再排序 → 最后看上下文。每一步都在缩小数据量,每一步的输出都直接决定下一步的参数。这种排查方法不依赖经验玄学,而是完全可复现的流程化操作。

7. 适时收手:组合拳解决不了的事,交给采集管道

组合拳适合单机日志的快速定位,但它有明确的边界。当你面对 10GB 级别的单文件,或者日志分散在几十台机器上,再靠grep加管道硬扫就不现实了。前者是时间问题,磁盘 IO 和重复遍历会让每条命令跑上几分钟;后者是结构问题,你不可能一台台ssh上去执行一遍同样的命令。

这时候应该把重心从“查日志”转移到“管日志”上。生产环境比较合理的做法是:日志统一落盘并按天/按大小轮转,然后由采集代理(比如 filebeat)把日志实时发送到集中检索平台,长期趋势和全文检索交给专业的日志系统来做。grep组合拳的价值区间,是处理“已有日志文件、需要快速定位某一次具体问题”的临时诊断场景。它应该是最后一道防线,而不是日常唯一的手段。

即便上了集中采集,日志格式的规范化也值得做。一方面,统一格式让时间字段、状态码、耗时字段位置固定,awk解析起来才不容易出错;另一方面,埋好 traceId 能把一次请求跨服务串起来,排查问题时直接用 traceId 做关键字过滤,比切时间窗定位准确得多。我见过太多团队日志格式五花八门,排查问题时每个人都要先花十分钟猜字段位置,时间全耗在解析格式上。

再回头说那天教完同事之后的效果。他后来遇到类似问题,不再打开编辑器硬翻了,而是先grep -c估算、再sed -n切时间窗、再用awk做聚合。查一次日志从原来的十分钟变成了两分钟,更大的变化是,他拿到结果之后能够直接说清楚问题影响面和时间点,而不是含糊地描述“有一些报错”。

我个人用得最多的,其实不是某条具体命令,而是那套“先缩小范围,再统计特征,最后看上下文”的排查思路。命令参数忘了可以查man,思路错了再多的命令也只会越查越乱。如果你刚接触这套组合拳,建议先从这一条练起:下次再遇到日志分析任务,先别急着全文搜索,先问自己三个问题,时间窗是什么、关键字是什么、影响面有多大。想清楚再动手,你的第一条命令就该是grep -c或者sed -n,而不是编辑器里的 Ctrl+F。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询