「バックアップスクリプトがディレクトリごとに逐次実行しているせいで、朝までに終わらない」
シェルスクリプトで運用を自動化していると、こうした「待ち時間をなんとかしたい」場面に必ず出くわします。逐次実行では対象が増えるほど線形に時間がかかり、10台のサーバーなら10倍のリードタイムが生じます。
この記事では、bashの
&(バックグラウンド実行演算子)と wait コマンドを使った並列処理設計を解説します。基本的な仕組みから、複数サーバーへの同時SSH実行・最大同時実行数の制御・競合状態の防止まで、実務で使えるパターンをまとめました。RHEL 9.4 / Ubuntu 24.04 LTS の bash 5.x 環境で動作確認済みです。この記事のポイント
・コマンド末尾に & を付けるとバックグラウンド実行になり、waitで全完了を待てる
・$! で直前の子プロセスのPIDを取得し、個別にwait/killで管理できる
・最大同時実行数はカウンタ変数と wait -n(bash 4.3以降)の組み合わせで制御できる
・並列書き込みの競合にはflockを、子プロセスのエラー検出には wait $pid の戻り値を使う
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトに並列処理が必要なのか
サーバー運用では「複数の対象に同じ処理を行う」タスクが頻繁に発生します。・5台の構成サーバーにパッケージアップデートをかける
・30個のディレクトリをそれぞれ圧縮してS3にアップロードする
・監視対象20サービスの死活確認を毎分行う
これらを逐次実行すると、1回の処理に5秒かかるコマンドを10台に実行するだけで50秒かかります。並列化すれば理論上は5秒程度で済みます。
bashには並列処理のための組み込み機能が揃っています。外部ツール(GNU parallel等)を使わなくても、
& と wait の組み合わせで多くのユースケースに対応できます。&とwaitの基本(バックグラウンド実行の仕組み)
1. &でコマンドをバックグラウンドへ送る
コマンドの末尾に& を付けると、そのコマンドはバックグラウンドで起動し、シェルは即座に次の行へ進みます。#!/bin/bash # 通常の逐次実行(10秒かかる) sleep 5 sleep 5 # バックグラウンド実行(約5秒で終わる) sleep 5 & sleep 5 & wait echo "両方の sleep が完了した"
& でバックグラウンド化したコマンドは、シェルから独立した子プロセスとして動作します。コマンドを起動した直後にシェルはジョブ番号とPIDを表示します。$ sleep 5 & [1] 14823 $ sleep 5 & [2] 14827 $ jobs [1]- Running sleep 5 & [2]+ Running sleep 5 &
[1] がジョブ番号、14823 がPID(プロセスID)です。2. waitで子プロセスの完了を待つ
wait(引数なし)は、シェルが起動したすべての子プロセスが終了するまで待機します。並列実行後に「全部終わったら次へ進む」という設計の基本形です。#!/bin/bash # 3つのコマンドを並列実行して全完了を待つ rsync -a /data/dir1/ backup01:/backup/dir1/ & rsync -a /data/dir2/ backup01:/backup/dir2/ & rsync -a /data/dir3/ backup01:/backup/dir3/ & wait echo "全バックアップ完了: $(date)"
3. $!でPIDを取得して個別管理する
$! は直前にバックグラウンドへ送ったコマンドのPIDを保持するシェル変数です。PIDを配列に保存しておくことで、子プロセスを個別に管理できます。#!/bin/bash PIDS=() rsync -a /data/dir1/ backup01:/backup/dir1/ & PIDS+=($!) # PIDをPIDS配列に追加 rsync -a /data/dir2/ backup01:/backup/dir2/ & PIDS+=($!) rsync -a /data/dir3/ backup01:/backup/dir3/ & PIDS+=($!) # 個別に終了ステータスを取得する for PID in "${PIDS[@]}"; do wait "$PID" echo "PID $PID: 終了ステータス $?" done
wait $PID は指定した子プロセスの終了ステータスを返します。逐次実行の $? と同じ感覚で使えるため、「どの子プロセスが失敗したか」を個別に確認できます。実務で使う並列処理パターン
1. 複数サーバーへのSSH同時実行
複数台への一斉展開やヘルスチェックは、並列処理の最も典型的なユースケースです。#!/bin/bash set -euo pipefail SERVERS=(web01.example.com web02.example.com db01.example.com) PIDS=() echo "=== 並列ヘルスチェック開始: $(date) ===" for SERVER in "${SERVERS[@]}"; do ssh -o ConnectTimeout=10 "${SERVER}" "hostname && uptime" & PIDS+=($!) done # 全サーバーの完了を待つ echo "--- 出力を待機中 ---" wait echo "=== 全サーバーの確認完了 ==="
=== 並列ヘルスチェック開始: Thu Jul 31 10:14:22 JST 2026 === --- 出力を待機中 --- web02.example.com 10:14:23 up 42 days, 3:15, 1 user, load average: 0.12, 0.08, 0.05 web01.example.com 10:14:23 up 156 days, 12:33, 2 users, load average: 0.05, 0.03, 0.01 db01.example.com 10:14:24 up 89 days, 7:41, 1 user, load average: 0.33, 0.28, 0.25 === 全サーバーの確認完了 ===
2. ファイルを並列処理するループ設計
大量のファイルを処理するバッチスクリプトは、forループ内で& を付けるだけで並列化できます。#!/bin/bash # ログファイルを並列で圧縮してアーカイブディレクトリへ移動する LOGDIR="/var/log/app" ARCHDIR="/backup/logs/$(date +%Y%m)" mkdir -p "${ARCHDIR}" PIDS=() for LOGFILE in "${LOGDIR}"/*.log; do [[ -f "${LOGFILE}" ]] || continue ( gzip -c "${LOGFILE}" > "${ARCHDIR}/$(basename "${LOGFILE}").gz" && rm -f "${LOGFILE}" ) & PIDS+=($!) done # 全ファイルの処理完了を待ち、失敗をカウントする FAILED=0 for PID in "${PIDS[@]}"; do wait "${PID}" || FAILED=$((FAILED + 1)) done if [[ $FAILED -gt 0 ]]; then echo "ERROR: ${FAILED}件のファイルで圧縮に失敗しました" >&2 exit 1 fi echo "全ログファイルの圧縮完了: ${ARCHDIR}"
( ... ) でコマンドをグループ化し、複数コマンドを1つのバックグラウンドジョブとして扱っている点がポイントです。wait "${PID}" || FAILED=$((...)) で個別の失敗を集計しています。3. 最大同時実行数を制御する設計
対象が100個・1,000個になると、すべてを一度にバックグラウンド実行するのは危険です。ロードアベレージの急上昇やメモリ枯渇を招きます。「常に最大N個だけ実行する」セマフォパターンを実装します。並列化したいと考えているエンジニアの方は、make -j で並列ビルドするのと同じ思想でシェルスクリプトを設計できます。
#!/bin/bash set -euo pipefail MAX_PARALLEL=5 # 最大同時実行数 PIDS=() process_file() { local FILE="$1" # ここにファイルの処理を記述する gzip -k "${FILE}" echo "処理完了: ${FILE}" } for FILE in /data/files/*.csv; do [[ -f "${FILE}" ]] || continue # 実行中のジョブ数がMAX_PARALLELに達したら1つ完了するまで待つ if [[ ${#PIDS[@]} -ge $MAX_PARALLEL ]]; then wait -n 2>/dev/null || true # bash 4.3以降: 任意の1子プロセスを待つ # 終了したPIDをPIDS配列から削除する NEW_PIDS=() for PID in "${PIDS[@]}"; do kill -0 "${PID}" 2>/dev/null && NEW_PIDS+=("${PID}") done PIDS=("${NEW_PIDS[@]}") fi process_file "${FILE}" & PIDS+=($!) done # 残りの全ジョブを待つ wait echo "全ファイルの処理完了"
wait -n(bash 4.3以降)は任意の子プロセスが1つ終わるまで待つコマンドです。RHEL 8以降・Ubuntu 20.04以降では標準で使用できます。古い環境(RHEL 7 / bash 4.2)では wait -n が使えないため、後述のトラブルシュートセクションで代替手段を解説します。$ bash --version GNU bash, バージョン 5.1.8(1)-release (x86_64-redhat-linux-gnu) # wait -n が使えるかどうかの確認方法 $ bash -c 'sleep 0 & wait -n; echo $?' 0
並列処理のトラブルと対処法
1. 競合状態(レースコンディション)をflockで防ぐ
複数のバックグラウンドプロセスが同じファイルに書き込むと、出力が混在して壊れることがあります。競合状態(レースコンディション)と呼ばれるこの問題は、flock コマンドでロックを取得してから書き込むことで防げます。#!/bin/bash LOGFILE="/var/log/parallel-result.log" LOCKFILE="/var/lock/parallel-result.lock" write_result() { local SERVER="$1" local RESULT="$2" # flockでロックを取得してから書き込む(タイムアウト: 10秒) flock --timeout 10 "${LOCKFILE}" bash -c "echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${SERVER}: ${RESULT}" >> "${LOGFILE}"" } SERVERS=(web01 web02 web03 app01 app02) for SERVER in "${SERVERS[@]}"; do ( RESULT=$(ssh "${SERVER}" "uptime" 2>&1) write_result "${SERVER}" "${RESULT}" ) & done wait
2. 子プロセスの終了ステータスを正確に取得する
wait(引数なし)は全子プロセスを待ちますが、終了ステータスは最後に終了した子プロセスのものだけを返します。どのサーバーで失敗したかを特定するには、PIDを配列に保存して個別に wait $PID を呼ぶ設計が必須です。#!/bin/bash SERVERS=(web01.example.com web02.example.com db01.example.com app01.example.com) declare -A SERVER_PIDS # 連想配列でサーバー名とPIDを対応させる for SERVER in "${SERVERS[@]}"; do ssh -o ConnectTimeout=10 "${SERVER}" "systemctl is-active nginx" & SERVER_PIDS["${SERVER}"]=$! done FAILED_SERVERS=() for SERVER in "${SERVERS[@]}"; do PID="${SERVER_PIDS[$SERVER]}" if wait "${PID}"; then echo "OK: ${SERVER} - nginx 起動中" else echo "ERROR: ${SERVER} - nginx が停止または応答なし" >&2 FAILED_SERVERS+=("${SERVER}") fi done if [[ ${#FAILED_SERVERS[@]} -gt 0 ]]; then echo "要対応サーバー: ${FAILED_SERVERS[*]}" >&2 exit 1 fi echo "全サーバーの nginx 正常稼働を確認"
OK: web01.example.com - nginx 起動中 OK: web02.example.com - nginx 起動中 ERROR: db01.example.com - nginx が停止または応答なし OK: app01.example.com - nginx 起動中 要対応サーバー: db01.example.com
3. bash 4.2(RHEL 7)で最大同時実行数を制御する代替手段
wait -n が使えない bash 4.2 環境では、jobs -p で実行中のジョブ数を数えながらポーリングする方法が代替になります。#!/bin/bash MAX_PARALLEL=5 # 実行中のジョブ数を返す関数 count_jobs() { jobs -p | wc -l } for FILE in /data/files/*.csv; do [[ -f "${FILE}" ]] || continue # 実行中ジョブ数がMAXに達したら100msごとに確認する while [[ $(count_jobs) -ge $MAX_PARALLEL ]]; do sleep 0.1 done gzip -k "${FILE}" & done wait echo "全処理完了"
jobs -p は現在のシェルのバックグラウンドジョブのPIDを1行ずつ出力します。wc -l で行数を数えることで実行中のジョブ数を把握できます。wait -n と比べてsleepを挟むポーリングになるため効率は落ちますが、互換性が高い方法です。4. タイムアウト付き並列実行
SSH実行でリモートホストが無応答になった場合、wait が永遠にブロックするリスクがあります。-o ConnectTimeout オプションをSSHに付けるのが最も確実な対策です。コマンド全体にタイムアウトを設けるには timeout コマンドと組み合わせます。#!/bin/bash SERVERS=(web01.example.com web02.example.com db01.example.com) PIDS=() for SERVER in "${SERVERS[@]}"; do # timeout: コマンド全体に30秒の上限を設ける # SSH: 接続確立に最大10秒 timeout 30 ssh -o ConnectTimeout=10 -o BatchMode=yes "${SERVER}" "df -h /" & PIDS+=($!) done FAILED=0 for i in "${!PIDS[@]}"; do PID="${PIDS[$i]}" SERVER="${SERVERS[$i]}" wait "${PID}" STATUS=$? if [[ $STATUS -eq 124 ]]; then echo "TIMEOUT: ${SERVER} - 30秒で応答なし" >&2 FAILED=$((FAILED + 1)) elif [[ $STATUS -ne 0 ]]; then echo "ERROR: ${SERVER} - 終了ステータス ${STATUS}" >&2 FAILED=$((FAILED + 1)) fi done [[ $FAILED -eq 0 ]] && echo "全サーバー正常" || exit 1
timeout コマンドは制限時間を超えた場合に終了ステータス 124 を返します。これを wait $PID の戻り値で確認することで、タイムアウトと通常のエラーを区別できます。シェルスクリプトの並列処理設計をさらに体系的に学びたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で即使える設計パターンを実機ハンズオンで身につけられます。
本記事のまとめ
| やりたいこと | 設計パターン |
|---|---|
| コマンドを並列で起動する | コマンド & PIDS+=($!) |
| 全子プロセスの完了を待つ | wait |
| 子プロセスを個別に待ち終了ステータスを得る | wait $PID |
| 最大同時実行数を制御する(bash 4.3以降) | wait -n で1つ完了するまで待機する |
| 最大同時実行数を制御する(bash 4.2以前) | jobs -p | wc -l でポーリングして制御する |
| 並列書き込みの競合を防ぐ | flock --timeout N ロックファイル bash -c "書き込みコマンド" |
| SSH無応答によるブロックを防ぐ | timeout 秒数 ssh -o ConnectTimeout=N ホスト コマンド & |
| タイムアウト終了を判定する | wait $PID の戻り値 124 はタイムアウトを示す |
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでデプロイを自動化する方法|ロールバック・排他制御・通知を組み込んだリリース設計
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます