1. Shell并发编程的必要性与基础概念
在企业级运维场景中,我们经常需要处理日志分析、批量部署、数据迁移等重复性任务。传统单进程脚本就像超市只开一个收银台,顾客排起长队等待结账。我曾负责过一个日志分析项目,单进程处理10GB日志需要6小时,而通过多进程改造后仅需40分钟——这就是并发编程的价值所在。
Shell实现并发处理的本质是通过fork()系统调用创建子进程。虽然不像Java/Python那样有原生线程支持,但通过进程管理技术完全可以实现高效的并发效果。这里需要明确几个关键概念:
- 后台进程:在命令后添加
&符号使其在后台运行,释放当前终端 - 进程间通信:通过命名管道、临时文件或共享内存交换数据
- 信号机制:使用trap命令处理SIGCHLD等信号实现进程状态监控
- 进程池:控制并发进程数量避免系统过载
重要提示:Linux系统中进程创建成本较高(相比线程),建议单个脚本并发进程数不要超过CPU核心数的2倍。我的经验值是核心数+2,比如8核机器最多10个并发进程。
2. 基础并发模式实现
2.1 后台执行基础版
最简单的并发实现是将命令放入后台:
bash复制#!/bin/bash
# 任务列表
tasks=("task1" "task2" "task3" "task4")
for task in "${tasks[@]}"; do
execute_task "$task" & # &符号使命令后台运行
done
wait # 等待所有后台任务完成
echo "所有任务执行完毕"
这种模式的问题在于:
- 无法控制并发数量,可能压垮系统
- 缺乏任务状态监控
- 子进程输出会混在一起
2.2 改进版进程控制
通过计数器实现简单的并发控制:
bash复制#!/bin/bash
max_workers=4 # 根据CPU核心数设置
task_list=(/* 任务数组 */)
current_workers=0
for task in "${task_list[@]}"; do
while [ $current_workers -ge $max_workers ]; do
wait -n # 等待任意一个子进程结束
((current_workers--))
done
((current_workers++))
{
# 实际任务执行代码
process_task "$task"
} &
done
wait
这个版本解决了无限制并发的问题,但仍有改进空间。我在实际项目中发现wait -n在较老的bash版本(<4.3)可能不支持,此时可以用sleep轮询替代。
3. 高级并发框架实现
3.1 进程池管理器
下面展示一个更健壮的进程池实现:
bash复制#!/bin/bash
# 配置区
POOL_SIZE=$(nproc) # 默认按CPU核心数设置
TASK_TIMEOUT=300 # 单个任务超时时间(秒)
LOG_DIR="./task_logs" # 日志目录
[ -d "$LOG_DIR" ] || mkdir -p "$LOG_DIR"
# 进程池函数
manage_pool() {
local task_id=0
declare -A pid_table # 进程ID记录表
for task in "${TASKS[@]}"; do
while [ $(jobs -rp | wc -l) -ge $POOL_SIZE ]; do
sleep 0.1 # 等待空闲进程槽
done
((task_id++))
(
trap 'echo "[ERROR] 任务${task_id}超时"; exit 124' SIGTERM
( sleep $TASK_TIMEOUT; kill -s SIGTERM $$ ) &
watchdog_pid=$!
# --- 实际任务开始 ---
execute_task "$task" > "${LOG_DIR}/task_${task_id}.log" 2>&1
# --- 实际任务结束 ---
kill $watchdog_pid 2>/dev/null
) &
pid_table[$!]=$task_id # 记录PID与任务关系
done
wait # 等待所有后台任务完成
}
这个实现包含以下关键改进:
- 动态获取CPU核心数作为默认并发数
- 为每个任务设置独立日志文件
- 增加任务超时强制终止机制
- 使用进程表跟踪任务状态
3.2 性能优化技巧
根据我的调优经验,以下参数对性能影响最大:
-
并发数设置:
bash复制# 最佳实践公式 POOL_SIZE=$(( $(nproc) * 2 - 1 )) -
任务分片策略:
- I/O密集型:增加并发数(最高可达CPU核心数×3)
- CPU密集型:减少并发数(建议CPU核心数×0.8)
-
内存限制:
bash复制# 在任务执行前设置内存限制 ulimit -Sv 2000000 # 限制2GB内存
4. 实战案例:分布式日志分析
假设我们需要分析Nginx日志统计不同URL的访问量,下面是并发实现方案:
4.1 任务分片
bash复制# 将大日志文件拆分为多个小文件
split -l 100000 access.log segment_
# 生成任务列表
TASKS=(segment_*)
4.2 分析任务脚本
bash复制analyze_segment() {
local segment=$1
awk '{print $7}' "$segment" | sort | uniq -c | sort -nr > "${segment}.result"
# 更复杂的分析可以替换这里的awk脚本
}
4.3 主控脚本
bash复制#!/bin/bash
source ./analyze_functions.sh # 载入分析函数
POOL_SIZE=$(( $(nproc) + 2 ))
RESULT_DIR="./analysis_results"
mkdir -p "$RESULT_DIR"
# 进程池执行
current_jobs=0
for segment in segment_*; do
while [ $(jobs -rp | wc -l) -ge $POOL_SIZE ]; do
sleep 0.5
done
((current_jobs++))
{
echo "开始处理 $segment"
analyze_segment "$segment"
mv "${segment}.result" "$RESULT_DIR/"
} &
done
wait
# 合并结果
cat "$RESULT_DIR"/*.result | sort -k2 | awk '{
count[$2]+=$1
} END {
for(url in count) print count[url], url
}' | sort -nr > final_result.txt
这个方案在我司生产环境处理50GB日志时,将处理时间从18小时缩短到2小时。
5. 异常处理与调试技巧
5.1 常见问题排查
-
僵尸进程累积:
bash复制trap 'wait $!' SIGCHLD # 正确处理子进程退出 -
任务卡死:
bash复制timeout 300 ./task.sh || echo "任务超时" >> errors.log -
资源竞争:
bash复制( flock -x 200 # 获取文件锁 # 临界区代码 ) 200>/var/lock/mylock
5.2 调试建议
-
记录详细的执行日志:
bash复制exec > >(tee -a "$LOG_FILE") 2>&1 -
使用进程状态监控:
bash复制watch -n 1 'ps -eo pid,ppid,pgid,cmd | grep -E "task|worker"' -
性能分析工具:
bash复制
strace -ff -o trace ./master_script.sh
6. 进阶话题:分布式任务队列
对于跨服务器的任务分发,可以结合SSH和消息队列:
bash复制# Worker节点配置
while true; do
task=$(redis-cli rpop task_queue)
[ -z "$task" ] && sleep 5 && continue
execute_task "$task"
done
# Master节点
for task in "${TASKS[@]}"; do
redis-cli lpush task_queue "$task"
done
这种架构可以轻松扩展到数百台机器,我在实际项目中用这个方案实现了日均处理千万级任务的能力。
最后分享一个实用技巧:在长时间运行的并发脚本中,添加心跳检测机制:
bash复制# 在主循环中添加
while true; do
echo "[HEARTBEAT] $(date) - 运行中" >> status.log
sleep 60
done
这可以帮助你确认脚本是否仍在正常运行,特别是在无人值守的后台任务中。