SRE本
21章 過負荷への対応
過負荷を避けるのがロードバランシングの目標
いつかは過負荷になる時が来るため、その状況を上手く扱う必要がある
過負荷を扱う方法
- レスポンスの品質を下げる
- 検索時に範囲全体を検索するのではなく、ページングなどを利用して1部分のみを検索する
- 最新ではないかもしれないがキャッシュを使用する
- データセンター間のトラフィックバランス調整
- ロードバランシングの時点でデータセンターのタスク実行状況を確認し、上限を超えるならそもそもデータセンターに送信しない
- リソース制限に応じた動作をするようクライアントやバックエンドを構築する
- 可能ならリダイレクトして低品質の結果を返す
- 最悪の場合リソースエラーを透過的(存在を意識せず)に扱えるようにする
21.1 「クエリ/秒」の落とし穴
クエリのコストはあらゆる要因(時間帯など)によって大きく左右される
ロードバランサーの設計と実装に置いて、有効性が変化するような特徴量は指標として不十分
もっとも良い方法は、利用可能なリソースから直接キャパシティを計測する
リクエストのコスト(リクエストが消費したCPU消費量)はプロビジョニングがうまくいっているかどうかのシグナルとしてうまく使える
- GCを行う場合はメモリのプレッシャー(負荷)が自然とCPU消費の増加として反映される
- GCを行わない場合はCPUが使い切られる前に他のリソースが使い切られないようにプロビジョニングできる
リソースの消費について考える際は、それぞれのシステムリソースを個別に考慮する
※プロビジョニング
必要に応じてネットワークやコンピュータの設備などのリソースを提供できるよう予測し、準備しておくこと
21.2 顧客単位での制限
グローバルでの過負荷が生じた場合、問題のある顧客にはエラーレスポンスだけを返し、その他の顧客に影響が出ないようにする
キャパシティをプロビジョニングし、顧客ごとの上限を設定する
この際、すべての顧客が同時にリソースを限度まで使うことはほぼ無いため、全てを足し合わせた時にバックエンドが持つリソースを超えていても問題無い
googleはリアルタイムで全バックエンドタスクの利用情報を集計して、そのデータから個々のバックエンドタスクの上限を更新している
個々のリクエストが消費するリソースの量(CPU消費量)をリアルタイムで計算している
21.3 クライアント側でのスロットリング
顧客が上限を超えた場合、バックエンドタスクはリクエストを拒否し、エラーを返すことが求められる
この際、リソース消費は正しくリクエストを処理する場合よりもはるかに少なくしなければならない
ただし、エラーを返したからと言って多少のリソースを消費していることは変わらなく、積み重なれば過負荷になる恐れがある
クライアント側でのスロットリング(適応型スロットリング)
クライアントが発行した直近のリクエストの大半が上限オーバーのエラーで拒否された場合、自主規制を始め、バックエンドに投げないようにする
- 追跡情報
- requests
アプリケーションレイヤーが受けたリクエスト数 - accepts
バックエンドが受け付けたリクエスト数
- requests
通常はrequestとacceptsの数が等しくなる
バックエンドがリクエストを拒否し始めるとacceptsが減少していき、クライアントはrequestがacceptsのK倍(googleは2)になるまでリクエストをバックエンドに投げ続ける
この上限を超えると21-1式で出た確率に従って、クライアント側で拒否する
あくまで確率で拒否するだけで完全に止めないのがミソで、それによりバックエンドの状況確認ができる
このアプローチの最大のメリットはクライアントが完全にローカルの情報のみでシンプルに判断できているということ
クライアントからバックエンドに対してリクエストがきわめて少ない場合は、バックエンドの状態を聞きに行く頻度が減る(例えば散発的に来たリクエストを過負荷状態のバックエンドがたまたま受け入れる場合など)ということなのでバックエンドの状態を正しく判断できなくなるので注意。
21.4 重要度
4つの重要度
- CRITICAL_PASS
超重要。 - CRITICAL
デフォルト。CRITICAL_PASSとCRITICALは全て捌けるようにプロビジョニングしておく - SHEDDABLE_PLUS
数分から数時間後にリトライしてもいいようなバッチジョブのデフォルト。 - SHEDDABLE
最悪処理されなくてもいい。
顧客がクォータを使い切ると下の重要度から拒否していき、ある重要度以下の重要度が全て拒否されて初めてその重要度のリクエストが拒否される。
適応型スロットリング(21.3のrequestsとacceptsの計算式で出た確率で拒否するやつ)もこの4つの重要度ごとに動く。
重要度の自動伝搬
バックエンドがリクエストAを受信したらその関連処理は一貫してリクエストAと同じ重要度を使用する。
例えば、リクエストBはただ検索するだけの処理かもしれないがそれがリクエストAの決済処理に使用されている場合は重要度がMAXになる
Googleでは重要度を標準化することで、あらゆる処理が上位で指定された重要度に従うようになった。
21.5 利用率のシグナル
CPUの利用率
タスクに対して割り当てられたCPUの利用率も考慮に入れる。
重要度に応じて設定された利用率の閾値が設定できる。CRITICALは90%までだけどCRITICAL_PLUSは95%までみたいな。
エグゼキュータ平均負荷
利用率を%だけで見てしまうと1回負荷のかかる処理が来ただけで毎回拒否が起こることになる。
そういった一瞬の負荷(スパイク)を均すために平滑化を行って負荷を判断する。
CPUだけでなくバックエンドに合わせてメモリなど必要な利用率を追加でプラグインとして追加できる
21.6 過負荷によるエラーへの対応
バックエンドから過負荷のエラーが返ってきた時にクライアントがどうするか。
過負荷状況
- データセンター内のバックエンドタスクの大部分が過負荷になっている
ロードバランシングがうまく行っているなら生じない。 - データセンター内のバックエンドタスクの一部だけが過負荷になっている
ロードバランシングが不完全であることから引き起こされる。一時的な偏りなので空いてるタスクがあるはず。
大部分がエラーの場合は、リトライはせずに呼び出し元までエラーを返すべき。
一部が過負荷になる場合はすぐリトライする。これは頻繁に生じる。
データセンター間のロードバランシングシステム
通常は最も近いデータセンターに投げる。この場合はリクエストをリトライすることで数回ネットワークの往復が発生してレイテンシが加わっても無視できる程度になる。
Googleはロードバランシングがいい感じに空いているバックエンドに振り分けるのでリトライ専用のロジックが存在しない。これを入れると正常形とリトライを考慮する複雑なAPIになってしまう。
新規リクエストとリトライリクエストは区別されないのでまたロードバランシングからやり直して適切なタスクを見つけるだけ。
21.6.1 リトライの判断
リクエストごとのリトライバジェット
3回が上限
3回失敗したら大体はデータセンター全体が過負荷だろうという判断
クライアントごとのリトライバジェット
リクエストに対するリトライ数を追跡
例:データセンターが大部分のリクエストを拒否しているとする。この場合リトライを行うので合計のリクエスト回数は単位時間あたり3倍にもなる(リクエストごとのリトライバジェットのおかげで3倍で抑えられている)。これによるリクエストの拒否コストも増える。ここでクライアントごとのリトライバジェットがあれば、リトライの比率は10%となるので1.1倍で済む。
リクエストにリクエストの試行回数カウンター(0~2)を含める
2になるとリクエストごとのリトライバジェットでリトライが停止する。
バックエンドはこの値のヒストグラムを履歴として持っていて、それにより大量のリトライが発生していることがわかれば大体他のバックエンドタスクも過負荷になっているはずと判断し、通常の過負荷エラーではなく、リトライ禁止も含めてエラーで返す。
21.2の図
普通だとバックエンドBはDBフロントエンドに3回投げて失敗したら、バックエンドAに戻ってバックエンドAが後2回バックエンドBに対してリトライする。そうするとまたバックエンドBは3回までDBフロントエンドにリトライする。
このようにリトライの組合せ爆発が起きるのでリトライ禁止のエラーを返して爆発を止める。
21.7 接続によって生じる負荷
私たちは受信したリクエストから直接的に生じるバックエンドでの負荷(処理する側の負荷)だけしか考慮しないことがある。
アクティブではないクライアントは定期的にヘルスチェックを実行しなければならない。
設定された期間接続がアイドル状態なら、TCP接続を切りUDPでのヘルスチェックに切り替える。(通信コスト減)
アイドル状態のクライアントが大量にあると、接続のヘルスチェックのリソースが実際のリクエスト処理に必要なリソースを上回り本末転倒になる。
これは接続のパラメータを注意深くチューニングしたり(ヘルスチェックの頻度を大きく下げるなど)、接続の生成と破棄を動的に行うといったアプローチで改善できる。
一斉に新しい接続要求があった場合の処理
一度に莫大な数のワーカークライアントタスクを生成するような巨大バッジジョブで発生。
大量の接続のネゴシエーションと管理を一斉に行う必要があるのでバックエンドのグループが容易に過負荷に陥る。
- 対策1:負荷をデータセンター間のロードバランシングアルゴリズムに公開する
実質的にリクエストから生じる負荷はキャパシティに余裕があるデータセンターへ送られる - 対策2:バッチクライアントジョブは独立したバッチプロキシーだけを使用する
バッチプロキシーはクライアントとバックエンドの間で受け渡しをするだけ。
これがないと直接バックエンドに投げつけることになるので接続負荷がバックエンドにかかって処理に使えるメモリやCPUが減ってしまう。
バッチプロキシーがヒューズとなってバックエンドを守ってくれる。
また、バッチプロキシーがバランシングしたりすることでバックエンドに対する接続数を下げられたりもする。
21.8 まとめ
- タスクが過負荷に対して保護されていることが重要
キャパが100で1000のトラフィックが来ても壊れずに100ずつ処理し続けるべき - 過負荷だからと言ってバックエンドをシャットダウンしてはいけない
無理になるところできちんと拒否するべき
過負荷対策の例としてはこんなのも