ヘルスチェックは緑なのに注文が入らない
一言でいうと
デプロイは、「新バージョンが起動している」ではなく、「新バージョンで業務が1件最後まで通る」ことを確認したときに終わります。バージョンをディレクトリで分け、シンボリックリンク1つで一度に切り替えれば、確認に失敗したとき、同じ方法で一度に戻せます。
なぜ必要なのか
顧客の注文受付APIを1.5.0に上げた日、デプロイの担当者は、ヘルスチェックが緑なのを見て、完了を報告しました。その夜、画面は注文のたびに「受付済み」と表示しましたが、倉庫には1件も入っていませんでした。新バージョンで、保存先のパスの設定が抜けていたのです。ヘルスチェックは嘘をついていませんでした。プロセスは生きていて、/healthは200を返していました。ただ、その質問は「生きているか」であり、顧客が知りたかったのは「注文が入るか」でした。
FDEは、顧客のホストで、デプロイの自動化がない、あるいは貧弱な状態で働くことが多いです。KubernetesのローリングアップデートやBlue/Greenの切り替えを使えない、VM1台でも、同じ原理を、ファイルシステムとシェルスクリプトで実装できる必要があります。
どう動くのか
構造は単純です。
/root/site/
├── releases/
│ ├── 1.4.0/ (VERSION, app.py)
│ └── 1.5.0/
├── current -> releases/1.4.0
├── shared/ orders.jsonl · app.pid · app.log (판이 바뀌어도 남는 것)
└── deploy.log 배포 한 번에 JSON 한 줄
アプリは、常にcurrent/app.pyで起動します。バージョンを切り替えることは、ファイルを上書きすることではなく、currentが指す先を変えることであり、元に戻すのも同じ動作です。古いバージョンのディレクトリがそのまま残っているので、戻すために、もう一度ダウンロードしたり展開したりする必要はありません。
核心は「切り替える瞬間」です。rename(2)は、newpathがすでにあれば、原子的に置き換え、他のプロセスが、その名前がない瞬間を見ないことを保証します。newpathがシンボリックリンクなら、リンク自体が上書きされます。そのため、同じディレクトリに一時的なリンクを作り、currentの上にrenameすれば済みます。同じディレクトリでなければならない理由も、同じドキュメントにあります。異なるマウントの間のrenameは、EXDEVで失敗します。Pythonでは、os.replaceが同じことを行い、成功すれば原子的だと(POSIXの要件)、ドキュメントに書かれています。
では、よく使うln -sfnはどうでしょうか。GNU lnのマニュアルは、-fを「既存の対象を削除する」と説明しつつも、--backupを使わなければ、対象がないごく短い瞬間はないと書いています。これは古い動作ではありません。coreutilsのNEWSの8.27(2017-03-08)の項目が、ln -f A Bが、もうBを先に削除しないと知らせています。ラボのイメージ(coreutils 9.4)で実測したところ、あるプロセスがln -sfnで3,000回、一時的なリンク+mv -Tでさらに3,000回、currentを交互に切り替えている間に、別のプロセスが約1,000万回readlinkして、両方の方式で、「なし」を一度も見ませんでした。したがって、違いは、ツールのバージョンに頼るか、システムコールの保証に頼るかです。顧客のホストのlnが何バージョンかわからないなら、renameのほうが説明しやすいです。
より頻繁に事故を起こすのは、-nです。マニュアルは、-nを「最後の引数が、ディレクトリを指すシンボリックリンクのとき、特別に扱わない」と説明しています。実測では、currentがreleases/aを指しているときに、ln -sf releases/b currentを実行すると、currentはそのままで、releases/aの中にbというリンクが新しくできました。コマンドは成功で終わるので、誰も気づきません。
ヘルスチェックとスモークの試験は、役割が違います。
| 確認 | 尋ねること | このラボの方法 |
|---|---|---|
| ヘルスチェック | 新しいプロセスが起動して応答するか | /healthが200で、さらにversionが新バージョンか |
| スモーク | 業務の経路1件が最後まで通るか | 注文を1件POSTしてから、GETで読み直す |
ヘルスチェックでversionを確認するのには、理由があります。古いプロセスを停止できないまま、新しいプロセスがポートの衝突で落ちると、/healthに答えるのは古いバージョンです。緑のランプが、新バージョンのものかを、まず確認する必要があります。また、新バージョンがキャッシュを読み込むのに、2秒以上かかることがあるので、上限まで待ちますが、プロセスがすでに落ちていたら、待ちません。
現場での姿
ロールバックで最もよくある失敗は、記録なしで戻すことです。翌朝、顧客は「昨夜1.5.0が上がったと聞いたのに、なぜ1.4.0なのか」と尋ねます。何を、なぜ、どのバージョンに戻したのか、1行あってはじめて、会話が始まります。失敗したバージョンのディレクトリも、消しません。調査の対象だからです。
2つ目は、保存の整理です。ディスクを節約するために、「最新の3つだけを残す」をcronに設定すると、ロールバックのあとは、currentが最新ではないことがあります。1.6.0を戻して1.5.2に戻った状態で、1.7.0、1.7.1が続けて失敗すると、最新順の整理が、いま動いているバージョンを消します。currentと、currentに問題があるときに戻る直前のバージョンは、日付と関係なく守ります。
実務で本当に大切なこと
- 切り替えとロールバックは、同じ動作でなければなりません。戻す手順が別にあると、急いでいるときに間違えます。
- ヘルスチェックは、「誰が」答えたのかまで見ます。スモークは、顧客がお金を払う経路を見ます。
- スモークの注文は、見分けがつかなければなりません(このラボは、SMOKE-という接頭辞)。顧客のデータと混ざると、精算が狂います。
- ロールバックは失敗ではなく、手順の結果の1つです。終了コードと記録で区別して残します。
次のラボですること
ビルドを手で展開してcurrentを作ったあと、原子的な切り替えのスクリプト、ヘルスチェックとスモークが入ったデプロイのスクリプト、ロールバック、保存の整理を、順に作ります。採点ツールは、毎回異なるバージョン番号で、正常なビルド・スモークでだけ壊れるビルド・起動した途端に落ちるビルド・起動が遅いビルドを作って、あなたのスクリプトを実行し、実際に応答しているバージョンとシンボリックリンクを、測り直します。最後に、顧客の1.5.0を自分でデプロイして戻し、事件の記録を残します。