こんにちは。システム統括本部プラットフォーム開発本部配信プラットフォーム部の大久保諒です。
過去に何度か紹介している通り、ヤフーでは静的コンテンツのキャッシュを行うためにオープンソースの HTTP プロキシサーバである Apache Traffic Server (以下 ATS) を用いて行っています。
さて、 ATS のような HTTP キャッシュを行うサーバにおいて、短時間である一つのオブジェクトに対する大量の HTTP リクエストが来た際に使用できるキャッシュがない場合、オリジンサーバの負荷が増大する問題が存在します。
ヤフーに限らず、近年スマートフォンのユーザ数の増加とプッシュ通知の効率化により、サーバが短時間で大量の HTTP リクエストを受け付ける可能性が高くなっており、この問題の重要性が高まっています。
本記事では、上記のオリジンサーバ負荷増大問題と ATS と他のキャッシュサーバ実装での対策について紹介していきます。
バックエンドシステムのエンジニアの方はもちろん、スマートフォンアプリなどでサービスを提供するエンジニアの方々の参考になれば幸いです。
キャッシュシステムにおけるオリジンサーバ負荷増大問題
なぜオリジンサーバの負荷が増大するのか
以前 こちら で紹介した通り、ヤフーでは HTTP キャッシュサーバで画像や css ファイルなど静的(もしくは一定期間変更が発生しない)コンテンツをキャッシュすることで、突発的なリクエストの増加が起こった際のオリジンサーバの負荷を軽減しています。
このようなキャッシュシステムの導入はヤフーのみならず、規模の大きい配信システムを抱えるサービスでは、自社のキャッシュシステムや CDN 事業者を利用して行っているものと思われます。
ATS のようなキャッシュサーバの動作としてはオリジンサーバへの HTTP リクエストを仲介し、もし有効期間内のキャッシュが存在しなければオリジンサーバにリクエストを転送、キャッシュが存在すればキャッシュサーバ自身でレスポンスを返すことになります。
もしキャッシュを持たない(あるいはキャッシュを持っていたが有効期限が切れた、 stale した)場合に、キャッシュサーバに対してキャッシュを持つ(あるいはキャッシュが有効であることを検証する)までの期間に大量のリクエストが来た場合は何が起こるでしょうか。
単純に「キャッシュが存在すればオリジンサーバにリクエストを転送しない」ロジックだけで動作してしまうと、キャッシュを持つまでの期間に来たリクエストはすべてオリジンサーバに転送されることになります。
この動作では、「リクエストの増加が起こった際にオリジンサーバの負荷を軽減する」というキャッシュサーバを利用する目的を果たせなくなる可能性があります。
上記に挙げるような、「キャッシュが存在しない際に本来は 1 個のリクエストのみが転送されるべき状況で、複数リクエストが転送されてしまう」問題は、しばしばキャッシュシステムにおける Thundering Herd 問題、あるいは cache stampede などと呼ばれます。
この問題を解決する意義
オリジンサーバの負荷が増大してレスポンスが悪化、あるいはそもそもレスポンスが返せない状況になった場合、もちろんながらサービスが想定通りに利用できなくなってしまいます。
たとえばプッシュ通知をきっかけにユーザからのリクエストが殺到した際に、オリジンサーバがダウンしてしまい、誘導したコンテンツを参照できないような状況が起こり得ます。
もちろんオリジンサーバの台数を増やすなどの対応も可能ですが、サーバやネットワークの設備増強や大量のサーバを運用するコストが生じてしまいます。
またプッシュ通知の例などリクエストが増加するタイミングをサービス提供者がコントロール可能であれば事前にキャッシュサーバにキャッシュを保持させることも可能ですが、リクエストが増加するタイミングで毎回そのための作業を実施するのはサービス運用コストを増大させてしまいます。
キャッシュサーバでこの問題を解決することで、このようなコストを伴わずサービスの質を低下させずに済みます。
ATS やその他サーバにおける、オリジンサーバへのリクエスト転送の効率化対策
ATS の場合
最新バージョンである ATS 6.0.0 の段階で、この問題の対策のための機能は 3 つ挙げられます。
まず Stale While Revalidate についてですが、これは RFC5861 で定義されている HTTP キャッシュが stale した際の振る舞いに関する拡張仕様となります。
この仕様では具体的にはキャッシュが stale である(有効期限が切れた)際に、指定された期間中はキャッシュがいまだに有効か検証しつつ stale なキャッシュをクライアントに返すような振る舞いを許すことができます。
ATS では Stale While Revalidate をプラグインとしてサポートしてはいるのですが、その振る舞いは RFC5861 に沿っておらず、 stale なキャッシュに対する最初のリクエストに対しては stale なキャッシュを返せるのですが、それに続くリクエストに対しては検証中はレスポンスをすぐに返さずブロックする振る舞いになっています。
このプラグインでは同一オブジェクトに対するオリジンサーバへの同時リクエスト数は 1 個に制限できるのですが、 stale なキャッシュをレスポンスできない仕様になっておりレイテンシの増加につながってしまいます。
Read While Writer は ATS のコア機能です。これを用いることで同一オブジェクトに対するオリジンサーバへのリクエストは 1 個に制限する機能が存在します。
ただしこちらはオリジンサーバからレスポンスヘッダが返ってくるまでは同時リクエストをブロックしない仕様になっているため、状況によってはオリジンサーバの負荷を軽減できない可能性があります。
Open Write Fail は ATS 6.0.0 から追加された新しいコア機能です。こちらは stale なキャッシュをもっている場合、最初に来たリクエスト以外は stale なキャッシュを返却できる機能になっています。ただし現状は最初に来たリクエストに対してはエラーレスポンスを返してしまう仕様になっており、この点を改善し Stale While Revalidate の仕様を満たすような機能を実装することが求められています。
これらの機能については、先日 ATS コミュニティ向けイベントである Fall 2015 ATS Summit にて発表された Yahoo!Inc. の Sudheer のスライド でも解説されています。
Varnish の場合
Varnish では 同一オブジェクトに対するリクエストの転送は同時に 1 個しか行わず、キャッシュを持つまでに複数のリクエストが来た場合はキャッシュを持つまでブロックするような挙動になっています。
またこの挙動に合わせて、あるリクエストに対してレスポンスがキャッシュできないという情報 (hit-for-pass) を持つことで、キャッシュできないオブジェクトに対するリクエストをブロックせずオリジンサーバに転送することも可能です。
さらに Grace mode という stale したキャッシュを指定期間はレスポンス可能にする機能を提供しており、これを用いて Stale While Revalidate のような振る舞いも記述できます。
これらの特徴から Varnish のリクエスト転送制御は優れていると言えそうですが、 Varnish は再起動することでキャッシュが失われる(Storage Backends の選択肢に persistent があるものの、 Varnish 4 では非推奨)ため運用する上で注意が必要です。
nginx の場合
nginx もオリジンサーバへのリクエストの転送の制御のための機能を提供しています。
proxy_cache_lock を on にすることでキャッシュミスした際のオリジンサーバへの転送リクエスト数を 1 個に制限できます。
また proxy_cache_lock_age で最後にリクエストを転送してから指定秒数後にまだオリジンサーバからのレスポンスが来ていなければ 1 個リクエストを転送する、 proxy_cache_lock_timeout で指定秒数後にオリジンサーバからのレスポンスが来ていなければオリジンサーバにリクエストを転送するなどといった細かい制御も可能です。
またキャッシュが stale した後、キャッシュを更新している間は stale なキャッシュを返すことを許可する設定が proxy_cache_use_stale ディレクティブに updating を与えることで可能です。ただし stale なキャッシュに対する最初のリクエストはキャッシュの更新が完了するまでブロックされるような挙動になっています。
おわりに
キャッシュシステムにおけるオリジンサーバの負荷増大問題に関する記事、いかがでしたでしょうか?
普段キャッシュサーバの動作を意識しない方でも、サーバ側のこのような挙動を知っておくことは無駄にはならないと思います。
もし参考になる点がありましたら幸いです。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました