Google Cloud Platform

信頼性の高いリリースとロールバック : CRE が現場で学んだこと

編集部注 : サービス停止の原因としてよくあることの 1 つに、サービス バイナリの新リリースが挙げられます。どれだけテストや QA をきちんと行っていても、一部のバグは、その影響を受けるコードが稼働するまで表面に出てこないものなのです。Google の Site Reliability Engineering(SRE)チームは、リリースに起因するサービス停止を長きにわたって数多く見てきており、今ではすべての新リリースに 1 つ以上のバグが含まれていると考えています。

ソフトウェア開発者は皆、自分のサービスに新しい機能を追加したいと思うものです。とはいえ、すべてのリリースには不具合のリスクがつきものです。変更個所をカバーするユニット テストや機能テストを追加し、システムのパフォーマンスに何か重大な影響が出ないか把握するために負荷テストを実施したとしても、本番環境でのトラフィックに驚かされることがあるのです。そのときの驚きは、通常あまり気持ちのいいものではありません。

新しいバイナリのリリースによってサービスが停止するというのは、よくあることです。システムの信頼性を担当するエンジニアの観点から見ると、このときの対応には次の 3 つの基本的なタスクが必要となります。

  1. 新しいリリースが実際にいつ壊れたのかを把握する
  2. 壊れたリリースから、修正された「であろう」リリースへとユーザーを安全に移行させる
  3. 最初の段階で壊れたリリースの影響を受けるクライアントの数を抑えるようにする(「カナリア リリース」を実施する)
今回の分析では、nginx などのロード バランサの背後にあるマシンや VM で多くのサービス インスタンスを稼働させるとともに、新しいバイナリのためにサービスをアップグレードするときは各サービス インスタンスを一度停めてからスタートする必要があると仮定します。

また、Stackdriver のようなツールを使用してシステムを監視し、内部のトラフィックやエラー率を計測することも前提としています。このようなモニタリング ツールがない場合は、信頼性について意味のある議論をすることが困難なのです。SRE の書籍に書かれている “Hierarchy of Reliability”(信頼性の階層)によると、モニタリングは信頼性の高いシステムにとって最も基本的な要件です。

検知

不具合のあるリリースにとっていちばん好都合なケースは、そのリリースによってサービス インスタンスが再スタートすることです。そうすると、不適切に処理されたリクエストの大半は HTTP 502 などのエラーを出すか、レスポンスのレイテンシが非常に高くなります。この場合、サービス インスタンスのロールアウトが進むにつれて全体のサービス エラー率が高まるので、リリースに問題があると気づくのです。

もっと微妙なのは、新しいバイナリが比較的少ない一部のクエリに対してエラーを返すケースです。たとえば、ユーザー設定の変更リクエストや、正当な理由があるにしろないにしろ、名前にアポストロフィーが付いているユーザーだけがエラーになったとしましょう。こうした不具合は、サービス インスタンスの大半がアップグレードされ、総合的なモニタリングを実施して初めてわかることです。そのため、サービス インスタンスのエラーやレイテンシのサマリーをバイナリ リリースのバージョンごとに分類しておくと便利です。

ロールバック

新しいバイナリやイメージをサービスにロールアウトする前に、自分自身に問いかけてみてください。「このリリースに致命的なバグや気が滅入るようなバグ、迷惑なバグがあったらどうするべきか」と。このようなことが起こる可能性があるから考えるのではなく、早かれ遅かれこのような事態は避けられないことだとして、サービスが炎上する前に綿密な計画を立てておいたほうがよいのです。

バグが見つかっても特にそれが致命的ではないときは、すぐにパッチを作成し前に進みたいと思うものです。つまり、バグを修正するための最小限のコードを元のリリースに追加し、そこから新しいリリースを作るということです(修正の「選り好み」)。

ただし、この方法は一般的にあまりお勧めしません。特に、そのバグがユーザーにも見えるようなものだったり、(たとえばクエリのリソース コストが 2 倍になるなど)内部的に重大な問題を起こしていたりする場合は推奨できません。

なぜ前に進んではいけないのでしょうか。それは、ソフトウェア開発者の身になって考えてみると理解できるはずです。

あなたの机の横で飛び跳ねている上司の血圧が見るからに上がっていき、修正版がリリースされるのはいつだと聞いてきます。会社のプロダクト ディレクターがその上司に対し、ユーザーから苦情が来ているとうるさく言っているためです。このままでは毎分 1,000 人単位のユーザーがサービス エラーに遭遇することになるので、あなたには修正版に向けたコーディングを最速で行うことが求められます。このようなプレッシャーの中では、コーディングやテスト、デプロイにおいて間違いが発生することはほとんど避けられないでしょう。

このような状況を Google で何度も見てきました。前に進むべく急いでデプロイした修正版が元の問題を修正できなかったり、さらに状況を悪化させたりといったことがありました。

万が一、その修正版で問題が解決しても、システム内に潜んだ別のバグが後になって見つかることもあります。つまり、よくわかっている状態から、定期的に QA テストを実施する対象となっていない、いわば野生の世界へと引きずり込まれていくのです。

Google には「ロールバックは普通だ」という哲学があります。新しいリリースにエラーが見つかったりエラーが疑われたりすると、リリースを担当したチームは最初にロールバックを行い、次に問題を調査します。

ロールバックをお願いすることが、リリース担当チームや、バグを含んだコードを書いた開発者への攻撃だと見なされることはありません。むしろ、ユーザーにとってシステムをできるだけ信頼できるものにするための正しい行為だと考えられているのです。発見された問題がロールバックの変更リストに記述されている限り、「なぜこの変更をロールバックしたんだ?」などと言う人は誰もいません。

そこで、ロールバックを成功させるには、次のような点を暗黙の了解としておくべきでしょう。

  1. 簡単に実行できる
  2. 低リスクであると確信できる
この 2 つ目の項目を実現するにはどうすればよいのでしょうか。

ロールバックのテスト

もし数週間にわたってロールバックしていないのであれば、とにかくロールバックしてみましょう。互換性のないバージョンや、故障した自動化 / テストなどの落とし穴をすべて見つけられるようにするのです。

ロールバックがうまくいけば、すべてのログやモニタリングを確認したうえで再度前に進みましょう。ロールバックがうまくいかない場合は、前に進んで不具合を取り除き、ロールバックが失敗した原因を突き止めることに全力を尽くします。新しいリリースが火を噴いてサービスが強制的に停止したからといって、よくわかっている元のリリースに戻ろうとするのはいただけません。それよりも、新しいリリースがきちんと動いている間に不具合を見つけるほうが、ずっといいのです。

互換性のない変更

ロールバックが簡単にいかないことも必ずあります。たとえば、新しいリリースにてアプリのデータベース内に(新しいコラムといった)スキーマの変更が必要となるケースです。ここで危険なのは、新しいバイナリをリリースし、データベースのスキーマをアップグレードした後で、ロールバックが必要となるようなバイナリの問題が見つかることです。こうなると、新しいスキーマを受け付けず、テストもされていないバイナリが存在しているだけとなります。

このとき推奨される方法としては、機能なしのリリースを出すことです。バイナリのバージョン v から開始し、新しいデータベースのスキーマを安全に扱えること以外は v とまったく同じである新しいバージョン v+1 を構築しましょう。新しいスキーマを活用する新機能はバージョン v+2 に入ります。これにより、ロールアウトの計画は以下のようになります。

  1. バイナリ v+1 をリリースする
  2. データベースのスキーマをアップグレードする
  3. バイナリ v+2 をリリースする
これで、新しいバイナリのいずれかに問題があった場合、スキーマをロールバックすることなく前のバージョンにロールバックできるようになります。

ここで、より一般的な問題での特別なケースについて話しましょう。サービスの依存関係をグラフ化し、直接的な依存関係をすべて特定するときには、依存関係のいずれかが突然オーナーによってロールバックされた場合にどうするか計画を立てる必要があります。依存関係にあるサービス S のバージョンが r から r+1 になってからリリースしようとしているのであれば、S がバージョン r+1 でずっと「とどまる」ことを確認しておかなくてはなりません。

考えられる方法としては、どんなサービスもひとつ前のバージョンにロールバックする可能性があるといったエコシステムを想定することです。つまり、この場合は自分のサービスが r+1 の機能に依存するバージョンへと移る前に、S がバージョン r+2 になるのを待つということです。

まとめ

今回わかったことは、リリースに対応するロールバックの準備ができていない限り、優れたリリースにはならないということです。とはいえ、不具合のあるリリースによってサービス全体が影響を受けるのを回避しつつ、その中でロールバックするタイミングを見極めるにはどうすればよいのでしょうか。

この投稿のパート 2 では、カナリア リリース戦略を紹介します。これは、新しいリリースにおける稼働トラフィックのほとんどを危険にさらすことなく、実際の稼働段階での問題を検知する方法です。

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

- By Adrian Hilton, Customer Reliability Engineer