このプロジェクトについて
EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。
公開サイトはこちら →
https://stocks.happy-life-design.net
前回の記事はこちら →
https://qiita.com/rightcross-ae86/items/130f0bfefbb4337547e0
今回やったこと
症状: システムアップデート後、Webサーバーだけが無限に再起動
サーバーのカーネルアップデートによる再起動後、日次のデータ収集・分析処理(スケジューラ・ワーカーによるジョブ実行)自体は正常に稼働していましたが、ダッシュボード表示用のWebサーバーコンテナだけが数十秒間隔で再起動を繰り返し、再起動回数が166回を超える状態になっていました。
原因調査: pidfileベースの二重起動防止チェックがPID名前空間の特性と噛み合っていなかった
Webサーバーが使っているgunicornは、起動時に自身のPID番号をpidfileへ書き込み、次回起動時にそのPID番号が生きているか(os.kill(pid, 0))を確認するだけで「既に起動中かどうか」を判定する仕組みでした。
# gunicorn pidfile.py の生存確認ロジック(概念的に単純化)
def validate(self):
if not os.path.exists(self.fname):
return
with open(self.fname) as f:
pid = int(f.read())
try:
os.kill(pid, 0) # 生きていれば例外は出ない
except OSError:
return # プロセスは死んでいる → pidfileを上書きしてOK
raise RuntimeError("Already running")
今回はシステム再起動でコンテナ内のプロセスがunlink()する機会なく終了したため、pidfileにPID番号が残留していました。さらにコンテナはPID名前空間を持つため、再起動のたびに内部プロセスのPID番号は1から振り直されます。この2つが重なった結果、無関係な別プロセスが偶然pidfileに残っていたPID番号と同じ番号を取得してしまい、os.kill(pid, 0)の生存確認が誤って成功、「既に起動中」と誤判定してpidfileの上書きに失敗する、というループに陥っていました。
pidfileベースの二重起動防止方式に共通する古典的な弱点(PostgreSQLのpostmaster.pid等でも知られる)であることを、gunicorn自体のソースコードを確認して裏付けました。
応急対応
コンテナが起動してから次にクラッシュするまでの数十秒の間に、残留していたpidfileを手動で削除して復旧させました。
恒久対策: 起動前にpidfileを無条件削除
# Before
command: webserver
# After
command: ["bash", "-c", "rm -f /opt/airflow/airflow-webserver.pid; exec airflow webserver"]
Airflow公式のentrypointは、DB接続待機などの初期化処理を終えたあとにこのコマンド部分へ制御を渡す仕組みになっています。この性質を利用し、既存の起動シーケンスを変更することなくpidfileの無条件削除だけを追加しました。今回の構成は1コンテナにつき1プロセスしか動かさないため、そもそも二重起動防止の仕組み自体が不要という判断です。
検証
対策の反映後、再起動が発生しなくなったこと、ヘルスチェックが正常応答すること、pidfileが実際に稼働しているプロセスのPID番号に正しく更新されること、既存のテストスイートが全件通過することを確認しました。
なお対策の反映作業中、依存関係の設定を通じてデータベース・キャッシュ用のコンテナも意図せず再作成される場面がありましたが、データ自体は別領域に保存される設計のため、再作成後も全テーブルの件数が完全に一致することを確認しています(この依存関係が再作成を引き起こした詳細な経緯は未特定のまま持ち越しています)。
おわりに
pidfileという単純な仕組みでも、コンテナ特有のPID名前空間の振り直しと組み合わさると思わぬ形で壁にぶつかることを実感した障害対応でした。
気になる点・ご質問があればコメントでお知らせください。