检查脚本中的循环和重复操作
有时候一个看似简单的 shell 脚本跑得特别慢,问题可能出在循环里。比如你在处理上千个文件时,每轮都调用外部命令 like find 或 grep,就会不断启动新进程,拖慢整体速度。
举个例子,下面这种写法就很常见但效率低:
for file in *.log; do
grep "ERROR" $file >> errors.log
done
每次循环都会调用一次 grep,换成一行命令就能省下大量开销:
grep "ERROR" *.log > errors.log
避免频繁的磁盘读写
如果脚本一直在读写临时文件,比如每次处理一点数据就 echo >> temp.txt,这种小操作积少成多,会成为瓶颈。尽量把数据缓存在变量或管道中,减少 IO 次数。
比如要统计多个日志中某个字段的总和,别这么写:
for log in access_*.log; do
awk '{sum += $10} END {print sum}' $log >> tmp_sums
done
awk '{total += $1} END {print total}' tmp_sums
直接一条 awk 就能搞定:
awk '{sum += $10} END {print sum}' access_*.log
用内置功能代替外部命令
shell 本身支持一些字符串处理,比如截取、替换,用 ${var#*pattern} 比调用 sed 或 cut 快得多。特别是循环里,调一次外部命令的代价远高于内置操作。
比如提取文件名后缀,别总是依赖 basename 或 awk:
filename="data_2024.txt"
ext=${filename##*.}
这样更快,也不依赖外部工具。
并行处理提升效率
有些任务彼此独立,比如压缩多个大文件,一个个来太耗时。可以用 & 放到后台并发执行,控制好数量别把系统拖垮就行。
max_jobs=4
for file in *.txt; do
gzip "$file" &
# 控制并发数
if (( $(jobs -r | wc -l) >= max_jobs )); then
wait -n
fi
done
wait
这样同时最多跑 4 个 gzip,充分利用 CPU 空闲时间。
用更高效的工具替代部分逻辑
如果脚本里大量文本处理靠 while read 一行行啃,速度很难提上去。这时候不如交给 awk 或 perl 一把梭。比如要解析 CSV 并按列过滤,用 awk 几秒完事,shell 循环可能要几十秒。
原写法:
while IFS=, read name age city; do
if [[ $age -gt 30 ]]; then
echo $name
fi
done < users.csv
换成:
awk -F, '$2 > 30 {print $1}' users.csv
看看是不是 I/O 等待卡住了
运行脚本时用 top 或 htop 观察,如果 CPU 使用率很低,但脚本就是不动,大概率是卡在磁盘或网络读取上。比如从远程挂载的 NFS 目录读大量小文件,延迟会很明显。
可以先用 iotop 看看哪个进程在疯狂读写,再决定是否本地缓存数据或优化访问方式。
启用 shell 优化选项
Bash 有个 lastpipe 选项,在非交互模式下能让最后一个管道元素在主线程运行,避免子 shell 带来的变量作用域问题,有时也能省点开销。
set +m # 关闭作业控制
shopt -s lastpipe
echo "1 2 3" | while read a b c; do
value=$a
done
echo $value # 此时 value 能拿到值
用 time 和 strace 定位瓶颈
最实在的办法是动手测。用 time 包一层脚本,看总共花了多少时间。
time ./slow_script.sh
如果发现用户态时间(user)高,说明计算密集;系统态(sys)高,可能是系统调用太多。再用 strace 跟一下:
strace -c ./slow_script.sh
输出会告诉你哪些系统调用最频繁,比如一堆 openat 或 stat,那就知道该优化文件访问了。