十万行日志里捞线索:Linux grep/awk/sed 三剑客实战复盘

线上报错率突然升高,日志还没接入 ELK,手上只有一台终端和 15 万行 Nginx 日志。靠 grep 筛行、awk 取列做统计、sed 切片做编辑,再用管道串成流水线,一行行把根因捞了出来:报错集中在哪个接口、哪个 IP、哪个时间段,全靠三剑客这套硬功夫。

2024 年有一次线上接口报错率突然升高,我需要从一台服务器的 Nginx 访问日志里把问题摸清楚:哪个接口在报错、报的什么状态码、是哪些 IP、集中在哪个时间段、响应有多慢。这个日志文件有十几万行、好几百兆。当时我们的日志还没接入 ELK,手上只有一台终端。就在这种"裸奔"的环境里,我靠着 grep、awk、sed 这 Linux"三剑客",硬是把整件事的来龙去脉一行一行捞了出来。事后我把这套文本处理的套路认真整理了一遍——它是每个工程师都该有的硬功夫。本文用这次实战复盘把三剑客讲清楚。

问题背景

环境:CentOS 7,一台 Nginx 服务器
     日志文件 access.log,约 15 万行,420 MB
日志格式(典型的 Nginx combined 格式):
192.168.1.10 - - [14/May/2024:15:30:12 +0800] "GET /api/order HTTP/1.1" 200 1532 "-" "okhttp/3.14" 0.045

# 字段顺序:
# $1 客户端IP  $4 时间  $6 请求方法  $7 URL  $9 状态码
# $10 响应字节  $12 UserAgent  最后一列 0.045 是响应耗时(秒)

要查清的问题:
- 报错(5xx)的请求有多少,占比多大
- 报错集中在哪个接口
- 报错来自哪些 IP,是不是某个客户端在搞事
- 报错集中在哪个时间段
- 哪些接口响应最慢

手上的工具:只有一台终端,没有 ELK。
靠的就是 grep + awk + sed 三剑客。

修复 1:grep——先把目标行"捞"出来

# === grep 的定位:从海量文本里【筛选出匹配的行】 ===

# === 最基本:找出所有 5xx 报错的行 ===
$ grep ' 500 ' access.log
# 注意状态码两边带空格,避免匹配到 "5001" 之类

# 用正则匹配一整类:5 开头的三位状态码
$ grep -E '" 5[0-9]{2} ' access.log
# -E 启用扩展正则

# === grep 几个高频选项 ===
$ grep -c '" 5[0-9]{2} ' access.log     # -c 只输出匹配的【行数】
$ grep -n 'timeout' access.log          # -n 显示行号
$ grep -i 'error' access.log            # -i 忽略大小写
$ grep -v '200' access.log              # -v 反选:输出【不】匹配的行
$ grep -E '500|502|504' access.log      # 或的关系

# === 出错时,看看前后文(上下文)===
$ grep -A 3 -B 3 ' 502 ' access.log
# -A 3 匹配行后 3 行,-B 3 前 3 行,-C 3 是前后各 3 行
# 排错时这个特别有用,能看到出错请求前后发生了什么

# === 多文件 / 整个目录里搜 ===
$ grep -rn 'OutOfMemory' /var/log/
# -r 递归子目录,-n 带行号

# === 我们的第一步:报错总量和占比 ===
$ total=$(wc -l < access.log)
$ err=$(grep -cE '" 5[0-9]{2} ' access.log)
$ echo "总请求 $total,5xx 报错 $err"
# 总请求 152340,5xx 报错 3877
# 报错率约 2.5% —— 确实异常,平时不到 0.1%

修复 2:awk——按"列"切开,做统计

# === awk 的定位:把每行按【列】切开,做提取和统计 ===
# grep 管"挑出哪些行",awk 管"对这些行做什么"。

# === awk 最核心的概念:$1 $2 ... 是【列】 ===
# awk 默认按空格/制表符把一行切成若干字段,
# $1 是第 1 列,$2 第 2 列... $0 是整行,NF 是列数。

