- 適切なロードバランシングでシステム全体、データセンター全体が過負荷に陥ることを避ける
- ロードバランシングの指標をいくつか解説
ロードバランシングによって適切に負荷を分散することはできるが、単に分散させることだけを考えていると、アクセスが増加した時にいずれはどこかのバックエンドで過負荷状態が訪れる。これを避けるために、以下の方法が紹介されている。
- レスポンスの品質を落として計算しやすい状態にする
- 検索クエリに対してデータ全体から結果を返すのではなく、候補の一部から返す
- 永続化されたストレージにアクセスせずに、キャッシュなどの最新ではないかもしれないが高速にアクセスが可能な領域から結果を返す
- システムがさばき切れるに量にトラフィックを制限する
- 後の節でも紹介されているが、あまりにもトラフィックが増えすぎてしまうと、処理時間の他にもコンテキストスイッチなどの問題が発生する
- ロードバランシングの時点でバックエンドがこのような状態に陥らないように上限を定めておく
クエリが必要とするリソースの消費量を測る時、クエリが読み取るキーの量などの静的な特徴でモデリングしてしまうと、しばしば不十分な指標になってしまうことがある。これは、クエリの発行される時間帯やクライアントのコード、内部のバッチによる一時的なトラフィックの上昇などといった要因によって、その有効性が失われることがあるため。 Googleでは、あるデータセンターが使えるCPUおよびメモリのキャパシティを使ってモデル化するほうがうまくいくと考えられていて、リクエストが消費するCPU時間をコストとして考えてプロビジョニングを行う。
すべての顧客からのリクエストが適切な量で、なおかつ依存するバックエンド間のトラフィック調整が想定通りに行えるのであれば、過負荷などはそもそも発生しないはず。しかし現実はそんなに優しくないので、それに対応するための戦略が必要になる。もし一部の顧客から異常な量のリクエストなどが送られてきたときなどは、他の顧客が利用できるリソースが圧迫されないようにしないといけない。このために事前に顧客に提示している利用可能なキャパシティ(クォータ)に基づいて、リクエストを処理せずにエラーレスポンスを返すことで負荷の上昇を抑える必要がある。
- 「グローバル」という言葉の意味が掴めていない
前節で述べられていたように、顧客がクォータを超えたときに、バックエンドはリソース消費の少ないエラーレスポンスを返すことが求められる。ただし、レスポンスを生成するためのリソース消費がごく僅かなリクエストを拒否してもあまり意味がない。また、エラーレスポンスを返すことで本来起こるはずのリソース消費を少なく抑えられる場合でも、リクエスト/レスポンスの処理自体が多少の負荷になるということに変わりはない。エラーレスポンスでも大量に積み上がれば過負荷に繋がる可能性もある。
この問題を避けるための手段としてクライアント側でのスロットリングが挙げられている。クライアントの内部で直近のリクエスト数とエラーレートを保持しておき、ある閾値を超えた場合にはそもそもリクエストを送らないような処理を実装することで、バックエンドの負荷上昇を抑える目的をもっている。
ここでは doorman というツールが紹介されていた。
- 内部的な通信や開発元から提供されているアプリケーション、CLIツールのようなツールの上ではスロットリングが行えるのはわかる。外部に公開されている単なるREST APIのエンドポイントなどへリクエストに関しては諦める?
処理内容に基づいて各種リクエストに4段階の重要度を定め、重要度の低いリクエストから拒否が行われるようになっている。ただし、より重要度の低いリクエストのクォータが余っているならそちらを利用するようになっている。また、リクエストの重要度は複数のバックエンド間で伝播して使われるようになっていて、同じバックエンドに対するリクエストであっても、発行元である上位のバックエンドが指定した重要度によってクォータの上限は変化する。
- 重要度は伝播する中でバックエンドによって上書きされることがあるようだけど、どのような場面で使われるかイマイチわからなかった。
- 例えば顧客のリクエストに対するレスポンスを生成する処理では重要度を高くして、裏側で他のバックエンドと整合性を取るような処理を行う時に低く変えるとか?
タスクレベルでの過負荷に対する保護は基本的にCPU使用率を基準として行われ、場合によってはメモリの使用率なども考慮に入ることがある。利用率が上がると重要度に基づいてリクエストは拒否され始める。
また、エグゼキュータ平均負荷と呼ばれるアクティブなスレッド数によるシグナルも活用される。アクティブとはその時点で実行中か実行待ちのどちらかを指す。また、この数値は平滑化され、ごく短時間の間にスパイクが発生するようなリクエストが全体に影響を及ぼさないように考慮されている。なお、バックエンドによってはシグナルとしてメモリ利用率などもプラグインとして追加できるようにされている。
- 「タスクに対してのローカルな状態」の意味がいまいちわかっていない
- CPU時間なのかスレッド数なのか、もしくはどちらも使うのか読み取れなかった
過負荷であるというエラーを受け取ったクライアントがどのように振る舞うべきか。過負荷の状況には以下の2種類があるが、これらは区別して扱われる。
- データセンター内のバックエンドタスクの大部分が過負荷になっている
- 複数のデータセンター間でのロードバランシングが不完全で、トラフィックの調整が期待通りに行われていない
- この場合はリトライは行わずエラーを返すべき
- データセンター内のバックエンドタスクの一部だけが過負荷になっている
- データセンター内部でのロードバランシングが不完全な場合にこの状況が発生する
- 他のタスクのキャパシティが余っている状態
- リクエストのリトライを受け取るとき、過負荷になっているタスクを明示的に避けるような実装は必要以上の複雑さをもたらすので避けるべき
- バックエンドの数を活かして、リソースに余裕のあるタスクに振り分けられる可能性に依存している
また、クライアント側でリクエストをリトライするか判断するための仕組みを複数用意している。
- リクエストごとのリトライバジェット
- 同じリクエストが3回に渡って過負荷によるエラーとなる場合、データセンター全体が過負荷に陥っていると判断してリトライを行わない
- クライアントごとのリトライバジェット
- 各クライアントはリクエストに対するリトライ数を追跡しており、この比率が10%以下の場合のみリトライを行う
- クライアントからのリトライ回数を活用
- バックエンド側でクライアントからのリクエストに含まれるリトライ回数のメタデータを参照する
- リトライ回数の履歴から他のバックエンドも過負荷に陥っていると判断した場合、リトライを促すのではなくそれ以上のリトライを避けるようなメッセージを返す
あるバックエンドが複数のバックエンドに依存している時、一方の依存先の深いネストの先で起きたエラーに対して依存元がリトライを試みようすると、もう一方まで不要なリトライを行う可能性がある。これによってリトライ回数が爆発的に増加することを避けるため、リトライは直近の上位バックエンドだけが行うようにするべきである。
一定期間リクエストが行われない場合、クライアントはTCPの接続を切りUDPのヘルスチェックに切り替える。このようなクライアントが大量にあるとヘルスチェックに必要なリソースの方がリクエストの処理よりも大きくなる。この問題に対してはパラメータチューニングや接続の生成と破棄を動的に行うというアプローチで改善が期待できる。
また、バッチなどで一斉に新規接続があったときに接続の管理によって過負荷状態に陥ることもある。この問題に対する戦略としては
- 接続管理の負荷をロードバランシングのアルゴリズムに対して公開する
- バッチクライアントからのリクエストは専用のプロキシを通すようにして接続数のバーストを避ける
といった方法が紹介されている。
どれだけキャパシティを超える負荷がかかったとしても、タスク群に対して事前に予定された処理の量は保護されなければならないという観点で様々な対策が述べられていた。
- 一部の顧客からの大量のリクエストへの対策
- エラーレスポンスそのものに消費されるリソースの消費を抑える方法
- クライアントでのスロットリングによるリトライ量の調整
これらは処理可能ならリクエストを受け入れて、負荷が高まれば拒否するという処理を行う上で、どのような部分に注意深く目を向ければ想定外の問題を避けられるかを解説している。 これらの問題は適切なロードバランシングによって回避できるが、とても難しく銀の弾丸もないので頑張りましょう。