Google Cloud Platform

カナリアのおかげで命拾い : CRE が現場で学んだこと

信頼性の高いソフトウェアのリリースでは、何かおかしなことが起こったときにロールバックできることが何より重要です。そしてその方法は前回の投稿で説明しました。

ひとたびロールバックを身につけてしまえば、その次は、そもそも何かおかしなことが起こり始めていることをどうやって検知するかを知りたいと思うはずです。その方法の 1 つが、今回のテーマであるカナリア リリースです。

EWQqpj73NI8ofk0x9BEk9Zrh_8CM5GhfL5qFO7x9jhfv51-WKw9YvTyzcKMAvxLpkeWow-TW4EluBolnRp3WiTwcDwx5Is0cyQX3lxv42HqFWDJk0XSsugTVedkw4kHk9tVpf28easdh.PNG
Photo taken by David Carroll

カナリア リリースのコンセプトは、1913 年に生理学者の John Scott Haldane 氏が、一酸化炭素を検出するためにカゴの中の鳥を炭鉱に連れて行ったことが始まりです。かよわい鳥は人間よりもこの無臭ガスに敏感で、ガス漏れが起きているとすぐに木から落ちてしまうため、それが炭鉱員にとってその場から離れるべきサインとなるのです。

ソフトウェアにおけるカナリアは通常、バイナリや設定のロールアウトなど、何らかの設定を更新してライブ稼働した際にトラフィックを受信する最初のインスタンスのことを指します。新しいリリースは、まずカナリアのみに行くことになるわけです。

カナリアが実際のユーザー トラフィックを処理するという点が肝で、もし不具合があった場合は実際のユーザーが影響を受けます。そのため、カナリア リリースはデプロイ プロセスの最初のステップであるべきで、テストの最終ステップになっては意味がありません。

カナリア リリースを実装するにあたって最初のステップとなるのは、リリース エンジニアが新しいバイナリのリリースをカナリア インスタンスに対して開始する手動プロセスです。

その後、エンジニアはカナリアを監視し、エラーやレイテンシ、負荷が増加していないことを確認します。すべてうまく進んでいるようであれば、カナリア以外の稼働インスタンスに対してもリリースを開始します。

ところが、Google の SRE チームはこのやり方に疑問を呈しました。モニター グラフを手動で監視するだけでは、新しいリリースのパフォーマンス上の問題やエラー率の増加を検知する際の信頼性が十分ではないというのです。

ほとんどのリリースがうまくいっている場合、リリース エンジニアは問題がないことに慣れてしまい、低レベルの問題が発生してもモニターの異常をノイズだと暗黙的に合理化してしまうのです。実際、不具合のあったリリースを内部事後分析すると、根本的な原因が「リリース エンジニアが気にするほどカナリアのグラフが動いていなかったため」だったことが何度かありました。

そこで Google では自動分析に移行しました。これにより、カナリアのロールアウト サービスがカナリアのタスクを測定し、エラーやレイテンシ、負荷が上昇したことを自動的に検知、ロールバックも自動で行うようになりました(もちろん、これはロールバックが安全に実施できる場合にのみ適用されます)。

もしリリース時にカナリアを実装するのであれば、リリースの問題が見えやすくなるように工夫してください。カナリア タスクにおいてどのようにフォールト トレランスを実装するか、入念に考えるようにしましょう。カナリアがクエリに対して最善の力を尽くすようにしてもいいですが、内部エラーや依存しているサービスからのエラーが発生したときは、問題が生じたことがモニターに「大きな声で鳴いて」表示されるようにしなければなりません(ウェールズの炭鉱員がカナリアを有毒ガスに耐えられるように育てなかったり、カナリアに小型ガス マスクを装着しなかったりしたことには、正当な理由があるのですから)。

クライアントでのカナリア リリース

クライアント ソフトウェアをリリースする場合は、カナリア リリースのメカニズムがクライアントの新バージョンに必要であり、次の質問に答えられるようにしておかなくてはなりません。

  1. ごく少数のユーザーにだけ新バージョンをデプロイするにはどうすればよいのか。
  2. 新バージョンがクラッシュを繰り返したり、トラフィックを落としたり、ユーザー エラーを表示したりしたときは、どうやって検知するのか(クエリが発生していないことに対するモニター音はどうするのか)。
2 の解決策は、クライアントが自らバックエンド サービスにクライアントの存在を識別させることです。そこにクライアントの OS やアプリケーションのバージョン ID など、各リクエストの情報が含まれていることが理想です。また、サーバーにこれらの情報をログ化させます。クライアントが自らの存在を明確にカナリアとして識別させることができれば、それに越したことはありません。

そうすれば、クライアントの情報を異なる監視測定基準セットにエクスポートできます。クライアントがクエリ送信できていないことを検知するには、一般的に 1 日または 1 週間の特定の時間の着信トラフィックとして妥当な最も低い量を把握しておき、着信トラフィックがそれ以下になった場合にアラートを発するようにしましょう。

高可用性システムのカナリアに対するアラートのルールは通常、評価期間(問題発生となり、モニターがそのことを音で知らせるまでの時間)を主要システムより長めにしてあります。というのも、トラフィック量が少ないため、標準の通知がノイズとなってしまうからです。いくつかのサービス インスタンスが再スタートしたといったような比較的無害の問題によって、簡単にカナリアのエラー率が上がり、通常アラームのしきい値を超えてしまうのです。

リリースは通常、一部のアクティブ ユーザーを除き、幅広いタイプのユーザーをカバーするようにしなくてはなりません。Android クライアントであれば、Google Play Store がアプリケーション パッケージ ファイル(APK)の新バージョンを(実質無作為の)一部ユーザーに向けてデプロイすることを許可します。これは、国ごとに行うことが可能です。

ただし、この手法には制限やリスクがあります。以下では、Android APK のリリースについて考えてみます。

ウェブ クライアント

エンドユーザーが、アプリではなくデスクトップやモバイル ウェブを介してサービスにアクセスする場合は、実行される内容を制御しやすくなります。

UI が JavaScript で管理されている一般的なウェブ クライアントは制御がとても簡単で、アップデートした JavaScript のリソースを、ページが読み込まれるたびにクライアントに配信することも可能です。

しかし、JavaScript やそれに似たリソースをクライアント サイドにキャッシュすると、それ自体はサービス負荷やユーザーのレイテンシと帯域の消費を削減できるために便利なのですが、変更に不具合があったときにはロールバックが難しくなります。前回の投稿で説明したように、簡単で迅速なロールバックを阻害するようなものは、すべて問題なのです。

その解決策の 1 つは、JavaScript ファイルをバージョン付けすることです(最初のリリースを /v1/ のディレクトリに入れ、2 番目のリリースを /v2/ に入れるといった具合です)。そうすれば、ロールアウトでは単にルート ページ内のリソース リンクが変更され、新(または旧)バージョンを参照するようになります。

Android APK のリリース

Android アプリの新バージョンをロールアウトする際は、Play Store 内で段階的にロールアウトされたものを利用している既存ユーザーのうち、一定の割合のユーザーに対して行うとよいでしょう。そうすることで、ごく一部の既存ユーザーに新しいリリースを試してもらえます。その後、このリリースに自信が持てるようになれば、徐々に他のユーザーにもロールアウトすればよいのです。

一部ユーザーにリリースする方法では、新リリースを受けることができるユーザーの割合を示します。対象となるユーザーがモバイル デバイスを使って更新の有無を Play Store に問い合わせると、アプリに更新があることがわかり、更新が始まります。

ただし、この方法には以下の問題があります。

  • 更新の対象となるユーザーが、実際にいつ更新を確認するかはわかりません。ユーザーが適切にネット接続できる状況にあれば、通常は 24 時間以内でしょう。ただ、携帯電話や WiFi データ サービスが低速だったり、バイト単位の費用が高額だったりするような国では、そうとも限りません。
  • ユーザーがモバイル デバイスで更新を許諾するかどうかもわかりません。特に新リリースにおいて追加の許可が必要な場合は、ここが課題となります。
上述したカナリア リリースの手順に従えば、カナリアとなるアクティブ ユーザーの母数が増え、新しいトラフィックの特徴が明確になった時点で、新しいクライアント リリースに問題があるかどうかを判断できます。エラー率は上がったのか、レイテンシはどう変化したか、サーバーへのトラフィックが理由もなく急増していないか、などです。

バージョン v に不具合があるとわかっているアプリをうまく修復する方法は(ロールバックできない場合)、リリース v+1 内にバージョン v-1 というコード部分を構築してリリースし、すぐに 100 % 持ち直すことです。こうすることで、コード内に見つかった問題を修復する時間のプレッシャーから解放されます。

リリースの割合を段階的に増やす

新しいバイナリやアプリを徐々にリリースする場合は、アプリのリリースをどのような割合で増やしていくのか、いつ次に進めばよいのかを決めなくてはなりません。その際、次の点を考慮しましょう。

  1. (カナリアの)最初の段階では、監視やロギングによって問題が明確になるように十分なトラフィックを生成する必要があります。ユーザー数がどれだけなのかにもよりますが、だいたい全ユーザーの 1 % ~ 10 % 程度と考えるとよいでしょう。
  2. 各段階で手動の作業が数多く発生し、全体のリリースは遅れます。1 日 3 % ずつ段階的に作業すると、完全にリリースするまで 1 か月かかります。
  3. 一気に割合を増加させると(たとえば 10 % から 100 % にするなど)、小規模なトラフィックでは生じなかったトラフィックの大問題が発生する可能性があります。こうした懸念がある場合は、各段階で更新をかけるユーザー数を 2 倍以上増やさないようにしましょう。
  4. 新バージョンに問題がないときは、大半のユーザーにすぐにでも使ってもらいたいと考えるのが普通です。ロールバックする場合は、新しいリリースを出すときよりも 100 % 速くロールバックするようにしましょう。
  5. トラフィックのパターンは、通常は日中に最も混雑するなど 1 日を通して変化するものです。そのため、リリース後のトラフィック負荷のピークを把握するには、最低でも 24 時間が必要です。
  6. モバイル アプリの場合、ユーザーが新リリースを取り入れ、有効にして使い出すまでに時間がかかると考えましょう。
Android アプリのアップデートを数日以内に大半のユーザーにロールアウトしたいと考えているのであれば、Play Store の段階的アップデートを利用するのがよいかもしれません。10 % に対するロールアウトから開始し、次に 50 % へと増加させ、最後に 100 % にするといった具合です。

次のリリースまで最低 24 時間は空けるようにし、次のステップに進むまでモニターやログを確認するようにしてください。そうすれば、最初のリリースから 72 時間以内にほとんどのユーザーが新しいリリースを取り入れ、大抵の問題は手が終えなくなる前に検知できるはずです。

もしリリースによってサービスのトラフィックが急激に増える危険性があるとわかっているのであれば、10 %、25 %、50 %、100 % といった順番でリリースするか、さらに細かい割合で増加させるようにしましょう。

サービス インスタンスを直接アップデートする内部バイナリ リリースの場合は、1 %、10 %、100 % の順で実施してもよいでしょう。1 % のリリース時には、新リリースに全体的なエラーがないか確認します。たとえば、レスポンスの 90 % がエラーになるようなことはないか、といったことです。

10 % のリリースでは、さらに小さなエラーやレイテンシの増加を見つけ出し、全般的なパフォーマンスに違いが出ないかを確認します。

そして通常は、3 回目で完全にリリースします。パフォーマンスに敏感なシステムの場合、つまり通常 75 % 以上の容量で運用しているような場合ですが、ここに 50 % という段階を追加し、パフォーマンスが落ちていないか詳細に確認しましょう。システムの信頼性に対する目標値が高ければ高いほど、それぞれのステップで問題を検知するのに要する時間は長くなります。

理想的なマーケティングに沿ったリリースのシーケンスが 0 - 100 だとして(つまり全員に新機能を同時リリースします)、信頼性を担当するエンジニアの理想的なリリースのシーケンスが 0 - 0 なのだとしたら(何も変更しなければ問題は起こらないためです)、アプリの正しいリリースのシーケンスは交渉によって決まります。

以上が考慮すべきポイントです。お互いにとって納得のいくロールアウトの方法を決める際に、これらのポイントが理にかなった方法として役に立つことを願っています。

下のグラフは、8 日間のリリース期間という枠の中で、それぞれの戦略がどう作用するかを示しています。

wwK0wb0KijBvuCo1S89hyY5xGQRT5oOIfGdP3jwXeJZQbH0Q2JNhFEkFogTOJIvQlZzaKEO2B8Bz4Kh0l7Nqq0Ikx_ncmApHUX4lJR5yAmvqT6vT_ZKoYTVv8ZaVUVHAhuJOYj3K70gx.PNG

まとめ

私たち Google は、どんな状況でもうまくいくようなソフトウェア リリースの哲学を生み出しました。それは次のとおりです。

  • ロールバックは早期に行い、頻繁に行うこと : この哲学に従うようにサービスを持っていけば、サービスの平均修復時間(MTTR)を削減できます。
  • ロールアウトではカナリアを使うこと : どれだけテストや QA を実施したとしても、実稼働のトラフィック上でバイナリ リリースに問題が見つかることは少なくありません。効果的なカナリア戦略を取り入れ、正しく監視することで、問題の平均検知時間(MTTD)が短縮でき、影響を受けるユーザー数も大幅に削減できます。
とはいえ、結局のところ、最も優れたリリースは、その機能がバイナリのロールアウトとは関係なく独自に有効となることかもしれません。この件については、またの機会に。

* この投稿は米国時間 3 月 31 日、Customer Reliability Engineer である Adrian Hilton によって投稿されたもの(投稿はこちら)の抄訳です。

- By Adrian Hilton, Customer Reliability Engineer