# === 例:只打印 IP 和状态码两列 ===
$ awk '{print $1, $9}' access.log

# === awk 的结构:'条件 { 动作 }' ===
# 例:只打印状态码是 500 的行的 IP
$ awk '$9 == 500 {print $1}' access.log
# $9==500 是条件,{print $1} 是动作

# === 杀手锏:用数组做"分组计数" ===
# 统计每个状态码各出现了多少次:
$ awk '{count[$9]++} END {for (c in count) print count[c], c}' \
    access.log | sort -rn
#  148001 200
#    3877 500
#     460 502
# count[$9]++ :以状态码为 key 在数组里累加
# END {...}   :所有行处理完后,统一输出
# 这就是 awk 最常用的套路:数组计数 + END 输出

# === 问题1:报错集中在哪个接口 ===
$ awk '$9 ~ /^5/ {print $7}' access.log | \
    sort | uniq -c | sort -rn | head -5
#  3201 /api/order/detail
#   410 /api/order/list
# $9 ~ /^5/ :状态码以 5 开头(~ 是正则匹配)
# 真相浮现:报错高度集中在 /api/order/detail

# === 问题2:报错来自哪些 IP ===
$ awk '$9 ~ /^5/ {print $1}' access.log | \
    sort | uniq -c | sort -rn | head -5
#  1980 10.0.2.55
#   210 10.0.2.56
# 近一半报错来自同一个 IP —— 值得重点查这个客户端

# === 问题4:哪些接口最慢(最后一列是耗时)===
# 算每个接口的平均响应耗时:
$ awk '{sum[$7]+=$NF; cnt[$7]++}
       END {for(u in sum) printf "%.3f %d %s\n", sum[u]/cnt[u], cnt[u], u}' \
    access.log | sort -rn | head -5
# $NF 是最后一个字段(耗时),sum 累加、cnt 计数,
# END 里算平均。printf 控制输出格式。

修复 3:awk 进阶——时间段与条件过滤

# === 问题3:报错集中在哪个时间段 ===
# 时间字段是 $4,形如 [14/May/2024:15:30:12
# 我们想按"小时"聚合,需要从中切出小时。

# === 用 awk 的 substr 截取子串 ===
$ awk '$9 ~ /^5/ {print substr($4, 14, 2)}' access.log | \
    sort | uniq -c
#   120 09
#  3100 15      <-- 15 点这一小时爆了
#   210 16
# substr($4, 14, 2):从 $4 第 14 个字符起取 2 个字符
# [14/May/2024:15:30  ->  第14位开始的 "15"
# 结论:报错几乎全挤在 15:00-16:00 这一小时

# === 组合条件:15 点 + /api/order/detail + 5xx ===
$ awk '$9 ~ /^5/ && $7 ~ /order\/detail/ && substr($4,14,2)=="15"' \
    access.log | wc -l
# && 是与,|| 是或,可以把多个条件叠起来精确锁定

# === awk 也能做范围筛选:找耗时超过 1 秒的慢请求 ===
$ awk '$NF > 1.0 {print $NF, $9, $7}' access.log | sort -rn | head
# 把响应耗时 > 1 秒的请求挑出来,按耗时倒序

# === BEGIN / END:开头和结尾各做一次的动作 ===
$ awk 'BEGIN{print "=== 慢请求清单 ==="}
       $NF > 1.0 {n++; print $7, $NF}
       END{print "共", n, "条"}' access.log
# BEGIN 在处理任何行之前执行(打表头、初始化)
# END   在处理完所有行之后执行(打汇总)

# === 指定分隔符:-F ===
# 如果日志不是空格分隔,比如 CSV:
$ awk -F',' '{print $3}' data.csv
# -F',' 告诉 awk 用逗号切列

修复 4:sed——按"行"做编辑和替换

