强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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):    流式处理 + 并行

扩展阅读


下一章:第 15 章:最佳实践 — 代码风格、调试技巧、常见陷阱、生产力提升。