文本三剑客grep/sed/awk:日志处理的瑞士军刀

admin 2026-06-30 06:58:27 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文系统讲解Linux文本处理三剑客grep、sed、awk在日志分析中的实战应用。针对运维常见痛点如正则匹配错误、跨平台差异、大文件处理OOM、敏感信息泄露等,提供具体解决方案和风险规避方法。通过nginx日志分析案例演示从日志定位、过滤清洗到统计分析的完整流程,包含GNU/BSD环境差异说明和实时处理技巧,具有较强的工程指导价值。 综合评分: 87 文章分类: 安全工具,WEB安全,安全运营,实战经验,技术标准


cover_image

文本三剑客 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 出来直接贴到群里脱敏没做,泄露了。

这三个工具的难点不在“会不会敲”,而在于:

  1. 正则的贪婪与非贪婪、BRE/ERE/PCRE 三种风格、元字符转义,差一个字符结果全错。
  2. GNU 与 BSD 实现差异(sed -igrep -Pawk 函数库),跨平台脚本处处是坑。
  3. 大文件场景的内存与缓冲:awk 全表加载会 OOM、grep 缓冲不实时、sed 大文件 -i 用临时盘。
  4. 文件编码与行尾:CRLF、UTF-8 BOM、中文 locale,会让字段切分和正则匹配全部错位。
  5. 生产日志含敏感信息,处理时必须脱敏,输出要落地到权限受控目录。

本文把 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 -Esed -E(GNU)/sed -rawk 默认。+?{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 -uawk fflush()

  • tail -F

    默认行缓冲,适合实时跟踪。

3.6 高风险动作

  • sed -i

    不加备份后缀:原地改坏不可回滚。

  • sed -i

    正则写错:批量改错所有匹配行。

  • awk 大文件全表加载到关联数组:OOM。

  • grep 不脱敏输出敏感信息:泄露。

  • sed

    /awk 处理含特殊字符的输入未转义:注入或误匹配。

四、整体排查或实施思路

日志处理是一个完整闭环:现象 → 定位日志文件 → 粗筛(grep)→ 清洗(sed)→ 聚合统计(awk)→ 定位根因 → 验证 → 复盘。

对应到操作流程:

  1. 先确认日志在哪、多大、是否压缩。
  2. 用 grep 粗筛出关心的行(按关键字、状态码、时间、URL)。
  3. 用 sed 清洗(去字段、统一格式、脱敏)。
  4. 用 awk 按字段聚合统计(TopN、UV、错误率、耗时分布)。
  5. 结合多个指标交叉判断根因,不只凭单一指标。
  6. 输出落权限受控目录,脱敏后再分享。

五、实战步骤

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&nbsp;> /tmp/patterns.txt <<'EOF'
ERROR
WARN
FATAL
EOF
grep -f /tmp/patterns.txt /var/log/app.log

# 多模式 AND(grep 不直接支持,用管道)
grep&nbsp;'ERROR'&nbsp;/var/log/app.log | grep&nbsp;'order'

判断:

  • -c

    数的是“匹配行数”不是“匹配次数”,一行里出现多次只算 1。要算次数用 grep -o ... | wc -l

  • -l

    只出文件名,适合从一堆日志里找“哪个文件含错误”。

  • -A/-B/-C

    看上下文,排障定位错误前后发生了什么。

5.3 grep 正则实战

bash

# 锚点:行首行尾
grep&nbsp;'^2026-06-22'&nbsp;/var/log/app.log &nbsp; &nbsp; &nbsp; &nbsp;# 以日期开头
grep&nbsp;'ERROR$'&nbsp;/var/log/app.log &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 以 ERROR 结尾

# 字符类
grep&nbsp;'[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}'&nbsp;/var/log/app.log &nbsp;&nbsp;# BRE 日期
grep -E&nbsp;'[0-9]{4}-[0-9]{2}-[0-9]{2}'&nbsp;/var/log/app.log &nbsp; &nbsp; &nbsp;# ERE 日期,更清晰

# 任意字符与重复
grep -E&nbsp;'a.c'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp;# a 和 c 之间任意一个字符
grep -E&nbsp;'a+c'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp;# a 一次或多次
grep -E&nbsp;'a?c'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp;# a 零次或一次
grep -E&nbsp;'a{3,5}c'&nbsp;file &nbsp; &nbsp;# a 3~5 次

# 非捕获、词边界(ERE 无,需 PCRE)
grep -P&nbsp;'\berror\b'&nbsp;file &nbsp;# 词边界,error 不在 errorx 里匹配(GNU grep 支持 -P)

# IP 地址(ERE)
grep -E&nbsp;'([0-9]{1,3}\.){3}[0-9]{1,3}'&nbsp;/var/log/nginx/access.log

# 状态码 5xx
grep -E&nbsp;' 5[0-9]{2} '&nbsp;/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&nbsp;'ERROR'&nbsp;/var/log/app.log |&nbsp;head&nbsp;-n 100

# 实时跟踪关键字
tail&nbsp;-F /var/log/app.log | grep --line-buffered&nbsp;'ERROR'

# 压缩日志不解压
zgrep&nbsp;'ERROR'&nbsp;/var/log/nginx/access.log.1.gz
zcat /var/log/nginx/access.log.1.gz | grep&nbsp;'ERROR'

# 多个压缩文件一起查
zgrep&nbsp;'ERROR'&nbsp;/var/log/nginx/access.log.*.gz

# 只看最近 N 行的匹配(先 tail 再 grep)
tail&nbsp;-n 100000 /var/log/app.log | grep&nbsp;'ERROR'

判断:

  • tail -F | grep --line-buffered

    是实时关键字告警的基础,buffering 不开会延迟。

  • zgrep

    等价 zcat | grep,对大压缩日志省去解压落盘。

  • 超大日志先 tail -n N 限定范围,避免全量扫描。

5.5 sed 基础与替换

bash

# 替换每行第一个匹配
sed&nbsp;'s/error/ERROR/'&nbsp;file

# 替换所有匹配
sed&nbsp;'s/error/ERROR/g'&nbsp;file

# 替换第 N 个匹配
sed&nbsp;'s/error/ERROR/2'&nbsp;file

# 只在特定行替换(如第 5 行)
sed&nbsp;'5s/error/ERROR/'&nbsp;file

# 行范围替换(5~10 行)
sed&nbsp;'5,10s/error/ERROR/'&nbsp;file

# 最后行
sed&nbsp;'$s/error/ERROR/'&nbsp;file

# 正则分组与反向引用(ERE)
echo&nbsp;'2026-06-22'&nbsp;| sed -E&nbsp;'s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\1\/\2\/\3/'
# 输出 2026/06/22

# & 代表整个匹配
echo&nbsp;'abc'&nbsp;| sed&nbsp;'s/b/[&]/'
# 输出 a[b]c

# 多命令 -e
sed -e&nbsp;'s/error/ERROR/g'&nbsp;-e&nbsp;'s/warn/WARN/g'&nbsp;file

# 删除行
sed&nbsp;'/DEBUG/d'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 删含 DEBUG 的行
sed&nbsp;'5d'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 删第 5 行
sed&nbsp;'5,10d'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 删 5~10 行
sed&nbsp;'/^$/d'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 删空行

# 只打印匹配行(-n 配 p)
sed -n&nbsp;'/ERROR/p'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 等价 grep ERROR
sed -n&nbsp;'5,10p'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 只看 5~10 行

判断:

  • s

    命令第三个参数:g 全部、数字第 N 个、p 打印、i 忽略大小写(GNU)。

  • -n

    抑制默认输出,配 p 命令只输出匹配行。

  • &

    是整个匹配,\1..\9 是分组。

5.6 sed 原地编辑与备份(重点)

bash

# GNU sed:-i 加后缀自动备份
sed -i.bak&nbsp;'s/error/ERROR/g'&nbsp;config.conf
# 生成 config.conf.bak,原文件改

# GNU sed:-i 不备份(危险)
sed -i&nbsp;'s/error/ERROR/g'&nbsp;config.conf

# BSD sed(macOS):-i 必须带参数,空参数要显式空串
sed -i&nbsp;''&nbsp;'s/error/ERROR/g'&nbsp;config.conf &nbsp; &nbsp; &nbsp; &nbsp;# macOS 不备份
sed -i&nbsp;'.bak'&nbsp;'s/error/ERROR/g'&nbsp;config.conf &nbsp; &nbsp;# macOS 带备份

风险与判断:

  • sed -i

    不加备份后缀是生产翻车重灾区。一旦正则写错,全文件改坏且无回滚。

  • 生产强制 sed -i.bak,且改完 diff 检查再决定是否保留 .bak

  • 跨平台脚本要区分 GNU/BSD:sed -i.bak(GNU 自动备份)vs sed -i '.bak'(BSD)。

5.7 sed 处理 CRLF 与行尾

Windows 导出的文件含 \r\n,sed/awk 不处理 \r 会留在字段里:

bash

# 删行尾 \r
sed -i&nbsp;'s/\r$//'&nbsp;file.csv

# 删 BOM(UTF-8 BOM 在行首)
sed -i&nbsp;'1s/^\xEF\xBB\xBF//'&nbsp;file.csv

# 看 \r 是否存在
cat&nbsp;-A file.csv |&nbsp;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&nbsp;'/BEGIN/,/END/p'&nbsp;file

# 删除两个模式之间的行
sed&nbsp;'/BEGIN/,/END/d'&nbsp;file

# N 命令读下一行合并
sed&nbsp;'N;s/\n/ /'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp;# 两行合并成一行

# 多行替换(GNU)
sed&nbsp;':a;N;$!ba;s/\n/ /g'&nbsp;file &nbsp;&nbsp;# 整个文件合并成一行,用于单行化处理

# 大括号分组
sed -n&nbsp;'/ERROR/{
&nbsp; s/ERROR/FATAL/
&nbsp; p
}'&nbsp;file

