Bash スクリプトでの `timeout` 活用法
(heitorpb.github.io)- Bash スクリプトで Web サーバーの状態確認 のために接続を繰り返し試みると、サーバーが予期せず 無限ループ に陥る問題が発生することがある
- これを解決するためのツールである
timeoutは、コマンドの実行制限時間を設定し、超過時にシグナルを送って プロセス終了 を試みる untilのような shell built-in には直接適用できないため、bash プロセスでのラップ またはスクリプト分離によって対応できる
Bash スクリプトでの Web サーバー待機と無限ループの問題
- 実務では Bash スクリプトを使って Web サーバーのセットアップと状態チェック を行っている
- サーバーが起動するまで次の処理を保留する構成で、通常は問題なく動作する
- しかしサーバー起動中に クラッシュ が発生すると、無限ループに陥るため対策が必要になった
until の使用例と限界
- 次のような構文で Web サーバーのヘルスチェック を繰り返す
until curl --silent --fail-with-body 10.0.0.1:8080/health; do sleep 1 done - サーバーが失敗していると、
sleep 1が永遠に繰り返される 状況が発生する
timeout ユーティリティの導入
timeoutコマンドは、指定した時間内にコマンドが完了しない場合、シグナル(SIGTERM など) を送って終了させる- 例:
timeout 1s sleep 5の場合、1 秒経過後に sleep プロセスの終了 を試みる - 終了時には 異常終了コード(例: 124) を返す
timeout と until の組み合わせの試行と問題点
- 自然に
timeoutとuntilを次のように組み合わせようとするtimeout 1m until curl ...; do sleep 1 done - しかし
timeoutは プロセスを対象にシグナル送信 できる一方、untilはシェルの組み込みキーワードであり 直接適用は不可能 である
解決方法: Bash プロセスでラップするか外部スクリプトを使う
untilループ全体を bash -c でラップして別プロセスとして実行すれば、timeoutを適用できるtimeout 1m bash -c "until curl ...; do sleep 1; done"- あるいはループ部分を 外部 Bash スクリプトに分離 し、そのスクリプトに
timeoutを適用することもできるtimeout 1m ./until.sh - shell built-in には直接
timeoutを適用できないが、上記の方法で望んだ動作を実現できる
1件のコメント
Hacker Newsの意見
私が最も気に入っている、あまり知られていないテクニックは、
straceの fault injection を使ってさまざまなシステムコールの失敗をテストする方法の紹介です。関連リンクでさらに詳しく説明されています。
この機能は本当にすごいと思うし、以前から知っていればよかったという体験の共有です。
失敗分岐をテストする方法がなく、関数の一部だけを一時的なコードに置き換えることがあったが、このテクニックのおかげでより簡潔なアプローチが可能になりそうです。
この方法は本当に便利そうだという意見です。
Windows にもこれに似た機能があるのか気になるという話です。
サービスのヘルスチェックでは、最大タイムアウト時間と最大リトライ回数の両方を設定するのが最適な方法だという提案です。
通常は X 回までリトライし、最大 Y 時間以内で失敗と判断します。
長時間待ちすぎるより、できるだけ早く失敗を確定する必要性を強調しています。
標準的なサービスでは、コンテナ依存関係が十分に保証され、動作準備が整ってから初めてヘルスチェックを開始します。
Kubernetes では Init Container、AWS ECS では dependsOn、Docker Compose では depends_on の設定を参照しています。
POSIX シェルスクリプトの例が示されています。
ただし
curl自体にこの機能が組み込まれているので、別途スクリプトを書かずに次のように使えると述べています。Mac では
timeoutコマンドが標準では提供されていないため、bash ビルトインだけでタイムアウトを実装しようといろいろ試した経験の共有です。sleepコマンドは POSIX 標準なので使えることを説明しています。以下のようなタイムアウト機能の実装例を示しています。
times_upという関数でタイムアウト処理を行います。10 秒タイムアウトで
for文を 20 回繰り返してテストする例も示されています。12 年前に Stack Overflow の助言に従って似た方法を実装した経験の共有です。
参考リンクで詳細を確認できます。
shell builtins と
sleepだけを使っており、そのコードでは POSIX 互換が必須だったことを強調しています。例にある bash の
{1..20}構文は POSIX ではないため注意が必要だと言及しています。自分の改善点は、タイムアウトが発生しなければ
true、発生したらfalseを返すようにして、スクリプト内のエラー処理を簡単にできるようにしたことです。以下のようにコマンドと
sleepを並列実行し、指定時間が過ぎたらシグナルでコマンドを終了させる非常に単純な方法の共有です。13 年前に
read -tを使ってタイムアウトを実装したスクリプトの事例共有です。リンク
curlにはすでに--retry-connrefusedフラグがあり、シェルループなしでもこの機能をそのまま使えるという案内です。bash -cを使うときに変数の受け渡しが必要なら、次のように引数を追加する方法が勧められています。"--"を使う理由とargv[0]の役割について説明しています。printf %qを使うこともできるが、Bourne 互換の方式を好むと言及しています。"--"は bash およびほとんどの Unix/Linux CLI でオプション終了の合図として意味が非常に明確だと説明しています。関連参考
Busybox は
argv[0]の値に基づいて実行するプログラムを決定するため、"ls"、"mv"、"cp"など任意のコマンド名に指定できるという共有です。リトライロジックが必要なときに私が主によく使う方法は次のようなものです。
あまり洗練されてはいないが、たいてい正確で、さらに進めるなら指数バックオフも適用できると述べています。
拡張性の面でも利点があります。
shellcheck ではこの問題を
_変数で処理する方法を推奨しているとのことです。参考リンク
eventually_succeeds関数は状況によってtimeoutや別の防御的コーディングが必要かもしれない点を強調しています。POSIX / プロセス / IO では常に防御的なコードを書く必要があることを改めて示しています。
昔、子どもたちが小さかったころ、30 分間だけ番組を 1 本見られるように、次のコマンドを一種のペアレンタルコントロールとして使っていた経験の共有です。
このアイデアはとても便利だったという評価です。
サブプロセスにシグナルを送る必要があるため、コマンドのインライン記述や一時スクリプトファイルを使うのはあまり好まないと述べています。
自分が好む方法は、必要な複雑なロジックを関数にして export し、
timeout bash -cで包むやり方の提案です。aidenn0 が言及した引数の安全な受け渡し方法とも関係しています。
"$@"を必ず使わなければならないと指摘しています。そうしないと、空白を含む引数が正しく渡されない問題が発生します。
この点を確認できる
long_fnの例も共有されています。以前
timeoutに触れたブログ記事の内容を思い出したという話です。関連ブログでは、シェルではなく一般的なプログラミング言語や内部動作原理にさらに興味がある場合の参考として勧めています。
Kubernetes の設定でコマンドタイムアウトを追加した経験の共有です。
await-cmd.sh、await-http.sh、await-tcp.shのような POSIX シェルスクリプトは成熟しており、特定の状況ではかなり有用に使えると案内しています。関連プロジェクトリンク