デプロイを502エラー0にした方法
Mythexのバックエンドをデプロイするたびに、いくつかのリクエストが502 Bad Gatewayで失敗していました。数は多くありません。しかしデプロイ中の502は、誰かのクリックが何の効果も生まなかったことや、見守っていたビルドが一瞬止まったことを意味します。原因は2つの別々の問題にたどり着きました。デプロイと競合していた自己修復型のウォッチドッグと、サーバーがリッスンをやめてから、ロードバランサーがそのサーバーへのトラフィックを止めるまでの間の隙間です。修正後の本番デプロイ3回を通して、件数は10件、6件、そして0件と減りました。
構成
APIとオーケストレーターは、AWS Application Load Balancer(ALB)の背後にある小さなサーバー群で、コンテナとして動いています。デプロイでは、一度に1台のサーバーを更新します。古いコンテナをドレインし、新しいコンテナを起動して、次へ進みます。各サーバーのウォッチドッグは、コンテナが起動しているかを確認し、停止しているものを再起動します。夜中に落ちたプロセスが、誰も起きなくても戻るようにするための仕組みです。
どちらも単体では妥当です。しかし、組み合わさると妥当ではありませんでした。
問題1:ウォッチドッグがデプロイと競合していた
ウォッチドッグは1分に1回動き、ヘルスチェックに1回失敗しただけで「修復」していました。デプロイは、最大150秒間サービスを意図的にドレインします。その間、ヘルスチェックは失敗しているべきです。そのため、ウォッチドッグはデプロイのたびに発動していたのです。
ほとんどの場合、ウォッチドッグは競争に負けても無害でした。ところが一度だけ勝ちました。デプロイの途中でコンポーズファイル(どのコンテナを動かすべきかを記述したもの)を書き換え、サーバーからゲートウェイのコンテナを消してしまったのです。その間、デプロイジョブは成功と報告していました。
修正は次のとおりです。
- ロックを1つにしました。 デプロイは、サーバーのブートストラップスクリプトと同じファイルロックを取るようになりました。これでウォッチドッグとデプロイが同時に走ることは決してありません。
- 1回ではなく4回のストライク。 ウォッチドッグは、ヘルスチェックに4回連続で失敗するまで行動しません。ドレイン中のサービスは、もう死んでいるように見えません。
- 定義は1つ。 コンポーズファイルは、サーバーに配信される1つのソースだけを持つようになりました。コピーがずれていく心配はありません。
- 信用せず、検証します。 デプロイジョブは、コマンドが戻ったというだけでは成功しません。成功する前に、すべてのサーバーのAPI、オーケストレーター、ゲートウェイ、公開ワーカーを確認します。
問題2:トラフィックより先にサーバーが去っていた
2つ目の問題は、より目立ちにくいものでした。デプロイがサーバーをドレインするとき、サーバーはヘルスエンドポイントで503を返し始め、ロードバランサーがそのサーバーをローテーションから外すようにします。私たちのサーバーは、これを5秒間行ってから停止していました。
しかしALBは、一定の間隔でしかヘルスをチェックせず、ターゲットへのルーティングを止める前に、連続して複数回失敗する必要がありました。全体で最大約60秒です。つまり約55秒の間、ロードバランサーは、すでにリッスンをやめているサーバーに、実際のリクエストを送り続けていたのです。それが私たちの502でした。
修正は、2つのタイムラインを揃えることでした。
- ターゲットグループは5秒ごとにチェックし、2回失敗するとターゲットを異常とみなすようになりました。気づくまでは約10秒です。
- ドレインは、ヘルスチェックに失敗し始めてから15秒間、実際のトラフィックを提供し続けます。そのため、常に重なりがあります。
- これらの設定は、コンソールで誰かが一度クリックした場所ではなく、インフラを構成するスクリプトの中にあります。
数字同士が整合するようにする
このようなタイムアウトは互いに依存しています。ドレインは、ロードバランサーが気づくまでの時間より長くなければなりません。ドレインとシャットダウンの合計は、コンテナが強制終了される前に終わる必要があります。誰かが6か月後に1つの数字を変えて、このバグを静かに再び持ち込むのは簡単です。
そこで、これらの関係をテストとして書き留めました。ドレインは、ヘルスチェックの間隔に失敗のしきい値を掛けたものより長くなければなりません。再び正常と判定されるまでの時間は短く保たなければなりません。ドレイン全体は、コンテナの停止猶予期間の内側に収まらなければなりません。どれか1つの数字だけを変えると、CIが失敗します。テスト自体も変異テストで確かめました。各数字を誤った値に変え、テストが検出することを確認したのです。
デプロイの測定は間違えやすい
最初の測定では、問題のないデプロイが約3%壊れていると示されました。しかし、そうではありませんでした。200以外のすべてのレスポンスを数えていたのです。ドレイン中のヘルスエンドポイントからの503は、失敗ではありません。ドレインが仕事をしているのです。ロードバランサーに、サーバーが実際のリクエストを提供し続けている間は、そのサーバーを選ばないよう伝えているのです。
利用者が感じるのは502です。リクエストが何にも届かなかった場合です。その数だけを数えると、本当の姿が見えました。3回の本番デプロイで、10件、6件、そして0件でした。
得られた教訓
- 自己修復は、計画された停止を知っている必要があります。 デプロイと障害を区別できないウォッチドッグは、いずれデプロイと戦うことになります。
- ドレインはハンドシェイクです。 ロードバランサーがトラフィックを送るのをやめたと確実に分かるまで、サーバーは提供を続けなければなりません。そしてそのタイミングは、サーバーではなく、ロードバランサーに設定されます。
- 不変条件はテストとして書いてください。 運用上の数字は、ずれていきます。互いの関係をテストで縛っておけば、ずれません。
- 測定する前に、何を測るかを決めてください。 「エラー」は誤った指標でした。「何にも届かなかったリクエスト」が正しい指標でした。
Mythexで公開アプリを作っているなら、このことは見えないはずです。それこそが狙いです。関連記事:再公開の間も、公開したアプリが稼働し続ける仕組み。