技术社群的这篇文章《30s定位大面积故障!如何用awk+grep+sed搭建日志处理神器?》给我们讲解了如何用awk、grep、sed检索日志文件,这个需求看着很琐碎,但其实是非常重要的技能。
无论是开发、运维,如果在Linux平台工作,都可能会有检索文件的需求,相较于Windows,Linux提供了很多便捷的指令,以及全面的参数,可以让我们在海量文件中搜索到需要的,每个指令都蕴藏着设计的精妙。
一、概述
2024年双十一那天凌晨三点,我被电话吵醒。线上服务出现大面积超时,用户投诉量激增。登录服务器一看,最近4小时产生了将近12GB的Nginx访问日志。领导在群里催着要故障原因,我需要在最短时间内从这堆日志里找出问题根源。
如果用传统的方式打开日志文件,光是vim加载就得等好几分钟。用Python写脚本?来不及了。这时候,grep、sed、awk这三板斧就成了救命稻草。
我用一行命令组合,30秒内就定位到了问题:某个爬虫IP在疯狂请求一个数据库密集型的接口,直接把数据库连接池打满了。封禁IP后,服务立刻恢复。
这就是Shell三剑客的魅力。干了10年SRE,我越来越觉得,这三个工具是运维工程师的核心竞争力。不管你用多么高级的日志分析平台,遇到紧急情况时,能在命令行里快速处理数据的能力永远不会过时。
为什么grep、sed、awk能做到如此高效?这得从它们的设计哲学说起。
1)流式处理:三剑客都采用流式处理机制,一次只读取一行到内存,处理完就释放。这意味着处理10GB文件和处理10MB文件的内存消耗是一样的。相比之下,如果你用Python的readlines()把整个文件读进内存,10GB文件直接就把机器干趴下了。
2)C语言实现:这三个工具都是用C语言写的,经过了几十年的优化,底层直接调用系统I/O,效率极高。我做过测试,处理相同的日志文件,awk比Python快5-10倍是很正常的。
3)管道机制:Unix的管道设计是天才之作。多个命令通过管道串联,数据像水一样流过去,不需要中间临时文件。这不仅节省了磁盘I/O,还让命令可以并行执行。
4)正则表达式引擎:grep和sed的正则表达式引擎经过高度优化,特别是grep -F(固定字符串搜索)和GNU grep的DFA引擎,在特定场景下性能惊人。
在实际工作中,我是这样区分三者使用场景的:
1)grep:专注搜索过滤。需要从海量日志中快速找出包含特定关键词的行时用它。它干的活很专一,但干得特别好。
2)sed :擅长文本替换。批量修改配置文件、格式转换、删除特定行等场景用它。我把它理解成"流编辑器",数据流过它的时候被修改。
3)awk:复杂数据处理。需要对字段进行计算、统计、聚合时用它。它其实是一门完整的编程语言,只是我们通常只用到它的一小部分功能。
根据我的经验,以下场景特别适合用三剑客:
1)故障排查:快速定位错误日志、统计错误频率、找出异常请求
2)日志分析:统计访问量、分析响应时间分布、识别恶意IP
3)配置管理:批量修改配置文件、格式转换、数据清洗
4)数据处理:CSV/JSON处理、报表生成、数据校验
5)自动化脚本:作为Shell脚本的核心组件处理文本数据
不适合的场景也要说清楚:
1)复杂的多文件关联分析:这种场景还是用ELK或者写Python脚本更合适
2)需要持久化存储的场景:三剑客是一次性处理,不会保存状态
3)需要复杂数据结构的场景:awk虽然有数组,但处理复杂嵌套结构还是力不从心
本文的所有命令都在以下环境测试通过:
操作系统:Ubuntu 22.04 LTS / CentOS 8 / macOS SonomaGNU grep:3.8+GNU sed:4.8+GNU awk (gawk):5.1+ripgrep:14.0+(可选,高性能替代方案)
如果你用的是macOS,系统自带的是BSD版本的工具,语法和GNU版本略有差异。我强烈建议用Homebrew安装GNU版本:
brew install grep sed gawk ripgrep安装后,GNU版本的命令前缀是g,比如ggrep、gsed、gawk。你也可以在.bashrc里设置别名:
alias grep='ggrep'alias sed='gsed'alias awk='gawk'
版本检查命令:
grep --version | head -1sed --version | head -1awk --version | head -1
二、详细步骤
1)生成测试数据
在正式开始之前,我们需要一些测试数据。下面这个脚本能生成类似真实环境的Nginx访问日志:
#!/bin/bash# generate_nginx_log.sh - 生成模拟Nginx日志LOG_FILE="access.log"TOTAL_LINES=10000000 # 约1GB# IP池IPS=("192.168.1.100" "192.168.1.101" "10.0.0.50" "10.0.0.51""172.16.0.10" "8.8.8.8" "1.1.1.1" "203.0.113.50""198.51.100.23" "185.220.101.42")# URL池URLS=("/api/users" "/api/orders" "/api/products" "/api/search""/static/js/main.js" "/static/css/style.css" "/images/logo.png""/api/payment" "/health" "/metrics" "/api/v2/data")# 状态码STATUS_CODES=("200" "200" "200" "200" "200" "201" "301" "302""400" "401" "403" "404" "500" "502" "503")# User-Agent池USER_AGENTS=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36""Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36""Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)""curl/7.88.1""python-requests/2.28.0""Googlebot/2.1 (+http://www.google.com/bot.html)")echo "开始生成日志文件,共 $TOTAL_LINES 行..."for ((i=1; i<=TOTAL_LINES; i++)); doip=${IPS[$RANDOM % ${#IPS[@]}]}url=${URLS[$RANDOM % ${#URLS[@]}]}status=${STATUS_CODES[$RANDOM % ${#STATUS_CODES[@]}]}ua=${USER_AGENTS[$RANDOM % ${#USER_AGENTS[@]}]}size=$((RANDOM % 50000 + 100))response_time=$(awk -v min=0.001 -v max=5.0 'BEGIN{srand(); print min+rand()*(max-min)}')timestamp=$(date "+%d/%b/%Y:%H:%M:%S +0800")echo "$ip - - [$timestamp] \"GET $url HTTP/1.1\" $status$size \"$ua\" $response_time"done > "$LOG_FILE"echo "日志生成完成:$LOG_FILE,大小:$(du -h $LOG_FILE | cut -f1)"
这个脚本生成的日志格式是标准的Nginx combined格式,最后多加了一个响应时间字段。1000万行大约1GB,生成时间取决于你的机器性能。
如果你想快速生成更大的文件用于性能测试,可以用下面这个更高效的方法:
# 先生成一个小文件,然后复制扩展head -1000000 access.log > access_small.logfor i in {1..10}; do cat access_small.log >> access_10gb.log; done
2)了解日志格式
在开始处理之前,我习惯先看几行日志,了解数据格式:
head -5 access.log输出类似:
192.168.1.100 - - [06/Jan/2025:10:23:45 +0800] "GET /api/users HTTP/1.1" 200 1234 "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" 0.045分析一下字段结构:
字段1:客户端IP
字段2-3:用户标识(通常是-)
字段4-5:时间戳(带方括号)
字段6-7-8:请求行(方法、URL、协议)
字段9:HTTP状态码
字段10:响应大小
字段11+:User-Agent
最后一个字段:响应时间
这个分析过程很重要,因为后面用awk处理时,字段编号直接决定了取数的正确性。
1)grep核心参数
grep的参数不少,但常用的就那么几个。我按使用频率排序:
# 最常用的参数组合grep -n "ERROR" # -n 显示行号,排查问题必备grep -i "error" # -i 忽略大小写grep -c "ERROR" # -c 只显示匹配行数,快速统计grep -v "DEBUG" # -v 反向匹配,排除某些行grep -A 5 "Exception" # -A 显示匹配行后5行,看堆栈grep -B 2 "ERROR" # -B 显示匹配行前2行,看上下文grep -C 3 "FATAL" # -C 显示前后各3行grep -l "password" # -l 只显示包含匹配的文件名grep -r "TODO" ./src # -r 递归搜索目录grep -w "error" # -w 全词匹配,避免匹配到errorsgrep -o "ip=[0-9.]+" # -o 只输出匹配的部分grep -E "err|warn" # -E 使用扩展正则表达式grep -P "\d{4}" # -P 使用Perl正则(更强大)grep -F "fixed[str]" # -F 固定字符串搜索,不解释正则
-E和-P的区别:这是个常见困惑点。-E是POSIX扩展正则,支持+、?、|、()这些元字符不用转义。-P是Perl兼容正则(PCRE),支持更多高级特性,比如\d(数字)、\s(空白)、(?=)(前瞻)等。
我的选择标准:简单匹配用-E,需要用到\d、\w这类简写或者零宽断言时用-P。但要注意,macOS的grep不支持-P参数,需要安装GNU grep。
2)sed核心参数
sed的参数相对少,但理解工作模式很重要:
# 基本替换sed 's/old/new/' # 替换每行第一个匹配sed 's/old/new/g' # 替换每行所有匹配sed 's/old/new/gi' # 全局替换,忽略大小写# 原地修改(危险操作!)sed -i 's/old/new/g' file # Linux语法sed -i '' 's/old/new/g' file # macOS BSD语法# 指定行范围sed '10,20s/old/new/g' # 只处理第10-20行sed '/pattern/s/old/new/g' # 只处理匹配pattern的行# 删除操作sed '/DEBUG/d' # 删除包含DEBUG的行sed '1,10d' # 删除前10行sed '/^$/d' # 删除空行# 打印操作sed -n '5,10p' # 只打印第5-10行(-n抑制默认输出)sed -n '/ERROR/p' # 只打印匹配行# 多个操作sed -e 's/a/b/' -e 's/c/d/' # 多个-esed 's/a/b/; s/c/d/' # 分号分隔# 分组和引用sed 's/\(.*\):\(.*\)/\2:\1/' # 交换冒号两边的内容sed -E 's/(.*):(.*):\2:\1/' # -E模式下括号不用转义
重要提醒:sed -i会直接修改文件,没有撤销操作。生产环境一定要先备份或者用sed -i.bak创建备份文件。我见过太多人用sed -i把配置文件改坏了,又没有备份,只能从别的机器拷贝。
3)awk核心参数
awk是三剑客中最复杂的,因为它是一门完整的编程语言。核心概念:
# 基本语法awk '{print $1}' # 打印第一个字段awk '{print $NF}' # 打印最后一个字段awk '{print NR, $0}' # 打印行号和整行awk -F: '{print $1}' # 指定分隔符为冒号awk -F'[,;:]' '{print $1}' # 多个分隔符# 条件过滤awk '$3 > 100 {print}' # 第3字段大于100的行awk '/ERROR/ {print}' # 包含ERROR的行awk 'NR==1 || NR%1000==0' # 打印第1行和每隔1000行# 内置变量# NR - 当前行号# NF - 当前行的字段数# FS - 字段分隔符# RS - 记录(行)分隔符# OFS - 输出字段分隔符# ORS - 输出记录分隔符# BEGIN和END块awk 'BEGIN{sum=0} {sum+=$1} END{print sum}' # 求和awk 'BEGIN{FS=":"} {print $1}' # 在BEGIN中设置分隔符# 数组awk '{count[$1]++} END{for(ip in count) print ip, count[ip]}' # 统计频次# 格式化输出awk '{printf "%-15s %10d\n", $1, $2}' # 格式化输出# 内置函数awk '{print length($0)}' # 字符串长度awk '{print substr($1, 1, 5)}' # 子字符串awk '{gsub(/old/, "new"); print}' # 替换awk '{print tolower($1)}' # 转小写
在正式开始处理日志之前,我习惯先做几个验证:
# 1. 确认文件大小和行数ls -lh access.logwc -l access.log# 2. 看前几行,确认格式head -5 access.log# 3. 看最后几行,确认文件完整tail -5 access.log# 4. 随机抽样检查shuf -n 10 access.log# 5. 检查是否有异常字符file access.log
这些检查能帮你避免很多坑,比如:
文件格式是不是UTF-8(如果有中文可能是GBK)
有没有被截断
行尾是LF还是CRLF(Windows下载的文件经常是CRLF)
三、示例代码和配置
场景一:统计Nginx日志中的TOP10 IP
这是最常见的需求,几乎每次排查问题都会用到:
# 方法1:传统三剑客组合awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10# 执行流程解析:# awk '{print $1}' - 提取IP地址(第一个字段)# sort - 按IP排序(为uniq准备)# uniq -c - 统计连续相同行的数量# sort -rn - 按数字倒序排列# head -10 - 取前10条# 方法2:纯awk实现(内存占用更低)awk '{ip[$1]++} END{for(i in ip) print ip[i], i}' access.log | sort -rn | head -10# 方法3:如果只关心某个时间段awk '/06\/Jan\/2025:10/ {ip[$1]++} END{for(i in ip) print ip[i], i}' access.log | sort -rn | head -10
性能对比:处理1GB日志(约1000万行)
方法2更快的原因是:awk在内存中用关联数组直接计数,避免了中间管道传输和sort的排序开销。
场景二:统计TOP10 URL及其响应时间
# 统计访问量TOP10 URLawk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10# 统计平均响应时间TOP10 URL(响应时间在最后一列)awk '{url=$7; time=$NF; count[url]++; sum[url]+=time}END{for(u in count) printf "%s %.3f %d\n", u, sum[u]/count[u], count[u]}' \access.log | sort -t' ' -k2 -rn | head -10# 更友好的输出格式awk '{url=$7; time=$NF; count[url]++; sum[url]+=time}END{printf "%-40s %10s %10s\n", "URL", "AvgTime", "Count"printf "%-40s %10s %10s\n", "---", "-------", "-----"for(u in count)printf "%-40s %10.3f %10d\n", u, sum[u]/count[u], count[u]}' access.log | sort -t' ' -k2 -rn | head -12
场景三:找出5xx错误的请求详情
# 找出所有500错误grep '" 500 ' access.log# 找出5xx错误并统计awk '$9 ~ /^5/ {print}' access.log | head -20# 统计5xx错误按URL分布awk '$9 ~ /^5/ {error[$7]++} END{for(u in error) print error[u], u}' \access.log | sort -rn | head -10# 统计5xx错误按小时分布(时间字段需要解析)awk '$9 ~ /^5/ {split($4, t, ":")hour = t[2]count[hour]++} END{for(h in count) print h":00", count[h]}' access.log | sort
场景四:从应用日志提取并聚合错误堆栈
这个需求稍微复杂一些,因为错误堆栈通常跨多行:
# 假设Java日志格式:# 2025-01-06 10:23:45.123 ERROR [main] c.e.Service - Error processing request# java.lang.NullPointerException: null# at com.example.Service.process(Service.java:45)# at com.example.Controller.handle(Controller.java:23)# 提取错误信息(ERROR行及其后续堆栈行)sed -n '/ERROR/,/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/p' app.log | sed '$d'# 更实用:提取ERROR和后续5行grep -A 5 "ERROR" app.log# 统计错误类型出现频率grep -oP "Exception: .*" app.log | sort | uniq -c | sort -rn | head -10# 统计异常类名频率grep -oP "[a-zA-Z]+Exception" app.log | sort | uniq -c | sort -rn# 完整的错误聚合脚本awk '/ERROR/ {error_line = $0exception = ""getlineif (/Exception|Error/) {exception = $0gsub(/^[ \t]+/, "", exception)}if (exception != "") {errors[exception]++}}END {for (e in errors) {print errors[e], e}}' app.log | sort -rn | head -20
案例一:批量修改配置文件
场景:需要把所有服务器上的Nginx配置中的worker_processes auto改成worker_processes 8:
# 先预览要修改的文件grep -rl "worker_processes auto" /etc/nginx/# 预览修改效果(不实际修改)sed -n 's/worker_processes auto/worker_processes 8/p' /etc/nginx/nginx.conf# 带备份的原地修改sed -i.bak 's/worker_processes auto/worker_processes 8/' /etc/nginx/nginx.conf# 批量修改所有匹配文件find /etc/nginx -name "*.conf" -exec sed -i.bak 's/worker_processes auto/worker_processes 8/' {} \;# 验证修改结果grep "worker_processes" /etc/nginx/nginx.confdiff /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
更复杂的例子:修改Redis配置中的多个参数
# 创建sed脚本文件cat > update_redis.sed << 'EOF's/^maxmemory .*/maxmemory 4gb/s/^maxmemory-policy .*/maxmemory-policy allkeys-lru/s/^# requirepass .*/requirepass YourSecurePassword123/s/^bind 127.0.0.1/bind 0.0.0.0/EOF# 应用修改sed -i.bak -f update_redis.sed /etc/redis/redis.conf# 验证grep -E "^(maxmemory|bind|requirepass)" /etc/redis/redis.conf
案例二:日志分析生成CSV报表
这个需求在月度汇报、容量规划时很常见:
# 生成每小时访问量统计CSVawk 'BEGIN {FS=" "OFS=","print "hour,requests,avg_response_time,error_rate"}{# 解析时间split($4, datetime, ":")hour = datetime[2]# 统计requests[hour]++response_time[hour] += $NFif ($9 >= 400) errors[hour]++}END {for (h=0; h<24; h++) {hh = sprintf("%02d", h)req = requests[hh] + 0avg_time = (req > 0) ? response_time[hh] / req : 0err_rate = (req > 0) ? errors[hh] * 100.0 / req : 0printf "%s:00,%d,%.3f,%.2f%%\n", hh, req, avg_time, err_rate}}' access.log > hourly_stats.csv# 生成每个URL的详细统计awk 'BEGIN {OFS=","print "url,requests,avg_time,min_time,max_time,p99_time"}{url = $7time = $NFcount[url]++sum[url] += timeif (!(url in min) || time < min[url]) min[url] = timeif (time > max[url]) max[url] = time}END {for (u in count) {avg = sum[u] / count[u]printf "%s,%d,%.3f,%.3f,%.3f\n", u, count[u], avg, min[u], max[u]}}' access.log | sort -t',' -k2 -rn > url_stats.csv
生成可视化友好的报表:
# 使用column命令美化输出awk '{ip[$1]++bytes[$1] += $10}END {printf "%-18s %10s %15s\n", "IP Address", "Requests", "Total Bytes"printf "%-18s %10s %15s\n", "-----------", "--------", "-----------"for (i in ip) {printf "%-18s %10d %15d\n", i, ip[i], bytes[i]}}' access.log | sort -t' ' -k2 -rn | head -20 | column -t
案例三:实时监控日志
三剑客配合tail可以实现简单的实时监控:
# 实时监控错误日志tail -f /var/log/app/error.log | grep --line-buffered "ERROR"# 实时统计每秒请求量tail -f access.log | awk '{count++if (NR % 100 == 0) {cmd = "date +%H:%M:%S"cmd | getline nowclose(cmd)print now, "- Requests in last 100 lines:", countcount = 0}}'# 实时监控5xx错误tail -f access.log | awk '$9 >= 500 {print strftime("%Y-%m-%d %H:%M:%S"), "5xx Error:", $7, "Status:", $9fflush()}'# 实时报警:某IP请求超过阈值tail -f access.log | awk '{ip[$1]++if (ip[$1] == 100) {print "ALERT: IP " $1 " reached 100 requests!"fflush()}}'
案例四:多文件关联分析
场景:从多个日志文件中关联分析同一个请求ID:
# 假设request_id在日志中的格式是 [req-xxx-xxx-xxx]# 从access.log找出慢请求的request_idawk '$NF > 5.0 {match($0, /\[req-[a-z0-9-]+\]/, arr); print arr[0]}' \access.log > slow_request_ids.txt# 用这些ID从app.log中提取完整日志grep -f slow_request_ids.txt app.log > slow_requests_detail.log# 一行命令完成(管道方式)awk '$NF > 5.0 {match($0, /\[req-[a-z0-9-]+\]/, arr); print arr[0]}' access.log | \xargs -I {} grep {} app.log
四、最佳实践和注意事项
经过这些年的实践,我总结了一些提升处理速度的技巧:
1)能用grep过滤的先过滤
这是最重要的原则。如果你只需要处理包含"ERROR"的行,先用grep过滤可以大幅减少后续处理的数据量:
# 慢:awk处理所有行awk '/ERROR/ {print $1}' access.log# 快:先用grep过滤grep "ERROR" access.log | awk '{print $1}'
grep的过滤速度比awk快很多,特别是使用-F(固定字符串)时。
2)使用并行处理
对于大文件,可以用GNU parallel或者xargs实现并行:
# 将大文件分割后并行处理split -l 1000000 access.log chunk_for f in chunk_*; doawk '{ip[$1]++} END{for(i in ip) print ip[i], i}' "$f" &done | wait | awk '{ip[$2]+=$1} END{for(i in ip) print ip[i], i}' | sort -rn | head -10# 使用GNU parallel(更优雅)cat access.log | parallel --pipe -L 1000000 'awk "{ip[\$1]++} END{for(i in ip) print ip[i], i}"' | \awk '{ip[$2]+=$1} END{for(i in ip) print ip[i], i}' | sort -rn | head -10# 多文件并行处理ls *.log | parallel -j4 'grep -c ERROR {}'
3)使用ripgrep替代grep
ripgrep(rg)是用Rust写的grep替代品,在大多数场景下都比GNU grep快:
# 安装ripgrep# Ubuntu: apt install ripgrep# macOS: brew install ripgrep# 基本用法rg "ERROR" access.log# 速度对比(1GB日志文件)time grep "ERROR" access.log | wc -l # 约2.5秒time rg "ERROR" access.log | wc -l # 约0.8秒# ripgrep独有功能rg -t java "Exception" # 只搜索Java文件rg --stats "ERROR" access.log # 显示统计信息rg -C 3 "ERROR" access.log # 上下文显示rg --json "ERROR" access.log # JSON输出,便于后续处理
4)避免不必要的排序
sort是性能杀手,尤其是处理大文件时会产生临时文件:
# 如果只需要TOP N,用awk+sort比全排序快# 不好:先排序所有数据sort access.log | uniq -c | sort -rn | head -10# 好:用awk预处理后再排序awk '{count[$1]++} END{for(i in count) print count[i], i}' access.log | sort -rn | head -10
5)使用LC_ALL=C加速
设置LC_ALL=C可以禁用UTF-8处理,大幅提升速度:
# 默认(支持UTF-8)time grep "ERROR" access.log | wc -l# 纯ASCII模式(更快)LC_ALL=C time grep "ERROR" access.log | wc -l# 速度差异可达2-3倍
6)利用mmap和buffer
处理大文件时,调整buffer大小可以提升性能:
# grep使用较大的buffergrep --buffer-size=1M "ERROR" access.log# 使用pv监控处理进度pv access.log | grep "ERROR" | wc -l
1)永远不要直接在生产环境执行sed -i
# 错误做法sed -i 's/old/new/g' /etc/nginx/nginx.conf# 正确做法cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup.$(date +%Y%m%d_%H%M%S)sed -i 's/old/new/g' /etc/nginx/nginx.confnginx -t && systemctl reload nginx || cp /etc/nginx/nginx.conf.backup.* /etc/nginx/nginx.conf
2)处理用户输入时防止命令注入
# 危险:直接拼接用户输入user_input="$1"grep "$user_input" access.log# 安全:使用-F固定字符串,或者转义grep -F "$user_input" access.log# 或者user_input=$(printf '%s' "$1" | sed 's/[[\.*^$()+?{|]/\\&/g')grep "$user_input" access.log
3)限制资源使用
# 限制内存使用ulimit -v 2097152 # 限制为2GB虚拟内存# 限制CPU时间timeout 60 awk '{...}' huge.log# 使用nice降低优先级(不影响线上服务)nice -n 19 awk '{...}' huge.log
错误1:字段分隔符理解错误
# 问题:默认分隔符是空白字符(空格和Tab),连续空格被当作一个分隔符echo "a b c" | awk '{print $2}' # 输出:b# 如果需要严格按单个空格分隔echo "a b c" | awk -F' ' '{print $2}' # 输出:空(第二个空格)# 处理CSV时常见错误# 错误:字段中有空格会出问题awk -F',' '{print $2}' data.csv# 正确:需要处理引号awk -F',' '{gsub(/"/, ""); print $2}' data.csv
错误2:正则表达式贪婪匹配
# 问题:想匹配第一个引号内的内容echo '"hello" "world"' | grep -oP '".*"' # 输出:"hello" "world"# 正确:使用非贪婪匹配echo '"hello" "world"' | grep -oP '".*?"' # 输出:"hello" 和 "world"echo '"hello" "world"' | grep -oP '"[^"]*"' # 另一种写法
错误3:多行处理的坑
# 问题:想匹配跨行的模式# 这不会工作,因为grep是逐行处理的grep "start.*end" file.log# 正确:使用sed或awk处理多行sed -n '/start/,/end/p' file.logawk '/start/,/end/' file.log# 或者使用grep -z(用NUL作为行分隔符)grep -zoP "start[\s\S]*?end" file.log
错误4:忘记处理特殊字符
# 问题:文件名或内容包含特殊字符# 这会出错grep $variable file.log# 正确:加引号grep "$variable" file.loggrep -- "$variable" file.log # 如果variable可能以-开头
错误5:awk数值精度问题
# 问题:awk默认的数值精度echo "0.1 0.2" | awk '{print $1 + $2}' # 可能输出:0.3或0.30000000000000004# 解决:使用printf控制精度echo "0.1 0.2" | awk '{printf "%.2f\n", $1 + $2}'# 或者使用bc处理高精度计算echo "0.1 + 0.2" | bc -l
五、故障排查和监控
1)快速定位问题的技巧
# 1. 先看文件大小和时间范围ls -lh *.loghead -1 app.log && tail -1 app.log# 2. 快速统计错误级别分布grep -oP "(DEBUG|INFO|WARN|ERROR|FATAL)" app.log | sort | uniq -c# 3. 查看错误趋势(按小时)grep "ERROR" app.log | awk '{print substr($1, 1, 13)}' | uniq -c# 4. 找出最近10分钟的错误awk -v start=$(date -d '10 minutes ago' +%Y-%m-%dT%H:%M) '$1 > start && /ERROR/' app.log# 5. 随机采样查看shuf -n 100 access.log | less
2)结构化日志处理
现代应用越来越多使用JSON格式日志:
# 提取JSON日志中的特定字段grep "ERROR" app.log | jq -r '.message'# 三剑客配合jq使用awk '/ERROR/' app.log | jq -r '[.timestamp, .level, .message] | @tsv'# 如果没有jq,用grep+sed也能凑合grep "ERROR" app.log | sed 's/.*"message":"\([^"]*\)".*/\1/'# 统计JSON日志中的错误类型grep "ERROR" app.log | grep -oP '"error_type":"\K[^"]+' | sort | uniq -c | sort -rn
1)常见问题诊断流程
问题1:命令执行很慢
# 诊断:检查文件大小ls -lh target.logwc -l target.log# 诊断:检查是否有复杂正则# 避免使用 .* 这类可能导致回溯的模式# 解决:使用ripgrep或者加LC_ALL=CLC_ALL=C rg "pattern" target.log
问题2:awk结果不正确
# 诊断:打印字段看看是否符合预期head -5 target.log | awk '{for(i=1;i<=NF;i++) print i": "$i}'# 诊断:检查分隔符cat -A target.log | head -1 # 显示不可见字符# 常见原因:# - 文件是DOS格式(行尾有\r)# - 字段分隔符不是空格# - 数据中有意外的空格
问题3:sed替换没生效
# 诊断:先不加-i测试sed 's/old/new/g' file | head -10# 诊断:确认模式能匹配grep "old" file# 常见原因:# - 特殊字符没转义# - 使用了正则表达式元字符# - 原文本包含Tab而非空格
问题4:内存不足
# 诊断:监控内存使用top -b -n 1 | grep awk# 解决方案1:使用更节省内存的方法# 避免把所有数据加载到awk数组# 解决方案2:分片处理split -l 1000000 huge.log part_for f in part_*; doprocess "$f" >> result.txtdone# 解决方案3:使用sort的外部排序sort -T /path/to/tmp --buffer-size=1G huge.log
1)简易的日志监控脚本
#!/bin/bash# log_monitor.sh - 简易日志监控脚本LOG_FILE="/var/log/app/app.log"ALERT_THRESHOLD=10CHECK_INTERVAL=60 # 秒send_alert() {local message="$1"# 发送告警(这里用curl调用webhook为例)curl -X POST "https://your-webhook-url" \-H "Content-Type: application/json" \-d "{\"text\": \"$message\"}"}while true; do# 统计最近1分钟的错误数start_time=$(date -d '1 minute ago' '+%Y-%m-%d %H:%M')error_count=$(awk -v start="$start_time" '$1" "$2 >= start && /ERROR/' "$LOG_FILE" | wc -l)if [ "$error_count" -ge "$ALERT_THRESHOLD" ]; thensend_alert "[ALERT] Error count in last minute: $error_count"fi# 检查5xx错误if [ -f "/var/log/nginx/access.log" ]; thenfive_xx=$(tail -10000 /var/log/nginx/access.log | awk '$9 ~ /^5/ {count++} END {print count+0}')if [ "$five_xx" -ge 50 ]; thensend_alert "[ALERT] High 5xx error rate: $five_xx in last 10k requests"fifisleep "$CHECK_INTERVAL"done
2)配合cron的定时分析
# /etc/cron.d/log_analysis# 每小时生成访问统计0 * * * * root /opt/scripts/hourly_log_analysis.sh# 每天凌晨2点生成日报0 2 * * * root /opt/scripts/daily_report.sh# 每5分钟检查错误率*/5 * * * * root /opt/scripts/error_rate_check.sh
#!/bin/bash# hourly_log_analysis.shLOG_FILE="/var/log/nginx/access.log"REPORT_DIR="/var/log/reports"HOUR=$(date -d '1 hour ago' '+%Y%m%d_%H')mkdir -p "$REPORT_DIR"# 提取上一小时的日志hour_pattern=$(date -d '1 hour ago' '+%d/%b/%Y:%H')grep "$hour_pattern" "$LOG_FILE" > "/tmp/hour_${HOUR}.log"# 生成统计报告{echo "Hourly Report: $HOUR"echo "========================="echo ""echo "Total Requests: $(wc -l < /tmp/hour_${HOUR}.log)"echo ""echo "Status Code Distribution:"awk '{count[$9]++} END{for(s in count) print s, count[s]}' /tmp/hour_${HOUR}.log | sort -k2 -rnecho ""echo "Top 10 IPs:"awk '{print $1}' /tmp/hour_${HOUR}.log | sort | uniq -c | sort -rn | head -10echo ""echo "Top 10 URLs:"awk '{print $7}' /tmp/hour_${HOUR}.log | sort | uniq -c | sort -rn | head -10echo ""echo "Top 10 Slowest Requests:"sort -t' ' -k"$NF" -rn /tmp/hour_${HOUR}.log | head -10} > "${REPORT_DIR}/hourly_${HOUR}.txt"# 清理临时文件rm -f "/tmp/hour_${HOUR}.log"
六、总结
经过这些年处理各种日志问题的经验,我总结出以下关键点:
1)选择合适的工具
简单搜索用grep(或ripgrep)
文本替换用sed
复杂统计用awk
三者组合能解决90%的日志处理需求
2)性能优化原则
先过滤再处理(grep在前,awk在后)
使用LC_ALL=C加速纯ASCII处理
大文件考虑并行处理
ripgrep通常比grep快3倍以上
3)安全第一
生产环境修改文件必须先备份
用户输入要防止命令注入
资源密集型操作用nice降低优先级
4)调试技巧
先用head测试小数据集
打印中间结果验证每一步
注意分隔符和特殊字符
如果你想在三剑客上更进一步,我建议按这个顺序学习:
1)初级阶段
熟练使用grep的基本参数(-n, -i, -c, -v, -A/B/C)
掌握sed的基本替换和删除操作
学会awk按字段处理数据
2)中级阶段
理解正则表达式的各种模式(贪婪、非贪婪、分组)
掌握sed的多行处理和脚本文件
学会awk的数组和BEGIN/END块
3)高级阶段
深入理解各工具的性能特点,能根据场景选择最优方案
能写复杂的awk程序,包括自定义函数
能把三剑客组合进Shell脚本,实现自动化运维
三剑客之外,还有一些现代工具值得关注:
**ripgrep (rg)**:Rust写的grep替代品,更快更好用
fd:find的现代替代品,语法更友好
jq:JSON数据处理利器,处理结构化日志必备
miller:CSV/JSON处理工具,比awk更适合处理结构化数据
xsv:超快的CSV处理工具
这些工具不是要替代三剑客,而是在特定场景下的补充。核心还是要把grep、sed、awk用熟,它们是所有Linux系统的标配,不需要额外安装。
参考资料
GNU Grep Manual: https://www.gnu.org/software/grep/manual/
GNU Sed Manual: https://www.gnu.org/software/sed/manual/
GAWK Manual: https://www.gnu.org/software/gawk/manual/
Regular Expressions Info: https://www.regular-expressions.info/
ripgrep GitHub: https://github.com/BurntSushi/ripgrep
grep速查
sed速查
awk速查
基础元字符
PCRE扩展(grep -P)
# 统计IP访问量TOP10awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10# 统计HTTP状态码分布awk '{print $9}' access.log | sort | uniq -c | sort -rn# 找出响应时间超过5秒的请求awk '$NF > 5' access.log# 统计每小时访问量awk '{split($4, a, ":"); print a[2]}' access.log | sort | uniq -c# 找出5xx错误并统计URLawk '$9 >= 500 {print $7}' access.log | sort | uniq -c | sort -rn | head -10# 提取某时间段的日志awk '$4 >= "[06/Jan/2025:10:00:00" && $4 <= "[06/Jan/2025:11:00:00"' access.log# 统计带宽消耗TOP10 IPawk '{ip[$1]+=$10} END{for(i in ip) print ip[i], i}' access.log | sort -rn | head -10# 分析User-Agent分布awk -F'"' '{print $6}' access.log | sort | uniq -c | sort -rn | head -10# 批量替换文件中的IP地址sed -i 's/192\.168\.1\.1/10.0.0.1/g' *.conf# 删除日志中的敏感信息sed 's/password=[^&]*/password=****/g' app.log# 提取JSON日志中的message字段grep -oP '"message":"\K[^"]+' app.log# 合并多个日志文件并按时间排序cat *.log | sort -k1,2# 实时监控错误日志tail -f app.log | grep --line-buffered "ERROR"# 统计日志文件中的唯一IP数awk '{print $1}' access.log | sort -u | wc -l# 找出包含多个关键词的行grep "ERROR" app.log | grep "database" | grep "timeout"# 提取特定时间范围的错误sed -n '/2025-01-06 10:00/,/2025-01-06 11:00/p' app.log | grep ERROR
测试环境:
CPU: Intel Xeon E5-2680 v4 @ 2.40GHz (8核)
内存: 32GB
磁盘: NVMe SSD
日志大小: 10GB(约1亿行)
如果您认为这篇文章有些帮助,还请不吝点下文章末尾的"点赞"和"在看",或者直接转发朋友圈,
