AWK & SED 生产力教程 / 第 14 章:性能优化
第 14 章:性能优化
当文件从 MB 变成 GB,当请求从千变成百万,性能优化就成了必修课。
14.1 性能瓶颈分析
常见瓶颈类型
| 瓶颈类型 | 表现 | 原因 |
|---|---|---|
| I/O 瓶颈 | 磁盘读写慢 | 大文件顺序读取、频繁随机读取 |
| CPU 瓶颈 | 正则匹配慢 | 复杂正则、大量计算 |
| 内存瓶颈 | 内存溢出 | 大数组、多行处理 |
| 进程瓶颈 | 启动开销大 | 多次启动外部命令 |
性能测试方法
# 使用 time 测量执行时间
$ time awk '{print $1}' huge.log | sort -u | wc -l
# 使用 pv 监控管道吞吐量
$ cat huge.log | pv -l | awk '{print $1}' | pv -l > /dev/null
# 使用 strace 查看系统调用
$ strace -c awk '{print $1}' huge.log 2>&1 | tail -20
# 使用 /usr/bin/time 获取详细统计
$ /usr/bin/time -v awk '{print $1}' huge.log > /dev/null 2>&1
14.2 AWK 性能优化
减少不必要的输出
# ❌ 慢:打印所有行
$ awk '{print}' huge.log > /dev/null
# ✅ 快:只做计数
$ awk 'END{print NR}' huge.log
# ❌ 慢:每行都做字符串连接
$ awk '{result = result $0 "\n"} END{print result}' huge.log
# ✅ 快:直接输出
$ awk '{print}' huge.log
优化正则表达式
# ❌ 慢:复杂正则
$ awk '/^.*error.*[0-9]+.*$/' huge.log
# ✅ 快:简单匹配 + 锚定
$ awk '/error/' huge.log
# ❌ 慢:每次都编译正则
$ awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/) print}' huge.log
# ✅ 快:预编译正则(GNU AWK)
$ awk 'BEGIN{digit_re = /^[0-9]+$/} {for(i=1;i<=NF;i++) if($i ~ digit_re) print}' huge.log
减少字段分割
# ❌ 慢:AWK 自动分割所有字段
$ awk '{print $1}' huge.log
# ✅ 快:如果只需要第一列,用 substr 或 index
$ awk '{print substr($0, 1, index($0, " ")-1)}' huge.log
# ✅ 更快:使用 cut
$ cut -d' ' -f1 huge.log
优化数组操作
# ❌ 慢:使用 split 创建数组
$ awk '{n=split($0, a, ","); count[a[1]]++}' huge.csv
# ✅ 快:使用 -F 分隔符
$ awk -F, '{count[$1]++}' huge.csv
# ❌ 慢:在 END 中遍历大数组排序
$ awk '{count[$1]++} END{for(k in count) print count[k], k | "sort -rn"}' huge.log
# ✅ 快:使用 PROCINFO["sorted_in"](GNU AWK)
$ awk 'BEGIN{PROCINFO["sorted_in"]="@val_num_desc"} {count[$1]++} END{for(k in count) print count[k], k}' huge.log
使用 next 提前跳过
# ❌ 慢:检查所有条件
$ awk '{
if ($1 == "skip") { }
else if ($9 >= 400) { print }
else { }
}' huge.log
# ✅ 快:尽早跳过不需要的行
$ awk '$1 == "skip" {next}
$9 >= 400 {print}' huge.log
14.3 SED 性能优化
减少替换次数
# ❌ 慢:在所有行上执行替换
$ sed 's/old/new/g' huge.log
# ✅ 快:只在匹配行上执行
$ sed '/old/s/old/new/g' huge.log
# ✅ 更快:第一次替换后跳到下一行(不加 g)
$ sed 's/old/new/' huge.log
优化地址范围
# ❌ 慢:扫描整个文件
$ sed 's/old/new/g' huge.log
# ✅ 快:限制行范围
$ sed '1,1000s/old/new/g' huge.log
# ✅ 更快:使用 quit 提前退出
$ sed '/PATTERN/!{s/old/new/}' huge.log
减少命令数量
# ❌ 慢:多次调用 sed
$ cat file | sed 's/a/b/' | sed 's/c/d/' | sed 's/e/f/'
# ✅ 快:合并为一个 sed
$ sed 's/a/b/; s/c/d/; s/e/f/' file
# ✅ 更快:使用 -e 选项
$ sed -e 's/a/b/' -e 's/c/d/' -e 's/e/f/' file
14.4 管道优化
减少进程数量
# ❌ 慢:5 个进程
$ cat file | grep 'pattern' | awk '{print $1}' | sort | uniq -c
# ✅ 快:3 个进程(减少 cat 和 uniq)
$ grep 'pattern' file | awk '{count[$1]++} END{for(k in count) print count[k], k}' | sort -rn
# ✅ 更快:1 个进程
$ awk '/pattern/ {count[$1]++} END{for(k in count) print count[k], k}' file | sort -rn
提前过滤
# ❌ 慢:先提取再过滤
$ awk '{print $1, $9}' huge.log | grep '404'
# ✅ 快:先过滤再提取
$ grep '404' huge.log | awk '{print $1, $9}'
# ✅ 更快:在 AWK 中同时过滤和提取
$ awk '$9 == "404" {print $1, $9}' huge.log
使用 LC_ALL=C 加速
# 默认 locale 处理 UTF-8 比较慢
$ sort huge.txt
# 使用 C locale 可以加速 2-5 倍
$ LC_ALL=C sort huge.txt
# 在 AWK 中同样有效
$ LC_ALL=C awk '{print $1}' huge.log | LC_ALL=C sort -u
14.5 大文件处理
分块处理
# 将大文件分成小块处理
$ split -l 1000000 huge.txt chunk_
$ for f in chunk_*; do
awk '{count[$1]++} END{for(k in count) print count[k], k}' "$f" > "${f}.result"
done
$ cat chunk_*.result | awk '{count[$2]+=$1} END{for(k in count) print count[k], k}' | sort -rn
$ rm chunk_*
使用 head/tail 进行采样
# 快速采样分析
$ head -10000 huge.log | awk '{count[$1]++} END{for(k in count) print count[k], k}' | sort -rn
# 随机采样(1%)
$ awk 'BEGIN{srand()} rand() < 0.01' huge.log | wc -l
流式处理避免内存问题
# ❌ 危险:将所有行存入数组
$ awk '{lines[NR] = $0} END{for(i=NR; i>=1; i--) print lines[i]}' huge.log
# ✅ 安全:使用 tac 命令
$ tac huge.log
# ❌ 危险:存储所有唯一值
$ awk '{seen[$0]++} END{for(k in seen) print k}' huge.log
# ✅ 安全:使用 sort -u
$ sort -u huge.log
使用 FILENAME 和多文件
# 分文件处理,避免混合处理
$ awk '
FILENAME != prev_file {
if (prev_file != "") process_file(prev_file)
prev_file = FILENAME
delete count
}
{ count[$1]++ }
END { process_file(FILENAME) }
function process_file(f) {
for (k in count) print f, k, count[k]
}
' file1.txt file2.txt file3.txt
14.6 并行处理
使用 xargs 并行
# 并行处理多个文件
$ find . -name "*.log" | xargs -P 4 -I {} sh -c '
awk "{count[\$9]++} END{for(k in count) print FILENAME, k, count[k]}" {}
'
# 并行统计
$ find . -name "*.log" | xargs -P $(nproc) -I {} wc -l {} | awk '{sum+=$1} END{print sum}'
使用 GNU Parallel
# 安装 GNU Parallel
$ sudo apt install parallel # Debian/Ubuntu
$ sudo yum install parallel # CentOS/RHEL
# 并行处理
$ find . -name "*.log" | parallel 'awk "{count[\$9]++} END{for(k in count) print {}, k, count[k]}"'
# 带进度条的并行处理
$ find . -name "*.log" | parallel --bar 'grep -c "error" {}'
# 控制并行数
$ find . -name "*.log" | parallel -j 4 'awk "/error/ {count++} END{print {}, count}" {}'
合并并行结果
# 并行处理并合并结果
$ find . -name "*.log" | parallel 'awk "{count[\$1]++} END{for(k in count) print count[k], k}" {}' \
| awk '{count[$2]+=$1} END{for(k in count) print count[k], k}' | sort -rn
14.7 内存管理
AWK 内存使用模式
# ❌ 内存密集型:存储所有行
$ awk '{lines[NR]=$0} END{...}' huge.log # 内存 = 文件大小
# ❌ 内存密集型:存储所有唯一键
$ awk '{seen[$0]++} END{...}' huge.log # 内存 = 唯一键数量
# ✅ 流式处理:逐行处理
$ awk '{process($0)}' huge.log # 内存 ≈ 0
监控内存使用
# 监控 AWK 进程的内存使用
$ awk '{count[$1]++} END{for(k in count) print k, count[k]}' huge.log &
PID=$!
while kill -0 $PID 2>/dev/null; do
ps -o rss= -p $PID
sleep 1
done
# 使用 /usr/bin/time 获取内存统计
$ /usr/bin/time -v awk '{count[$1]++} END{for(k in count) print k, count[k]}' huge.log 2>&1 | grep "Maximum resident"
限制数组大小
# 定期清理数组
$ awk '
{
count[$1]++
# 每 100 万行输出一次并清理
if (NR % 1000000 == 0) {
for (k in count) print count[k], k
delete count
}
}
END {
for (k in count) print count[k], k
}' huge.log | awk '{count[$2]+=$1} END{for(k in count) print count[k], k}' | sort -rn
14.8 性能对比测试
基准测试框架
#!/bin/bash
# benchmark.sh — 性能基准测试
LOG_FILE="${1:?用法: $0 <日志文件>}"
ITERATIONS=3
run_benchmark() {
local name="$1"
local cmd="$2"
local total_time=0
echo "测试: $name"
for ((i=1; i<=ITERATIONS; i++)); do
start=$(date +%s%N)
eval "$cmd" > /dev/null
end=$(date +%s%N)
elapsed=$(( (end - start) / 1000000 ))
total_time=$((total_time + elapsed))
echo " 第 ${i} 次: ${elapsed}ms"
done
avg=$((total_time / ITERATIONS))
echo " 平均: ${avg}ms"
echo ""
echo "$avg" > "/tmp/bench_${name}.txt"
}
# 运行基准测试
run_benchmark "grep+awk" "grep 'error' $LOG_FILE | awk '{print \$1}' | sort -u"
run_benchmark "纯awk" "awk '/error/ && !seen[\$1]++ {print \$1}' $LOG_FILE"
run_benchmark "LC_ALL=C" "LC_ALL=C awk '/error/ && !seen[\$1]++ {print \$1}' $LOG_FILE"
14.9 性能优化速查
# 通用优化原则
1. 先过滤再处理(减少数据量)
2. 减少进程数量(合并命令)
3. 使用 LC_ALL=C(加速字符处理)
4. 避免不必要的排序(使用 uniq -c 替代 sort | uniq -c)
5. 使用流式处理(避免存储大数组)
6. 预编译正则(AWK 的 BEGIN 块)
7. 使用 xargs -P 或 parallel(并行处理)
8. 采样分析(head -N 进行快速测试)
# 工具选择
小文件 (<1MB): 任何工具都行
中等文件 (1-100MB): AWK/SED 优化版
大文件 (>100MB): 并行处理 + 分块
超大文件 (>1GB): 流式处理 + 并行
扩展阅读
- GNU AWK Performance Tips
- GNU Parallel Tutorial
- Systems Performance — Brendan Gregg
下一章:第 15 章:最佳实践 — 代码风格、调试技巧、常见陷阱、生产力提升。