「複数のサーバーから同じスクリプトを呼び出すと、データが壊れることがある」
こうした問題は、排他制御(ロック)の仕組みを使えば防ぐことができます。Linuxには
flock という便利なコマンドがあり、シェルスクリプトに数行追加するだけでスクリプトの二重実行を確実に防げます。この記事では、
flock コマンドの基本的な使い方から、cronとの組み合わせ方、ロックが残り続けるデッドロックの防ぎ方まで、RHEL 9.4 / Ubuntu 24.04 LTS で動作確認した実践的な手順を解説します。この記事のポイント
・flock コマンドはロックファイルを使ってスクリプトの二重実行を防ぐ
・flock -n でロック失敗時にすぐ終了、-w でタイムアウト付き待機ができる
・cron + flock の組み合わせで定期ジョブの重複実行を防ぐのが定番構成
・スクリプト終了時にロックは自動解放されるためデッドロックになりにくい
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトに排他制御が必要なのか
cronで毎分起動するスクリプトや、複数のサービスから同時に呼ばれるスクリプトを書いていると、必ず直面する問題があります。それが「競合」です。具体的にはこういった状況です。
・cronで毎時0分に起動するバックアップスクリプトが、前回の処理に時間がかかって終わっていないのに次の実行が始まる
・複数のサーバーが共有ストレージ上のスクリプトを同時に実行し、ログファイルの書き込みが混在する
・デプロイスクリプトを誰かが実行中なのに、別の担当者が同じスクリプトを走らせてしまう
このような競合が起きると、データの二重処理、ファイルの破損、不完全なデプロイといった深刻な問題につながります。
Linuxには、こうした問題を解決する仕組みとして「ファイルロック」があります。複数のプロセスが同じファイルに対してロックを試みたとき、先にロックを取得したプロセスだけが処理を進め、他は待機または終了するというシンプルな排他制御の仕組みです。
その仕組みをシェルスクリプトから手軽に使えるようにしたのが
flock コマンドです。flock コマンドの基本構文と仕組み
flock は、指定したロックファイルに対して排他ロックを取得し、ロックが取れた場合のみコマンドを実行します。基本的な構文は2通りあります。
1. コマンドラインで直接実行する形式
# 書式 flock [オプション] ロックファイル コマンド [引数...] # 例: /var/run/myscript.lock を取得してから backup.sh を実行 flock /var/run/myscript.lock /usr/local/bin/backup.sh # 例: タイムアウト付き(30秒待って取れなければ終了) flock -w 30 /var/run/myscript.lock /usr/local/bin/backup.sh
2. シェルスクリプト内で使う形式(fd番号を指定)
スクリプトの中で使う場合は、ファイルディスクリプタ(fd)を使った書き方が柔軟です。#!/bin/bash # ロックファイルのfd番号を9番に割り当てる exec 9>/var/run/myscript.lock # fd 9 に排他ロックをかける(-n: 取れなければ即終了) if ! flock -n 9; then echo "別のプロセスが実行中のため終了します" exit 1 fi # --- ここから本来の処理 --- echo "処理開始: $(date)" sleep 10 echo "処理完了: $(date)"
flock の主なオプション
・-n / --nonblock:ロックが取得できない場合、待機せず即座に終了コード 1 を返す・-w N / --wait N:N秒待ってもロックが取得できない場合は終了コード 1 を返す
・-s / --shared:共有ロック(読み取りロック)を取得する(複数プロセスが同時に取得可能)
・-u / --unlock:ロックを明示的に解放する
・-e / --exclusive:排他ロック(デフォルト)
・-x:-e と同じ(排他ロック)
実務では
-n(二重実行を検知して即終了)か -w(一定時間待って諦める)の2つをよく使います。実務でよく使うパターン
1. cron ジョブの二重実行防止
cronで定期実行するスクリプトにflock を組み込む最も簡単な方法は、crontab の中で直接 flock を使うことです。# crontab -e で設定する内容 # 毎時0分にバックアップスクリプトを実行(二重実行防止付き) 0 * * * * /usr/bin/flock -n /var/run/backup.lock /usr/local/bin/backup.sh # タイムアウト付き(前回の処理が30分以内に終わる前提で待機する場合) 0 * * * * /usr/bin/flock -w 1800 /var/run/backup.lock /usr/local/bin/backup.sh
-n を使った場合、前の実行がまだ動いていれば新しい実行はすぐに終了します。ログに記録されないため、「なぜ処理されなかったか」が分からなくならないよう、スクリプト側にログ出力を加えるのが実務のコツです。2. スクリプト内でのロック制御(終了時の処理を保証する)
スクリプト内でロックを取得し、trap と組み合わせてロックを確実に解放する書き方です。#!/bin/bash LOCK_FILE=/var/run/deploy.lock LOG_FILE=/var/log/deploy.log # fd 9 をロックファイルに割り当て exec 9>"$LOCK_FILE" # ロック取得を試みる(取れなければ即終了) if ! flock -n 9; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] 別のデプロイが実行中です" >> "$LOG_FILE" exit 1 fi # スクリプト終了時にロックファイルを削除するtrap trap 'rm -f "$LOCK_FILE"' EXIT echo "[$(date '+%Y-%m-%d %H:%M:%S')] デプロイ開始" >> "$LOG_FILE" # --- デプロイ処理 --- rsync -avz /srv/app/ /var/www/html/ >> "$LOG_FILE" 2>&1 systemctl reload nginx >> "$LOG_FILE" 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] デプロイ完了" >> "$LOG_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT は、スクリプトが正常終了・異常終了・強制終了(kill シグナル以外)のどの場合でもロックファイルを削除します。trap コマンドの詳しい使い方はtrapコマンドでbashスクリプトのシグナルを捕捉・処理する方法も参照してください。3. while ループ内でのポーリング待機
ロックが取れるまで一定間隔で待機し続けるパターンです。-w オプションと組み合わせて使います。#!/bin/bash LOCK_FILE=/var/run/myprocess.lock MAX_WAIT=300 # 最大5分待機 exec 9>"$LOCK_FILE" echo "ロック取得を試みます(最大${MAX_WAIT}秒待機)..." if ! flock -w "$MAX_WAIT" 9; then echo "エラー: ${MAX_WAIT}秒以内にロックを取得できませんでした" exit 1 fi echo "ロック取得成功。処理を開始します。" # 実際の処理...
4. 一時ファイルと組み合わせる(安全なロックファイル管理)
ロックファイルのパスをスクリプト内で動的に生成するには、mktemp との組み合わせが便利です。mktempコマンドでシェルスクリプトの一時ファイルを安全に作成する方法も合わせて参照してください。#!/bin/bash # /var/run/配下にスクリプト名でロックファイルを作成する慣例 SCRIPT_NAME=$(basename "$0" .sh) LOCK_FILE="/var/run/${SCRIPT_NAME}.lock" exec 9>"$LOCK_FILE" flock -n 9 || { echo "already running"; exit 1; } trap 'rm -f "$LOCK_FILE"' EXIT
実行例と出力確認
実際に動作確認した環境: RHEL 9.4(Rocky Linux 9.4 互換)ターミナルを2つ開いて、同時にスクリプトを実行してみます。
# テスト用スクリプト(/tmp/test-lock.sh) cat > /tmp/test-lock.sh << 'EOF' #!/bin/bash exec 9>/tmp/test.lock if ! flock -n 9; then echo "$$: ロック取得失敗 - 別プロセスが実行中" exit 1 fi echo "$$: ロック取得成功 - 処理中..." sleep 5 echo "$$: 処理完了" EOF chmod +x /tmp/test-lock.sh
# ターミナル1での実行 $ /tmp/test-lock.sh 12345: ロック取得成功 - 処理中... # ターミナル2で同時に実行(ターミナル1の実行中に) $ /tmp/test-lock.sh 12346: ロック取得失敗 - 別プロセスが実行中 # ターミナル1の処理完了後にターミナル2で再実行 $ /tmp/test-lock.sh 12347: ロック取得成功 - 処理中... 12347: 処理完了
$$ はプロセスIDで、どのプロセスがロックを取得しているか確認できます。現在ロックを保持しているプロセスを確認するには
fuser コマンドが使えます。# ロックファイルを保持しているプロセスIDを確認 $ fuser /tmp/test.lock /tmp/test.lock: 12345 # PIDが見つかった場合はそのプロセスの情報を確認 $ ps -p 12345 -o pid,comm,args PID COMM COMMAND 12345 bash /tmp/test-lock.sh
トラブルシュート・よくある問題
1. flock コマンドが存在しない
flock は util-linux パッケージに含まれています。通常は標準インストールされていますが、最小構成の環境では入っていない場合があります。# インストール確認 $ which flock /usr/bin/flock $ flock --version flock from util-linux 2.37.4 # インストールされていない場合(RHEL/AlmaLinux/Rocky Linux) $ sudo dnf install util-linux # Ubuntu/Debian の場合 $ sudo apt install util-linux
2. ロックファイルが残り続けてロックが取れなくなった
flock のロックはカーネルレベルのファイルロックであるため、スクリプトが終了すれば自動的に解放されます。ロックファイル(/var/run/xxx.lock)が残っていても、ロックを保持しているプロセスがいなければ次回の実行でロックは取得できます。ロックが取れない状態が続く場合は、本当に別プロセスが動いているかを確認してください。
# ロックファイルを保持しているプロセスがいるか確認 $ fuser /var/run/myapp.lock /var/run/myapp.lock: 3142 # 存在しているプロセスか確認 $ ps -p 3142 -o pid,state,comm,args PID S COMM COMMAND 3142 S bash /usr/local/bin/myapp.sh # ゾンビプロセスや意図せず残ったプロセスの場合は kill $ kill -TERM 3142
3. NFS 上のロックファイルでは機能しない場合がある
flock は POSIX ファイルロック(fcntl)を使っており、NFS(バージョンや設定に依存)では正しく動作しないことがあります。複数サーバーからNFS経由で同じロックファイルを使う構成は避け、ローカルパス(/var/run/ や /tmp/)を使うのが原則です。4. set -e 環境で flock の失敗がスクリプトを即終了させる
set -e(エラー即終了)を設定したスクリプトで flock -n が失敗すると、if 文の外でそのまま書くとスクリプト全体が終了します。必ず if ! flock -n ...; then の形で書いてください。#!/bin/bash set -e # エラー即終了モード exec 9>/var/run/myscript.lock # NG: set -e 環境では flock が失敗した瞬間にスクリプトが終了 # flock -n 9 # OK: if文で明示的にハンドリング if ! flock -n 9; then echo "already running" >&2 exit 0 # エラーとしてではなく「正常な終了」として扱う fi
set -e やデバッグオプションの詳細はset -xコマンドでシェルスクリプトをデバッグする方法を参照してください。本記事のまとめ
| やりたいこと | コマンド・書き方 |
|---|---|
| ロックが取れなければ即終了 | flock -n /var/run/app.lock コマンド |
| N秒待ってロックが取れなければ終了 | flock -w N /var/run/app.lock コマンド |
| スクリプト内でfdを使ってロック | exec 9>/var/run/app.lock; flock -n 9 |
| crontab に直接 flock を組み込む | 0 * * * * flock -n /var/run/app.lock /path/to/script.sh |
| ロックを保持しているプロセスを確認 | fuser /var/run/app.lock |
| ロックの解放は | スクリプト終了時に自動解放(手動解放は flock -u) |
flock はシンプルなコマンドですが、cronジョブの二重実行防止やデプロイスクリプトの排他制御といった現場でよく出てくる問題を確実に解決できます。trap と組み合わせて終了時にロックを確実に解放する実装にしておくと、予期しない障害が起きた時も安心です。シェルスクリプトをより堅牢に書くためには、getoptsコマンドでbashスクリプトの引数を処理する方法やtrapコマンドでbashスクリプトのシグナルを捕捉・処理する方法も合わせて参照してください。
スクリプトの品質を上げるには、サーバー設計の「型」を知ることが近道です
flockのような排他制御を正しく使いこなすには、シェルスクリプトの設計とLinuxのプロセス管理を体系的に理解することが重要です。独学で断片的に学ぶより、現場で実際に使われる設計パターンを一度体系的に身につけることで、トラブルを未然に防ぐスクリプトが書けるようになります。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼントしています。
「独学の時間がもったいない」「プロから直接、現場の技術を最短で学びたい」という本気の方には、2日で実務レベルのスキルが身につく【初心者向けハンズオンセミナー】も開催しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 次のページへ:socatコマンドでLinuxのネットワーク通信をデバッグする方法|ポートフォワード・SSL・ファイル転送の実践例
- 前のページへ:mkfifoコマンドで名前付きパイプを作成する方法|プロセス間通信とパイプの実践例
- この記事の属するカテゴリ:Linuxtips・シェルスクリプトへ戻る

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