管道与文本处理
这一课你将能做到
- 用管道把 grep、sort、uniq、wc、head 串起来,一条命令统计出 Nginx 日志里访问量 Top 10 的 IP
- 熟练使用 grep 的 -i / -v / -n / -r / -A / -B / -C / -E 选项,在 Spring Boot 日志里精准定位异常上下文
- 会用 tail -f 实时盯日志、用 awk/sed 完成字段提取和批量替换
为什么要学
线上排查问题的 80% 时间在翻日志。Spring Boot 一个应用一天能产出几百 MB 日志,IDE 都打不开。但 Linux 的设计哲学是「每个程序只做一件事,组合起来解决复杂问题」——grep 负责过滤、awk 负责取列、sort/uniq 负责统计、tail -f 负责实时盯,管道 | 把它们串成流水线。掌握这条流水线,你排查问题的速度比同事快一个量级,面试聊「怎么排查线上 502」时也能直接甩出命令。
人话:管道就像工厂流水线。原料(日志文件)先进 grep 这台机器筛掉杂质,半成品流进 awk 切成规格件,再进 sort/uniq 打包计数,最后 head 装箱出货。每台机器只干一件事,但串起来威力巨大。
先建立图像
一条经典流水线的数据流转:
nginx access.log(100 万行)
│ cat / 直接读
▼
grep " 502 " → 只剩错误行(比如 3000 行)
▼
awk '{print $1}' → 每行只留第 1 列:客户端 IP
▼
sort → 相同 IP 排到一起
▼
uniq -c → 每种 IP 计数:312 1.2.3.4 ...
▼
sort -rn → 按次数从大到小排
▼
head -5 → 取前 5 名(Top 5 攻击源)
日志行的列号直觉(Nginx 默认 combined 格式):
192.168.1.10 - - [01/Oct/2026:10:00:00] "GET /api/user HTTP/1.1" 200 1024
$1 $2 $3 $4 $5... $9 $10
awk 里 $1 是第一列,$NF 是最后一列,$0 是整行。
跟着做
步骤 1:造一份仿真日志
mkdir -p ~/pipe-lab && cd ~/pipe-lab
cat > access.log << 'EOF'
192.168.1.10 - - [01/Oct/2026:10:00:01 +0800] "GET /api/user HTTP/1.1" 200 1024
192.168.1.20 - - [01/Oct/2026:10:00:02 +0800] "GET /api/user HTTP/1.1" 502 512
192.168.1.10 - - [01/Oct/2026:10:00:03 +0800] "POST /api/order HTTP/1.1" 200 2048
192.168.1.30 - - [01/Oct/2026:10:00:04 +0800] "GET /api/user HTTP/1.1" 502 512
192.168.1.20 - - [01/Oct/2026:10:00:05 +0800] "GET /static/logo.png HTTP/1.1" 200 9999
192.168.1.10 - - [01/Oct/2026:10:00:06 +0800] "GET /api/user HTTP/1.1" 502 512
192.168.1.40 - - [01/Oct/2026:10:00:07 +0800] "GET /actuator/health HTTP/1.1" 200 15
EOF
<< 'EOF' 是 here-document,从终端直接粘多行进文件,引号包住 EOF 表示不做变量展开。
步骤 2:grep 基础五连
grep "502" access.log # 含 502 的行
grep -c "502" access.log # 只数行数:3
grep -n "502" access.log # 带行号,方便回头定位
grep -v "200 " access.log # 反选:不含 200 的行
grep -i "error" app.log # 忽略大小写
grep -E "502|504" access.log # 扩展正则:502 或 504
步骤 3:上下文抓取(排查异常堆栈神器)
# 造一份 Java 报错日志
printf '2026-10-01 INFO started\n2026-10-01 ERROR NullPointer\n\tat com.x.Service.run(Service.java:20)\n\tat com.x.Main.main(Main.java:8)\n2026-10-01 INFO recovered\n' > app.log
grep -A 2 "ERROR" app.log # 匹配行 + 之后 2 行(After,堆栈主体)
grep -B 1 "ERROR" app.log # 匹配行 + 之前 1 行(Before,触发前的请求)
grep -C 1 "ERROR" app.log # 前后各 1 行(Context)
线上翻异常:grep -A 20 "NullPointerException" app.log | head -50,把堆栈一次拉出来。
步骤 4:流水线统计 Top IP
grep " 502 " access.log | awk '{print $1}' | sort | uniq -c | sort -rn
# 期望输出:
# 2 192.168.1.10
# 1 192.168.1.20
# 1 192.168.1.30
拆开看:grep 过滤 → awk 取第 1 列 → sort 让相同 IP 相邻(uniq 只认相邻重复)→ uniq -c 计数 → sort -rn 按数字倒序。想看访问总量 Top:去掉 grep 直接 awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3。
步骤 5:awk 取列与条件过滤
awk '{print $1, $9}' access.log # IP 和状态码
awk '$9 == 502 {print $1}' access.log # 状态码等于 502 的 IP
awk -F: '{print $1}' /etc/passwd # -F 指定冒号为分隔符,取用户名
awk 'END{print NR}' access.log # 总行数(相当于 wc -l)
步骤 6:sed 批量替换与实时盯梢
sed 's/502/BAD_GATEWAY/' access.log # 每行第一个 502 替换,输出到屏幕
sed -E 's/GET /api\/user/GET \/api\/v2\/user/' access.log # 扩展正则替换
sed -i.bak 's/8080/8081/g' nginx.conf # 直接改文件,g 全局替换,.bak 留备份
tail -f /opt/myapp/logs/app.log # 实时追加显示,Ctrl+C 退出
tail -f app.log | grep --line-buffered "ERROR" # 盯着日志只看错误行
tail -n 200 app.log # 最近 200 行
讲清楚要点
- 管道只传 stdout,不传 stderr:
grep xxx app.log 2>/dev/null | wc -l 里 2>/dev/null 是丢弃错误流。想连错误一起统计用 cmd 2>&1 | ...。
- uniq 必须先 sort:uniq 只合并「相邻」重复行,乱序数据直接 uniq 得到的统计是错的。
sort | uniq -c 是固定搭配。
- grep -r 可递归搜目录:
grep -rn "spring.datasource" /opt/myapp/config/,在配置目录里找某个配置项是日常高频操作,-n 告诉你文件和行号。
- awk 默认按空白切列,
-F 换分隔符:日志、/etc/passwd(冒号分隔)、CSV(逗号分隔)都能处理。$NF 最后一列在 URL 带空格时会失效,此时优先用正则提取。
sed -i 直接改文件,务必先备份或先预览:先跑不带 -i 的命令看输出,确认无误再加 -i.bak。生产配置文件被 sed 改错一行,Nginx 起不来就是故障。
tail -f 盯日志时配合 grep 过滤:全量刷屏会把关键行冲走;grep --line-buffered 避免缓冲导致输出延迟。想看错误但保留上下文,用 grep -C 3。
你可能会懵的地方
-
awk 和 sed 到底谁干嘛?
一句话:awk 按「列」处理,sed 按「行」处理。取第 3 列、按条件汇总统计用 awk;删除行、替换字符串用 sed。两者都支持正则,互相能干对方的活,但各有所长。
-
grep " 502 " 前后为什么要加空格?
防止误匹配:15024、5021 这些数字里也含「502」。加上空格后匹配的是独立的状态码字段。更严谨的写法是 awk '$9==502',按列精确匹配。
-
单引号和双引号在这个场景怎么选?
grep/awk/sed 的模式里没有变量就用单引号(Shell 不做任何展开,模式原样传递);需要把 Shell 变量拼进模式用双引号:grep "$DATE" app.log。
-
日志文件几个 GB,grep 会很慢吗?
grep 是流式逐行读,内存占用极小,1GB 日志秒级出结果。真正要避免的是 cat 大文件 直接刷屏——永远在后面接 | head 或 | grep,不要裸看。
-
2>&1 是什么意思?
把标准错误(2)重定向到标准输出(1)当前指向的地方。nohup java -jar app.jar > app.out 2>&1 & 就是「输出和报错都写进 app.out」——第 9 课进程部分会正式用到。
错法对照
| 错法 | 问题 | 正解 |
|---|
uniq -c 前没加 sort | 统计结果碎片化、错误 | `sort |
sort -rn 写成 sort -r | 按字符串排序,10 排在 2 前面 | 数值排序必须 -n |
sed -i 不留备份直接改生产配置 | 改错无法回滚 | sed -i.bak 或先手动 cp |
双引号里写 awk 模式 $9 | 被 Shell 当变量展开成空 | awk 模式用单引号 |
| `tail -f | grep ERROR` 输出延迟 | grep 全缓冲 |
卡点急救
- 管道结果为空:从左往右逐段验证——先跑第一段看有没有输出,再一段段往后接,哪一段开始为空问题就在哪。新手常犯:模式拼写和日志实际大小写/空格不一致。
- grep 匹配到大量无关行:模式太宽。用
-w 整词匹配、加锚定(^2026-10-01.*ERROR 只匹配今天开头的错误行)、或用 -E 精确枚举多个状态码。
tail -f 报 file truncated 或文件轮转后失效:日志按天切割后 inode 变了,用 tail -F(大写)代替,它会跟踪文件名重新打开。
面试常问
-
统计 Nginx 日志中访问最多的前 10 个 IP,怎么写?
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10。追问「为什么 uniq 前要 sort」:uniq 只合并相邻重复;追问「为什么 -rn」:按第一列数字倒序。能完整解释每个环节才是满分答案。
-
怎么在日志里快速定位一次异常的完整堆栈?
先 grep -n "Exception" app.log | tail 拿到行号,再用 sed -n '开始行,结束行p' app.log 精确截取;或一步 grep -A 20 "NullPointerException" app.log | head -50。实时排查则 tail -f app.log | grep -A 20 --line-buffered "Exception"。
-
awk 取列和 cut 取列有什么区别,怎么选?
cut 轻量,cut -d: -f1 按单字符分隔符取列,适合格式极规整的数据;awk 自带空白容错(连续空格视为一个分隔符),支持条件过滤、计算、汇总,功能全面。日志处理默认 awk;格式绝对规整且追求速度用 cut。
自测三问
grep -C 3 "ERROR" app.log 中 -C 是什么含义?
- 写出统计 access.log 中 504 状态码出现次数的完整管道。
- uniq 为什么必须搭配 sort 使用?
小结一句话
grep 过滤、awk 取列、sort+uniq 统计、head 收尾、tail -f 实时盯——这条流水线是日志排查的母语,练熟它就掌握了线上运维一半的日常。
延伸