文章总结: 本文系统讲解Linux文本处理三剑客grep、sed、awk在日志分析中的实战应用。针对运维常见痛点如正则匹配错误、跨平台差异、大文件处理OOM、敏感信息泄露等,提供具体解决方案和风险规避方法。通过nginx日志分析案例演示从日志定位、过滤清洗到统计分析的完整流程,包含GNU/BSD环境差异说明和实时处理技巧,具有较强的工程指导价值。 综合评分: 87 文章分类: 安全工具,WEB安全,安全运营,实战经验,技术标准
文本三剑客 grep / sed / awk:日志处理的瑞士军刀
点击关注 👉 点击关注 👉
马哥Linux运维
2026年6月22日 17:29 广东
在小说阅读器读本章
去阅读
文本三剑客 grep / sed / awk:日志处理的瑞士军刀
面向初中级运维、DevOps、系统管理员的 Linux 文本处理实战手册。围绕日志处理这条主线,把 grep、sed、awk 的真实用法、性能陷阱、BSD/GNU 差异、生产风险一次讲清楚,所有命令可在 CentOS 7/8、Ubuntu 20.04/22.04、GNU coreutils/sed/gawk 环境验证。
一、问题背景
线上排障一半时间在看日志,看日志一半时间在用 grep/sed/awk。但这三个工具看着简单,生产里翻车的方式花样百出:
- 想从 nginx access.log 统计每个 URL 的 Top10 访问量,敲了一串
grep -o 'GET /api/.*' | sort | uniq -c,结果正则贪婪匹配把整行都吃进去,统计出来全是同一个超长串。 - 用
sed -i批量改配置文件,没加备份后缀,改完发现正则写错,全文件被改坏,且没法回滚。 - 在 macOS 上跑的
sed -i 's/a/b/' file报错,因为 BSD sed 的-i必须带参数,和 GNU sed 行为不一样。 - 用 awk 统计 UV,
awk '{uv[$1]++} END{print length(uv)}',结果日志几个 GB,awk 内存炸了,OOM 被 kill。 - 处理 Windows 导出的带
\r的 CSV,awk 拆字段最后一列带个看不见的\r,统计全错。 - grep 一个 50GB 的日志,没加
--line-buffered接到 tail -F,结果缓冲了几分钟才出数据,错过实时排障窗口。 - 生产日志里有用户手机号、身份证,grep 出来直接贴到群里脱敏没做,泄露了。
这三个工具的难点不在“会不会敲”,而在于:
- 正则的贪婪与非贪婪、BRE/ERE/PCRE 三种风格、元字符转义,差一个字符结果全错。
- GNU 与 BSD 实现差异(
sed -i、grep -P、awk函数库),跨平台脚本处处是坑。 - 大文件场景的内存与缓冲:awk 全表加载会 OOM、grep 缓冲不实时、sed 大文件
-i用临时盘。 - 文件编码与行尾:CRLF、UTF-8 BOM、中文 locale,会让字段切分和正则匹配全部错位。
- 生产日志含敏感信息,处理时必须脱敏,输出要落地到权限受控目录。
本文把 grep、sed、awk 按“过滤—清洗—统计”三段拆开,给出可落地的命令、脚本、判断方法和回滚预案,并落到一个完整的“nginx 日志排障”闭环里。
二、适用场景
| 场景 | 推荐工具 | 说明 | | — | — | — | | 从日志里找含某关键字的行 | grep | 简单快速 | | 找含正则模式的行(OR、字符类、锚点) | grep -E | ERE 正则 | | 统计某关键字出现次数 | grep -c | 计数 | | 批量替换文本(配置、CSV 清洗) | sed | 流编辑器 | | 按字段统计、聚合、计算 | awk | 字段处理 | | 多文件汇总、按列分组 | awk | 关联数组 | | 实时跟踪日志关键字 | tail -F + grep | 实时流 | | 处理压缩日志(.gz) | zgrep / zcat | 不解压直接查 | | 系统日志查询 | journalctl | systemd 日志 | | 超大日志采样、分片 | split + 三剑客 | 避免 OOM |
环境假设:
- 操作系统:CentOS 7/8、Ubuntu 20.04/22.04
- 工具:GNU grep 3.x、GNU sed 4.x、gawk 5.x(CentOS 默认 awk 是 gawk)
- macOS 用户注意 BSD 差异,文中标注
- 日志样例:nginx access.log,行格式见下
nginx access.log 行格式(combined 格式,本文以它为样例):
192.168.1.10 - - [22/Jun/2026:14:55:03 +0800] "GET /api/orders?id=1001 HTTP/1.1" 200 1234 "https://shop.com/" "Mozilla/5.0 ..."
字段顺序:$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"。
三、核心知识点
3.1 三个工具的分工
- grep:行级过滤。输入一堆行,输出“匹配模式”的行。它不改变行内容,只筛选。强项是快、简单。
- sed:流编辑器。逐行读入,按脚本对每行做编辑(替换、删除、插入、打印),输出编辑后的行。强项是替换和清洗。
- awk:字段处理。逐行读入,按分隔符拆字段,对字段做计算、统计、聚合。强项是按列计算和关联数组。
一句话总结:grep 选行、sed 改行、awk 算列。
3.2 正则三种风格
- BRE(Basic Regular Expression):默认正则。
grep 'a.b'、sed 's/a/b/'默认用 BRE。+、?、{n,m}、(、)、|在 BRE 里是普通字符,要元语义需转义成\+、\?、\{n,m\}、\(、\)、\|。 - ERE(Extended Regular Expression):
grep -E、sed -E(GNU)/sed -r、awk默认。+、?、{n,m}、()、|直接是元字符。 - PCRE(Perl Compatible Regular Expression):
grep -P(GNU 扩展)。支持非贪婪.*?、lookahead(?=)、命名捕获等。但 PCRE 不是所有 grep 都支持(BSD grep 不支持-P),跨平台慎用。
判断与选择:
- 简单模式用 BRE/ERE 即可,不依赖 PCRE。
- 需要非贪婪、lookahead 才用
-P,且确认运行环境是 GNU grep。 - sed 替换里用 ERE 更清晰:
sed -E 's/(a)(b)/\2\1/'。
3.3 分隔符与字段
-
awk 默认分隔符是“连续的空白(空格和 Tab)”,多个空白算一个分隔符。
-
用
awk -F','指定单字符分隔符,awk -F'\t'指定 Tab,awk -F'|'指定竖线。 -
多字符分隔符用
awk -F'\\|\\|'(双竖线)或awk -v FS='::'。 -
OFS是输出分隔符,
print $1,$2用 OFS 连接,默认是空格,awk -v OFS='|'改。
3.4 变量与特殊字段
-
$0:整行。
-
$1…
$NF:第 1 到最后一个字段。 -
NR:当前行号(累计所有文件)。
-
FNR:当前文件内行号。
-
NF:当前行字段数。
-
FILENAME:当前文件名。
-
FS/
OFS/RS/ORS:输入/输出字段分隔符、输入/输出记录分隔符。
3.5 缓冲与实时性
-
grep/sed/awk 默认块缓冲输出,重定向到文件或管道时不会立刻输出。
-
实时跟踪要加
--line-buffered(grep)、sed -u、awk fflush()。 -
tail -F默认行缓冲,适合实时跟踪。
3.6 高风险动作
-
sed -i不加备份后缀:原地改坏不可回滚。
-
sed -i正则写错:批量改错所有匹配行。
-
awk 大文件全表加载到关联数组:OOM。
-
grep 不脱敏输出敏感信息:泄露。
-
sed/
awk处理含特殊字符的输入未转义:注入或误匹配。
四、整体排查或实施思路
日志处理是一个完整闭环:现象 → 定位日志文件 → 粗筛(grep)→ 清洗(sed)→ 聚合统计(awk)→ 定位根因 → 验证 → 复盘。
对应到操作流程:
- 先确认日志在哪、多大、是否压缩。
- 用 grep 粗筛出关心的行(按关键字、状态码、时间、URL)。
- 用 sed 清洗(去字段、统一格式、脱敏)。
- 用 awk 按字段聚合统计(TopN、UV、错误率、耗时分布)。
- 结合多个指标交叉判断根因,不只凭单一指标。
- 输出落权限受控目录,脱敏后再分享。
五、实战步骤
5.1 配置前检查:定位与量化日志
bash
# 找日志位置
ls -lh /var/log/nginx/
find /data/log -name 'access*.log' -printf '%s\t%p\n' | sort -rn | head
# 看行数(大文件用 wc -l 慢,先估算)
wc -l /var/log/nginx/access.log
# 看前几行确认格式
head -n 3 /var/log/nginx/access.log
# 看是否压缩
ls -lh /var/log/nginx/access.log*.gz
file /var/log/nginx/access.log.1.gz
判断:
- 日志几个 GB,先采样
head -n 100000跑通命令再全量。 - 压缩日志用
zgrep/zcat不解压。 - 确认行格式和分隔符,决定 awk 的
-F。
5.2 grep 基础与常用参数
bash
# 基本查找
grep 'ERROR' /var/log/app.log
# 不区分大小写
grep -i 'error' /var/log/app.log
# 显示行号
grep -n 'ERROR' /var/log/app.log
# 只输出匹配部分
grep -o 'ERROR[a-z]*' /var/log/app.log
# 反向匹配(不含 ERROR 的行)
grep -v 'DEBUG' /var/log/app.log
# 统计匹配行数
grep -c 'ERROR' /var/log/app.log
# 只输出有匹配的文件名
grep -l 'ERROR' /var/log/app/*.log
# 递归查目录
grep -r 'ERROR' /var/log/app/
# 上下文:匹配行后 2 行
grep -A 2 'ERROR' /var/log/app.log
# 前 2 行
grep -B 2 'ERROR' /var/log/app.log
# 前后各 2 行
grep -C 2 'ERROR' /var/log/app.log
# 高亮(终端交互用)
grep --color=auto 'ERROR' /var/log/app.log
# 多模式 OR(-E ERE)
grep -E 'ERROR|WARN' /var/log/app.log
# 多模式 OR(-f 模式文件)
cat > /tmp/patterns.txt <<'EOF'
ERROR
WARN
FATAL
EOF
grep -f /tmp/patterns.txt /var/log/app.log
# 多模式 AND(grep 不直接支持,用管道)
grep 'ERROR' /var/log/app.log | grep 'order'
判断:
-
-c数的是“匹配行数”不是“匹配次数”,一行里出现多次只算 1。要算次数用
grep -o ... | wc -l。 -
-l只出文件名,适合从一堆日志里找“哪个文件含错误”。
-
-A/-B/-C看上下文,排障定位错误前后发生了什么。
5.3 grep 正则实战
bash
# 锚点:行首行尾
grep '^2026-06-22' /var/log/app.log # 以日期开头
grep 'ERROR$' /var/log/app.log # 以 ERROR 结尾
# 字符类
grep '[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' /var/log/app.log # BRE 日期
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' /var/log/app.log # ERE 日期,更清晰
# 任意字符与重复
grep -E 'a.c' file # a 和 c 之间任意一个字符
grep -E 'a+c' file # a 一次或多次
grep -E 'a?c' file # a 零次或一次
grep -E 'a{3,5}c' file # a 3~5 次
# 非捕获、词边界(ERE 无,需 PCRE)
grep -P '\berror\b' file # 词边界,error 不在 errorx 里匹配(GNU grep 支持 -P)
# IP 地址(ERE)
grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' /var/log/nginx/access.log
# 状态码 5xx
grep -E ' 5[0-9]{2} ' /var/log/nginx/access.log
判断与陷阱:
-
BRE 里
{}要转义\{3\},ERE 里{3}直接用。这是最常见的正则错误来源。 -
IP 正则
([0-9]{1,3}\.){3}[0-9]{1,3}不校验范围(会匹配 999.999.999.999),日志粗筛够用,严格校验要用更复杂正则或工具。 -
grep -P非贪婪
.*?在大行上比贪婪快且准,但要确认 GNU grep。
5.4 grep 大文件与实时跟踪
bash
# 大文件流式,避免缓冲
grep --line-buffered 'ERROR' /var/log/app.log | head -n 100
# 实时跟踪关键字
tail -F /var/log/app.log | grep --line-buffered 'ERROR'
# 压缩日志不解压
zgrep 'ERROR' /var/log/nginx/access.log.1.gz
zcat /var/log/nginx/access.log.1.gz | grep 'ERROR'
# 多个压缩文件一起查
zgrep 'ERROR' /var/log/nginx/access.log.*.gz
# 只看最近 N 行的匹配(先 tail 再 grep)
tail -n 100000 /var/log/app.log | grep 'ERROR'
判断:
-
tail -F | grep --line-buffered是实时关键字告警的基础,buffering 不开会延迟。
-
zgrep等价
zcat | grep,对大压缩日志省去解压落盘。 -
超大日志先
tail -n N限定范围,避免全量扫描。
5.5 sed 基础与替换
bash
# 替换每行第一个匹配
sed 's/error/ERROR/' file
# 替换所有匹配
sed 's/error/ERROR/g' file
# 替换第 N 个匹配
sed 's/error/ERROR/2' file
# 只在特定行替换(如第 5 行)
sed '5s/error/ERROR/' file
# 行范围替换(5~10 行)
sed '5,10s/error/ERROR/' file
# 最后行
sed '$s/error/ERROR/' file
# 正则分组与反向引用(ERE)
echo '2026-06-22' | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\1\/\2\/\3/'
# 输出 2026/06/22
# & 代表整个匹配
echo 'abc' | sed 's/b/[&]/'
# 输出 a[b]c
# 多命令 -e
sed -e 's/error/ERROR/g' -e 's/warn/WARN/g' file
# 删除行
sed '/DEBUG/d' file # 删含 DEBUG 的行
sed '5d' file # 删第 5 行
sed '5,10d' file # 删 5~10 行
sed '/^$/d' file # 删空行
# 只打印匹配行(-n 配 p)
sed -n '/ERROR/p' file # 等价 grep ERROR
sed -n '5,10p' file # 只看 5~10 行
判断:
-
s命令第三个参数:
g全部、数字第 N 个、p打印、i忽略大小写(GNU)。 -
-n抑制默认输出,配
p命令只输出匹配行。 -
&是整个匹配,
\1..\9是分组。
5.6 sed 原地编辑与备份(重点)
bash
# GNU sed:-i 加后缀自动备份
sed -i.bak 's/error/ERROR/g' config.conf
# 生成 config.conf.bak,原文件改
# GNU sed:-i 不备份(危险)
sed -i 's/error/ERROR/g' config.conf
# BSD sed(macOS):-i 必须带参数,空参数要显式空串
sed -i '' 's/error/ERROR/g' config.conf # macOS 不备份
sed -i '.bak' 's/error/ERROR/g' config.conf # macOS 带备份
风险与判断:
-
sed -i不加备份后缀是生产翻车重灾区。一旦正则写错,全文件改坏且无回滚。
-
生产强制
sed -i.bak,且改完diff检查再决定是否保留.bak。 -
跨平台脚本要区分 GNU/BSD:
sed -i.bak(GNU 自动备份)vssed -i '.bak'(BSD)。
5.7 sed 处理 CRLF 与行尾
Windows 导出的文件含 \r\n,sed/awk 不处理 \r 会留在字段里:
bash
# 删行尾 \r
sed -i 's/\r$//' file.csv
# 删 BOM(UTF-8 BOM 在行首)
sed -i '1s/^\xEF\xBB\xBF//' file.csv
# 看 \r 是否存在
cat -A file.csv | head
# 行尾 ^M 就是 \r
判断:
-
cat -A显示行尾
^M$,^M是\r,$是行尾。有^M就是 CRLF。 -
处理 CSV 前先统一转 LF:
sed -i 's/\r$//' file.csv。 -
BOM 会让第一行第一字段带
\xEF\xBB\xBF,awk 切字段会把它带进去。
5.8 sed 高级:地址范围与多行模式
bash
# 打印两个模式之间的行
sed -n '/BEGIN/,/END/p' file
# 删除两个模式之间的行
sed '/BEGIN/,/END/d' file
# N 命令读下一行合并
sed 'N;s/\n/ /' file # 两行合并成一行
# 多行替换(GNU)
sed ':a;N;$!ba;s/\n/ /g' file # 整个文件合并成一行,用于单行化处理
# 大括号分组
sed -n '/ERROR/{
s/ERROR/FATAL/
p
}' file
判断:
-
高级 sed 命令(N/D/P/分支)可读性差,能 awk 替代就别用 sed 高级模式。
-
:a;N;$!ba;是把整个文件读进 pattern space 的经典写法,大文件会占内存,慎用。
5.9 awk 基础与字段
bash
# 默认空白分隔,打印第 1、7 字段
awk '{print $1, $7}' file
# 指定分隔符
awk -F',' '{print $1, $3}' file.csv
awk -F'\t' '{print $1}' file.tsv
# 打印最后一列
awk '{print $NF}' file
# 倒数第二列
awk '{print $(NF-1)}' file
# 行号 NR
awk '{print NR": "$0}' file
# 字段数 NF
awk '{print NF}' file
# 条件过滤
awk '$3 > 100 {print $0}' file # 第 3 列大于 100
awk '$1 == "ERROR" {print $0}' file # 第 1 列等于 ERROR
awk '/ERROR/ {print $0}' file # 含 ERROR 的行
# 多条件
awk '$3 > 100 && $5 == "OK" {print}' file
awk '$3 > 100 || $7 == "FAIL" {print}' file
判断:
-
awk 条件可以放在
{}前作为模式,也可以在{}里用if。 -
$1 == "ERROR"是字符串相等,
$3 > 100是数值比较,awk 按上下文自动转换类型。
5.10 awk BEGIN/END 与聚合统计
bash
# 行数
awk 'END{print NR}' file
# 求和(第 3 列)
awk '{sum += $3} END{print sum}' file
# 平均值
awk '{sum += $3; n++} END{print sum/n}' file
# 最大值
awk 'NR==1{max=$3} $3>max{max=$3} END{print max}' file
# 关联数组:统计每状态码出现次数(nginx 日志第 9 字段是 status)
awk '{count[$9]++} END{for (s in count) print s, count[s]}' /var/log/nginx/access.log
# TopN:排序
awk '{count[$9]++} END{for (s in count) print count[s], s}' /var/log/nginx/access.log | sort -rn | head
# BEGIN 初始化
awk 'BEGIN{FS=","; OFS="|"} {print $1, $2}' file.csv
判断:
-
END块在所有行处理完执行,做汇总。
-
关联数组
count[key]++是 awk 聚合的核心,类似 MapReduce 的 reduce。 -
for (k in arr)遍历顺序无序,要排序接
sort。
5.11 awk 统计 PV / UV / 状态码
nginx access.log 统计实战:
bash
# PV(总请求数)
awk 'END{print NR}' /var/log/nginx/access.log
# UV(去重 IP 数,第 1 字段)
awk '{ip[$1]++} END{print length(ip)}' /var/log/nginx/access.log
# 状态码分布
awk '{code[$9]++} END{for (c in code) printf "%s\t%d\n", c, code[c]}' /var/log/nginx/access.log | sort
# 5xx 错误率
awk '{total++; if ($9 ~ /^5/) err++} END{printf "total=%d err=%d rate=%.2f%%\n", total, err, err/total*100}' /var/log/nginx/access.log
# Top10 IP
awk '{ip[$1]++} END{for (i in ip) print ip[i], i}' /var/log/nginx/access.log | sort -rn | head -10
# Top10 URL(第 7 字段是 request,拆出 URL)
awk '{split($7, a, " "); url[a[2]]++} END{for (u in url) print url[u], u}' /var/log/nginx/access.log | sort -rn | head -10
# 每个 URL 的平均响应大小(第 10 字段是 body_bytes_sent)
awk '{split($7, a, " "); u=a[2]; sum[u]+=$10; cnt[u]++} END{for (u in sum) printf "%s\tavg=%d\tcnt=%d\n", u, sum[u]/cnt[u], cnt[u]}' /var/log/nginx/access.log
注意:不同 nginx 日志格式字段位置不同,上面假设 combined 格式 $9 是 status、$10 是 body_bytes_sent。自定义 log_format 要重新数字段位置,不同版本字段位置随格式变化,先 head -1 看实际格式。
5.12 awk 多文件与 FILENAME/FNR
bash
# 多文件统计
awk '{count[FILENAME]++} END{for (f in count) print f, count[f]}' *.log
# 区分文件处理(FILENAME)
awk 'FILENAME=="a.log"{sum_a+=$1} FILENAME=="b.log"{sum_b+=$1} END{print "a="sum_a" b="sum_b}' a.log b.log
# FNR/NR 区别
awk '{print NR, FNR, $0}' a.log b.log
# NR 是全局行号,FNR 是当前文件内行号
# FNR==1 表示每个文件第一行
awk 'FNR==1{print "==", FILENAME, "=="} {print}' a.log b.log
判断:多文件汇总用 FILENAME 或 FNR==1 判断边界,比分别跑再合并更高效。
5.13 awk 大文件与内存
awk 把数据放进关联数组,大文件会 OOM:
bash
# 危险:亿行日志全 IP 进数组
awk '{ip[$1]++} END{...}' huge.log
# 安全:先排序再去重计数,不进数组
awk '{print $1}' huge.log | sort | uniq -c | sort -rn | head
# 采样:只取部分行
awk 'NR%100==0{print $1}' huge.log # 每 100 行取 1 行采样
判断与选择:
- UV 这种去重必须进数组,大文件用
sort | uniq -c替代,靠磁盘排序避免内存炸。 - 统计总和、计数不需要数组,单变量即可,任意大文件都安全。
- 采样适合“看趋势不看精确值”的快速估算。
5.14 脱敏处理
生产日志含手机号、身份证、IP、邮箱,输出前必须脱敏:
bash
# 脱敏手机号:保留前 3 后 4
sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g' app.log
# 脱敏身份证:保留前 6 后 4
sed -E 's/([0-9]{6})[0-9]{8}([0-9]{4})/\1********\2/g' app.log
# 脱敏 IP:保留前两段
sed -E 's/([0-9]+\.[0-9]+)\.[0-9]+\.[0-9]+/\1.*.*/g' app.log
# awk 脱敏并统计
awk '{
ip=$1
split(ip, a, ".")
masked=a[1]"."a[2]".*.*"
cnt[mask]++
} END{for (m in cnt) print cnt[m], m}' /var/log/nginx/access.log | sort -rn | head
判断与安全:
- 脱敏后输出落
/data/secure/,权限 700,分享前确认无敏感残留。 - 脱敏正则要覆盖所有可能格式,漏一种就泄露。
- 定期扫描输出目录是否有未脱敏残留:
grep -rE '1[3-9][0-9]{9}' /data/secure/。
六、常用命令速查
6.1 grep 参数速查
| 参数 | 作用 |
| — | — |
| -i | 不区分大小写 |
| -n | 显示行号 |
| -v | 反向匹配 |
| -c | 匹配行数 |
| -o | 只输出匹配部分 |
| -l | 有匹配的文件名 |
| -L | 无匹配的文件名 |
| -r/-R | 递归 |
| -E | ERE 正则 |
| -P | PCRE 正则(GNU) |
| -F | 固定字符串(不当正则) |
| -f FILE | 从文件读模式 |
| -A N | 匹配后 N 行 |
| -B N | 匹配前 N 行 |
| -C N | 前后各 N 行 |
| --color=auto | 高亮 |
| --line-buffered | 行缓冲 |
| -w | 词边界匹配 |
| -x | 整行匹配 |
| -a | 当二进制文件当文本处理 |
| -h | 多文件时不显示文件名 |
6.2 sed 参数与命令速查
| 参数/命令 | 作用 |
| — | — |
| -n | 抑制默认输出 |
| -i[SUFFIX] | 原地编辑,GNU 自动备份 |
| -E/-r | ERE 正则 |
| -e SCRIPT | 多脚本 |
| -f FILE | 从文件读脚本 |
| -u | 无缓冲 |
| s/pat/rep/flag | 替换,flag: g N p i |
| d | 删除行 |
| p | 打印行 |
| a\text | 行后追加 |
| i\text | 行前插入 |
| c\text | 替换整行 |
| y/abc/xyz/ | 字符映射 |
| N | 读下一行合并 |
| D | 删除 pattern space 首行 |
| P | 打印 pattern space 首行 |
| :label | 标签 |
| b label | 跳转 |
6.3 awk 参数与变量速查
| 参数/变量 | 作用 |
| — | — |
| -F FS | 输入分隔符 |
| -v var=val | 赋值变量 |
| -f FILE | 从文件读脚本 |
| $0 | 整行 |
| $1..$NF | 字段 |
| NR | 全局行号 |
| FNR | 文件内行号 |
| NF | 字段数 |
| FILENAME | 文件名 |
| FS/OFS | 输入/输出分隔符 |
| RS/ORS | 输入/输出记录分隔符 |
| length(s) | 字符串长度 |
| split(s,a,fs) | 拆分到数组 |
| substr(s,m,n) | 子串 |
| gsub(r,s) | 全局替换 |
| match(s,r) | 匹配位置 |
| printf | 格式化输出 |
| getline | 读下一行 |
| system(cmd) | 执行 shell 命令 |
6.4 配合命令
bash
# 排序去重计数 TopN
grep ERROR app.log | sort | uniq -c | sort -rn | head
# cut 按列切
cut -d' ' -f1,7 file
cut -c1-10 file
# tr 字符转换
tr 'a-z' 'A-Z' < file # 转大写
tr -d '\r' < file # 删 \r
tr -s ' ' < file # 压缩连续空格
# tee 边输出边落盘
cmd | tee output.log
# split 分片大文件
split -l 1000000 -d large.log part_
# 按 100 万行切,生成 part_00 part_01 ...
# parallel 并行处理
ls *.log | parallel -j 8 'grep ERROR {} > {.}.err'
# wc 行数单词数
wc -l file # 行数
wc -w file # 单词数
wc -L file # 最长行长度
七、配置示例
7.1 nginx log_format 与字段对应
nginx
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time';
对应 awk 字段(空格分隔时,$request 带引号会破坏字段计数,需特殊处理):
bash
# 用更稳健的方式:先提取 request 再处理
awk '{
# $1 ip, $4 时间开始, $7 request(带引号), 之后是 status...
# 因为 request 含空格,字段位置会偏移
# 用正则提取更稳
match($0, /"([A-Z]+) ([^ ]+) HTTP/); req=$2
match($0, /" ([0-9]{3}) /); code=substr($0, RSTART+2, RSTART+RLENGTH-3)
print code, req
}' /var/log/nginx/access.log
判断:自定义 log_format 让字段含空格(如 $request)会破坏 awk 默认字段切分,要么改 log_format 用分隔符(如 |),要么 awk 里用正则提取。生产建议 log_format 用竖线分隔便于 awk:
nginx
log_format kv '$remote_addr|$time_local|$request|$status|$body_bytes_sent|$request_time';
# awk -F'|' '{print $4, $6}' 直接取
7.2 实时关键字告警脚本
bash
#!/usr/bin/env bash
# /usr/local/bin/log_alert.sh
# 实时跟踪日志,匹配关键字则记录并发告警
set -uo pipefail
LOG="/var/log/app/app.log"
KEYWORDS="(ERROR|FATAL|OOM|OutOfMemory)"
ALERT_LOG="/var/log/app/alert.log"
ALERT_DIR="/data/alert"
mkdir -p "$ALERT_DIR"
tail -F "$LOG" 2>/dev/null \
| grep --line-buffered -E "$KEYWORDS" \
| while IFS= read -r line; do
ts="$(date '+%F %T')"
# 脱敏
masked="$(echo "$line" | sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g')"
printf '%s %s\n' "$ts" "$masked" | tee -a "$ALERT_LOG" >> "$ALERT_DIR/alert_$(date +%F).log"
done
风险与审校:
-
tail -F文件轮转时自动跟随新文件,
-F比-f稳健。 -
--line-buffered保证实时。
-
脱敏在告警前做,避免敏感信息进告警。
-
while 循环逐行处理,
IFS=保留行首空格,read -r不解释反斜杠。 -
该脚本常驻,用 systemd 管理:
ini
# /etc/systemd/system/log_alert.service
[Unit]
Description=Log Alert Watcher
After=network.target
[Service]
ExecStart=/usr/local/bin/log_alert.sh
Restart=always
RestartSec=10
User=ops
[Install]
WantedBy=multi-user.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now log_alert
八、日志或指标观察方法
8.1 日志增长速率
bash
# 每秒新增行数
watch -n 1 'wc -l /var/log/app/app.log'
# 粗估
tail -F /var/log/app/app.log > /tmp/count.log &
sleep 60
wc -l /tmp/count.log # 60 秒新增行数
kill %1
8.2 错误率趋势
bash
# 每 5 分钟错误数
awk '
{
# 假设 $4 是时间 [22/Jun/2026:14:55:03,提取到分钟
match($4, /:([0-9]{2}:[0-9]{2}):/, m)
minute=m[1]
total[minute]++
if ($9 ~ /^5/) err[minute]++
}
END {
for (m in total) printf "%s total=%d err=%d rate=%.2f%%\n", m, total[m], err[m]+0, (err[m]+0)/total[m]*100
}' /var/log/nginx/access.log | sort
注意:上面用了 gawk 的 match(s, r, arr) 三参数形式(gawk 扩展,POSIX awk 不支持),不同 awk 实现字段可能略有差异,mawk 不支持三参数 match,需改用 substr+index 手动提取。生产脚本确认是 gawk 再用三参数 match。
8.3 监控指标采集思路
日志关键字计数可输出 Prometheus 文本指标,由 node_exporter textfile 采集:
bash
#!/usr/bin/env bash
# /usr/local/bin/log_metric.sh
METRIC="/var/log/node_exporter/log_metric.prom"
ERR_COUNT=$(grep -c 'ERROR' /var/log/app/app.log)
FATAL_COUNT=$(grep -c 'FATAL' /var/log/app/app.log)
cat > "$METRIC" <<EOF
# HELP log_error_count ERROR 关键字累计计数
# TYPE log_error_count gauge
log_error_count{app="app"} $ERR_COUNT
# HELP log_fatal_count FATAL 关键字累计计数
# TYPE log_fatal_count gauge
log_fatal_count{app="app"} $FATAL_COUNT
EOF
告警阈值(结合业务基线调整,非绝对标准):
- ERROR 每分钟 > 100 告警。
- FATAL 任何出现立即告警。
- 5xx 错误率 > 1% 告警,> 5% 紧急。
九、排查路径:nginx 5xx 飙升完整闭环
现象
14:50 业务反馈页面打不开,监控显示 nginx 5xx 从 0.1% 飙到 15%,时间窗口 14:45~14:55。
初步判断
5xx 暴增通常是上游(应用/数据库)故障或 nginx 配置错误,先从日志定位是哪些 URL、哪些上游 5xx。
命令检查
bash
# 1. 确认 5xx 量级和时间分布
awk '$9 ~ /^5/ {print $4}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# 看哪个时间段 5xx 集中
# 2. 5xx 涉及哪些 URL
awk '$9 ~ /^5/ {split($7,a," "); print a[2]}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
# 3. 5xx 来自哪些上游(如果日志记了 upstream)
grep -E ' 5[0-9]{2} ' /var/log/nginx/access.log | grep -oE 'upstream:[^ ]+' | sort | uniq -c | sort -rn
# 4. 对应时间段的 error.log
awk '$4 ~ /14:[45][0-9]:/' /var/log/nginx/error.log | grep -E 'upstream|connect|timeout|refused'
关键指标
- 5xx 集中在某 1~2 个 URL:可能是该接口的上游故障。
- 5xx 全 URL 都有:可能是 nginx 自身或全局上游问题。
- error.log 有
connect() refused/upstream timed out:上游不可达或慢。
根因定位
假设 error.log 显示:
2026/06/22 14:46:12 [error] 1234#0: *567 connect() failed (111: Connection refused) while connecting to upstream
且应用服务器 10.0.0.21 的进程在 14:46 挂了。根因:应用进程崩溃,nginx 转发失败返回 502。
进一步定位应用为何崩:
bash
# 应用日志
ssh 10.0.0.21 'tail -n 1000 /var/log/app/app.log | grep -E "ERROR|OOM|FATAL"'
# 系统 OOM
ssh 10.0.0.21 'dmesg -T | grep -i "killed process" | tail'
# 进程状态
ssh 10.0.0.21 'systemctl status app'
若 dmesg 显示 Killed process 12345 (java) total-vm:...,根因是 OOM,应用内存泄漏或实例内存不足。
修复
bash
# 1. 重启应用(恢复服务优先)
ssh 10.0.0.21 'sudo systemctl restart app'
# 2. 验证 5xx 下降
awk '$4 ~ /14:5[5-9]/ {code[$9]++} END{for(c in code) print c, code[c]}' /var/log/nginx/access.log
# 3. 治本:扩内存或排查内存泄漏(后续)
风险:重启服务会断当前连接,确认影响范围后操作,优先切流量到健康实例再重启故障实例。
验证
bash
# 5xx 回落
awk '$9 ~ /^5/ {err++} END{print "5xx="err}' /var/log/nginx/access.log
# 业务监控确认
# 抽样访问验证
curl -I https://shop.com/api/orders
回滚
重启应用后若仍故障:
- 切回有状态的应用实例(若保留)。
- 回滚最近发布的应用版本(若是发布导致 OOM)。
- nginx 临时摘掉该上游,保留部分服务。
复盘
- 加 OOM 告警,内存使用率 > 80% 告警。
- 进程崩溃自动重启(systemd Restart=always)。
- nginx 加上游健康检查,故障上游自动摘除。
- 日志保留:error.log 至少 7 天,便于回溯。
十、排查路径:日志解析字段错位
现象
统计 nginx 状态码,awk '{code[$9]++} END{...}' 输出全是乱七八糟的字符串,不是 200/404/500。
初步判断
字段位置错位。可能日志里有含空格的字段(如 $request 带引号、$http_user_agent 带空格),awk 默认按空格切分,字段数和预期不一致。
命令检查
bash
# 看一行实际字段数
awk '{print NF}' /var/log/nginx/access.log | sort | uniq -c
# 如果字段数不一,说明有的行有额外空格
# 看一行各字段
head -1 /var/log/nginx/access.log | awk '{for(i=1;i<=NF;i++) print i": ["$i"]"}'
根因
combined 格式 $request 是 "GET /api/orders HTTP/1.1",含空格,awk 把它切成 3 个字段,导致 $9 不再是 status。
修复
方案一:改 log_format 用分隔符(推荐,见 7.1)。
方案二:awk 里用正则提取 status:
bash
awk 'match($0, /" ([0-9]{3}) /, m){code[m[1]]++} END{for(c in code) print c, code[c]}' /var/log/nginx/access.log
方案三:用 FPAT(gawk)按字段模式而非分隔符切:
bash
awk 'BEGIN{FPAT="([^ ]+)|(\"[^\"]+\")"} {code[$8]++} END{for(c in code) print c, code[c]}' /var/log/nginx/access.log
FPAT 是 gawk 扩展,定义“字段长什么样”而非“分隔符是什么”,处理带引号字段最稳。不同 awk 实现可能不支持 FPAT,确认 gawk 再用。
验证
bash
# 状态码分布正常
awk '...' /var/log/nginx/access.log | sort
# 输出 200/404/500 等正常码
十一、排查路径:sed -i 批量改坏配置
现象
用 sed -i 's/port=3306/port=3307/g' /etc/app/*.conf 批量改端口,改完应用起不来,发现正则写成了 s/port=.*/port=3307/,把每行 port 后面的内容全替换没了。
初步判断
贪婪匹配 .* 把整行后面内容吃掉,且 -i 没加备份,无法回滚。
命令检查
bash
# 看改坏的样子
grep '^port=' /etc/app/app.conf
# 显示 port=3307(原本 port=3306 max=100 这样的行后半段没了)
# 找备份
ls -l /etc/app/*.conf.bak
# 没有,因为 sed -i 没加后缀
根因
-
.*贪婪匹配到行尾。
-
sed -i无备份后缀。
-
没 dry-run。
修复与回滚
- 若有配置版本管理(git/etcd 备份),从版本库恢复。
- 若无备份,从同类机器 rsync 一份配置覆盖:
bash
rsync -av 10.0.0.22:/etc/app/ /etc/app/
# 风险:覆盖目标机器配置,确认 10.0.0.22 配置正确且与目标机器角色一致
- 或从监控系统的配置快照(如 Ansible playbook 重新 apply)恢复。
验证
bash
# 配置恢复
diff /etc/app/app.conf 10.0.0.22:/etc/app/app.conf
# 应用启动正常
sudo systemctl restart app
sudo systemctl status app
预防
bash
# 1. 永远加备份后缀
sed -i.bak 's/port=3306/port=3307/g' /etc/app/app.conf
# 2. 先 dry-run(不加 -i 看输出)
sed 's/port=3306/port=3307/g' /etc/app/app.conf | grep '^port='
# 3. 精确匹配,避免贪婪
sed -i.bak -E 's/^port=[0-9]+/port=3307/' /etc/app/app.conf
# 只替换 port=数字 部分,保留行后内容
# 4. diff 检查
diff /etc/app/app.conf /etc/app/app.conf.bak
十二、风险提醒
12.1 高风险操作清单
| 操作 | 风险 | 必须做的事 |
| — | — | — |
| sed -i 无备份 | 改坏不可回滚 | 强制 sed -i.bak |
| sed -i 贪婪正则 | 误替换过多 | 精确匹配 + dry-run |
| awk 大文件全数组 | OOM | 用 sort/uniq 替代或采样 |
| grep 输出敏感信息 | 泄露 | 脱敏后输出 |
| grep -P | 跨平台不兼容 | 确认 GNU grep,否则用 ERE |
| sed 处理含特殊字符输入 | 注入误匹配 | 转义或用固定字符串 |
| CRLF 文件未处理 \r | 字段错位 | sed 's/\r$//' 预处理 |
| BOM 文件 | 第一字段带 BOM | sed '1s/^\xEF\xBB\xBF//' |
| 实时告警脚本常驻 | 内存/句柄泄漏 | systemd 管理 + Restart |
| 批量重定向覆盖 | 覆盖已有文件 | > 前确认,用 >> 追加 |
12.2 正则陷阱
-
BRE vs ERE:
{}、+、?、()、|在 BRE 要转义,ERE 不用。 -
贪婪
.*会匹配到行尾,需要非贪婪用.*?(PCRE)或改写正则。 -
.不匹配换行(默认),多行匹配要特殊处理。
-
^``$锚点,
^在字符类[^abc]里是取反。 -
grep -F固定字符串模式,要搜的字面含正则元字符时用
-F避免误解释。
12.3 性能陷阱
-
grep 大文件无
--line-buffered接管道:延迟。 -
awk
length(arr)在大数组上 O(n),频繁调用慢。 -
sed
-i大文件用临时文件占盘,盘满会失败。 -
sort大文件默认内存排序,加
--buffer-size或分片。 -
多次 grep 同一文件不如一次 awk 多条件。
十三、验证方式
13.1 验证命令语义
复杂管道先小样本跑通:
bash
head -n 1000 /var/log/nginx/access.log | awk '{code[$9]++} END{for(c in code) print c, code[c]}'
# 输出符合预期再全量
13.2 验证 sed 替换
bash
# dry-run
sed 's/old/new/g' file | grep 'new'
# diff 看变化
sed -i.bak 's/old/new/g' file
diff file file.bak
13.3 验证脱敏
bash
# 脱敏后扫描残留敏感信息
grep -rE '1[3-9][0-9]{9}|[0-9]{17}[0-9Xx]' /data/secure/
# 无输出即脱敏干净
13.4 验证统计准确性
bash
# 用两种方法交叉验证 UV
awk '{ip[$1]++} END{print length(ip)}' /var/log/nginx/access.log
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l
# 两个数应一致
十四、回滚方案
14.1 sed -i 回滚
bash
# 有 .bak
mv file.bak file
# 或
cp file.bak file
# 无 .bak:从版本库/同类机器/Ansible 恢复
git checkout -- file
rsync -av peer:/path/file file
ansible-playbook -t config site.yml
14.2 脚本输出回滚
awk/sed 输出到新文件而非覆盖:
bash
awk '...' input.log > output.new
# 验证 output.new 无误再替换
mv output.new output.final
14.3 告警脚本回滚
bash
sudo systemctl stop log_alert
sudo systemctl disable log_alert
# 删常驻脚本
sudo rm /etc/systemd/system/log_alert.service
sudo systemctl daemon-reload
十五、生产环境注意事项
-
sed -i强制加备份后缀,先 dry-run。
-
大文件 awk 优先 sort/uniq 替代全数组,或采样。
-
grep/sed/awk 输出敏感信息前脱敏。
-
跨平台脚本区分 GNU/BSD(
sed -i、grep -P)。 -
CRLF/BOM 文件预处理
sed 's/\r$//'。 -
自定义 log_format 用分隔符便于 awk,避免含空格字段。
-
实时跟踪加
--line-buffered/-u/fflush。 -
常驻脚本用 systemd 管理,Restart=always。
-
监控指标靠 textfile 采集,阈值结合业务基线。
-
日志保留至少 7 天,压缩归档,权限受控。
十六、总结
grep/sed/awk 是日志处理的三把瑞士军刀,分工明确:
- grep 选行:按关键字/正则快速过滤,
-E/-o/-c/--line-buffered是高频参数。 - sed 改行:替换/删除/插入,
-i.bak强制备份,CRLF/BOM 要预处理,GNU/BSD-i差异要分清。 - awk 算列:字段切分/聚合统计,关联数组是核心,大文件防 OOM,FPAT/match 三参数是 gawk 扩展。
纪律:grep 输出脱敏、sed 改前备份、awk 大文件防 OOM、跨平台分清实现差异、实时跟踪开行缓冲、常驻脚本 systemd 管。这六条做到,日志处理才不会在排障时掉链子。
记住:日志处理的终点不是敲出一条炫酷的 awk 单行,而是把“现象—过滤—清洗—统计—根因—验证”跑成一个可复现、可脱敏、可回滚的流程。
附录 A:grep 实战菜谱
A.1 多条件 AND
bash
# grep 不直接支持 AND,用管道
grep 'ERROR' app.log | grep 'order'
# 或用 -P 的 lookahead
grep -P '(?=.*ERROR)(?=.*order)' app.log
A.2 多文件统计
bash
# 各文件 ERROR 数
grep -c 'ERROR' /var/log/app/*.log
# 总数
grep -h 'ERROR' /var/log/app/*.log | wc -l # -h 不显示文件名
A.3 只匹配 IP
bash
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort | uniq -c | sort -rn | head
A.4 提取两个时间点之间的日志
bash
# 假设日志行首是时间戳
awk '/2026-06-22 14:00:00/,/2026-06-22 15:00:00/' app.log
# sed 版
sed -n '/2026-06-22 14:00:00/,/2026-06-22 15:00:00/p' app.log
判断:awk/sed 的范围模式 pat1,pat2 输出从匹配 pat1 到匹配 pat2 之间的行,时间范围提取最常用。
A.5 找不含某关键字的行
bash
grep -v 'DEBUG' app.log # 不含 DEBUG
grep -vE 'DEBUG|INFO' app.log # 不含 DEBUG 也不含 INFO
A.6 统计字符出现次数(非行)
bash
grep -o 'error' app.log | wc -l
# 一行出现多次 error 都计入
A.7 二进制文件处理
bash
# grep 默认对二进制文件只报 Binary file matches
grep -a 'pattern' binfile # -a 当文本处理
grep -I 'pattern' * # -I 跳过二进制文件
附录 B:sed 实战菜谱
B.1 配置文件改值
bash
# 改 key=value 形式配置
sed -i.bak -E 's/^(max_connections[[:space:]]*=[[:space:]]*)[0-9]+/\11000/' /etc/my.cnf
# 把 max_connections = 200 改成 1000,保留 key 和等号格式
B.2 注释/取消注释
bash
# 注释某行(行首加 #)
sed -i.bak '/^max_connections/s/^/#/' /etc/my.cnf
# 取消注释
sed -i.bak '/^#max_connections/s/^#//' /etc/my.cnf
B.3 插入行
bash
# 在第 5 行后插入
sed -i '5a\new line content' file
# 在第 5 行前插入
sed -i '5i\new line content' file
# 在匹配行后插入
sed -i '/\[mysqld\]/a\new_param=1' /etc/my.cnf
B.4 删除空行和注释行
bash
sed -i '/^$/d; /^#/d; /^[[:space:]]*#/d' file
# 删空行、# 开头注释、行首空白后 #
B.5 大小写转换
bash
sed 's/[a-z]/\U&/g' file # 转大写(GNU)
sed 's/[A-Z]/\L&/g' file # 转小写(GNU)
# 或用 tr
tr 'a-z' 'A-Z' < file
B.6 CSV 列调换
bash
# 调换第 2、3 列
sed -E 's/([^,]+),([^,]+),([^,]+)/\1,\3,\2/' file.csv
B.7 行号范围操作
bash
sed -n '10,20p' file # 打印 10~20 行
sed '10,20d' file # 删除 10~20 行
sed '10q' file # 打印前 10 行(等价 head -10)
附录 C:awk 实战菜谱
C.1 按条件过滤并格式化
bash
awk -F',' '$3 > 100 && $5 == "OK" {printf "%-20s %8d %s\n", $1, $3, $5}' file.csv
C.2 分组求和与平均
bash
# 按第 1 列分组,对第 3 列求和与平均
awk -F',' '{sum[$1]+=$3; cnt[$1]++} END{for(k in sum) printf "%s\tsum=%d\tavg=%.2f\n", k, sum[k], sum[k]/cnt[k]}' file.csv
C.3 按多列去重
bash
# 按第 1、2 列组合去重,保留第一次出现
awk -F',' '!seen[$1,$2]++' file.csv
判断:!seen[key]++ 是 awk 去重惯用法,第一次见 key 时 seen[key] 为 0 取反为真打印,之后为假不打印。
C.4 行列转换
bash
# 行转列:每行 3 个字段转成 3 行
awk '{for(i=1;i<=NF;i++) print $i}' file
# 列转行:3 行合一列
awk '{a[NR]=$0} END{for(i=1;i<=NR;i++) printf "%s%s", a[i], (i%3==0?"\n":",")}' file
C.5 统计字符出现频率
bash
# 统计每个字符出现次数
awk '{for(i=1;i<=length($0);i++) freq[substr($0,i,1)]++} END{for(c in freq) print c, freq[c]}' file | sort -k2 -rn
C.6 合并两个文件(类似 join)
bash
# file1: id,name ; file2: id,score;按 id 合并
awk -F',' 'NR==FNR{name[$1]=$2; next} {print $1, name[$1], $2}' file1 file2
# NR==FNR 时是第一个文件,建立映射;之后是第二个文件,查映射输出
判断:NR==FNR{...; next} 是 awk 合并文件的经典模式,第一文件建索引,第二文件查。
C.7 计算分位数
bash
# 计算第 3 列的 P95(需先排序)
awk '{a[NR]=$3} END{
n=NR
asort(a) # gawk 排序函数
p95=int(n*0.95)
if(p95<1) p95=1
print "P95=", a[p95]
}' file
注意:asort 是 gawk 扩展,POSIX awk 没有。不同 awk 实现可能略有差异,确认 gawk。
C.8 按时间窗口聚合
bash
# 假设 $1 是时间戳到分钟 2026-06-22 14:55
awk -F'|' '{
min=substr($1,1,16)
total[min]++
if($4 ~ /^5/) err[min]++
} END{
for(m in total) printf "%s\ttotal=%d\terr=%d\trate=%.2f%%\n", m, total[m], err[m]+0, (err[m]+0)/total[m]*100
}' /var/log/nginx/access.log | sort
附录 D:组合管道实战
D.1 Top10 慢请求 URL
bash
# 假设 log_format 最后字段是 request_time
awk -F'|' '{print $NF, $3}' /var/log/nginx/access.log \
| sort -rn | head -10
D.2 每小时请求量
bash
awk -F'|' '{print substr($2,1,13)}' /var/log/nginx/access.log \
| sort | uniq -c
# substr 取到小时 2026-06-22 14
D.3 404 请求的 Top URL
bash
awk -F'|' '$4=="404"{print $3}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head
D.4 抓取某 IP 的所有访问
bash
grep '^192.168.1.10' /var/log/nginx/access.log \
| awk -F'|' '{print $3, $4}' | sort | uniq -c | sort -rn
D.5 找出响应大小异常大的请求
bash
awk -F'|' '$5 > 1000000 {print $2, $3, $5}' /var/log/nginx/access.log
# body_bytes_sent > 1MB 的请求
附录 E:大文件分片并行处理
bash
# 1. 分片
split -l 5000000 -d large.log part_
# 2. 并行处理
ls part_* | xargs -P 8 -I{} sh -c 'awk "{code[\$9]++} END{for(c in code) print c, code[c]}" {} > {}.out'
# 3. 合并结果
cat part_*.out | awk '{code[$1]+=$2} END{for(c in code) print c, code[c]}' | sort -k2 -rn
判断:分片并行适合 CPU 密集的 awk 统计,IO 密集(如 grep)并行收益小甚至更慢(抢盘)。并行度按 CPU 核数,监控 IO。
风险:分片产生临时文件占盘,处理完即删;并行输出合并要正确累加。
附录 F:BSD vs GNU 差异速查
| 操作 | GNU | BSD (macOS) |
| — | — | — |
| sed -i 备份 | sed -i.bak | sed -i '.bak' |
| sed -i 不备份 | sed -i | sed -i '' |
| sed ERE | -E 或 -r | -E (不支持 -r) |
| grep -P | 支持 | 不支持 |
| grep --color | 支持 | 支持 |
| awk | gawk(功能全) | 原版 awk(弱) |
| awk asort/asorti | gawk 支持 | 不支持 |
| awk match 三参数 | gawk 支持 | 不支持 |
| sed \U \L 大小写 | GNU 支持 | BSD 不支持 |
| tr -d '\r' | 一致 | 一致 |
| sort -R 随机 | GNU | BSD 部分支持 |
跨平台建议:
- macOS 装
gsed/gawk/ggrep(brew install coreutils gnu-sed gawk grep),脚本里优先用 GNU 版。 - 或脚本检测平台分支:
if [[ "$(uname)" == "Darwin" ]]; then SED=gsed; else SED=sed; fi。
附录 G:性能对比与选型
| 场景 | 推荐 | 理由 | | — | — | — | | 简单关键字过滤 | grep | 最快 | | 多关键字 OR | grep -E | 一条命令 | | 多关键字 AND | 管道 grep 或 awk | awk 单次扫描更快 | | 替换 | sed | 流编辑器 | | 按列统计 | awk | 关联数组 | | 超大文件去重 | sort/uniq | 磁盘排序不 OOM | | 超大文件求和 | awk 单变量 | 不进数组,安全 | | 多文件合并 | awk NR==FNR | 单次扫描 | | 实时跟踪 | tail -F + grep –line-buffered | 实时 | | 压缩日志 | zgrep/zcat | 不解压 |
选型心法:能用 grep 不用 awk(grep 更快),能用 awk 不用 sed(awk 更强),能用 sort/uniq 不用 awk 全数组(防 OOM),实时场景必开行缓冲。
附录 H:常见报错与排查
| 现象 | 原因 | 解决 |
| — | — | — |
| sed: -i may not be used with stdin | -i 用在管道输入 | -i 只能用于文件 |
| sed: invalid option | BSD sed -i 缺参数 | sed -i '' |
| awk syntax error | mawk 不支持 gawk 扩展 | 确认 gawk 或改写 |
| grep invalid range | 字符类范围 locale 问题 | 设 LC_ALL=C |
| awk 字段错位 | 含空格字段 | 用 FPAT 或正则提取 |
| sed 替换没生效 | BRE 元字符未转义 | 用 -E 或转义 |
| awk OOM killed | 数组太大 | sort/uniq 替代 |
| grep 无输出 | 大小写/正则不匹配 | -i 或检查正则 |
| sort: write failed: No space | 临时盘满 | 清理 /tmp 或 sort -T /data/tmp |
| 中文乱码 | locale 不对 | export LANG=zh_CN.UTF-8 |
排查心法:
- 报错先看原文,区分语法错误、版本差异、资源不足。
- 语法错误查 BRE/ERE 转义、GNU/BSD 差异。
- 资源不足(OOM、盘满)换策略:sort/uniq 替代数组、
sort -T换临时盘。 - 输出不符预期先小样本
head跑通再全量。
附录 I:日志分析自动化脚本
把常用分析固化为脚本,排障时一键跑:
bash
#!/usr/bin/env bash
# /usr/local/bin/nginx_log_report.sh
# nginx 日志快速分析报告
set -uo pipefail
LOG="${1:-/var/log/nginx/access.log}"
OUT="/data/report/nginx_$(date +%F_%H%M).txt"
mkdir -p /data/report
{
echo "===== Nginx 日志分析报告 ====="
echo "日志: $LOG"
echo "生成时间: $(date '+%F %T')"
echo
echo "## 总览"
echo "总请求数: $(wc -l < "$LOG")"
echo "去重 IP(UV): $(awk '{print $1}' "$LOG" | sort -u | wc -l)"
echo
echo "## 状态码分布"
awk -F'|' '{code[$4]++} END{for(c in code) printf "%s\t%d\n", c, code[c]}' "$LOG" | sort
echo
echo "## Top10 IP"
awk -F'|' '{ip[$1]++} END{for(i in ip) print ip[i], i}' "$LOG" | sort -rn | head -10
echo
echo "## Top10 URL"
awk -F'|' '{u[$3]++} END{for(x in u) print u[x], x}' "$LOG" | sort -rn | head -10
echo
echo "## 5xx 错误 Top URL"
awk -F'|' '$4 ~ /^5/{u[$3]++} END{for(x in u) print u[x], x}' "$LOG" | sort -rn | head -10
echo
echo "## 大响应 Top10(>100KB)"
awk -F'|' '$5 > 102400 {print $5, $3}' "$LOG" | sort -rn | head -10
echo
echo "## 慢请求 Top10(request_time)"
awk -F'|' '{print $NF, $3}' "$LOG" | sort -rn | head -10
} | tee "$OUT"
echo "报告已保存: $OUT"
审校要点:
- 输出落
/data/report,权限受控。 - 假设 log_format 是
|分隔,实际按 7.1 配置。 - 各统计独立 awk,互不影响,便于增删。
- 报告含脱敏前数据,分享前手工脱敏 IP。
附录 J:grep/sed/awk 自检清单
日志处理脚本上线前逐项打勾:
- [ ] sed -i 强制加备份后缀
- [ ] sed 替换正则精确匹配,避免贪婪
- [ ] awk 大文件不进全数组,用 sort/uniq 或采样
- [ ] 输出敏感信息已脱敏
- [ ] 跨平台脚本区分 GNU/BSD
- [ ] CRLF/BOM 文件已预处理
- [ ] log_format 字段位置已确认
- [ ] 实时跟踪开 –line-buffered
- [ ] 常驻脚本 systemd 管理
- [ ] 临时文件处理完即删
- [ ] 输出落权限受控目录
- [ ] 复杂管道先小样本验证
- [ ] 统计结果交叉验证(两种方法对比)
- [ ] 监控阈值结合业务基线
- [ ] 日志保留 ≥ 7 天,压缩归档
附录 K:一句话命令卡片
bash
# 状态码分布
awk -F'|' '{code[$4]++} END{for(c in code) print c, code[c]}' access.log | sort
# Top10 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# 5xx 错误率
awk '$9 ~ /^5/{e++} END{print e/NR*100"%"}' access.log
# 实时跟踪 ERROR
tail -F app.log | grep --line-buffered ERROR
# 脱敏手机号
sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g'
# 删 CRLF
sed -i 's/\r$//' file
# 改配置带备份
sed -i.bak -E 's/^(port=)[0-9]+/\13307/' config
# 去重保留首次
awk '!seen[$1]++' file
# 时间范围提取
sed -n '/14:00:00/,/15:00:00/p' app.log
# 大文件求和
awk '{sum+=$3} END{print sum}' big.log
# 压缩日志查
zgrep ERROR access.log.gz
把这张卡片贴在工位,日志排障时大半场景能直接套。
附录 L:日志轮转与三剑客的配合
日志被 logrotate 轮转后,处理历史日志要考虑压缩和归档:
bash
# logrotate 典型配置
cat /etc/logrotate.d/nginx
# 每天轮转,保留 30 天,压缩
# 处理轮转后的压缩日志
# 1. 跨所有归档统计
zcat /var/log/nginx/access.log*.gz | awk '...'
# 2. 跨当前+归档统计
zcat /var/log/nginx/access.log.*.gz /var/log/nginx/access.log | awk '{code[$9]++} END{...}'
# 3. 按日期分文件统计
for f in /var/log/nginx/access.log*.gz; do
echo "$f:"
zcat "$f" | awk '{code[$9]++} END{for(c in code) print c, code[c]}'
done
判断与风险:
-
zcat多文件解压流式输出,不落盘,省空间。
-
跨多个归档统计耗时长,建议先
for逐文件出结果再合并,避免中途失败重跑。 -
logrotate 用
copytruncate时日志有短暂丢失窗口,统计会有微小偏差,属正常。
logrotate 配置示例
bash
# /etc/logrotate.d/app
/var/log/app/app.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 app app
sharedscripts
postrotate
systemctl reload app 2>/dev/null || true
endscript
}
风险:postrotate 里的 systemctl reload 属重启类操作,确认 app 支持 reload 且配置正确,否则可能中断服务。delaycompress 推迟一轮压缩,避免刚轮转的文件还在被写时压缩损坏。
附录 M:systemd journalctl 与三剑客
systemd 服务日志在 journald,用 journalctl 查,配合三剑客:
bash
# 查某服务日志
journalctl -u app --since '14:00' --until '15:00'
# 实时跟踪
journalctl -u app -f
# 只看错误优先级
journalctl -u app -p err
# 输出 JSON 便于 awk
journalctl -u app -o json | jq '.MESSAGE'
# 配合 grep
journalctl -u app --since today | grep ERROR
# 配合 awk 统计
journalctl -u app --since '14:00' --no-pager | awk '{...}'
判断:
-
journalctl --since/--until时间过滤比 grep 日志时间戳更准(journald 按时间索引)。
-
--no-pager取消分页,管道处理必备。
-
-o json输出结构化,适合复杂解析,但需
jq,awk 处理 JSON 较弱。 -
journald 日志持久化要配
/etc/systemd/journald.conf的Storage=persistent,否则重启丢失。
附录 N:监控告警脚本完整版
结合脱敏、限频、去重的告警脚本:
bash
#!/usr/bin/env bash
# /usr/local/bin/log_alert_smart.sh
# 智能日志告警:脱敏、限频、去重
set -uo pipefail
LOG="/var/log/app/app.log"
STATE="/var/lib/log_alert/state"
ALERT_LOG="/var/log/app/alert.log"
KEYWORDS="(FATAL|OOM|OutOfMemory|OutOfMemoryError)"
DEDUP_WINDOW=300 # 5 分钟内相同关键字去重
mkdir -p "$(dirname "$STATE")"
tail -F "$LOG" 2>/dev/null \
| grep --line-buffered -E "$KEYWORDS" \
| while IFS= read -r line; do
# 脱敏
masked=$(printf '%s' "$line" | sed -E \
-e 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g' \
-e 's/([0-9]{17})[0-9Xx]/\1*/g')
# 提取关键字做去重 key
key=$(printf '%s' "$masked" | grep -oE 'FATAL|OOM|OutOfMemory' | head -1)
now=$(date +%s)
state_file="$STATE/${key}.ts"
# 限频:同 key 在窗口内只告警一次
if [[ -f "$state_file" ]]; then
last=$(cat "$state_file" 2>/dev/null || echo 0)
if (( now - last < DEDUP_WINDOW )); then
continue
fi
fi
echo "$now" > "$state_file"
ts=$(date '+%F %T')
printf '%s [%s] %s\n' "$ts" "$key" "$masked" | tee -a "$ALERT_LOG"
# 触发外部告警(示例思路,实际接企业微信/钉钉/邮件)
# send_alert "$key" "$masked"
done
审校要点:
-
脱敏在告警前做,多重 sed 处理手机号、身份证。
-
限频用文件 state 记录上次告警时间,同关键字窗口内去重,避免刷屏。
-
send_alert是示例思路,实际接入需用对应 webhook,且 token 用环境变量或配置文件,不硬编码。
-
常驻脚本 systemd 管理,Restart=always。
systemd unit
ini
# /etc/systemd/system/log-alert.service
[Unit]
Description=Smart Log Alert Watcher
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/log_alert_smart.sh
Restart=always
RestartSec=10
User=ops
# 安全加固
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/log_alert /var/log/app
[Install]
WantedBy=multi-user.target
判断:ProtectSystem=strict 限制文件系统写入,只允许 ReadWritePaths 列出的目录,减少脚本被篡改或误写的风险。
附录 O:三剑客学习路线与练习
把掌握三剑客的进阶路线列出来,便于团队培养:
阶段一:能查能统计
- grep 关键字过滤、
-c/-n/-v/-i。 - awk 按字段打印、求和、计数。
- sed 简单替换
s/a/b/g。
练习:从 access.log 统计 Top10 IP、状态码分布。
阶段二:正则与管道
- BRE/ERE 差异、字符类、锚点、重复。
- 多命令管道
grep | sort | uniq -c | sort -rn | head。 - awk 条件、关联数组、BEGIN/END。
练习:提取某时间段日志、按 URL 分组统计平均响应时间。
阶段三:清洗与脱敏
- sed
-i.bak、CRLF/BOM 处理、配置改值。 - 脱敏正则、awk 字段重组。
- 跨平台 GNU/BSD 差异。
练习:批量改配置文件、脱敏日志后输出报告。
阶段四:大文件与性能
- awk 防 OOM、sort/uniq 替代、采样。
- 分片并行、
--line-buffered。 - 压缩日志 zgrep/zcat。
练习:处理 10GB 日志统计 UV 不 OOM。
阶段五:自动化与监控
- 报告脚本、告警脚本、systemd 管理。
- 监控指标采集、阈值调优。
- 日志轮转、journald。
练习:写一个实时告警 + 每日报告的完整方案。
每个阶段都做一次真实日志排障演练,从现象到根因到复盘,跑通闭环才算过关。
附录 P:运维自检与复盘模板
日志排障后用模板复盘,沉淀经验:
【故障时间】2026-06-22 14:45~14:55
【现象】nginx 5xx 从 0.1% 飙到 15%
【影响】订单接口不可用,影响 30 分钟
【发现】监控告警 + 客户反馈
【排查过程】
1. awk 统计 5xx 时间分布 → 集中 14:46~14:55
2. awk 统计 5xx URL → 集中 /api/orders
3. grep error.log upstream → connect refused 10.0.0.21
4. ssh 10.0.0.21 dmesg → OOM killed process
5. 根因:应用内存泄漏导致 OOM 进程崩溃
【处理】重启应用恢复,扩内存,排查泄漏代码
【验证】5xx 回落 0.1%,业务正常
【改进】
- 加 OOM 告警(内存 > 80%)
- 进程 Restart=always
- nginx 上游健康检查
- 日志保留延长到 30 天
【涉及命令】
awk '$9 ~ /^5/' / grep upstream / dmesg -T | grep killed
把每次复盘的“涉及命令”沉淀成团队命令库,新人照着学,排障效率指数级提升。
附录 Q:最终心法
三剑客的终极心法,浓缩成五条:
- 先小样本后全量:复杂管道必先
head跑通。 - 改前备份:sed -i.bak 是底线。
- 防 OOM:大文件优先 sort/uniq,慎用全数组。
- 脱敏先行:输出敏感信息前先脱敏。
- 闭环排障:现象—过滤—清洗—统计—根因—验证,不跳步。
这五条贯穿 grep/sed/awk 的所有场景,是日志处理的底线纪律。能做到这五条,三剑客就是真正的瑞士军刀;做不到,就是凌晨两点把自己埋进日志里的铲子。
文末福利
今天给大家分享一份超级牛掰的Linux学习笔记,足足有1456页!是一位Linux运维大佬整理分享的,分享是获得大佬同意的,大家有需要的尽管收藏起来!
笔记介绍
这份笔记非常全面且详细,从Linux基础到shell脚本,再到防火墙、数据库、日志服务管理、Nginx、高可用集群、Redis、虚拟化、Docker等等,与其说Linux学习笔记,不如说是涵盖了运维各个核心知识。
并且图文并茂,代码清晰,每一章下面都有更具体详细的内容,十分适合Linux运维学习参考!
笔记展示
笔记下载
扫描下方二维码,回复暗号“1456页Linux笔记“,即可100%免费领取成功
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《文本三剑客 grep / sed / awk:日志处理的瑞士军刀》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论