本番環境でのテスト: 難しい側面(上)

Hodagi
16 min readApr 18, 2020

--

この記事は Cindy Sridhara氏によって書かれたTesting in Production: the hard parts (本番環境でのテスト: 難しい側面)の記事の抄訳です。非常に素晴らしい元記事は著者の熱量もあって膨大な長さなので、勝手ではございますが上下に分けて掲載します。

Fred Hebertに。この投稿の草案を読んで、いくつかのすばらしい提案をしてくれてありがとう。これは、分散システムのテストに関する私のシリーズの3回目の記事です。このシリーズの投稿は次のとおりです。

Testing Microservices, the sane way (published December 2017)
マイクロサービスのテスト、その正道 (2017年12月初出)

Testing in Production, the safe way (published March 2018)
本番環境でのテスト、 安全なやり方 (2018年3月初出)

Testing in Production: the hard parts (published in September 2019)
本番環境でのテスト: 難しい側面 (2019年9月初出)

Testing in Production: the fate of state (published June 2020)
本番環境でのテスト: 状態の運命 (2020年6月予定)

本番環境でのテストの長所については、最近では多くの議論があります。自分自身、1年以上前にこのトピックについて書いています。この投稿は、なぜ本番環境でテストを行うべきなのか、という議論ではありません(上記のリンク先の投稿は、なぜ本番環境でのテストがある種のシステムに不可欠である理由について説得力のある事例に違いありませんが)。これらの形式のテストを実施することそのものの課題についての率直な分析です。