判断:

  • 高级 sed 命令(N/D/P/分支)可读性差,能 awk 替代就别用 sed 高级模式。

  • :a;N;$!ba;

    是把整个文件读进 pattern space 的经典写法,大文件会占内存,慎用。

5.9 awk 基础与字段

bash

# 默认空白分隔,打印第 1、7 字段
awk&nbsp;'{print $1, $7}'&nbsp;file

# 指定分隔符
awk -F','&nbsp;'{print $1, $3}'&nbsp;file.csv
awk -F'\t'&nbsp;'{print $1}'&nbsp;file.tsv

# 打印最后一列
awk&nbsp;'{print $NF}'&nbsp;file

# 倒数第二列
awk&nbsp;'{print $(NF-1)}'&nbsp;file

# 行号 NR
awk&nbsp;'{print NR": "$0}'&nbsp;file

# 字段数 NF
awk&nbsp;'{print NF}'&nbsp;file

# 条件过滤
awk&nbsp;'$3 > 100 {print $0}'&nbsp;file &nbsp; &nbsp; &nbsp;&nbsp;# 第 3 列大于 100
awk&nbsp;'$1 == "ERROR" {print $0}'&nbsp;file &nbsp;# 第 1 列等于 ERROR
awk&nbsp;'/ERROR/ {print $0}'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp;# 含 ERROR 的行

# 多条件
awk&nbsp;'$3 > 100 && $5 == "OK" {print}'&nbsp;file
awk&nbsp;'$3 > 100 || $7 == "FAIL" {print}'&nbsp;file

判断:

  • awk 条件可以放在 {} 前作为模式,也可以在 {} 里用 if

  • $1 == "ERROR"

    是字符串相等,$3 > 100 是数值比较,awk 按上下文自动转换类型。

5.10 awk BEGIN/END 与聚合统计

bash

# 行数
awk&nbsp;'END{print NR}'&nbsp;file

# 求和(第 3 列)
awk&nbsp;'{sum += $3} END{print sum}'&nbsp;file