# === sed 的定位:对文本做【流式编辑】——替换、删除、抽取 ===

# === 最常用:替换 s/旧/新/ ===
$ sed 's/HTTP\/1.1/HTTP\/2/g' access.log
# s/.../.../g  替换,g 表示一行里所有匹配都换(不加只换第一个)
# 注意:sed 默认只是把结果打到屏幕,【不改原文件】

# === 真正改文件要加 -i(务必先备份!)===
$ sed -i.bak 's/old/new/g' config.txt
# -i.bak:改原文件,同时把原文件备份成 config.txt.bak
# 直接 -i 不带后缀也行,但没备份,改错就回不去了

# === 按行号 / 范围抽取 ===
$ sed -n '100,200p' access.log     # 只打印第 100-200 行
# -n 取消默认输出,p 表示打印。组合起来 = 只看这一段
$ sed -n '50p' access.log          # 只看第 50 行

# === 按内容删除行 ===
$ sed '/health-check/d' access.log
# 把含 health-check 的行删掉(d = delete)
# 排查时常用它先剔除掉健康检查、监控探测之类的噪音行

# === 我们的实战用法:抽取关键时间窗口 ===
# 先定位到 15:00 的第一行和 16:00 的第一行行号,
$ grep -n '14/May/2024:15:00' access.log | head -1
98012:...
$ grep -n '14/May/2024:16:00' access.log | head -1
131890:...
# 再用 sed 把这一段精确切出来单独分析:
$ sed -n '98012,131890p' access.log > window.log
# 这样后续所有分析都只针对故障那一小时,又快又聚焦

# === 提取匹配的一部分(配合分组)===
# 从日志里只把 URL 抽出来:
$ sed -nE 's/.*"(GET|POST) ([^ ]+) HTTP.*/\2/p' access.log | head
# (...) 是分组,\2 引用第 2 个分组,只打印替换成功的行

修复 5:管道——把三剑客串成一条流水线

# === 三剑客真正的威力,在于用管道 | 把它们串起来 ===
# 每个工具只做自己最擅长的一件事,数据在它们之间流动。

# === 一条龙:故障那一小时,最频繁报错的接口 Top5 ===
$ sed -n '98012,131890p' access.log \
  | grep -E '" 5[0-9]{2} ' \
  | awk '{print $7}' \
  | sort | uniq -c | sort -rn | head -5
# 拆解这条流水线:
# sed  -> 先切出 15:00-16:00 这一段
# grep -> 从中筛出 5xx 报错的行
# awk  -> 每行只取出 URL($7)那一列
# sort -> 排序(uniq 要求相邻才能去重,所以先排)
# uniq -c -> 相邻相同行合并,并数出每组的数量
# sort -rn -> 按数量倒序
# head -5 -> 只要前 5 名

# === 这套组合是日志分析的"万能模板" ===
#   grep 筛行  ->  awk 取列  ->  sort | uniq -c | sort -rn
# 把它记牢,以后"统计 XX 出现频率"几乎都是套这个。

# === 再来一条:报错 IP + 接口的组合分布 ===
$ grep -E '" 5[0-9]{2} ' access.log \
  | awk '{print $1, $7}' \
  | sort | uniq -c | sort -rn | head -10
# 同时看 IP 和接口两列,能发现"某个 IP 专打某个接口"

# === 统计去重数量:到底有多少个不同的报错 IP ===
$ grep -E '" 5[0-9]{2} ' access.log \
  | awk '{print $1}' | sort -u | wc -l
# sort -u 去重,wc -l 数行 -> 不同 IP 的个数

# === 实时跟日志 + 过滤(故障还在持续时)===
$ tail -f access.log | grep --line-buffered -E '" 5[0-9]{2} '
# --line-buffered:让 grep 别缓存,实时一行行吐出来
# 一边发生一边看,报错出现立刻就能看到

修复 6:这次排查的结论与处置

# === 三剑客把事实一条条摆了出来 ===
# 1. 总请求 15.2 万,5xx 报错 3877,报错率 2.5%(平时 <0.1%)
# 2. 报错 83% 集中在 /api/order/detail 这一个接口
# 3. 报错 51% 来自同一个 IP 10.0.2.55
# 4. 报错几乎全挤在 15:00-16:00 这一小时
# 5. /api/order/detail 平均耗时从 50ms 涨到了 2.3s

# === 顺着事实定位根因 ===
# 把 10.0.2.55 在那一小时的请求单独拉出来看:
$ sed -n '98012,131890p' access.log \
  | awk '$1=="10.0.2.55" {print $4, $7}' | head -20
# 发现它在用一个空的查询条件疯狂翻页拉 order/detail,
# 每秒几十次 —— 是对方一个批处理脚本写错了,
# 把本该凌晨跑的全量拉取,配成了白天每秒高频请求。

# === 处置 ===
# ① 应急:先对这个 IP 限流
$ # 在 Nginx 里对该接口加 limit_req,挡住异常高频
# ② 沟通:通知对方修正它的批处理脚本和调度时间
# ③ 根治:给 /api/order/detail 加上分页大小上限和
#         必填查询条件校验,不允许无条件全量翻页
# ④ 补课:推动把日志接入 ELK ——
#         这次靠三剑客硬扛下来了,但下次该有更顺手的工具

# === 复盘心得 ===
# 没有 ELK 的那一个小时,三剑客就是我的 ELK。
# grep 筛行、awk 取列做统计、sed 切片做编辑,
# 再用管道串起来 —— 这套硬功夫,任何一台 Linux
# 终端上都现成可用,关键时刻不掉链子。

命令速查

需求                          命令
=============================================================
筛出匹配的行                  grep '关键字' file
正则筛行                      grep -E '5[0-9]{2}' file
统计匹配行数                  grep -c '关键字' file
反选不匹配的行                grep -v '关键字' file
看匹配行的上下文              grep -A3 -B3 '关键字' file
递归搜目录                    grep -rn '关键字' dir/
打印指定列                    awk '{print $1, $9}' file
按条件过滤行                  awk '$9 ~ /^5/' file
分组计数(核心套路)          awk '{c[$9]++} END{for(k in c)print c[k],k}'
截取子串                      awk '{print substr($4,14,2)}'
指定分隔符                    awk -F',' '{print $3}' file
替换文本                      sed 's/old/new/g' file
改原文件并备份                sed -i.bak 's/old/new/g' file
打印指定行范围                sed -n '100,200p' file
删除匹配的行                  sed '/noise/d' file
频率统计万能模板              grep ... | awk '{print $N}' | sort | uniq -c | sort -rn

口诀:grep 筛行 -> awk 取列做统计 -> sed 切片做编辑 -> 管道串起来

避坑清单

  1. 分清三剑客分工:grep 管筛行,awk 管按列提取统计,sed 管按行编辑替换
  2. grep 匹配状态码等数字要带边界(两边空格或用正则),否则会误匹配 5001 之类
  3. grep -A/-B/-C 看上下文,排错时能看清出错请求前后发生了什么
  4. awk 的 $1 $2 是列,NF 是最后一列,NF 是列数,别记混
  5. awk 数组计数 + END 输出是分组统计的核心套路,务必记牢
  6. awk 多条件用 && 和 ||,能把时间、接口、状态码叠起来精确锁定
  7. sed 默认只打到屏幕不改文件,要改文件必须 -i,且强烈建议用 -i.bak 留备份
  8. sed -n 配合 p 才能精确抽取行范围,单独的 p 会导致重复输出
  9. uniq 只能合并相邻的相同行,所以 uniq 前必须先 sort,否则统计不准
  10. 频率统计的万能模板是 grep 筛 → awk 取列 → sort | uniq -c | sort -rn,套用即可

总结

