キーポイント
- システムを開発するだけでなく、大規模なマイグレーションを実行するために、複数の予防的および緩和的なメカニズムを設計しなければならない。
- 強力なシャドウと検証メカニズムの開発は、読み書きAPIとメッセージ・バスに公開されるイベントにとって不可欠である。
- ダウンタイムをゼロにしないためには、ビジネス・コンテキストの重い新旧システムへのトラフィックを排出するメカニズムを開発することが重要である。
- 後方互換性レイヤーは、大規模な移行のリスクを大幅に軽減する。
- クラウドデータベースを使うという特殊な性質上、移行をスムーズかつ信頼性の高いものにするためには、入念なウォームアップと二重化が必要だ。
キーポイント
大規模な分散システムでは、重要なシステムをあるアーキテクチャから別のアーキテクチャに移行することは技術的に困難であり、繊細な移行プロセスを伴う。Uberは、世界でもっとも複雑なリアルタイム・フルフィルメント・システムの1つを運用している。この記事では、このようなワークロードをオンプレミスからハイブリッド・クラウド・アーキテクチャに、ダウンタイムとビジネスインパクトをゼロにして移行するテクニックを紹介する。
システムの複雑さ
Uber社社のフルフィルメント・システムは、リアルタイムで一貫性があり、利用可能性が高い。ユーザーは一貫してアプリに関与し、移動を開始、キャンセル、変更する。
レストランは絶えず注文状況を更新し、宅配業者は荷物を届けるために街を移動する。システムは毎秒200万件以上のトランザクションを実行し、プラットフォーム上のユーザーと移動の状態を管理している。
私は、このフルフィルメント・システムの最後の再設計を、オンプレミ・システムからハイブリッド・クラウド・アーキテクチャへと推進するエンジニアリング・リーダーでした。新しいシステムを開発するために必要なコードや検証のほかに、このプロジェクトでもっとも困難だったのは、ダウンタイムや顧客への影響をゼロにする移行戦略を設計することでした。
旧システムのアーキテクチャ
以前のフルフィルメント・システムを簡単に説明しよう。このシステムは、異なるユーザーとトリップ・エンティティの状態をメモリ上に保持するいくつかのサービスで構成されていた。さらに、ロック、検索、データセンター間のレプリケーションをサポートするストレージシステムを管理するサポートサービスがあった。
乗客のトランザクションはすべて「デマンド」サービスにルーティングされ、ドライバーのトランザクションはすべて「サプライ」サービスにルーティングされた。この2つのシステムは分散トランザクション技術によって同期が保たれていた。
これらのサービスはポッドと呼ばれる1つのユニットとして運用された。ポッドとは、1つの都市をフルフィルメントするために、多くのサービスが相互に作用し合う自給自足のユニットである。リクエストがポッドに入ると、ポッド外のサービスからのデータにアクセスする必要がない限り、ポッド内のサービス内に留まる。
下の画像では、緑と青のサービスグループが2つのポッドを表し、ポッド内に各サービスのレプリカがある。都市1からのトラフィックはポッド1に、都市2からのトラフィックはポッド2にルーティングされる。
さらにシステムは、サービスA、B、Cの間で分散トランザクションを促進し、sagaパターンを採用して、その中に格納されたエンティティの同期を確保した。エンティティの一貫性は、メモリ内データ管理とシリアライズによって各サービス内で維持された。
同時に、このシステムはUberの初期段階で拡張された。このシステムには、Uberの需要拡大につれて、もっとうまく機能したであろうアーキテクチャ上の選択がいくつかある。第一に、このシステムは一貫性よりも可用性を重視して設計されている。これは、複数のシステムが異なるエンティティを保存している場合、それらを横断して変更を加えるための真のacid準拠システムが欠如していないと、最終的にシステムが一貫していることを意味した。すべてのデータはメモリに保持されていたため、システムは垂直方向のスケーリングや、与えられたサービスのノード数にも本質的に制限があった。
再設計されたシステム
新しいシステムは、より少ないサービスで構成されていた。「デマンド」や「サプライ」といったエンティティを保存するサービスは、クラウド・データストアに支えられた単一のアプリケーションに統合された。トランザクションの責任はすべてデータストア層に移された。
新システムと既存サービスの利用者
このような重要な移行には、移行の様々な側面をカバーする多方面の戦略が必要である。マイクロサービス環境では、すべてのシステムが周囲の多くのシステムと相互作用する。相互作用の2つの主要な方法は、APIと、サービスによってメッセージバスに公開されるイベントである。
新システムは、すべてのコア・データモデル、APIコントラクト、イベント・スキーマを変更した。システムのAPIコーラーとコンシューマーが100以上あるため、それらすべてを同時に移行することは選択肢にない。我々は、既存のAPIとイベント契約を維持する「後方互換性」レイヤーの戦略を採用した。
後方互換レイヤーを作成することで、すべてのコンシューマーが古いインターフェースを消費している間に、システムを再構築できる。今後数年間で、コンシューマはこの再設計とカップリングすることなく、徐々に新しいAPIとイベントに移行できる。同様に、旧システムからのイベントのコンシューマは、後方互換性レイヤによって公開された旧スキーマでそれらのイベントを受信し続ける。
プラットフォーム上のエンティティのライフサイクル
この移行を複雑にしているのは、エンティティが継続的に状態を変化させていることだ。システム内の3つのエンティティ(乗客、ドライバー、移動)を考えてみよう。以前のシステムでは、これらのエンティティは異なるインメモリ・システムにあり、新しいシステムではそれをすべてデータベースに反映させなければならない。
移行の課題は、この移行において、すべてのエンティティの状態とその関係を、高い速度でアクティブに変化させながらも、確実に保持することである。
移行戦略
既存システムから新システムへトラフィックを移動させるために、ロールアウト前、ロールアウト中、ロールアウト後という各段階で複数の戦略を採用しなければならない。次のセクションで、それぞれについて詳しく説明する。
ロールアウト前
シャドーバリデーション
旧システムと新システム間のAPIコントラクトを検証するシステムを持つことは、新システムが既存システムと同等であるという確信を得るために非常に重要である。後方互換性レイヤーは、旧システムと新システム間のAPIとイベントの整合性を検証する重要な機能を提供する。
すべてのリクエストは、旧システムと新システムの両方に送られる。両システムからのレスポンスは、キーと値ごとに比較され、その差異がオブザーバビリティ・システムに公開される。それぞれの起動の前に、2つのレスポンスに差異がないことを確認することが目的である。
リアルタイムシステムにはいくつかのニュアンスがある。過渡的なシステムでは、すべての呼び出しが正確な応答で成功するとは限らない。一つの戦略は、成功したすべてのコールを高い一致率でコホートとしてレビューし、ほとんどのAPIコールが成功するようにすることである。新システムへの呼び出しが失敗する有効な状況もある。ドライバーは自分のアプリでオフラインになるためにAPIを要求するが、新システムにはユーザーがオンラインになったという事前の記録がない場合、クライアントエラーが発生する。このようなエッジケースでは、パリティの不一致は無視できるが、注意が必要である。
さらに、読み取り専用APIのパリティは、副作用がないため、もっとも簡単である。書き込みAPIでは、旧システムがリクエスト・レスポンスを共有キャッシュに記録し、新システムがそのレスポンスを再生するというシステムを導入した。トレース・ヘッダがあるので、新システムは旧システムがもともと受け取ったレスポンスを透過的にピックアップし、外部システムとの完全な外部コールを行う代わりに、キャッシュから新システムに入れる。これにより、依存関係にあるシステムからのレスポンスとフィールドの一貫性が高まり、外部依存関係による干渉を受けずに、新旧のシステムをよりよく一致させることができる。
E2E統合テスト
大規模な書き換えは、エンドツーエンド(E2E)テストのカバレッジを向上させるのに適した瞬間である。そうすることで、新システムがトラフィックの規模を拡大し、新しいコードが新システムで日常的に立ち上げられるようになったときに、新システムを保護できる。今回の移行では、テストを300以上に増やした。
E2Eテストは、エンジニアがコードを作成するとき、ビルドのデプロイ前検証のとき、そして本番環境での継続的なテストなど、開発ライフサイクルの複数の時点で実行する必要がある。
カナリアにおけるブラックボックステスト
クリティカルなシステムで悪いコードを防ぐには、リクエストの小さなサブセットだけにコードを公開し、爆発半径を小さくすることで達成できる。さらに改善するために、E2Eテストを継続的に実行する本番環境だけにコードを公開もできる。カナリア環境が正常にパスした場合のみ、デプロイするシステムは先に進むことができる。
負荷テスト
新システムに大きな本番負荷がかかる前に、包括的な負荷テストを実施しなければならない。後方互換性のあるレイヤーを経由して新システムにトラフィックをリダイレクトする当社の能力により、新システムへの完全な本番トラフィックを可能できる。カスタム負荷テストは、データベース・ネットワーク・システムのようなユニークな統合を検証するために作成された。
データベースのウォーミングアップ
クラウド・プロバイダーによるデータベースやコンピュートのスケーリングは、即座に行われるわけではない。大きな負荷がかかると、スケーリング・システムがすぐに追いつかない。分散データストアのデータ再バランス、コンパクション、パーティション分割は、ホットスポットを引き起こす可能性がある。我々は、これらのシステムに対する合成データ負荷をシミュレートし、一定レベルのキャッシュウォーミングと分割を実現した。これにより、本番トラフィックがこれらのシステムに向けられたときに、システムの急激なスケーリングニーズなしに動作することが保証された。
フォールバック・ネットワーク・ルート
アプリケーションとデータベース・システムは、2つの異なるクラウド・プロバイダー、1つはオンプレ、もう1つはクラウドにまたがっているため、ネットワーク・トポロジーの管理には特別な注意を払う必要がある。2つのデータセンターには、専用のネットワーク・バックボーンを通じてインターネット経由でデータベースに接続するために、少なくとも3つのネットワーク・ルートが確立されていた。これらのネットワークを3倍以上のキャパシティで検証し、負荷テストを行うことは、本番での信頼性を確保するために非常に重要だった。
ロールアウト中
トラフィックのピン留め
マーケットプレイスでは、複数のアプリが同じ移動で相互に作用している。ドライバーの移動と乗客の移動は同じ移動エンティティを表している。乗客からのAPIコールを新システムに移行する際、対応する移動とドライバーの状態を旧システムに残すことはできない。我々は、移動が開始したのと同じシステムで移動を継続し、終了するよう強制するルーティング・ロジックを実装した。この判断を下すために、マイグレーションを実行する30分ほど前に、すべての消費者識別子の記録を開始する。対応するエンティティへのリクエストは、同じシステムに固定され続ける。
段階的に展開する
ある都市で最初に移行される一連の移動は、E2Eテストである。次に、アクティブな移動に参加していないアイドル状態の乗客とドライバーが新システムに移行される。この移行後、新しい移動はは新システムでのみ開始される。この期間は、2つのシステムが2つの市場に並行してサービスを提供している。旧システムで進行中の移動が完了すると、アイドル状態になった乗客とドライバーは新システムに徐々に移行される。時間以内に、街全体が新システムに移行する。新システムで障害が発生した場合は、同じシーケンスを逆に実行できる。
ロールアウト後
オブザーバビリティとロールバック
2つのシステム間の切り替えを実行する際には、両方のシステムにわたる堅牢なオブザーバビリティが重要である。私たちは、一方のシステムからトラフィックが流出する一方で、新システムでトラフィックが回復する様子を示す精巧なダッシュボードを開発した。
私たちの目標は、トリップ量、利用可能な供給量、トリップ完了率、トリップ開始率といった主要なビジネス指標が、移行中も変わらないようにすることだった。総合的な指標は横ばいのままであるべきだが、新旧システムの組み合わせは移行の瞬間に変化する。
Uber社は数千の都市で数多くの機能を提供しているため、これらの指標を都市の粒度で観察することは極めて重要である。100以上の都市を移行する際に、すべてのメトリクスを手作業で観察は不可能だった。これらのメトリクスを観察し、もっとも逸脱したメトリクスをハイライトするための専門ツールを構築しなければならない。また、異なる都市のトラフィックに不適切に調整されたアラートが急増しても、移行の信頼性にはつながらないため、アラートも無駄だった。オペレーターが都市の健全性を分析できる静的なツールは、すべての都市が順調に稼働していることを確認する上で極めて重要だった。
ある都市が不健全に見えた場合、オペレーターはその都市だけを旧システムに戻し、他の都市はすべて新システムに残すことができた。
本番でのブラックボックステスト
「カナリアにおけるブラックボックステスト」戦略と同様に、同じテストを本番システムに対して継続的に実行し、問題を発見する。
移行を成功させる
新システムの開発の邪魔にならないよう、旧システムで十分な走破性を確保することが重要だ。私たちは、新システムの開発に着手する前に、旧システムの信頼性を高めるために4ヶ月を費やした。最初は旧システムで機能を開発し、新システムの開発の途中で旧システムと新システムで並行して機能を開発し、最終的に新システムの後半では新システムのみで機能を開発するような、プロジェクトをまたいだ計画が重要である。このような利害関係者のやりくりは、2つのシステムの維持に永遠に行き詰まることを避けるために、極めて重要である。
強力な一貫性保証が必要で、ビジネスのアップタイムにとって主要な、絶対的に重要なシステムを移行する場合、新しいスタックを構築する際に、エンジニアリングの労力の40%近くを、オブザーバビリティ、移行ツール、堅牢なロールバックシステムに費やすべきである。
重要な技術的要素には、個々のユーザーレベルでのAPIトラフィックの完全な制御、強力なフィールドレベルのパリティとシャドウマッチングの仕組み、システム全体にわたる堅牢なオブザーバビリティが含まれる。さらに、移行の瞬間は、明示的なビジネスロジックや製品開発を必要とする可能性のある、競合条件や特殊な処理をはらんだ重要なエンジニアリング上の課題である。
新システムへのトラフィックの移行は、通常横ばいで、初期の反復ではゆっくりと成長する。ひとたび新システムへの信頼が高まると、下の図に示すように、トラフィックは指数関数的に加速するのが普通である。
早期に加速することの難点は、同じように重要なトラフィック量を持つ2つのシステムを、より長い期間維持しなければならなくなることだ。新システムのスケールにより発生する停止は、本番ユーザーへの影響が大きくなる。比較的短い期間で新システムへのトラフィック移行をスケーリングする前に、最小の数に対してすべての機能を長期間検証するのが最善である。
Uber社では、開発と移行を通じて数千人以下のユーザーしか影響を受けずに、この移行を実現した。これまで論じた手法は、我々のシステムから全く新しいフルフィルメント・システムへのスムーズな移行を支援するための基礎となる方法であった。