Hướng dẫn chạy song song trong Bash với background process, wait, jobs, xargs -P, GNU parallel, quản lý concurrency, race condition và file lock flock trong DevOps.
Trong bài viết trước, chúng ta đã tìm hiểu cách thu thập metric hệ thống và gửi cảnh báo tự động. Nhưng có một vấn đề mà monitoring script thường gặp phải: khi bạn cần kiểm tra 50 server cùng lúc, chạy tuần tự từng máy sẽ mất hàng phút — thậm chí hàng giờ nếu mỗi server mất vài giây để phản hồi.
Parallel Execution giải quyết vấn đề này: thay vì chờ server A xong mới qua server B, bạn chạy cả hai cùng lúc. Bash cung cấp nhiều cơ chế để thực hiện song song — từ &/wait đơn giản, xargs -P tiện lợi, đến GNU parallel mạnh mẽ. Bài viết này sẽ hướng dẫn bạn cách tận dụng song song hóa để tối ưu hiệu suất script DevOps.
Cơ chế parallel cơ bản trong Bash
Background process với &
Ký hiệu & ở cuối lệnh sẽ chạy lệnh đó trong background, cho phép Bash tiếp tục thực thi câu tiếp theo:
1
2
3
4
5
6
7
8
9
# Chạy tuần tự: mất ~9 giâysleep 3sleep 3sleep 3# Chạy song song với &: mất ~3 giâysleep 3&sleep 3&sleep 3&
Đợi tất cả hoàn thành với wait
wait chặn cho đến khi tất cả background process hoàn thành:
1
2
3
4
5
6
echo"Bắt đầu..."sleep 3&sleep 2&sleep 1&waitecho"Tất cả đã xong!"
Kiểm tra tiến trình đang chạy với jobs
1
2
3
4
5
6
7
8
# Liệt kê các background jobjobs -l
# Chỉ đếm job đang chạyjobs -r | wc -l
# Chỉ đếm job đã dừngjobs -s | wc -l
Quản lý PID
Mỗi background process có một PID duy nhất. Bạn có thể lưu PID để kiểm soát:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Lưu PIDsleep 60&PID=$!echo"Started background job with PID: $PID"# Kiểm tra job còn chạy khôngifkill -0 "$PID" 2>/dev/null;thenecho"Job $PID is still running"elseecho"Job $PID has finished"fi# Dừng jobkill"$PID"# Dừng mạnhkill -9 "$PID"
Giới hạn concurrency
Chạy quá nhiều background process cùng lúc có thể gây overload. Cách phổ biến nhất để giới hạn là dùng file descriptor hoặc PID array:
MAX_PARALLEL=3pids=()for i in $(seq 1 10);doecho"Starting task $i..." sleep 2&# Mô phỏng taskpids+=($!)# Đợi khi đạt giới hạnif[[${#pids[@]} -ge $MAX_PARALLEL]];thenwait"${pids[0]}"pids=("${pids[@]:1}")# Xóa PID đầu tiênfidone# Đợi tất cả còn lạifor pid in "${pids[@]}";dowait"$pid"doneecho"All tasks completed."
Phương pháp 2: Dùng jobs để đếm
1
2
3
4
5
6
7
8
9
10
11
12
13
MAX_PARALLEL=3for server in server1 server2 server3 server4 server5;do ssh "$server""uptime"&# Đợi khi có quá nhiều job đang chạywhile[[$(jobs -r | wc -l) -ge $MAX_PARALLEL]];do sleep 0.1
donedonewaitecho"All servers checked."
xargs — Parallel với -P
xargs là công cụ mạnh mẽ để chạy lệnh trên danh sách input. Flag -P cho phép chạy song song:
1
2
3
4
5
6
7
8
# Chạy ssh trên 3 server song songprintf"%s\n" server1 server2 server3 | xargs -n 1 -P 3 -I {} ssh user@{}"hostname"# Kiểm tra disk trên nhiều serverprintf"%s\n" server{1..10}| xargs -n 1 -P 5 -I {} bash -c '
disk=$(ssh user@{} "df / | tail -1 | awk \"{print \$5}\"" 2>/dev/null)
echo "{}: ${disk:-FAIL}"
'
Giải thích flags:
-n 1: Mỗi lần chỉ lấy 1 đối số từ input.
-P 3: Tối đa 3 process chạy đồng thời.
-I {}: Placeholder để chèn giá trị input vào lệnh.
Ví dụ practical: Backup nhiều database cùng lúc
1
2
3
4
5
6
7
8
9
10
11
DATABASES=("db_users""db_orders""db_inventory""db_logs""db_analytics")printf"%s\n""${DATABASES[@]}"| xargs -n 1 -P 3 -I {} bash -c '
echo "Backing up {}..."
mysqldump --single-transaction {} > /backup/{}_$(date +%Y%m%d).sql
if [[ $? -eq 0 ]]; then
echo "SUCCESS: {}"
else
echo "FAILED: {}"
fi
'
GNU parallel — Công cụ mạnh nhất
GNU parallel là công cụ chuyên dụng cho parallel execution, mạnh hơn xargs với nhiều tính năng:
# Chạy ssh trên nhiều serverparallel -j 3 ssh user@{}"df -h /" ::: server1 server2 server3 server4
# Chạy với nhiều tham sốparallel -j 3echo"Processing {1} with {2}" ::: A B C ::: 123# Đọc input từ fileparallel -j 5 < server_list.txt ssh {}"uptime"
Tính năng nâng cao
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Hiển thị tiến trìnhparallel --progress -j 3 sleep {} ::: 12345# Hiển thị ETAparallel --eta -j 3 sleep {} ::: 12345# Kết quả theo thứ tựparallel --keep-order -j 3echo{} ::: C B A
# Ghi output ra file riêng cho mỗi jobparallel --results /tmp/results/ -j 3echo{} ::: task1 task2 task3
# Dry-run (chỉ in lệnh, không chạy)parallel --dry-run -j 3 ssh user@{}"apt update" ::: server1 server2 server3
#!/usr/bin/env bash
# parallel-ssh.sh — Chạy lệnh trên nhiều server song song với GNU parallelset -euo pipefail
readonlySERVERS_FILE="${1:-servers.txt}"readonlyCOMMAND="${2:?Usage: $0 <servers.txt> <command>}"readonlyMAX_JOBS="${MAX_JOBS:-5}"readonlySSH_OPTS="-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"if[[ ! -f "$SERVERS_FILE"]];thenecho"Error: Server file $SERVERS_FILE not found" >&2exit1fiecho"Running on $(grep -cvE '^\s*#|^\s*$'"$SERVERS_FILE") servers with max $MAX_JOBS parallel jobs..."grep -vE '^\s*#|^\s*$'"$SERVERS_FILE"| parallel -j "$MAX_JOBS" --keep-order --joblog /tmp/parallel-ssh.log '
result=$(ssh '"$SSH_OPTS"' {} "'"${COMMAND}"'" 2>&1)
exit_code=$?
if [[ $exit_code -eq 0 ]]; then
echo "[OK] {}"
else
echo "[FAIL] {} (exit: $exit_code)"
fi
echo " Output: $result"
'echo""echo"Job log saved to /tmp/parallel-ssh.log"
Race condition và File lock
Khi nhiều process cùng ghi vào một file, race condition có thể xảy ra — dữ liệu bị ghi đè hoặc lẫn lộn. flock giải quyết vấn đề này bằng file lock:
Vấn đề race condition
1
2
3
4
5
6
# NGUY HIỂM: Nhiều process cùng ghi log → race conditionfor i in $(seq 1 10);doecho"Task $i completed" >> /tmp/shared.log &donewait# Kết quả: Có thể mất dòng hoặc bị乱序
Giải pháp với flock
1
2
3
4
5
6
7
8
9
# An toàn: Dùng flock để lock file trước khi ghifor i in $(seq 1 10);do( flock 200# Lock file descriptor 200echo"Task $i completed at $(date '+%H:%M:%S.%N')" >> /tmp/shared.log
) 200>/tmp/shared.log &donewait# Kết quả: Đúng thứ tự, không mất dòng
Checking 8 servers with max 5 parallel jobs...
OK web-01
OK web-02
WARN db-01 disk:87% docker:down
OK web-03
OK worker-01
OK worker-02
FAIL bastion SSH connection failed
OK web-04
========================================
SUMMARY
Total: 8
OK: 6
WARN: 1
FAIL: 1
========================================
Failures:
FAIL bastion SSH connection failed
Ghi chú triển khai
Giới hạn concurrency luôn là bắt buộc: Chạy quá nhiều SSH session hoặc HTTP request cùng lúc có thể gây overload network hoặc server nguồn. Luôn thiết lập MAX_JOBS phù hợp — thường 5-10 cho SSH, 20-50 cho HTTP request.
Race condition khi ghi log: Khi parallel script ghi vào cùng một file, luôn dùng flock hoặc mỗi process ghi vào file riêng rồi aggregate sau.
Test tuần tự trước khi parallel: Luôn chạy script ở chế độ sequential (MAX_JOBS=1) để xác nhận logic đúng, sau đó mới tăng parallelism.
GNU parallel vs xargs:xargs -P đủ cho大多数 use case đơn giản. GNU parallel mạnh hơn khi cần: giữ nguyên thứ tự output, hiển thị progress/ETA, xử lý lỗi phức tạp, hoặc chạy trên nhiều host.
Timeout: Luôn đặt timeout cho SSH và HTTP request để tránh script treo vô hạn khi một server không phản hồi.
Lời kết
Parallel Execution là kỹ năng nâng cao nhưng cực kỳ hữu ích trong Bash scripting. Từ background process đơn giản với &/wait, đến xargs -P tiện lợi và GNU parallel mạnh mẽ — bạn có thể giảm đáng kể thời gian xử lý các task lớn. Kết hợp với flock để tránh race condition, bạn sẽ có một hệ thống parallel execution an toàn và hiệu quả.
Ở bài tiếp theo, chúng ta sẽ khám phá Bash và REST API: gọi API với curl, parse JSON với jq, gửi auth bearer token, và tự động hóa workflow với GitHub/Slack API.