这次没有 ELK、只靠一台终端硬扛下来的日志排查,让我重新认识了 grep、awk、sed 这"三剑客"的分量。在平时,我们习惯了把日志一股脑丢进 ELK、丢进各种花哨的可视化平台,点几下鼠标就有图有表,久而久之,很容易产生一种错觉,觉得离开了这些平台就没法分析日志了。可这次的现实是,日志还没接入 ELK,故障又必须当场查清,我手上就只有一台 Linux 终端和一个十几万行、几百兆的文本文件。也正是在这种"裸奔"的处境里,我才真切地体会到,三剑客这套东西不是什么过时的老古董,而是一种任何一台 Linux 机器上都现成可用、关键时刻绝不掉链子的硬功夫。要用好它们,第一件事是把三者的分工想清楚,因为它们各管一摊、互不替代:grep 管的是"筛行",它的使命是从浩如烟海的文本里把你关心的那些行挑出来;awk 管的是"取列",它会把每一行按列切开,让你能精确地拿到第几列,并且用数组做分组统计;sed 管的是"按行编辑",替换、删除、按行号抽取范围,都是它的活。把这个分工刻进脑子里,你拿到一个文本处理需求时,就能立刻知道该派哪一把"剑"上场。在这三者里,awk 是最值得花力气吃透的,因为真正的统计分析几乎都靠它,而 awk 里又有一个堪称"万能套路"的模式:用数组以某个字段为 key 做累加计数,再在 END 块里把结果统一输出。这次排查里,"哪个接口报错最多""哪些 IP 在报错""报错集中在哪个小时",本质上全都是同一个动作——分组计数,全都是这一个套路的变体。第二件要想清楚的事,是三剑客的威力其实不在于单独某一把剑有多强,而在于用管道把它们串成一条流水线。这次我用得最多、也最该被记住的,就是那条"频率统计万能模板":先用 grep 把符合条件的行筛出来,再用 awk 把你要统计的那一列取出来,然后 sort 排序、uniq -c 数出每组的数量、再 sort -rn 按数量倒序。这条流水线背后的哲学很美,就是 Unix 一以贯之的理念——每个工具只做一件事、并且把它做到极致,工具与工具之间通过管道让数据像水一样流动;你不需要一个无所不能的庞然大物,你只需要把几个小而锐利的工具,按正确的顺序拼起来。这里还藏着一个非常容易被忽略、却屡屡让人统计出错的细节:uniq 这个命令只能合并"相邻"的相同行,所以在 uniq 之前,你必须先 sort 把相同的行排到一起,否则数出来的数字是错的——这个坑我希望自己永远记住。回到这次故障本身,三剑客最终把一连串冰冷而确凿的事实一条条摆在了我面前:报错率从平时的不到千分之一飙到了百分之二点五,百分之八十三的报错死死集中在一个接口上,超过一半的报错来自同一个 IP,而且几乎全都挤在某一个小时之内。这些事实彼此印证、互相收敛,根因也就藏不住了——是对方一个批处理脚本配错了调度,把本该在凌晨低峰跑的全量拉取,变成了白天每秒几十次的高频无条件翻页。我想说的是,排查的功夫,从来不是去"猜"一个根因,而是用工具把一条条客观事实捞出来、摆到一起,让根因在事实的交叉点上自己浮现。当然,这次的经历也让我下定决心去推动把日志接入 ELK,因为靠三剑客在终端里硬扛终究是辛苦的、是不可持续的。但我同时也无比清楚:就算以后 ELK 用得再顺手,grep、awk、sed 这套功夫我也绝不会丢,因为平台会宕机、会还没搭好、会在你最需要的那个深夜恰好连不上,而那一台终端、那三把剑,永远都在,永远都能用。这,就是硬功夫的意义。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

服务进程隔几天就消失:一次 Linux 进程与信号排查的复盘

2026-5-20 17:23:36

Linux教程

定时任务明明配了却不执行:一次 Linux crontab 排查复盘

2026-5-20 17:28:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索