本番環境でのテストで最も難しい2つの問題は、爆風半径を抑制することと、状態を処理することです。この投稿では、爆風半径の縮小のトピックをより詳しく掘り下げてみたいと思います。次の記事では「Testing in Production: The Fate of State (本番環境でのテスト: 状態の運命」と題して、本番環境でのステートフルサービスのテストの複雑さを探っていきたいと思います。

爆風半径

2019年7月上旬、Cloudflareは、「ダークローンチ」を目的としたコードのデプロイが原因で、30分間、世界規模のシステム停止を経験しました。

本日13時42分( UTC)から、ネットワーク全体で世界規模のシステム停止が発生し、その結果、Cloudflareプロキシドメインへの訪問者に502エラー(「Bad Gateway」)を表示するようになりました。この停止は、新しいCloudflare WAFマネージドルールの定期的なデプロイ中に、Cloudflare Webアプリケーションファイアウォール(WAF)内で誤って設定されたたった一つのルールがデプロイされたことにより引き起こされました。

これらの新しいルールの目的は、攻撃に使用されるインラインJavaScriptのブロックを改善することでした。これらのルールは、問題が特定され、ログに記録されますが、顧客トラフィックはブロックされないというシミュレーションモードのもと、デプロイされていました。誤検知率を測定し、新しいルールが本番環境にデプロイされたときに問題が生じないことを確認していました。

残念ながら、これらのルールの1つに正規表現が含まれていたため、世界中のマシンでCPUが100%に急上昇してしまいました。この100%のCPUスパイクが原因で、お客様は502エラーを目にすることになりました。最悪の場合、トラフィックは82%減少しました。

本番環境でのテストの主なインセンティブの1つは、テストコードが本番環境のソースコードと同じ環境でユーザートラフィックを提供するということです。これにより、本番環境でテストされたコードが、すべてのユーザートラフィックを処理するように昇格した後でも、同様に動作するという信頼を気づくのに役立ちます。しかし、この利点は大きなリスクにもなりかねないという点で、両刃の剣でもあります。

本番環境でテストする場合、本番環境には2つのテナント―既知の正常なバージョンと完全に壊れている可能性がある新しいバージョン―が存在します。最悪の場合、Cloudflareの事件のように、これにより運用インフラストラクチャ全体がダウンし、ユーザーは停止されたシステムを目の当たりにします。これは、本番環境でテストを行う以上避けられません。もっとも、完全に分離された並列スタックに本番環境のトラフィックすべてをシャドーイングして、あらゆる方法で本番環境をミラーリングできれば話は別ですが、このような環境を設定する際の運用上の複雑さ、それに伴う追加費用の負担のために、大抵は実現可能ではありません。

ここで重要なのは、本番環境でのテストの目標は、テスト中のコードが本番環境に影響を与えることを完全に回避することではありません。むしろ、本番環境でのテストの目標は、本番前のテストでは表面化しない問題を早期に発見して、完全な停止が起こるのを防ぐことができるようにすることです。

本番環境でのテストで採用する最適な考え方は、テストが成功も失敗も可能性があり、そして失敗した場合は本番環境に何らかの影響がある可能性が非常に高いという現実を受け入れることです。そして 、テストが失敗した場合に与える影響の爆風半径について積極的に考えることは、本番環境でそのような実験を行う前に必要不可欠になります。

多くの点で、本番環境で安全にテストするためのベストプラクティスは、システムの爆風半径を削減するためのベストプラクティスと同じです。システムは、テストが失敗したときの影響範囲(ドメイン)が小さく、範囲内に収まるように設計する必要があります。同様に、サービスの復旧までの道のりは、慣れ親しんだ、よく練られたものでなければなりません。本番環境は常に変化しているため、サービスの復旧への道に常に轍が残されていることを保証する唯一の方法は、サービスの復旧作業を継続的にテストすることです。

Sarah Jamie Lewisが言ったことを分散システムの文脈に当てはめてみます:[実際の]分散システムエンジニアリングのポイントは、ある時点で障害が発生することを想定し、各時点でのダメージを最低限に抑え、迅速に回復し、リスクとコストのバランスが適切に取れるような方法でシステムを設計することです。Werner Vogelsは、Amazon Web Servicesでの10年間から学んだ10の教訓と題した投稿で次のように書いています。

失敗が何であるかをわからなくても、障害を当たり前のこととして受け入れるシステムを構築する必要がありました。 システムは「家が火事になっても」、稼働し続ける必要があります。システム全体を停止することなく、影響を受ける部分を管理できることが重要です。システム全体の正常性を維持できるように、障害発生時の「爆風範囲」を管理するという基本的なスキルを開発しました。

本番環境でのテストでは、システム設計の2つの側面を検証することができます。テストに合格した場合はシステムの堅牢性に対する信頼性が向上し、テストに失敗した場合はサービスを正常に復旧させるための緩和戦略を実行することが(それによってさらにテストを行うことすらも!)できます。これらは災害復旧計画の一部について、有効性に対する信頼を構築するのに役立ちます。サービスの復旧作業は、単に大規模な停電時に行うものではなく、定期的な保守作業になります。ある意味、本番環境でテストする際に準備作業と一般的な災害復旧能力は前提条件になります(単に本番環境でのテスト中に一緒に構築すればいいのではありません)。

テスト事故の防止と軽減

AWSの振り返り資料として誤りの訂正(”Correction of Errors”)というテンプレートがあることで有名ですが、このテンプレートでは、インシデントに関与したエンジニアは「同様のイベントの爆風半径を半分にするためにはどうすればできたか?」という質問に答える必要があります。私の経験では、複雑なシステムは予期せぬ方法で故障することがあり、またしばしば故障します。そのようなサービスの復元力を向上させることは、常にゴールポストを動かしながらの取り組みになります。

とはいえ、計画通りに進まなかったテストの爆風半径を最小化するのに役立つ特定のパターンがあります。これらはあくまでも一般的なガイドラインであり、決定的な真実ではないことを強調しておきます。つまるところ、Rick Bransonが述べているように:

安全で段階的なデプロイ

投資の中で最も影響力のある分野の1つは、デプロイとリリースを切り離することです。これらの2つの投稿[1] [2]では、デプロイとリリースの違いと、なぜこの2つを区別することがそれほど重要になるのかを説明しています。

デプロイメントとは、サービスの新しいバージョンのコードを本番環境にインストールするためのチームのプロセスです。「新しいバージョンのソフトウェアがデプロイされる(“a new version of software is deployed”)」ことは、本番環境のインフラストラクチャのどこかで実行されていることを意味します。それは、AWS上で新しく起動したEC2インスタンスであったり、データセンターのKubernetesクラスタ内のポッドで実行されているDockerコンテナーであったりします。ソフトウェアは正常に起動し、ヘルスチェックに合格し、本番トラフィックを処理する準備ができています(と思いたい!)が、実際には何も受信していないかもしれません。これは重要なポイントなので繰り返しますが…デプロイメントでは、顧客に新しいバージョンのサービスを提供する必要はありません。この定義を考えると、デプロイメントはほぼゼロリスクの活動になります

「新しいバージョンのサービスがリリースされる(“a version of a service is released”)」ことは、本番トラフィックを提供することを意味します。動詞形式(releasing)では、リリースは、本番環境のトラフィックを新しいバージョンに移行するプロセスです。この定義を踏まえると、新しいバイナリの配布に関連するすべてのリスク(停止、怒っている顧客、The Registerの批判記事)は、新しいソフトウェアのデプロイメントではなくリリースに関連しています。

さらに、堅牢なデプロイ-監視-リリース(“deploy-observe-release”)またはロールバック-監視-軽減(“rollback-observe-mitigate”)パイプラインを構築することは、デプロイのリスクプロファイルを減らすのに大いに役立ちます。トラフィックを段階的にシフトできるようにするための適切な計画を立てることは、段階的なデプロイ作業と密接な関係があります。 Spinnaker(訳注: Netflixが元々開発・運用していたCI/CDのOSS)などの継続的デリバリーツールには、このようなカナリアテストのサポートが含まれています
次に、ロールアウトは段階的に行う必要があります。 Netflixは高可用性のためのヒントについて有名なブログを書いていますが、それらのほとんどすべてが段階的なデプロイ作業に関係しています。段階的なロールアウトは、コードだけでなく(さらに重要なこととして)環境設定・構成にとっても標準である必要があります。2015年にFacebookが発表した論文は、そのようなロールアウトがどのように見えるかを明らかにしています。

我々は複数の方法で構成情報を保護します。
第一に、コンフィグレーションコンパイラーが自動的にバリデーターを実行して、構成情報に対して定義された不変条件を検証します。
第二に、構成情報の変更はコードの変更と同じように扱われ、同じ厳密なコードレビュープロセスを経ます。
第三に、フロントエンド製品に影響を与える構成情報の変更は、サンドボックス内の継続的な統合テストを自動的に通過します。
最後に、自動化されたカナリアテストツールは、構成情報の変更を段階的に本番環境に展開し、システムの状態を監視し、問題が発生した場合は自動的にロールバックします。
私たちが克服しなければならない主なハードルは、多数のバックエンドシステムの正常性を確実に判断することです。

構成情報の配信プロセスがコードの配信プロセスと同じであれば、多くの構成情報に関連したシステム障害(“outage”)を防ぐことができます。インフラストラクチャの小さなサブセットに変更セットをプッシュして、問題のサービスのアプリケーション、システムメトリクスならびに直近のアップストリームとダウンストリーム(ここが重要です!)を監視して、すべてが良好であることを確認し、徐々に適用して変更箇所を広げていきます。

ステートレスサービスの段階的なデプロイ作業は、データベースやコアプラットフォームソフトウェアなどの基盤となるステートフルサービスの段階的デプロイ作業よりもはるかに簡単です。フォローアップ記事では、本番環境でのステートフルシステムのテストに完全に焦点を当てています。本番環境でのプラットフォームソフトウェアのテストに関して、他のサービスが実行されているプラットフォームのコンポーネント(監視エージェント、サイドカー、スケジューラー、スケジューラーエージェントなど)のデプロイをテストしている場合、他の考慮すべき点があるかもしれません。 。これらのデプロイ作業は本当に不変(“immutable”)ですか?本番環境でテストする場合、更新はインプレース(”in-place”)で行われますか?それとも既存のサービスを古いプラットフォームから、新しくデプロイされたバージョンであるテスト対象のプラットフォームに移行する必要がありますか?これらの移行の影響は何ですか?移行に失敗した場合はどうなりますか?サービスを古いバージョンのプラットフォームに戻す方法はありますか?サービス所有者に提供する必要がある安全性の保証は何ですか?サービスは”churn”に耐えられるように設計されていますか?古いバージョンと新しいバージョンを同時に使用してサービスを安全に実行できますか?特に新しいバージョンに後方互換性のない変更がある場合は大丈夫ですか?リスクはサービスオーナーにどのように伝えていますか?プラットフォームの変更が本番環境でテストされている時に、サービスオーナーがサービスをデプロイするとどうなりますか?
繰り返しになりますが、これらは万能な(“one-size-fits-all”)解決策はなく、社会技術的に(“sociotechnical”)難しい問題です。そうではないふりをすることは、良い意味で単純、悪い意味で不正直です。

迅速なサービス復旧

段階的なロールアウトを実施する場合、何かが食い違ったときにその影響を軽減できることが不可欠です。アップストリームでさらに障害が発生したり、連鎖的な障害につながる一連のイベントを引き起こしたりする前に行わなくてはいけません。本番環境でのテスト実行が原因でサービスが利用できなかったり、パフォーマンスが低下したりすると、サービスの「エラーバジェット」(または時間の経過とともにSLOを追跡するために使用される他のヒューリスティックな方法)になります。本番環境でのテスト中に発生した事故が多すぎると、エラーバジェットを溶かしてしまい、サービスをオフラインにしなくてはいけない可能性がある、定期メンテナンスやその他の運用上の不測の事態に対する余裕がほとんどなくなります。

「ロールバック」なんてものはないと主張する人もいますが、ステートレスサービスのコード変更では、その変更がアップストリームサービスの状態に影響を与えない限り、変更を元に戻すことで問題を解決することができます(そして、多くの場合、問題は解決します)。これについては次回の投稿で説明します。また、ビルド済みの成果物にロールバックすることは、ビルドパイプラインを再度通過するよりもはるかに高速になります。この場合、「ローリングバック」は「ローリングフォワード」と比較して、インシデント解決のためのさらなる遅延を減らすことができるかもしれません。

コアプラットフォームのソフトウェアやステートフルサービスにとって、ロールバックは、ステートレスサービスよりも危険と不確実性を伴うものです。まれに、ロールバックなどの機能がないケースもあります。一方で特定のケースでは、ロールバックを試みるよりもバグのあるバージョンのプラットフォームを実行しつつ、問題を修正する方が好ましい場合もあります(「ロールフォワード」と呼ばれます)。そしてもちろん、徐々に「ロールフォワード」するハイブリッド戦略が存在します。
Fred Hebertがこの投稿のレビューで書いているように:

私がライブコードのアップグレードでやったことの一つは、ErlangでもAPI/サービスレベルでも、段階を設けて追加しながら設計することです。この方法では、両方のバージョンで同時に作業することになります。

これは、状態を維持するときに特に役立ちます(ただし、コストはかかります)。
古いバージョン: 古い状態で動作しています
新しいバージョン: 新しい状態で動作します
中間バージョン: 古いバージョンと同等の機能を維持しながら新しいコードを実行します

DBでは、古いカラムと新しいカラムを同時にダブルライトすることで行われることが多いです(これには4つのバージョンが必要になるかもしれません!1.古い状態、2.新しい状態…新しいカラムは無視して受け入れる、3.ダブルライト、4.最終形態)サービスは無傷の状態であることを確認するために必要な限り使用することができます。そして、新しいバージョンにロールフォワードすると、チェックポイントのように動作します。

ロールバックやロールフォワードの戦略がどのようなものであれ、軽減策が迅速であり、実際に意図した通りに機能することを定期的に検証することが重要です。

下編に続きます。

この記事はCindy Sridhara氏に許可をいただき訳してます。心よく許可を与えてくれたこと、カオスエンジニアリング大好きな自分にはたまらないテーマを書いてくださったことに深く感謝します。

--

--