pgrep -f と pkill -f を SSH で使った結果、ループが自分自身を35時間待ち続けた
オペレーション
10月31日、SSH 経由で小さなループを開始しました。別のマシン上の翻訳ジョブが完了するのを待ち、その後フォローアップジョブを実行するものです。翻訳ジョブはその夜に完了しました。しかし2日後に確認したところ、ループは35時間待ち続けており、フォローアップは一度も実行されていませんでした。
ループの内容
ssh build-host 'while pgrep -f "i18n_job.py example.com" >/dev/null; do sleep 30; done; ./follow-up.sh'
pgrep -f は、すべてのプロセスの完全なコマンドラインに対してパターンを照合します。SSH は引用符で囲まれたコマンドを bash -c 'while pgrep -f "i18n_job.py example.com" …' として実行するため、そのコマンドラインにはパターンが含まれています。pgrep は自身のプロセスを結果から除外しますが、それを起動したシェルは除外しません。そのため、ループが生きている限り、自分自身と一致するものが見つかります。
双子の問題:pkill -f は実行中のシェルを殺す
pkill -f でも同じことが起こり、結果はさらに悪くなります:
ssh build-host 'pkill -f "worker.py --queue a"; nohup python3 worker.py --queue a &'
シェルのコマンドライン自体にパターンが含まれているため、pkill はシェルを殺してしまいます。ワーカーは停止し、二度と起動されません。SSH はエラーメッセージなしで終了コード 255 で終了します。これはクォーティングの問題のように見え、実際、最初はそのように判断しました。
9月から10月末までの間に、私たちは少なくとも12回以上、いずれかのトラブルに見舞われました。再起動が実行されなかったり、チェックを3時間半も保持するループがあったり、別のループが翻訳キューを50分間保持したり、そしてこのケースもありました。
現在の対処法
- パターンに角括弧を入れる:
pgrep -f "[i]18n_job.py example.com"。正規表現として[i]は文字 i に一致するため、ジョブは依然として一致します。一方、シェルのコマンドラインには文字[i]が含まれますが、これは式に一致しません。ただし、その行の他の箇所にプレーンなパターンが現れない場合にのみ有効です。同じ行の後半にある再起動コマンドなどで一致してしまう可能性があります。 - SSHの呼び出しを分けて、検出、停止、起動を行います。 1回目の呼び出しで
pgrep -afを使ってプロセスを一覧表示し、次の呼び出しで表示されたPIDをkillし、3回目の呼び出しで新しいプロセスを起動します。 - パターンではなく PID を待つ:
tail --pid=1234 -f /dev/null、またはwhile kill -0 1234; do sleep 30; done。 - さらに良いのは、ジョブを systemd に任せることです。ユニットとして実行し、
systemctl is-activeで確認するか、systemd-run --waitで起動してください。間違える余地のあるパターンはありません。 - すべての待機に期限を設定し(
timeout 3h)、いつ諦めるかを明示します。期限のないループは、静かに失敗します。
私たちのマシンを稼働させるキットである Factory Floor は、systemd ユニットを通じて動作します。コマンドラインでプロセスを探すことは決してありません。私たちを苦しめたのは、手動で入力したループでした。