BÀI VIẾT // [DEVOPS] Bash nâng cao: Parallel Execution trong DevOps
Bash nâng cao: Parallel Execution trong DevOps

Bash nâng cao: Parallel Execution trong DevOps

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.

DEVOPS BASH

Parallel Execution trong Bash

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ây
sleep 3
sleep 3
sleep 3

# Chạy song song với &: mất ~3 giây
sleep 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 &
wait
echo "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 job
jobs -l

# Chỉ đếm job đang chạy
jobs -r | wc -l

# Chỉ đếm job đã dừng
jobs -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 PID
sleep 60 &
PID=$!
echo "Started background job with PID: $PID"

# Kiểm tra job còn chạy không
if kill -0 "$PID" 2>/dev/null; then
    echo "Job $PID is still running"
else
    echo "Job $PID has finished"
fi

# Dừng job
kill "$PID"

# Dừng mạnh
kill -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:

Phương pháp 1: Dùng PID array

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
MAX_PARALLEL=3

pids=()
for i in $(seq 1 10); do
    echo "Starting task $i..."
    sleep 2 &  # Mô phỏng task
    pids+=($!)

    # Đợi khi đạt giới hạn
    if [[ ${#pids[@]} -ge $MAX_PARALLEL ]]; then
        wait "${pids[0]}"
        pids=("${pids[@]:1}")  # Xóa PID đầu tiên
    fi
done

# Đợi tất cả còn lại
for pid in "${pids[@]}"; do
    wait "$pid"
done
echo "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=3

for server in server1 server2 server3 server4 server5; do
    ssh "$server" "uptime" &

    # Đợi khi có quá nhiều job đang chạy
    while [[ $(jobs -r | wc -l) -ge $MAX_PARALLEL ]]; do
        sleep 0.1
    done
done

wait
echo "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 song
printf "%s\n" server1 server2 server3 | xargs -n 1 -P 3 -I {} ssh user@{} "hostname"

# Kiểm tra disk trên nhiều server
printf "%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:

Cài đặt

1
2
3
4
5
6
7
8
# Debian/Ubuntu
apt-get install -y parallel

# CentOS/RHEL
yum install -y parallel

# macOS
brew install parallel

Sử dụng cơ bản

1
2
3
4
5
6
7
8
# Chạy ssh trên nhiều server
parallel -j 3 ssh user@{} "df -h /" ::: server1 server2 server3 server4

# Chạy với nhiều tham số
parallel -j 3 echo "Processing {1} with {2}" ::: A B C ::: 1 2 3

# Đọc input từ file
parallel -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ình
parallel --progress -j 3 sleep {} ::: 1 2 3 4 5

# Hiển thị ETA
parallel --eta -j 3 sleep {} ::: 1 2 3 4 5

# Kết quả theo thứ tự
parallel --keep-order -j 3 echo {} ::: C B A

# Ghi output ra file riêng cho mỗi job
parallel --results /tmp/results/ -j 3 echo {} ::: 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

Ví dụ: Parallel SSH execution trên production

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
# parallel-ssh.sh — Chạy lệnh trên nhiều server song song với GNU parallel

set -euo pipefail

readonly SERVERS_FILE="${1:-servers.txt}"
readonly COMMAND="${2:?Usage: $0 <servers.txt> <command>}"
readonly MAX_JOBS="${MAX_JOBS:-5}"
readonly SSH_OPTS="-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"

if [[ ! -f "$SERVERS_FILE" ]]; then
    echo "Error: Server file $SERVERS_FILE not found" >&2
    exit 1
fi

echo "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 condition
for i in $(seq 1 10); do
    echo "Task $i completed" >> /tmp/shared.log &
done
wait
# 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 ghi
for i in $(seq 1 10); do
    (
        flock 200  # Lock file descriptor 200
        echo "Task $i completed at $(date '+%H:%M:%S.%N')" >> /tmp/shared.log
    ) 200>/tmp/shared.log &
done
wait
# Kết quả: Đúng thứ tự, không mất dòng

Kèm flock trong script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/usr/bin/env bash
# safe-concurrent-write.sh — Ghi log an toàn khi chạy parallel

exec 200>/tmp/app.lock  # Mở file descriptor cho lock

for task in task1 task2 task3 task4 task5; do
    (
        flock 200
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] Processing $task" >> /tmp/app.log
        sleep $((RANDOM % 3))  # Mô phỏng task
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] Completed $task" >> /tmp/app.log
    ) &
done

wait
echo "All tasks finished. Log:"
cat /tmp/app.log

Ví dụ thực hành: Parallel system health check

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env bash
# ==============================================================================
# Script : parallel-healthcheck.sh
# Mục đích: Kiểm tra health nhiều server song song với kết quả aggregate
# ==============================================================================

set -euo pipefail

readonly SERVERS_FILE="${SERVERS_FILE:-/etc/healthcheck/servers.txt}"
readonly MAX_JOBS="${MAX_JOBS:-10}"
readonly TIMEOUT="${TIMEOUT:-10}"
readonly LOG_DIR="/var/log/healthcheck"
readonly TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
readonly RESULT_FILE="${LOG_DIR}/result_${TIMESTAMP}.txt"
readonly FAIL_FILE="${LOG_DIR}/failures_${TIMESTAMP}.txt"

mkdir -p "$LOG_DIR"

# Check functions
check_ssh() {
    local server="$1"
    ssh -o ConnectTimeout="$TIMEOUT" -o BatchMode=yes \
        "deploy@${server}" "echo OK" 2>/dev/null
}

check_disk() {
    local server="$1"
    ssh -o ConnectTimeout="$TIMEOUT" -o BatchMode=yes \
        "deploy@${server}" "df / | tail -1 | awk '{print \$5}' | tr -d '%'" 2>/dev/null
}

check_service() {
    local server="$1" service="$2"
    ssh -o ConnectTimeout="$TIMEOUT" -o BatchMode=yes \
        "deploy@${server}" "systemctl is-active --quiet ${service} && echo OK || echo DOWN" 2>/dev/null
}

# Worker function for parallel
check_server() {
    local server="$1"
    local status="OK"
    local details=""

    # Check SSH
    if ! check_ssh "$server" >/dev/null 2>&1; then
        echo "FAIL ${server} SSH connection failed"
        return 1
    fi

    # Check disk
    local disk
    disk=$(check_disk "$server")
    if [[ -n "$disk" && "$disk" -gt 85 ]]; then
        details+="disk:${disk}% "
        status="WARN"
    fi

    # Check services
    for svc in nginx docker sshd; do
        local svc_status
        svc_status=$(check_service "$server" "$svc")
        if [[ "$svc_status" == "DOWN" ]]; then
            details+="${svc}:down "
            status="WARN"
        fi
    done

    echo "${status} ${server} ${details}"
}

export -f check_ssh check_disk check_service check_server
export TIMEOUT

main() {
    if [[ ! -f "$SERVERS_FILE" ]]; then
        echo "Error: $SERVERS_FILE not found" >&2
        exit 1
    fi

    local total
    total=$(grep -cvE '^\s*#|^\s*$' "$SERVERS_FILE")

    echo "Checking $total servers with max $MAX_JOBS parallel jobs..."
    echo ""

    grep -vE '^\s*#|^\s*$' "$SERVERS_FILE" | \
        parallel -j "$MAX_JOBS" --keep-order --joblog "${LOG_DIR}/joblog_${TIMESTAMP}.log" \
        check_server {} 2>/dev/null | tee "$RESULT_FILE"

    # Summary
    local ok_count warn_count fail_count
    ok_count=$(grep -c "^OK" "$RESULT_FILE" 2>/dev/null || echo 0)
    warn_count=$(grep -c "^WARN" "$RESULT_FILE" 2>/dev/null || echo 0)
    fail_count=$(grep -c "^FAIL" "$RESULT_FILE" 2>/dev/null || echo 0)

    echo ""
    echo "========================================"
    echo "  SUMMARY"
    echo "  Total:  $total"
    echo "  OK:     $ok_count"
    echo "  WARN:   $warn_count"
    echo "  FAIL:   $fail_count"
    echo "========================================"

    # List failures
    if [[ "$fail_count" -gt 0 ]]; then
        echo ""
        echo "Failures:"
        grep "^FAIL" "$RESULT_FILE" | tee "$FAIL_FILE"
    fi

    # Exit with error if any failures
    [[ "$fail_count" -gt 0 ]] && exit 1
}

main "$@"

Kết quả mẫu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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.

THẢO LUẬN & BÌNH LUẬN