先月1ヶ月間に渡って、Netflixは同社が1年以上前に始めたAPI最適化に関する話を発表した。当初は通信を少なくしペイロードのサイズを小さくすることで性能の最適化しようとしていた[1]が、1年にわたる再設計の結果、APIの開発と運用を分散化しサービス層は非同期になった。再設計の第一の特徴はクライアントとサーバの責任の境界[2]を再定義したことだ。こうすることでコンテンツのフォーマッティングなどきめ細かいカスタマイズが可能になり、Netflixに接続する多様なクライアントデバイス間の差異(メモリ、キャパシティ、ドキュメント配信、モデル、ユーザインターフェース)に対処できる。
InfoQはNetflixのシニアソフトウエアエンジニアであるBen Christensen氏に設計の詳細について話を聞いた。
InfoQ:
1年前、性能最適化を始めたころのNetflix APIのアーキテクチャを教えてください。どのような問題があったのでしょうか。
再設計する前はNetflix APIは典型的なRESTfulサーバアプリケーションでした。下の図は単一のコードベースの"Restlet Resources"としてRESTfulなエンドポイントが静的に実装されていたことを示しています。
問題があったのは、
1) フォールトトレランス。Netflix APIプラットフォームのマネージャであるBen Schmausのブログ記事[3]で説明されている。
次の3つは私たちがAPIをより洗練させるにあたり、念頭に置いた主要な原則です。2) デバイスが求めるサービス水準との不適合。これは性能やイノベーションに影響を与える。これについてはNetflix APIプラットフォームのデレクターであるDaniel Jacobson氏がブログに書いている[2]。
- サービスの依存物で障害が発生した場合でも、ユーザエクスペリエンスを損なわない。
- サービスの依存物のひとつで障害が発生した場合、APIは自動的に修正処理を行うこと。
- APIは今何が起きているのか、15分から30分前は何が起きていたのか、先週は何が起きていたのかを示すこと。
Netflixのストリーミングサービスは800種類以降のデバイスで視聴できます。ほとんどのデバイスが私たちの非公開APIが提供するコンテンツを受け取ります。ひとつで全部のサイズに対応するAPIでこれら無数のデバイスをサポートするのは、上手くいってはいるものの、APIチームやUIチーム、そしてNetflixの顧客にとっては最適ではありません。簡単に言えば、
- RESTfulなAPIは最小公分母のソリューション。
- それぞれのエンドポイントがすべてのクライアントから利用されるので各デバイス向けのイノベーションが遅くなる。また、また機能開発はすべてデバイス向けに行わなければならない。
- APIチームがボトルネックになるので、イノベーションが遅くなる。顧客の要望は優先順位が付けられ対応の延期を余儀なくされる。
- ネットワークの通信回数は多く、ペイロードは不効率。
- 機能やデータタイプの有効無効を切り替える複雑なフラグがすべてのRESTfulエンドポイントに蔓延する。
InfoQ: 再設計の結果、最終的にはどうなりましたか。
下の図を見てください[4]。
[1] 動的エンドポイント
新しいウェブサービスのエンドポイントは実行時にすべて動的に定義されます。各クライアントチームは新しいエンドポイントを開発し、テストし、配置できますが、他のチームと協調する必要はありません(基盤APIサービス層の新しい機能に依存する場合は、その新しい機能が配置されてからエンドポイントを配置する必要があります)。
[2] エンドポイントコードリポジトリと管理
エンドポイントのコードはCassandraのマルチリージョンクラスタ(世界規模で分散されている)へ、RESTful Endpoint Management APIを通じて発行されます。このAPIはクライアントチームがエンドポイントを管理するために使います。
[3] 動的な複数言語対応JVMランタイム
どのようなJVM言語でもサポートします。各チームは最適な言語を選択できます。
サポートする言語として最初に選ばれたのはGroovyでした。関数のファーストクラスサポート(クロージャ)、リスト/ディクショナリの構文、性能やデバックの容易さが決定要因になりました。さらに、Groovyは多くの開発者が理解しやすい構文を提供します。
[4 & 5] 非同期Java API + 関数的反応プログラミングモデル
性能を得るには並列処理を上手く使うことが不可欠です。しかし、スレッド安全性や並列実行の実装の詳細をクライアント開発者から抽象化して、複雑さを低減し、彼らのイノベーションを速めることも同様に重要です。JavaのAPIを完全に非同期にするのが第一のステップでした。こうすることで基盤のメソッドの実装は、クライアントのコードを変更することなく、何を並列実行し何をしないのか制御できます。私たちはプログラミングの手法を用いて、非同期呼び出しの条件付きフローを制御することにしました。Rx Observablesを参考にしました。
[6] Hystrixによるフォルトトレランス
すべてのサービスのバックエンドシステムへの呼び出しはHystrixによるフォルトトレランス層(ダッシュボードがあり、最近オープンソースになりました)を経由します。この層は動的エンドポイントとAPIサービス層を不可避の障害から分離します。毎日APIからバックエンドのシステムへ数十億の呼び出しが行われており障害は不可避です。Hystrixレイヤはマルチスレッドで動作します。スレッドを使って依存関係を分離するからです。またスレッドはバックエンドのシステムのブロッキング呼び出しの並列実行にも利用されます。このような非同期リクエストは関数的反応フレームワーク経由で集約されます。
私たちは複数のフォルトトレランス手法を組み合わせたソリューションを開発することにしました。
どれも賛否が分かれる手法ですが、組み合わせることでユーザのリクエストと基盤の間に立つ包括的な防護壁になります。
- ネットワークのタイムアウトとリトライ
- 依存物毎のスレッドプールを使ったスレッドの分離
- セマフォ (tryAcquire経由、ブロッキンング呼び出しではない)
- サーキットブレーカ
[7] バックエンドサービスと依存物
APIサービス層はすべてのバックエンドサービスを抽象化します。その結果、エンドポイントのコードは“システム”よりも“機能”にアクセスすることになります。こうすることでAPIに依存しているコードへの影響を最小限にして基盤の実装や設計を変更できます。例えば、バックエンドシステムを2つの異なるサービスに分割する場合や3つをひとつに統合する場合、また、リモートネットワーク呼び出しをインメモリキャッシュで最適化する場合、これらの変更はエンドポイントのコードに影響を与えません。APIサービス層はオブジェクトモデルやその他の密結合物が抽象化され、エンドポイントのコードに“漏れ”ないようにします。
InfoQ: クライアントアダプタ層で利用するデザインパターンの選択について詳しく教えてください。クライアントアダプタはHTTPリクエストとHTTPレスポンスのループで実装されています。
クライアントチームはエンドポイントを実装し、下記を受け取ります。
- HTTPRequest
- HTTPResponse
- 機能へアクセスするためのAPIServiceGatewayインスタンス
- 現在の認証済みユーザを表すAPIUser
- リクエスト毎の環境変数を保持したAPIRequestContext。環境変数にはデバイスの種類や位置情報などが含まれる
エンドポイントにはHTTPリクエスト/レスポンスが与えられるので、望ましいスタイルの挙動を実装できます。
- RESTful
- RPC
- ドキュメントモデル + Path言語
- プログレシブやドキュメントレスポンス
リクエスト/レスポンスヘッダ、URIテンプレーティング、リクエスト引数を使うかどうかも自由です。また、戻りのデータもJSON、XML、PLIST、さらにバイナリフォーマットなど最適なフォーマットを利用できます。
次の図は、やって来たリクエストが正しいエンドポイントを見つけ、エンドポイントを実行する時のコントローラの流れを示しています。
InfoQ: なぜエンドポイントのコードのリポジトリとして、他のバージョニングシステムではなくCassandraを選んだのですか。まず、世界規模(AWSのリージョンをまたいで)でレプリケーションをする必要があったからです。Cassandraはこれができます。そして、CassandraはNetflixで利用経験があり支援もしているインフラコンポーネントだからです。
Cassandraは実際の開発やコードのバージョニングで使う標準的なバージョン管理システム(PerforceやGitなど)を置き換えるものではありません。配置したコードのためのリポジトリです。私たちはdev、test、integration、そして prodといった複数の異なる環境を持ち、それぞれに異なるCassandraクラスタを持っており、その環境のコードを管理しています。Cassandraに保存されたエンドポイントの改訂は多変量テストやカナリアテスト、漸進的なロールアウトや素早いロールバックを実現します。InfoQ: 動的言語のランタイムがクライアントアダプタエンドポイントコードをホストし、リクエストを非同期呼び出しを実現するJavaのAPIへのリクエストに分割するのですか。
はい、動的言語のランタイムがクライアントアダプタのコードをホストします。クライアントアダプタ(エンドポイント)にはAPIServiceGatewayインスタンスが与えられます。このインスタンスは非同期サービス呼び出しへのアクセスを提供します。関数的反応プログラミングの使い方と私たちのサービス例やがどのように非同期になっているか説明するブログ記事(サンプルコードあり)を現在準備中です。
InfoQ: サーキットブレーカとは何ですか。フォルトトレランスにどのように利用するのですか。
サーキュレートブレイカパターンについてはNetflix APIプラットフォームのマネージャであるBen Schmaus氏がブログで概説[3]している。私たちはAPIを再構成することで緩やかにフォールバックする仕組みを実現しています。また、サービスの呼び出しに呼び出し結果を追跡するコードを仕掛けています。サービスの障害が頻発することに気付いたら、呼び出されないようにして、障害を起こしたサービスの復旧中にフォールバックレスポンスを提供します。そして、定期的にサービスを呼び出し、呼び出しが成功したらすべての呼び出しにトラフィックを解放します。
このパターンから、Michael Nygard氏の著書"Release It! Design and Deploy Production-Ready Software"に書かれているサーキットブレーカパターンを思い出す方もいると思います。この本は私たちのサービスの依存性に仕掛けたコードの実装に影響を与えました。ただし、私たちの実装は基本的なサーキットブレーカパターンとは少し違い、フォールバックを複数の方法で起動します。
1. リモートサービスへのリクエストがタイムアウトする
2. サービスとやり取りするために使うスレッドプールとタスクキューのキャパシティが100%になる
3. サービスとやり取りしているクライアントライブラリで例外が発生する
これらの障害のバケットはあるひとつのサービスのエラー率に分解され、このエラー率が事前定義した閾値を超えた場合、私たちはサーキットに"出かけ"て、すぐにフォールバックを開始します。リモートサーバに繋がるかどうかも確かめません。
サーキットブレーカが使われている各サービスは次のどれかの手法を使ってフォールバックが実装されています。
1. カスタムフォールバック - サービスのクライアントライブラリがフォールバックメソッドを提供する。また、APIサーバ上のデータ(例えば、クッキーやローカルのJVMキャッシュ)を使ってフォールバックレスポンスを生成する
2. フェールサイレント - この場合、フォールバックメソッドは単純にnullを返す。リクエストを実行したクライアントに返却されるレスポンスにとって、サービスから提供されたデータが必須でない場合、nullは便利
3. フェールファースト - データが必須、またはフェールバックが動作せず、クライアントが500番台のレスポンスを受ける場合。この場合、デバイスのUXに良くない影響を与える可能性がある。しかし、APIサーバは正常に稼働しており、障害が発生したサービスが再開すれば、システムは素早く復旧する
参照: [1] http://techblog.netflix.com/2011/02/redesigning-netflix-api.html
[2] http://techblog.netflix.com/2012/07/embracing-differences-inside-netflix.html
[3] http://techblog.netflix.com/2011/12/making-netflix-api-more-resilient.html
[4] http://techblog.netflix.com/2013/01/optimizing-netflix-api.html