# 平均值
awk&nbsp;'{sum += $3; n++} END{print sum/n}'&nbsp;file

# 最大值
awk&nbsp;'NR==1{max=$3} $3>max{max=$3} END{print max}'&nbsp;file

# 关联数组:统计每状态码出现次数(nginx 日志第 9 字段是 status)
awk&nbsp;'{count[$9]++} END{for (s in count) print s, count[s]}'&nbsp;/var/log/nginx/access.log

# TopN:排序
awk&nbsp;'{count[$9]++} END{for (s in count) print count[s], s}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;-rn |&nbsp;head

# BEGIN 初始化
awk&nbsp;'BEGIN{FS=","; OFS="|"} {print $1, $2}'&nbsp;file.csv

判断:

  • END

    块在所有行处理完执行,做汇总。

  • 关联数组 count[key]++ 是 awk 聚合的核心,类似 MapReduce 的 reduce。

  • for (k in arr)

    遍历顺序无序,要排序接 sort

5.11 awk 统计 PV / UV / 状态码

nginx access.log 统计实战:

bash

# PV(总请求数)
awk&nbsp;'END{print NR}'&nbsp;/var/log/nginx/access.log

# UV(去重 IP 数,第 1 字段)
awk&nbsp;'{ip[$1]++} END{print length(ip)}'&nbsp;/var/log/nginx/access.log

# 状态码分布
awk&nbsp;'{code[$9]++} END{for (c in code) printf "%s\t%d\n", c, code[c]}'&nbsp;/var/log/nginx/access.log |&nbsp;sort

# 5xx 错误率
awk&nbsp;'{total++; if ($9 ~ /^5/) err++} END{printf "total=%d err=%d rate=%.2f%%\n", total, err, err/total*100}'&nbsp;/var/log/nginx/access.log

# Top10 IP
awk&nbsp;'{ip[$1]++} END{for (i in ip) print ip[i], i}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10

# Top10 URL(第 7 字段是 request,拆出 URL)
awk&nbsp;'{split($7, a, " "); url[a[2]]++} END{for (u in url) print url[u], u}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10

# 每个 URL 的平均响应大小(第 10 字段是 body_bytes_sent)
awk&nbsp;'{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]}'&nbsp;/var/log/nginx/access.log

注意:不同 nginx 日志格式字段位置不同,上面假设 combined 格式 $9 是 status、$10 是 body_bytes_sent。自定义 log_format 要重新数字段位置,不同版本字段位置随格式变化,先 head -1 看实际格式。

5.12 awk 多文件与 FILENAME/FNR

bash

# 多文件统计
awk&nbsp;'{count[FILENAME]++} END{for (f in count) print f, count[f]}'&nbsp;*.log

# 区分文件处理(FILENAME)
awk&nbsp;'FILENAME=="a.log"{sum_a+=$1} FILENAME=="b.log"{sum_b+=$1} END{print "a="sum_a" b="sum_b}'&nbsp;a.log b.log

# FNR/NR 区别
awk&nbsp;'{print NR, FNR, $0}'&nbsp;a.log b.log
# NR 是全局行号,FNR 是当前文件内行号
# FNR==1 表示每个文件第一行
awk&nbsp;'FNR==1{print "==", FILENAME, "=="} {print}'&nbsp;a.log b.log

判断:多文件汇总用 FILENAME 或 FNR==1 判断边界,比分别跑再合并更高效。

5.13 awk 大文件与内存

awk 把数据放进关联数组,大文件会 OOM:

bash

# 危险:亿行日志全 IP 进数组
awk&nbsp;'{ip[$1]++} END{...}'&nbsp;huge.log

# 安全:先排序再去重计数,不进数组
awk&nbsp;'{print $1}'&nbsp;huge.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

# 采样:只取部分行
awk&nbsp;'NR%100==0{print $1}'&nbsp;huge.log &nbsp; &nbsp;# 每 100 行取 1 行采样

判断与选择:

  • UV 这种去重必须进数组,大文件用 sort | uniq -c 替代,靠磁盘排序避免内存炸。
  • 统计总和、计数不需要数组,单变量即可,任意大文件都安全。
  • 采样适合“看趋势不看精确值”的快速估算。

5.14 脱敏处理

生产日志含手机号、身份证、IP、邮箱,输出前必须脱敏:

bash

# 脱敏手机号:保留前 3 后 4
sed -E&nbsp;'s/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g'&nbsp;app.log

# 脱敏身份证:保留前 6 后 4
sed -E&nbsp;'s/([0-9]{6})[0-9]{8}([0-9]{4})/\1********\2/g'&nbsp;app.log

# 脱敏 IP:保留前两段
sed -E&nbsp;'s/([0-9]+\.[0-9]+)\.[0-9]+\.[0-9]+/\1.*.*/g'&nbsp;app.log

# awk 脱敏并统计
awk&nbsp;'{
&nbsp; ip=$1
&nbsp; split(ip, a, ".")
&nbsp; masked=a[1]"."a[2]".*.*"
&nbsp; cnt[mask]++
} END{for (m in cnt) print cnt[m], m}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;-rn |&nbsp;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 |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

# cut 按列切
cut&nbsp;-d' '&nbsp;-f1,7 file
cut&nbsp;-c1-10 file

# tr 字符转换
tr&nbsp;'a-z'&nbsp;'A-Z'&nbsp;< file &nbsp; &nbsp; &nbsp; &nbsp;# 转大写
tr&nbsp;-d&nbsp;'\r'&nbsp;< file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 删 \r
tr&nbsp;-s&nbsp;' '&nbsp;< file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 压缩连续空格

# tee 边输出边落盘
cmd |&nbsp;tee&nbsp;output.log

# split 分片大文件
split&nbsp;-l 1000000 -d large.log part_
# 按 100 万行切,生成 part_00 part_01 ...

# parallel 并行处理
ls&nbsp;*.log&nbsp;| parallel -j 8&nbsp;'grep ERROR {} > {.}.err'

# wc 行数单词数
wc&nbsp;-l file &nbsp; &nbsp; &nbsp;# 行数
wc&nbsp;-w file &nbsp; &nbsp; &nbsp;# 单词数
wc&nbsp;-L file &nbsp; &nbsp; &nbsp;# 最长行长度

七、配置示例

7.1 nginx log_format 与字段对应

nginx

log_format&nbsp;main&nbsp;'$remote_addr&nbsp;-&nbsp;$remote_user&nbsp;[$time_local] '
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'"$request"&nbsp;$status&nbsp;$body_bytes_sent&nbsp;'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'"$http_referer" "$http_user_agent" '
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;'$request_time&nbsp;$upstream_response_time';

对应 awk 字段(空格分隔时,$request 带引号会破坏字段计数,需特殊处理):

bash

# 用更稳健的方式:先提取 request 再处理
awk&nbsp;'{
&nbsp; # $1 ip, $4 时间开始, $7 request(带引号), 之后是 status...
&nbsp; # 因为 request 含空格,字段位置会偏移
&nbsp; # 用正则提取更稳
&nbsp; match($0, /"([A-Z]+) ([^ ]+) HTTP/); req=$2
&nbsp; match($0, /" ([0-9]{3}) /); code=substr($0, RSTART+2, RSTART+RLENGTH-3)
&nbsp; print code, req
}'&nbsp;/var/log/nginx/access.log

判断:自定义 log_format 让字段含空格(如 $request)会破坏 awk 默认字段切分,要么改 log_format 用分隔符(如 |),要么 awk 里用正则提取。生产建议 log_format 用竖线分隔便于 awk:

nginx

log_format&nbsp;kv&nbsp;'$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&nbsp;-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&nbsp;-p&nbsp;"$ALERT_DIR"

tail&nbsp;-F&nbsp;"$LOG"&nbsp;2>/dev/null \
&nbsp; | grep --line-buffered -E&nbsp;"$KEYWORDS"&nbsp;\
&nbsp; |&nbsp;while&nbsp;IFS=&nbsp;read&nbsp;-r line;&nbsp;do
&nbsp; &nbsp; &nbsp; ts="$(date '+%F %T')"
&nbsp; &nbsp; &nbsp;&nbsp;# 脱敏
&nbsp; &nbsp; &nbsp; masked="$(echo&nbsp;"$line"&nbsp;| sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g')"
&nbsp; &nbsp; &nbsp;&nbsp;printf&nbsp;'%s %s\n'&nbsp;"$ts"&nbsp;"$masked"&nbsp;|&nbsp;tee&nbsp;-a&nbsp;"$ALERT_LOG"&nbsp;>>&nbsp;"$ALERT_DIR/alert_$(date +%F).log"
&nbsp; &nbsp;&nbsp;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&nbsp;systemctl daemon-reload
sudo&nbsp;systemctl&nbsp;enable&nbsp;--now log_alert

八、日志或指标观察方法

8.1 日志增长速率

bash

# 每秒新增行数
watch -n 1&nbsp;'wc -l /var/log/app/app.log'

# 粗估
tail&nbsp;-F /var/log/app/app.log > /tmp/count.log &
sleep&nbsp;60
wc&nbsp;-l /tmp/count.log &nbsp;&nbsp;# 60 秒新增行数
kill&nbsp;%1

8.2 错误率趋势

bash

# 每 5 分钟错误数
awk&nbsp;'
{
&nbsp; # 假设 $4 是时间 [22/Jun/2026:14:55:03,提取到分钟
&nbsp; match($4, /:([0-9]{2}:[0-9]{2}):/, m)
&nbsp; minute=m[1]
&nbsp; total[minute]++
&nbsp; if ($9 ~ /^5/) err[minute]++
}
END {
&nbsp; 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
}'&nbsp;/var/log/nginx/access.log |&nbsp;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&nbsp;'ERROR'&nbsp;/var/log/app/app.log)
FATAL_COUNT=$(grep -c&nbsp;'FATAL'&nbsp;/var/log/app/app.log)
cat&nbsp;>&nbsp;"$METRIC"&nbsp;<<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&nbsp;'$9 ~ /^5/ {print $4}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head
# 看哪个时间段 5xx 集中

# 2. 5xx 涉及哪些 URL
awk&nbsp;'$9 ~ /^5/ {split($7,a," "); print a[2]}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10

# 3. 5xx 来自哪些上游(如果日志记了 upstream)
grep -E&nbsp;' 5[0-9]{2} '&nbsp;/var/log/nginx/access.log | grep -oE&nbsp;'upstream:[^ ]+'&nbsp;|&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn

# 4. 对应时间段的 error.log
awk&nbsp;'$4 ~ /14:[45][0-9]:/'&nbsp;/var/log/nginx/error.log | grep -E&nbsp;'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&nbsp;'tail -n 1000 /var/log/app/app.log | grep -E "ERROR|OOM|FATAL"'
# 系统 OOM
ssh 10.0.0.21&nbsp;'dmesg -T | grep -i "killed process" | tail'
# 进程状态
ssh 10.0.0.21&nbsp;'systemctl status app'

若 dmesg 显示 Killed process 12345 (java) total-vm:...,根因是 OOM,应用内存泄漏或实例内存不足。

修复

bash

# 1. 重启应用(恢复服务优先)
ssh 10.0.0.21&nbsp;'sudo systemctl restart app'
# 2. 验证 5xx 下降
awk&nbsp;'$4 ~ /14:5[5-9]/ {code[$9]++} END{for(c in code) print c, code[c]}'&nbsp;/var/log/nginx/access.log
# 3. 治本:扩内存或排查内存泄漏(后续)

风险:重启服务会断当前连接,确认影响范围后操作,优先切流量到健康实例再重启故障实例。

验证

bash

# 5xx 回落
awk&nbsp;'$9 ~ /^5/ {err++} END{print "5xx="err}'&nbsp;/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&nbsp;'{print NF}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c
# 如果字段数不一,说明有的行有额外空格

# 看一行各字段
head&nbsp;-1 /var/log/nginx/access.log | awk&nbsp;'{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&nbsp;'match($0, /" ([0-9]{3}) /, m){code[m[1]]++} END{for(c in code) print c, code[c]}'&nbsp;/var/log/nginx/access.log

方案三:用 FPAT(gawk)按字段模式而非分隔符切:

bash

awk&nbsp;'BEGIN{FPAT="([^ ]+)|(\"[^\"]+\")"} {code[$8]++} END{for(c in code) print c, code[c]}'&nbsp;/var/log/nginx/access.log

FPAT 是 gawk 扩展,定义“字段长什么样”而非“分隔符是什么”,处理带引号字段最稳。不同 awk 实现可能不支持 FPAT,确认 gawk 再用。

验证

bash

# 状态码分布正常
awk&nbsp;'...'&nbsp;/var/log/nginx/access.log |&nbsp;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&nbsp;'^port='&nbsp;/etc/app/app.conf
# 显示 port=3307(原本 port=3306 max=100 这样的行后半段没了)

# 找备份
ls&nbsp;-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&nbsp;systemctl restart app
sudo&nbsp;systemctl status app

预防

bash

# 1. 永远加备份后缀
sed -i.bak&nbsp;'s/port=3306/port=3307/g'&nbsp;/etc/app/app.conf

# 2. 先 dry-run(不加 -i 看输出)
sed&nbsp;'s/port=3306/port=3307/g'&nbsp;/etc/app/app.conf | grep&nbsp;'^port='

# 3. 精确匹配,避免贪婪
sed -i.bak -E&nbsp;'s/^port=[0-9]+/port=3307/'&nbsp;/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&nbsp;-n 1000 /var/log/nginx/access.log | awk&nbsp;'{code[$9]++} END{for(c in code) print c, code[c]}'
# 输出符合预期再全量

13.2 验证 sed 替换

bash

# dry-run
sed&nbsp;'s/old/new/g'&nbsp;file | grep&nbsp;'new'
# diff 看变化
sed -i.bak&nbsp;'s/old/new/g'&nbsp;file
diff file file.bak

13.3 验证脱敏

bash

# 脱敏后扫描残留敏感信息
grep -rE&nbsp;'1[3-9][0-9]{9}|[0-9]{17}[0-9Xx]'&nbsp;/data/secure/
# 无输出即脱敏干净

13.4 验证统计准确性

bash

# 用两种方法交叉验证 UV
awk&nbsp;'{ip[$1]++} END{print length(ip)}'&nbsp;/var/log/nginx/access.log
awk&nbsp;'{print $1}'&nbsp;/var/log/nginx/access.log |&nbsp;sort&nbsp;-u |&nbsp;wc&nbsp;-l
# 两个数应一致

十四、回滚方案

14.1 sed -i 回滚

bash

# 有 .bak
mv&nbsp;file.bak file
# 或
cp&nbsp;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&nbsp;'...'&nbsp;input.log > output.new
# 验证 output.new 无误再替换
mv&nbsp;output.new output.final

14.3 告警脚本回滚

bash

sudo&nbsp;systemctl stop log_alert
sudo&nbsp;systemctl&nbsp;disable&nbsp;log_alert
# 删常驻脚本
sudo&nbsp;rm&nbsp;/etc/systemd/system/log_alert.service
sudo&nbsp;systemctl daemon-reload

十五、生产环境注意事项

  1. sed -i

    强制加备份后缀,先 dry-run。

  2. 大文件 awk 优先 sort/uniq 替代全数组,或采样。

  3. grep/sed/awk 输出敏感信息前脱敏。

  4. 跨平台脚本区分 GNU/BSD(sed -igrep -P)。

  5. CRLF/BOM 文件预处理 sed 's/\r$//'

  6. 自定义 log_format 用分隔符便于 awk,避免含空格字段。

  7. 实时跟踪加 --line-buffered/-u/fflush

  8. 常驻脚本用 systemd 管理,Restart=always。

  9. 监控指标靠 textfile 采集,阈值结合业务基线。

  10. 日志保留至少 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&nbsp;'ERROR'&nbsp;app.log | grep&nbsp;'order'
# 或用 -P 的 lookahead
grep -P&nbsp;'(?=.*ERROR)(?=.*order)'&nbsp;app.log

A.2 多文件统计

bash

# 各文件 ERROR 数
grep -c&nbsp;'ERROR'&nbsp;/var/log/app/*.log
# 总数
grep -h&nbsp;'ERROR'&nbsp;/var/log/app/*.log&nbsp;|&nbsp;wc&nbsp;-l &nbsp; &nbsp;# -h 不显示文件名

A.3 只匹配 IP

bash

grep -oE&nbsp;'([0-9]{1,3}\.){3}[0-9]{1,3}'&nbsp;access.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

A.4 提取两个时间点之间的日志

bash

# 假设日志行首是时间戳
awk&nbsp;'/2026-06-22 14:00:00/,/2026-06-22 15:00:00/'&nbsp;app.log
# sed 版
sed -n&nbsp;'/2026-06-22 14:00:00/,/2026-06-22 15:00:00/p'&nbsp;app.log

判断:awk/sed 的范围模式 pat1,pat2 输出从匹配 pat1 到匹配 pat2 之间的行,时间范围提取最常用。

A.5 找不含某关键字的行

bash

grep -v&nbsp;'DEBUG'&nbsp;app.log &nbsp; &nbsp; &nbsp; &nbsp;# 不含 DEBUG
grep -vE&nbsp;'DEBUG|INFO'&nbsp;app.log &nbsp;# 不含 DEBUG 也不含 INFO

A.6 统计字符出现次数(非行)

bash

grep -o&nbsp;'error'&nbsp;app.log |&nbsp;wc&nbsp;-l
# 一行出现多次 error 都计入

A.7 二进制文件处理

bash

# grep 默认对二进制文件只报 Binary file matches
grep -a&nbsp;'pattern'&nbsp;binfile &nbsp; &nbsp; &nbsp;# -a 当文本处理
grep -I&nbsp;'pattern'&nbsp;* &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# -I 跳过二进制文件

附录 B:sed 实战菜谱

B.1 配置文件改值

bash

# 改 key=value 形式配置
sed -i.bak -E&nbsp;'s/^(max_connections[[:space:]]*=[[:space:]]*)[0-9]+/\11000/'&nbsp;/etc/my.cnf
# 把 max_connections = 200 改成 1000,保留 key 和等号格式

B.2 注释/取消注释

bash

# 注释某行(行首加 #)
sed -i.bak&nbsp;'/^max_connections/s/^/#/'&nbsp;/etc/my.cnf
# 取消注释
sed -i.bak&nbsp;'/^#max_connections/s/^#//'&nbsp;/etc/my.cnf

B.3 插入行

bash

# 在第 5 行后插入
sed -i&nbsp;'5a\new line content'&nbsp;file
# 在第 5 行前插入
sed -i&nbsp;'5i\new line content'&nbsp;file
# 在匹配行后插入
sed -i&nbsp;'/\[mysqld\]/a\new_param=1'&nbsp;/etc/my.cnf

B.4 删除空行和注释行

bash

sed -i&nbsp;'/^$/d; /^#/d; /^[[:space:]]*#/d'&nbsp;file
# 删空行、# 开头注释、行首空白后 #

B.5 大小写转换

bash

sed&nbsp;'s/[a-z]/\U&/g'&nbsp;file &nbsp; &nbsp; &nbsp;&nbsp;# 转大写(GNU)
sed&nbsp;'s/[A-Z]/\L&/g'&nbsp;file &nbsp; &nbsp; &nbsp;&nbsp;# 转小写(GNU)
# 或用 tr
tr&nbsp;'a-z'&nbsp;'A-Z'&nbsp;< file

B.6 CSV 列调换

bash

# 调换第 2、3 列
sed -E&nbsp;'s/([^,]+),([^,]+),([^,]+)/\1,\3,\2/'&nbsp;file.csv

B.7 行号范围操作

bash

sed -n&nbsp;'10,20p'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 打印 10~20 行
sed&nbsp;'10,20d'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 删除 10~20 行
sed&nbsp;'10q'&nbsp;file &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 打印前 10 行(等价 head -10)

附录 C:awk 实战菜谱

C.1 按条件过滤并格式化

bash

awk -F','&nbsp;'$3 > 100 && $5 == "OK" {printf "%-20s %8d %s\n", $1, $3, $5}'&nbsp;file.csv

C.2 分组求和与平均

bash

# 按第 1 列分组,对第 3 列求和与平均
awk -F','&nbsp;'{sum[$1]+=$3; cnt[$1]++} END{for(k in sum) printf "%s\tsum=%d\tavg=%.2f\n", k, sum[k], sum[k]/cnt[k]}'&nbsp;file.csv

C.3 按多列去重

bash

# 按第 1、2 列组合去重,保留第一次出现
awk -F','&nbsp;'!seen[$1,$2]++'&nbsp;file.csv

判断:!seen[key]++ 是 awk 去重惯用法,第一次见 key 时 seen[key] 为 0 取反为真打印,之后为假不打印。

C.4 行列转换

bash

# 行转列:每行 3 个字段转成 3 行
awk&nbsp;'{for(i=1;i<=NF;i++) print $i}'&nbsp;file
# 列转行:3 行合一列
awk&nbsp;'{a[NR]=$0} END{for(i=1;i<=NR;i++) printf "%s%s", a[i], (i%3==0?"\n":",")}'&nbsp;file

C.5 统计字符出现频率

bash

# 统计每个字符出现次数
awk&nbsp;'{for(i=1;i<=length($0);i++) freq[substr($0,i,1)]++} END{for(c in freq) print c, freq[c]}'&nbsp;file |&nbsp;sort&nbsp;-k2 -rn

C.6 合并两个文件(类似 join)

bash

# file1: id,name ; file2: id,score;按 id 合并
awk -F','&nbsp;'NR==FNR{name[$1]=$2; next} {print $1, name[$1], $2}'&nbsp;file1 file2
# NR==FNR 时是第一个文件,建立映射;之后是第二个文件,查映射输出

判断:NR==FNR{...; next} 是 awk 合并文件的经典模式,第一文件建索引,第二文件查。

C.7 计算分位数

bash

# 计算第 3 列的 P95(需先排序)
awk&nbsp;'{a[NR]=$3} END{
&nbsp; n=NR
&nbsp; asort(a) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# gawk 排序函数
&nbsp; p95=int(n*0.95)
&nbsp; if(p95<1) p95=1
&nbsp; print "P95=", a[p95]
}'&nbsp;file

注意:asort 是 gawk 扩展,POSIX awk 没有。不同 awk 实现可能略有差异,确认 gawk。

C.8 按时间窗口聚合

bash

# 假设 $1 是时间戳到分钟 2026-06-22 14:55
awk -F'|'&nbsp;'{
&nbsp; min=substr($1,1,16)
&nbsp; total[min]++
&nbsp; if($4 ~ /^5/) err[min]++
} END{
&nbsp; 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
}'&nbsp;/var/log/nginx/access.log |&nbsp;sort

附录 D:组合管道实战

D.1 Top10 慢请求 URL

bash

# 假设 log_format 最后字段是 request_time
awk -F'|'&nbsp;'{print $NF, $3}'&nbsp;/var/log/nginx/access.log \
&nbsp; |&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10

D.2 每小时请求量

bash

awk -F'|'&nbsp;'{print substr($2,1,13)}'&nbsp;/var/log/nginx/access.log \
&nbsp; |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c
# substr 取到小时 2026-06-22 14

D.3 404 请求的 Top URL

bash

awk -F'|'&nbsp;'$4=="404"{print $3}'&nbsp;/var/log/nginx/access.log \
&nbsp; |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

D.4 抓取某 IP 的所有访问

bash

grep&nbsp;'^192.168.1.10'&nbsp;/var/log/nginx/access.log \
&nbsp; | awk -F'|'&nbsp;'{print $3, $4}'&nbsp;|&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn

D.5 找出响应大小异常大的请求

bash

awk -F'|'&nbsp;'$5 > 1000000 {print $2, $3, $5}'&nbsp;/var/log/nginx/access.log
# body_bytes_sent > 1MB 的请求

附录 E:大文件分片并行处理

bash

# 1. 分片
split&nbsp;-l 5000000 -d large.log part_

# 2. 并行处理
ls&nbsp;part_* | xargs -P 8 -I{} sh -c&nbsp;'awk "{code[\$9]++} END{for(c in code) print c, code[c]}" {} > {}.out'

# 3. 合并结果
cat&nbsp;part_*.out | awk&nbsp;'{code[$1]+=$2} END{for(c in code) print c, code[c]}'&nbsp;|&nbsp;sort&nbsp;-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 |

排查心法:

  1. 报错先看原文,区分语法错误、版本差异、资源不足。
  2. 语法错误查 BRE/ERE 转义、GNU/BSD 差异。
  3. 资源不足(OOM、盘满)换策略:sort/uniq 替代数组、sort -T 换临时盘。
  4. 输出不符预期先小样本 head 跑通再全量。

附录 I:日志分析自动化脚本

把常用分析固化为脚本,排障时一键跑:

bash

#!/usr/bin/env bash
# /usr/local/bin/nginx_log_report.sh
# nginx 日志快速分析报告
set&nbsp;-uo pipefail

LOG="${1:-/var/log/nginx/access.log}"
OUT="/data/report/nginx_$(date +%F_%H%M).txt"
mkdir&nbsp;-p /data/report

{
&nbsp;&nbsp;echo&nbsp;"===== Nginx 日志分析报告 ====="
&nbsp;&nbsp;echo&nbsp;"日志:&nbsp;$LOG"
&nbsp;&nbsp;echo&nbsp;"生成时间:&nbsp;$(date '+%F %T')"
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## 总览"
&nbsp;&nbsp;echo&nbsp;"总请求数:&nbsp;$(wc -l <&nbsp;"$LOG")"
&nbsp;&nbsp;echo&nbsp;"去重 IP(UV):&nbsp;$(awk '{print $1}'&nbsp;"$LOG"&nbsp;| sort -u | wc -l)"
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## 状态码分布"
&nbsp; awk -F'|'&nbsp;'{code[$4]++} END{for(c in code) printf "%s\t%d\n", c, code[c]}'&nbsp;"$LOG"&nbsp;|&nbsp;sort
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## Top10 IP"
&nbsp; awk -F'|'&nbsp;'{ip[$1]++} END{for(i in ip) print ip[i], i}'&nbsp;"$LOG"&nbsp;|&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## Top10 URL"
&nbsp; awk -F'|'&nbsp;'{u[$3]++} END{for(x in u) print u[x], x}'&nbsp;"$LOG"&nbsp;|&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## 5xx 错误 Top URL"
&nbsp; awk -F'|'&nbsp;'$4 ~ /^5/{u[$3]++} END{for(x in u) print u[x], x}'&nbsp;"$LOG"&nbsp;|&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## 大响应 Top10(>100KB)"
&nbsp; awk -F'|'&nbsp;'$5 > 102400 {print $5, $3}'&nbsp;"$LOG"&nbsp;|&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10
&nbsp;&nbsp;echo

&nbsp;&nbsp;echo&nbsp;"## 慢请求 Top10(request_time)"
&nbsp; awk -F'|'&nbsp;'{print $NF, $3}'&nbsp;"$LOG"&nbsp;|&nbsp;sort&nbsp;-rn |&nbsp;head&nbsp;-10
} |&nbsp;tee&nbsp;"$OUT"

echo&nbsp;"报告已保存:&nbsp;$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'|'&nbsp;'{code[$4]++} END{for(c in code) print c, code[c]}'&nbsp;access.log |&nbsp;sort

# Top10 IP
awk&nbsp;'{print $1}'&nbsp;access.log |&nbsp;sort&nbsp;|&nbsp;uniq&nbsp;-c |&nbsp;sort&nbsp;-rn |&nbsp;head

# 5xx 错误率
awk&nbsp;'$9 ~ /^5/{e++} END{print e/NR*100"%"}'&nbsp;access.log

# 实时跟踪 ERROR
tail&nbsp;-F app.log | grep --line-buffered ERROR

# 脱敏手机号
sed -E&nbsp;'s/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g'

# 删 CRLF
sed -i&nbsp;'s/\r$//'&nbsp;file

# 改配置带备份
sed -i.bak -E&nbsp;'s/^(port=)[0-9]+/\13307/'&nbsp;config

# 去重保留首次
awk&nbsp;'!seen[$1]++'&nbsp;file

# 时间范围提取
sed -n&nbsp;'/14:00:00/,/15:00:00/p'&nbsp;app.log

# 大文件求和
awk&nbsp;'{sum+=$3} END{print sum}'&nbsp;big.log

# 压缩日志查
zgrep ERROR access.log.gz

把这张卡片贴在工位,日志排障时大半场景能直接套。

附录 L:日志轮转与三剑客的配合

日志被 logrotate 轮转后,处理历史日志要考虑压缩和归档:

bash

# logrotate 典型配置
cat&nbsp;/etc/logrotate.d/nginx
# 每天轮转,保留 30 天,压缩

# 处理轮转后的压缩日志
# 1. 跨所有归档统计
zcat /var/log/nginx/access.log*.gz | awk&nbsp;'...'

# 2. 跨当前+归档统计
zcat /var/log/nginx/access.log.*.gz /var/log/nginx/access.log | awk&nbsp;'{code[$9]++} END{...}'

# 3. 按日期分文件统计
for&nbsp;f&nbsp;in&nbsp;/var/log/nginx/access.log*.gz;&nbsp;do
&nbsp;&nbsp;echo&nbsp;"$f:"
&nbsp; zcat&nbsp;"$f"&nbsp;| awk&nbsp;'{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 {
&nbsp; &nbsp; daily
&nbsp; &nbsp; rotate 30
&nbsp; &nbsp; compress
&nbsp; &nbsp; delaycompress
&nbsp; &nbsp; missingok
&nbsp; &nbsp; notifempty
&nbsp; &nbsp; create 0640 app app
&nbsp; &nbsp; sharedscripts
&nbsp; &nbsp; postrotate
&nbsp; &nbsp; &nbsp; &nbsp; systemctl reload app 2>/dev/null ||&nbsp;true
&nbsp; &nbsp; endscript
}

风险:postrotate 里的 systemctl reload 属重启类操作,确认 app 支持 reload 且配置正确,否则可能中断服务。delaycompress 推迟一轮压缩,避免刚轮转的文件还在被写时压缩损坏。

附录 M:systemd journalctl 与三剑客

systemd 服务日志在 journald,用 journalctl 查,配合三剑客:

bash

# 查某服务日志
journalctl -u app --since&nbsp;'14:00'&nbsp;--until&nbsp;'15:00'

# 实时跟踪
journalctl -u app -f

# 只看错误优先级
journalctl -u app -p err

# 输出 JSON 便于 awk
journalctl -u app -o json | jq&nbsp;'.MESSAGE'

# 配合 grep
journalctl -u app --since today | grep ERROR

# 配合 awk 统计
journalctl -u app --since&nbsp;'14:00'&nbsp;--no-pager | awk&nbsp;'{...}'

判断:

  • 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&nbsp;-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 &nbsp; &nbsp;&nbsp;# 5 分钟内相同关键字去重

mkdir&nbsp;-p&nbsp;"$(dirname&nbsp;"$STATE")"

tail&nbsp;-F&nbsp;"$LOG"&nbsp;2>/dev/null \
&nbsp; | grep --line-buffered -E&nbsp;"$KEYWORDS"&nbsp;\
&nbsp; |&nbsp;while&nbsp;IFS=&nbsp;read&nbsp;-r line;&nbsp;do
&nbsp; &nbsp; &nbsp;&nbsp;# 脱敏
&nbsp; &nbsp; &nbsp; masked=$(printf&nbsp;'%s'&nbsp;"$line"&nbsp;| sed -E \
&nbsp; &nbsp; &nbsp; &nbsp; -e&nbsp;'s/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g'&nbsp;\
&nbsp; &nbsp; &nbsp; &nbsp; -e&nbsp;'s/([0-9]{17})[0-9Xx]/\1*/g')

&nbsp; &nbsp; &nbsp;&nbsp;# 提取关键字做去重 key
&nbsp; &nbsp; &nbsp; key=$(printf&nbsp;'%s'&nbsp;"$masked"&nbsp;| grep -oE&nbsp;'FATAL|OOM|OutOfMemory'&nbsp;|&nbsp;head&nbsp;-1)
&nbsp; &nbsp; &nbsp; now=$(date&nbsp;+%s)
&nbsp; &nbsp; &nbsp; state_file="$STATE/${key}.ts"

&nbsp; &nbsp; &nbsp;&nbsp;# 限频:同 key 在窗口内只告警一次
&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;[[ -f&nbsp;"$state_file"&nbsp;]];&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; last=$(cat&nbsp;"$state_file"&nbsp;2>/dev/null ||&nbsp;echo&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(( now - last < DEDUP_WINDOW ));&nbsp;then
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;fi
&nbsp; &nbsp; &nbsp;&nbsp;fi
&nbsp; &nbsp; &nbsp;&nbsp;echo&nbsp;"$now"&nbsp;>&nbsp;"$state_file"

&nbsp; &nbsp; &nbsp; ts=$(date&nbsp;'+%F %T')
&nbsp; &nbsp; &nbsp;&nbsp;printf&nbsp;'%s [%s] %s\n'&nbsp;"$ts"&nbsp;"$key"&nbsp;"$masked"&nbsp;|&nbsp;tee&nbsp;-a&nbsp;"$ALERT_LOG"

&nbsp; &nbsp; &nbsp;&nbsp;# 触发外部告警(示例思路,实际接企业微信/钉钉/邮件)
&nbsp; &nbsp; &nbsp;&nbsp;# send_alert "$key" "$masked"
&nbsp; &nbsp;&nbsp;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:最终心法

三剑客的终极心法,浓缩成五条:

  1. 先小样本后全量:复杂管道必先 head 跑通。
  2. 改前备份:sed -i.bak 是底线。
  3. 防 OOM:大文件优先 sort/uniq,慎用全数组。
  4. 脱敏先行:输出敏感信息前先脱敏。
  5. 闭环排障:现象—过滤—清洗—统计—根因—验证,不跳步。

这五条贯穿 grep/sed/awk 的所有场景,是日志处理的底线纪律。能做到这五条,三剑客就是真正的瑞士军刀;做不到,就是凌晨两点把自己埋进日志里的铲子。

文末福利

今天给大家分享一份超级牛掰的Linux学习笔记,足足有1456页!是一位Linux运维大佬整理分享的,分享是获得大佬同意的,大家有需要的尽管收藏起来!

笔记介绍

这份笔记非常全面且详细,从Linux基础到shell脚本,再到防火墙、数据库、日志服务管理、Nginx、高可用集群、Redis、虚拟化、Docker等等,与其说Linux学习笔记,不如说是涵盖了运维各个核心知识。

并且图文并茂,代码清晰,每一章下面都有更具体详细的内容,十分适合Linux运维学习参考!

笔记展示

笔记下载

扫描下方二维码,回复暗号“1456页Linux笔记“,即可100%免费领取成功


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《文本三剑客 grep / sed / awk:日志处理的瑞士军刀》

评论:0   参与:  0