私が完璧なロードバランサーを見つけることに興味を持ったのは、職場でデータベースにアクセスするサービスが不安定な動作を示すという一連のインシデントが発生したときでした。私たちはまずデータベースの安定化に注力しましたが、データベースの複数の読み取りエンドポイント間でリクエストをより効果的にロードバランシングできていれば、サービスへの影響を大幅に軽減できたはずだと私は考えました。
技術の現状を調べれば調べるほど、この問題が未解決であることに驚きました。ロードバランサーはたくさんありますが、その多くは1つか2つの障害モードにしか対応しないアルゴリズムを使用しています。
この投稿では、高可用性のためのロードバランシングの現状について私が学んだこと、最も一般的なツールの問題となる動きについての私の理解、そしてここから私たちが進むべき方向性について述べます。
(免責事項:これは主に思考実験と何気ない観察に基づくもので、関連する学術文献を見つけることはあまりうまくいきませんでした。ご批判は大歓迎です!)
要約
ご理解いただきたいポイント
- サーバーの健全性は、クラスタの健全性の観点でしたか理解できない
- アクティブヘルスチェックを使用してサーバーを除外するロードバランサーは、ヘルスチェックが実際のトラフィックの健全性を適切に反映できない場合、必要以上にトラフィックを失う可能性がある。
- 実際のトラフィックのパッシブモニタリングをすることで、レイテンシーとエラー率の指標を適正な負荷分散に役立てることができる。
- サーバーの状態のわずかな違いがロードバランシングに大きな違いをもたらす場合、システムは予測不可能に不安定な振動を起こす可能性がある。
- ランダムネスは、モビングやその他の望ましくない相関行動を抑制することができる。
基礎知識
用語について簡単に説明しておきます。:この投稿では、 サーバーと通信するクライアント を「コネクション」や「ノード」などの用語を使わずに表現します。あるソフトウェアがクライアントとしてもサーバーとして、同時にあるいは同じリクエストフローで機能することもあるが、私が説明したシナリオでは、アプリケーションサーバーはデータベースサーバーのクライアントであり、このクライアントとサーバーの関係に焦点を当てます。
つまり、一般的なケースでは、N個のクライアントがM個のサーバーと通信していることになります:
また、リクエストの詳細については無視することにします。単純化するために、クライアントのリクエストはオプションではなく、フォールバックも不可能であるとします。コールに失敗すると、クライアントはサービスの低下を経験することになります。
そこで大きな疑問となるのは:クライアントがリクエストを受け取ったとき、どのようにサーバーを選ぶべきか?
(私が注目しているのはリクエストであって、安定したストリームやトラフィックのバースト、あるいはさまざまな間隔でリクエストを運ぶ可能性のある長寿命のコネクションではないことに注意してください。また、リクエストごとにコネクションが作られるか、コネクションを再利用するかは、全体的な結論には特に関係ないはずです。)
補足:CLIENT-SIDE VS. DEDICATED
一般的に「クライアントサイド・ロードバランシング」と呼ばれるものです。(この記事の用語ではロードバランサーもクライアントと呼んでますが・・・)。なぜクライアントにこの作業をさせるのでしょうか?すべてのサーバーをDedicated Load Balancerの後ろに置くのはよくあることです。
しかし、Dedicated Load Balancerノードが1つしかない場合、単一障害点を抱えることになります。そのため、少なくとも3つのノードを立てるのが一般的です。しかし、クライアントはどのロードバランサーと通信するかを選択する必要があり、各ロードバランサーノードは各リクエストをどのサーバーに送るかを選択する必要があることに注意してください!これでは問題は解消するどころか、単に問題を2倍にしているだけです。(問題は2つあります。)
Dedicated Load Balancerが悪いと言っているわけではありません。どのロードバランサーと通信するかという問題について、従来はDNSロードバランシングで解決されてきました。しかし、DNSロードバランサーは特定の障害モードの陥る可能性があり、一般的にクライアントサイドのロードバランサーよりも柔軟性に欠けるため、この問題を回避することはできません。
価値
では、ロードバランサーの何を重視するのか?何のために最適化するのか?
私たちのニーズによって、順番はある:
- サーバーやネットワークの障害がサービス全体の可用性に与える影響を軽減する。
- サービス遅延を低く抑える
- サーバー間の負荷を均等に分散
- 他のサーバーに余裕がある場合は、サーバーに過剰なストレスを与えないこと。
- 予測可能性:サービスのヘッドルームを確認しやすい
- スプレッド荷重 むらなく サーバーの容量が時間やサーバーによって異なる場合(均等配分ではなく、公平配分)
- サーバー起動直後の急激なスパイクや大量のトラフィックは、サーバーをウォームアップする時間を与えない可能性があります。同じトラフィックレベルまで徐々に増加させるのがちょうどいいかもしれません。
- アップデートのインストールなど、サービス以外のCPU負荷は、1台のサーバーで利用可能なCPUの量を減少させる可能性がある。
ナイーブ・ソリューション
すべてを解決しようとする前に、いくつかの単純な解決策を見てみましょう。すべてがうまくいっているとき、どのようにリクエストを均等に分散しますか?
- ラウンドロビン
- クライアントがサーバーを巡回
- 均等な分散を保証
- ランダム・セレクション
- 状態(コーディネーション/CPUのトレードオフ)を追跡することなく、統計的に均等な分散に近づける
- 静的な選択
- 各クライアントは、すべてのリクエストに対して1つのサーバーを選択する。
- DNSロードバランシングはこれを効果的に次のようなことを行います:
クライアントはサービスのドメイン名を1つ以上のアドレスに解決し、クライアントのネットワークスタックはその1つを選んでキャッシュします。ほとんどのDedicated Load Balancerでは、この方法で受信トラフィックを分散します。クライアントは複数のサーバーが存在していることを知る必要はありません。 - ランダムのようなもので、1) DNSのTTLが尊重され、2) クライアントの数がサーバーの数よりかなり多い(リクエストレートが同程度の)場合は問題なく動作する。
また、このような構成でサーバーの1台がダウンしたらどうなるだろうか?サーバーが3台あれば、3回に1回はリクエストが失敗することになる。成功率67%というのはかなり悪い。 (「9」すら1つもない!)完璧なロードバランサーと残り2台のサーバーに十分なキャパシティがあると仮定した場合、このシナリオで可能な最高の成功率は100%である。どうすればそこに到達できるだろうか?
健康の定義
通常の解決策はヘルスチェックである。ヘルスチェックはロードバランサが特定のサーバやネットワークの障害を検知し、 チェックに失敗したサーバにリクエストを送らないようにするものです。
一般的に、各サーバーがどの程度 "健全 "なのか、それがどういう意味であれ、知りたい:「このサーバーは、このリクエストを送ったら悪い応答をしそうですか?より高いレベルの質問もある:「このサーバーは、もっとトラフィックを送ったら不健康になりそうですか?(別の言い方をすれば、不健全さのあるケースは負荷に依存するかもしれないが、あるケースは負荷に依存しない。この違いを知ることは、不健全さが観察されたときにどのようにトラフィックをルーティングするかを予測するために不可欠である。
つまり大雑把に言えば、"健康 "とは予測のために外的状態をモデル化する方法なのだ。しかし、何をもって不健康とするのか?そしてそれをどのように測定するのか?
視点選び
詳細を説明する前に、2つの異なる視点があることに注意する必要がある:
- サーバーの本質的な健全性:サーバー・アプリケーションが動作しているかどうか、応答しているかどうか、それ自身の依存関係のすべてと対話できるかどうか、そして深刻なリソース競合状態に陥っていないかどうか。
- クライアントが観測したサーバーの健全性:サーバーの健全性だけでなく、サーバーのホストの健全性、介在するネットワークの健全性、さらにはクライアントがサーバーに対して有効なアドレスで設定されているかどうか。
実用的な観点からは、クライアントがサーバーに到達できなければ、サーバーの本質的な健全性は重要ではありません。従って、クライアントから観察されるサーバーの健全性に注目することになる。しかし、ここには微妙な点があります:サーバーへのリクエストレートが増加すると、ネットワークやホストではなく、サーバーアプリケーションがボトルネックになる可能性が高くなります。もしサーバーからの遅延や障害率が増加し始めたら、それはサーバーがリクエストの負荷に苦しんでいることを意味するかもしれません。あるいは、サーバーには十分な容量があり、クライアントは負荷に依存しない一過性のネットワーク問題を観測しているだけかもしれない。もしそうであれば、トラフィックの負荷を増やしても状況は変わらないだろう。一般的なケースでは、このようなケースを区別することは難しいため、一般的にはクライアントの観測結果を健全性の基準として使用します。
健康の尺度とは何か?
では、クライアントは、サーバーが発信しているコールから、サーバーの健康状態について何を知ることができるのだろうか?
- 待ち時間:応答が返ってくるまでの時間。これはさらに細かく分けることができる:接続確立時間、レスポンスの最初のバイトまでの時間、レスポンスが完了するまでの時間、最小値、平均値、最大値、さまざまなパーセンタイル。最小、平均、最大、さまざまなパーセンタイル。これは、ネットワーク条件とサーバーの負荷(負荷に依存しないソースと負荷に依存するソース)をそれぞれ混同していることに注意してください(ほとんどの場合)。
- 失敗率:リクエストの何割が失敗に終わるか。(失敗の意味についてもう少し詳しく)
- 同時実行:現在飛行中のリクエスト数はいくつですか?これはサーバとクライアントの動作による影響を混同しています。サーバがバックアップされているため、あるいはクライアントが何らかの理由でより多くのリクエストの割合を与えることにしたために、あるサーバに対してより多くのインフライトリクエストがあるかもしれません。
- キューのサイズ:クライアントが統一されたキューではなく、サーバごとにキューを管理している場合、より長いキューは、健康状態が悪いか、あるいは(やはり)クライアントによる負荷が不均等であることの指標となるかもしれない。
キューサイズと同時リクエスト数では、すべての測定値がそれ自体の健全性を 示すわけではなく、負荷の指標にもなりうることがわかります。これらは直接比較することはできませんが、クライアントはおそらく、より健全で負荷の少ないサーバにより多くのリクエストを与えたいと思うでしょう。したがって、これらの測定基準は、待ち時間や故障率のような、より本質的なものと一緒に使うことができます。
これらはすべて、クライアントの視点から測定されたものである。サーバーに利用率を自己申告させることも可能だが、この記事ではほとんど取り上げない。
これらはすべて、異なる時間間隔にわたって測定することもできる:直近の値、スライディングウィンドウ(またはローリングバケット)、減衰平均、またはこれらのいくつかの組み合わせ。
失敗の定義
これらの健全性指標の中で、おそらく最も重要なのは失敗率である:ほとんどのユースケースにおいて、呼び出し元はどんな種類の失敗よりも、むしろ遅い成功を得たいだろう。しかし、失敗にはさまざまな種類があり、それらはサーバーの状態について異なることを意味することがある。
通話がタイムアウトする場合、ネットワークやルーティングの問題で遅延が大き くなっているか、サーバーに大きな負荷がかかっている可能性がある。しかし、通話が高速で失敗する場合は、まったく異なる意味がある:DNSの設定ミス、サーバーの故障、ルート不良などである。高速障害は、負荷に依存する可能性は低い。ただし、サーバーが負荷分散を使用して、高負荷下で意図的に高速障害を起こしている場合は別である。
トランスポートレベルの失敗だけでなく、アプリケーションレベルの失敗を見る 場合、呼を失敗とマークする基準の選択に注意することが重要である。例えば、(タイムアウトなどで)戻れないHTTPコールは明確に失敗であるが、エラーステータスコード(4xxまたは5xx)の整った応答は、サーバーの問題を示さないかもしれない。個々のリクエストがデータ依存の500 Server Errorを引き起こしている可能性がありますが、これはサーバ全体の健全性を表しているわけではありません。しかし、影響を受けるのはその発信者だけであり、それだけでサーバーの健全性を判断するのは賢明ではありません。一方、読み込みのタイムアウトが不正なリクエストに特有である可能性は 低い。
健康保険はどうなる?
これまでは、クライアントがすでに行っているリクエストから、サーバーの 健全性に関する情報を受動的に得る方法について主に述べてきました。もう一つの方法は、能動的なヘルスチェックを使うことです。
AWSのElastic Load Balancer(ELB)のヘルスチェックはその一例だ。ロードバランサーは30秒ごとに各サーバーのHTTPエンドポイントを呼び出すように設定でき、ELBが5xxレスポンスかタイムアウトを2回連続で受け取ると、そのサーバーは通常のリクエストの対象から外される。しかし、ELBはhealthcheckの呼び出しを続け、10回連続で正常な応答があれば、そのサーバーはローテーションに戻されます。
これはヒステリシスを利用することで、ホストがすぐにバタバタと稼働したり停止したりしないようにしていることを示している。(ヒステリシスのよく知られた例は、エアコンのサーモスタットが希望温度付近の「許容範囲」を維持する方法です)。これは一般的なアプローチで、サーバーが常に健康か不健康かのどちらかであり、頻繁に状態が変化しないようなシナリオでは、それなりにうまく機能します。ヘルスチェックと通常のトラフィックの両方に影響を及ぼすような、約40%以下の持続的で低い障害率というあまり一般的でない状況では、デフォルト設定のELBでは、ホストをサービス停止に追い込むほど頻繁に連続した障害が発生することはないでしょう。
ヘルスチェックは、ロードバランサに間違った影響を与えないように注意深く 設計する必要があります。以下は、ヘルスチェックの呼び出しが提供する答えの種類です:
- スモークテスト:現実的な電話を1回かけ、期待通りの応答が返ってくるか確認する。
- 機能的な依存関係のチェック:サーバーはすべての依存関係に呼び出しを行い、どれかが失敗した場合は失敗を返す。
- 空席確認: サーバーが いずれも を呼び出す。
GET /ping
収穫高200 OK
のレスポンスボディを持つ。pong
ヘルスチェックは、可能な限り実際のトラフィックを代表するものであることが重要です。そうでなければ、許容できない偽陽性や偽陰性をもたらすかもしれない。例えば、サーバに多くのAPIルートがあり、そのうちの1つのルートだけが依存関係の失敗によって壊れている場合、そのサーバは健全でしょうか?スモークテストのヘルスチェックがそのルートにしかヒットしない場合、クライアントはそのサーバを完全に壊れていると見なすでしょう。
機能的なチェックはより包括的なものにすることができるが、これは必ずしも良いとは言えない。というのも、オプションの依存関係が1つでもダウンすると、サーバー(またはすべてのサーバー!)がダウンしたとマークされてしまう可能性があるからだ。これは運用監視には便利だが、負荷分散には危険である。そのため、多くの人は単純な可用性チェックだけを設定している。
能動的な健全性チェックは、たとえ時間をかけて追跡されたとしても、サーバーの健全性のバイナリビューを提供するのが一般的である。一方、トラフィックの健全性を受動的に監視することは、少なくともクライアントが リクエストのどの割合が失敗を受け取ったかを知っているため、健全性をスカラー的 に(あるいはさらに微妙に)見ることができます。(もちろん、どちらのタイプのチェックもレイテンシ情報を追跡することができる。これらの区別のいくつかは、故障率メトリクスに対してのみ有効である)。
バイナリー・ヘルスチェックと異常検知
このバイナリビューは、サーバー間の健全性比較ができないため、深刻な問題につながる可能性がある。なぜなら、サーバー間で健全性を比較することができないからである。サーバーは、代表的でないかもしれない単一のコールタイプに基づいて、単に "up "または "down "としてグループ化される。複数のヘルスチェック・コールがあったとしても、APIが拡張され、クライアントのニーズが変化するにつれて、それらがサーバーの健全性を代表し続ける保証はない。しかし、さらに悪いことに、相関性のある障害は、不必要な連鎖的障害につながる可能性がある。これらのシナリオを見てください:
- もし100%のホストがアクティブなヘルスチェックをパスしていれば、理想的なロードバランサは全てのホストにルーティングすべきです。
- 残りのクラスタは間違いなく負荷を処理できるのだから。
- もし10%しか合格していないのなら......すべてのホストにルーティングする。チェックに合格している10%をつぶすよりも、ヘルスチェックが間違っている(あるいは無関係である)ことに賭けた方がいい。
- もし0%なら、すべてのホストにルーティングする。
ホストの通過率がゼロに近づけば近づくほど、ホストの外部の何かに障害があるか、あるいはヘルスチェックに何か問題がある可能性が高くなります。ヘルスチェックがテスト用アカウントに依存していて、そのテスト用アカウントが削除されたとします。あるいは、ある依存関係がダウンしたけれども、ほとんどのリクエストは処理できたとします。それにもかかわらず、すべてのヘルスチェックは失敗します。ELB はすべてのホストをサービス停止にします。
ここから明らかなのは、健全性とは相対的なものだということだ:あるサーバーが近隣のサーバーよりも健全であることは、たとえそのサーバーのすべてに問題があったとしてもありうる。そして、ブーリアンではなくスカラーを使うことで、そのことがわかりやすくなる。
基本的には、ロードバランサーに何らかの簡単な異常検知をさせたい。もしごく一部のサーバーが異常な動作をしていたら、そのサーバーを除外して運用部に警告を送ればいい。ほとんど、あるいはすべてのサーバーの挙動がおかしい場合は?ほんの一握りのサーバーにすべての負荷をかけることで事態を悪化させないようにしましょう。
ここで重要なのは、サーバの健全性をアトミックに評価するのではなく、クラスタ全体を見て評価することだ。私がこれまで見た中で、これに最も近いのはEnvoyのロードバランサで、デフォルトでは "パニックしきい値 "があり、50%以上のホストのヘルスチェックが失敗した場合、全てのホストのサービスを継続するようになっている。もしあなたがロードバランサーでヘルスチェックを使っているなら、このようなアプローチを使うことを検討してください。
30〜70%のサーバーがチェックに失敗している場合、どうすればよいかという質問については割愛したことにお気づきだろうか。このような状況は、本当の障害を示しているのかもしれないし、負荷に依存しているのかもしれないし、負荷に依存していないのかもしれない。ロードバランサーがどちらの状況に当てはまるかを知ることは、たとえそれを知るために巧妙なA/Bトラフィック負荷実験をする気があったとしても、不可能だと思う。さらに悪いことに、比較的少数のサーバーにすべての負荷をかけると、それらのサーバーがダウンしてしまうかもしれない。ロードシェッディングの他に、この状況でできることはあまりない。そして、その中間の範囲内にある場合、それらのサーバーをサービスし続ける設計も、サービスを停止する設計も、どちらも非難できる自信がない。
サイドバー:飢餓の罠
これらのアクティブ・アプローチとパッシブ・アプローチのもう一つの違いは、アクティブ・チェックでは、サーバーの健全性に関する情報は、トラフィックの速度に関係なく、一定の速度で更新されることである。これは、トラフィックが低速の場合には長所となり、高速の場合には短所となる。(1秒間に10,000のリクエストがある場合、5秒間の障害は長い時間になる)。これに対してパッシブチェックでは、障害検出の速度はリクエスト率に比例する。
しかし、純粋なパッシブヘルスチェックには大きな欠点がある。サーバーがダウンすると、ロードバランサーはすぐにそのサーバーをサービスから外す。トラフィックがなくなるということは、クライアントが見るサーバーの健全性は変わらないということだ:それは永遠にゼロのままなのだ。
もちろん、これに対処する方法はあり、クライアントのスタートアップや、クライアントのサーバーリストにある1つのサーバーを置き換えるなど、他のデータなしのエッジケースにも対処するものもある。パッシブチェックを使用する場合は、これらすべてに特別に対処する必要があります。
健康総括
以上を総括する:
- トラフィックの受動的な監視は、能動的なチェックよりも、より包括的で微妙な健康状態の把握を可能にする。
- 健康を評価する軸は複数ある
- サーバーの健全性は、クラスタとの相対的な関係でしか把握できない。
しかし、その情報を使って何をするのか?低遅延、最小故障、均等な負荷分散という目標を達成するために、これらの実数値をどのように組み合わせればいいのだろうか?
最初に障害モードの一群について脱線し、次に健康状態を考慮したロードバランシングの一般的なアプローチについて議論し、最後に考えられる将来の方向性をいくつか挙げたい。
サイドバー:相関関係の微妙な危険性
協調性のない行動は、意外な結果をもたらすことがある。ある大企業のオフィスが従業員に電子メールを送ったとしよう:「本日、第2講堂で全社員を対象にマッサージを行います!いつでも来てください。人々はいつ来ると思いますか?私の予想では、1日のうち数時間は大混雑になるだろう:
- すぐに
- 昼食後
- 帰宅前の昼下がり
このような不均等な分布では、マッサージ・セラピストは時には誰も施術することができず、またある時には、人々があきらめてしまい、後で再挑戦することさえないかもしれないほど長い列ができる。どちらも望ましくない。協調性がまったくないため、協調性がなくても、人々はどういうわけか集団で現れる!このシナリオにおける偶然の相関行動は、ありふれた道具を使って簡単に防ぐことができる:サインアップシートである。(ソフトウェアの世界では、ジョブを受け入れ、自分の都合でスケジューリングし、結果を非同期で返すバッチ処理システムが最も近い類似品だろう)。
APIトラフィックには似たような現象がいくつもあり、しばしば雷鳴轟く群れ問題(thundering herd problem)と呼ばれている。典型的な例は、何百ものアプリケーションノードによって参照されるキャッシュサービスである。キャッシュ・エントリーの有効期限が切れると、アプリケーションは新しいデータで値を再作成する必要があり、そのためには余分な作業と(おそらく)他のサーバーへの余分なネットワーク呼び出しの両方が必要になる。何百ものアプリケーション・ノードが、人気のあるキャッシュ・エントリーが期限切れになるのを同時に観測した場合(なぜなら、すべてのノードがこのデータに対するリクエストを常に受信しているからだ)、すべてのノードが同時に再作成を試み、同時に新鮮なデータを生成するバックエンド・サービスを呼び出すことになる。これは無駄であるばかりでなく(最良のケースでは、1つのアプリ・ノードだけが、キャッシュの寿命ごとに1回だけ、このタスクを実行する)、通常はキャッシュの後ろに保護されているバックエンド・サーバーを押しつぶす可能性さえある。
キャッシュの有効期限切れにおけるカミナリの群れの問題に対する古典的な解決策は、キャッ シュ・エントリーをどこでも同じ瞬間に失効させるのではなく、発信者ごとに確率的に早期 に失効させることである。最も単純なアプローチは、クライアントがキャッシュを参照するたびに、有効期限から小さな乱数を減算するジッターを追加することである。このテクニックの改良版であるXFetchは、可能な限り最後の瞬間までリフレッシュを遅らせるように、ジッターにバイアスをかける。
APIを呼び出す定期的なタスクを多数のユーザーがセットアップした場合、もう一つのよくある問題が発生する。おそらくバックアップサービスの全てのユーザーが、真夜中にバックアップをアップロードするcronジョブをインストールするのでしょう。
ここでも標準的な解決策があります:新規ユーザーをオンボーディングする際に、ユーザーごとにランダムに選択された時間を使って、インストールするための推奨crontabファイルを生成する。バックアップソフト自身がcrontabファイルを書き、最初にインストールするときにランダムな時間を選択すれば、中央の調整ポイントなしでも機能します。(中央のサインアップシートが何らかの理由で使えない場合、同様のアプローチがマッサージのシナリオでも使えることに気づくかもしれません:従業員はそれぞれ空いている時間帯をランダムに選び、自分のスケジュールにとって必ずしも最適な時間帯でなくても、その時間帯に行くのです)
これらの2つの解決策、すなわち、期限切れとスケジューリングのランダム化は、どちらも、協調性がないにもかかわらず相関性のある行動への対抗策として、ランダム性を利用している。これは重要な原則である:ランダム性は相関を抑制する。ロードバランシングに関連するいくつかの課題に取り組むときに、このことが再び出てくるだろう。
また、マッサージのシナリオから、中央の調整ポイントに頼るという代替アプローチも見られる。これは、専用のロードバランサーに強力なサーバーの小さなクラスタを使用する利点の1つである。協調性を高めるもう1つの方法は、サーバーに利用率をそのレスポンスに寄生するメタデータとして自己報告させることである。これは常に可能というわけではないが、サーバーが報告する利用率は、クライアントが他の方法ではアクセスできない集約された情報を与える。これは、クライアント側のロードバランサーに、専用のロードバランサーが持つような、よりグローバルなビューを与える可能性がある。ボーナスとして、サーバーとネットワークの障害を区別するのに役立つかもしれない。
システムダイナミクスのこの側面を念頭に置いて、ロードバランサが健康情報をどのように使うかを見てみよう。
負荷分散における健康状態の利用
ロードバランサーは一般的に、健康情報の利用を2つの関心事に分けている:
- リクエストの候補となるサーバーを決定する。
- リクエストごとにどの候補者を選ぶかを決める
古典的なアプローチでは、これらは2つの全く別の階層として扱われる。例えばAWSのELB、ALB、NLBは負荷を分散するために様々なアルゴリズム(ランダム、ラウンドロビン、決定論的ランダム、最少アウトスタンディング)を使用するが、その選択プロセスに参加できるサーバーを決定するために、主にアクティブヘルスチェックに基づいた別のメカニズムがある。(ドキュメントによると、NLBはサーバーを追い出すかどうかを決定するためにパッシブ・モニタリングも使用するようだが、詳細は不明だ)。
ランダム、ラウンドロビン、および決定論的ランダム(フローハッシュなど)は、健全性を完全に無視する:サーバーはインかアウトかのどちらかである。一方、least-outstandingアルゴリズムは、受動的な健全性メトリックを使用する。(このサーバ選択アルゴリズムでさえ、クラスタからサーバを取り出すためのアクティブチェックとは完全に分離されていることに注意してください)。Least-outstanding("リクエストの同時実行数が最も少ないサーバーを選ぶ")は、 リクエストを割り当てるために受動的な健全性メトリクスを使ういくつかの アプローチの一つで、それぞれ先に述べたメトリクスの一つを最適化することに 基づいています:遅延、故障率、同時実行数、キューサイズ。
セレクション・アルゴリズム
ロードバランシングの選択アルゴリズムの中には、メトリックの値が最も良いサーバーを選択するものがあります。表面上、これは理にかなっています:これは表面的には理にかなっています。しかし、これは私がモビングと呼ぶものにつながる可能性があります:レイテンシが健全性の指標として選択され、あるサーバーが他のサーバーよりわずかに低いレイテンシを示す場合(すべてのクライアントから見て)、すべてのクライアントはすべてのトラフィックをそのサーバーに送る。サーバーが苦しみ始めると、その実効レイテンシは増加し、おそらく別のサーバーがグローバルで最も健全なサーバーの称号を得る。このようなことが周期的に繰り返され、初期の健全性のわずかな違いによって引き起こされることもある。
モビング行動には、システム内のいくつかの欠陥が重なっている:
- レイテンシは遅延ヘルス測定基準である。もし同時実行数(インフライトリクエスト数)が代わりに使われた場合、同時実行数測定 値は、より多くのリクエストがサーバーに割り当てられるとすぐにクライアント側で 更新されるので、クライアントは暴徒化しない。遅延測定は、たとえ減衰があっても、望ましくない振動や共振を引き起こす可能性がある。
- クライアントは状況を俯瞰していないため、協調性のない行動をとり、好ましくない相関行動を起こす。
- サーバーの健康状態のわずかな違いが、負荷分散の動作に大きな違いを生む。後者から前者へのフィードバックがあるため、これは初期条件に非常に敏感なカオスシステムの1つの説明に合致する。
私が考える救済策:
- 可能であれば、高速な健全性メトリクスを使用してください。実際、非常に一般的なロードバランシングの選択アルゴリズムは、 飛行中のリクエストが最も少ないサーバーにすべてのリクエストを送ることです。(コネクション指向かリクエスト指向かによって、least-connectionsあるいはleast-outstandingと呼ばれることもあります。)対照的に、pick-least-latencyアルゴリズムは見たことがない。
- 少数のサーバーで専用のロードバランサーを使用したり、サーバーから報告された利用率を取り入れたりして)状況を大局的に把握しようと試みるか、ランダム性を使用して望ましくない相関動作を抑制する。
- ほぼ同じ入力に対して、ほぼ同じ動作をするアルゴリズムを使う。連続的に変化する動作である必要はありませんが、ランダム性を使ってそれに近いものを実現することができます。
The Power of Two Random Choicesという論文で説明されているように、pick-the-bestに代わる一般的な方法としてtwo-choiceというものがある。この論文では、リソース割り当ての一般的なアプローチ(ロードバランサーに特化したものでもなければ、ロードバランサーを中心としたものでもないが、確かに関連している)について論じている。このアプローチでは、2つの候補が選択され、健康状態の良い方が使用されます。これは、すべてのサーバーの長期的な健全性が同じ値に近づいたときに、均等な分散に近づきますが、健全性にわずかな持続的な差があるだけでも、負荷分散のバランスを大きく崩す可能性があります。フィードバックのない単純なシミュレーションで、このことを説明します:
;; Select the index of one of N servers with health ranging ;; from 1000 to 1000-N, +/-N (defn selecttc [n] (let [spread n ;; top and bottom health ranges overlap by ~half ;; Compute health of a server, by index health (fn [i] (+ (- 1000 i spread) (* 2 spread (rand)))) ;; Randomly choose two servers, without replacement [i1 i2] (take 2 (shuffle (range n)))] ;; Pick the index of the healthier server (if (< (health i1) (health i2)) i2 i1)))
;; 5つのホストで10,000,000回の試行を行い、各ホストのインデックスが選択された回数を報告する ;; (sort-by key (frequencies (repeatedly 10000000 #(selecttc 5))) ;;= ([0 2849521] [1 2435167] [2 2001078] [3 1566792] [4 1147442])
増加した負荷が健全性の指標に影響を与えなかったと仮定すると、ホストが健全性のおおよそのランク付けさえしていれば、健全なホストとそうでないホストの間でリクエスト負荷に2.5倍の差が生じることになります。ホスト0の健全性の範囲は995-1005で、ホスト4のそれは991-1001であることに注意してください。絶対値では1-2%しか違わないにもかかわらず、このわずかな偏りは負荷の大きな不均衡に拡大されます。
二者択一はモビングを減少させるが(そして偏りが存在しない場合にはかなりうまくいくが、フィードバックが発生する場合にはそうなる可能性がある)、これは遅延ヘルスメトリクスで使用する適切な選択メカニズムではないことは明らかである。さらに、この論文は、同一のオプションのセットを与えられたときの最大負荷削減に焦点を当てているように見えるが、これはヘルスを意識したロードバランサーの場合ではない。
一方、二者択一は、フィードバックが瞬時であり、かつ自己修正可能であるため、最小アウトスタンディングとうまく機能する。最小値は、潜在的に小さく、量子化された値を持つという点で、それ自体が挑戦的である。接続が1つしか開いていないサーバーは、2つしか開いていないサーバーの2倍健全なのだろうか?ゼロと1ではどうだろう?もし各クライアントが1つのコネクションしかオープンしていなくても、300のクライアントがあれば、それらはまとめてその1つのサーバを攻撃するかもしれません。ランダム化された二者択一は、least-outstandingの小さな離散値から生じる暴徒化に対する自然な解毒剤となります。
非常に有望なオプションは、まだ学術的なものではあるが、加重ランダム選択である。各サーバーにはその健全性指標から導き出された重みが割り当てられ、その重みに従ってサーバーが選ばれる。例えば、サーバーの重みが7、3、1であれば、それぞれ70%、30%、10%の確率で毎回選択されることになる。このアルゴリズムの使用には、飢餓の罠を避けるための注意が必要で、ウェイトの導出には、他のサーバーの90%の健全性を持つサーバーが、おそらく相対的に20%だけ、大幅に減少したウェイトを受け取るように、よく選ばれた非線形関数を使用する必要がある。仕事では、このアプローチを実験している。ローカル統合の実験の後、私はこのアプローチに大きな期待を寄せている。もしうまくいったら、新しいロードバランシングアルゴリズムに関する将来の投稿でもっと詳しく説明することになるだろう。
健康指標の組み合わせ
複数の健康指標をどう使うか、という問題を私はずっと先延ばしにしてきた。私の考えでは、これが最も難しい部分であり、この問題全体の核心に迫るものだ:アプリケーションの健全性をどのように定義するか?
レイテンシー、故障率、同時実行性をトラッキングしているとしよう。これらをどのように組み合わせますか?故障率5%はレイテンシが10倍になるのと同じくらい悪いのか?(100倍?)他のサーバーがレイテンシーの急上昇を示しているときに、90%利用可能なサーバーでチャンスをつかむ方がいいでしょうか?つの一般的な戦略が思い浮かぶ。
各指標の許容範囲のしきい値を定義し、許容できる故障率のサーバーだけから選ぶ、もし何もなければ、許容できるレイテンシのサーバーから選ぶ、といった段階的なアプローチをとるかもしれない。もしかしたら、許容可能なプールが小さすぎる場合、次の階層からのサーバーも考慮されるように、スピルオーバーしきい値を定義しているかもしれない。(この考え方はEnvoyの優先度レベルに似ている)。
あるいは、ある連続関数に従ってメトリクスを組み合わせる、マージされたメトリクスを使うこともできます。おそらく、いくつかの指標をより重視することになるでしょう。私は現在、各健康指標の[0,1]重み係数を導き出し、それらを掛け合わせ、より高い乗数(2乗または3乗)にして、より重みを与えることを実験しています。(非常に大きな累乗を使えば、マージされたメトリクスのコンバイナーを使用しながらでも、段階的なアプローチのようなものを実装できるのではないかと思っています)。
また、これらのメトリクスがどのように変化し、サーバーと接続の健全性をより高度にモデリングすることによって、どのような利点が得られる可能性があるかを考えることも価値がある。あるサーバーが悪い状態に陥り、非常に速く障害応答を吐き出している場合を考えてみましょう。健全性の測定基準がレイテンシだけである場合、このサーバーはクラスタ内で最も健全であるように見えるため、より多くの トラフィックを受け取ることになる。高速であることが常に健全であるとは限らない!構成にもよりますが、マージされたアプローチではこの不正なサーバーへのトラフィックを十分に抑制することができるかもしれませんし、低い障害率を優先する階層化されたアプローチでは完全に排除することができるでしょう。
一般的に、レイテンシーと故障率は、明白ではない方法で互いに結びついている。失敗を素早く吐き出す」シナリオの他に、タイムアウトと非タイムアウトの失敗の問題もある。待ち時間が長い状況では、クライアントはタイムアウトエラーを何度も発生させる。これらは、それ自体が「失敗」なのでしょうか、それとも単にレイテンシが過度に高い応答なのでしょうか?これらはレイテンシの指標、失敗率の指標、あるいはその両方に影響するのでしょうか?不良DNSレコードやその他の高速接続障害による失敗と比較してください。私の推奨は、成功した場合、またはJavaのSocketTimeoutExceptionや同様のもののようなタイムアウトを示すことが分かっている失敗の場合のみ、レイテンシの数値を記録することである。(同僚は、レイテンシの平均が悪くなる場合には、失敗時のレイテンシ値のみを記録するという選択肢を提案している)。
ライフサイクル
上記のほとんどは、クライアントが静的なサーバーの集合と会話していることを想定している。しかし、サーバーは1つずつ、あるいは大きなグループで入れ替わる。新しいサーバーがクラスタに追加された場合、ロードバランサーはすぐにそのサーバーにトラフィックをフルシェアでぶつけるのではなく、ある程度の期間をかけてゆっくりとトラフィックを増やしていく必要がある。このウォームアップ期間によって、サーバーは完全に最適化される:ディスクと命令キャッシュのウォームアップ、Javaのホットスポットの最適化など。HAProxyはこのためにスロースタートを実装している。ウォームアップを越えて、この時間は不確実な時間でもある:クライアントはサーバーとの付き合いがないため、サーバーへの依存を制限することでリスクを抑えることができる。
メトリックを組み合わせたアプローチを使用している場合、サーバ年齢をペスードヘルスメトリックとして使用し、ゼロに近い状態から始めて、1分ほどかけてフルヘルスまで上昇させるのが便利でしょう(正確にゼロから開始するのは、アルゴリズムによっては危険かもしれません。(正確にゼロから開始することは、アルゴリズムによっては危険かもしれません。クライアントは、一度にサーバーのセットが完全に入れ替わったことを知るかもしれませんし、別のクラスタを指すように再設定され、一時的にすべてのサーバーの健全性がゼロであるとみなされるかもしれません)。サーバーリストの総入れ替えを処理するメカニズムであれば、クライアントの起動を処理するのにも十分でしょう。
荷揚げ
ロードシャッディングについては軽く触れただけで、これは、リクエストの負荷が高いサービスが、CPU負荷やその他のリソースの競合を減らすために、一部またはすべてのリクエストに失敗しても非常に迅速に対応しようとするものである。最善の努力だけでは十分でないこともあるし、サービスをスケールアウトできるほど長く存続させる必要があることもある。ロードシャッディングは、今50%のトラフィックに対して失敗を返せば、後で100%のトラフィックにうまく対応できるかもしれない、今すぐすべてのトラフィックを処理しようとすれば、サービスが完全にダウンしてしまうかもしれないという考えを前提としたギャンブルです。しかし、いつ、どれくらいの割合でそれを行うべきか、どうやって判断するのだろうか?
ロードバランサーが十分に負荷分散に優れていれば、Hystrixや並行性制限のようなものを前に置くだけで十分かもしれない。もしロードバランサーが負荷分散に十分な能力があれば、Hystrixや並行性制限のようなものを前に置くだけで十分かもしれない。私がメリットを感じられるのは、一部のサーバーが不健全な場合に、健全なサーバーにかかる追加負荷を管理することだ。健全なサーバーが20%しかない場合、そのサーバーが通常の5倍の負荷を受けるのは妥当だろうか?ロードバランサーは、過剰な負荷の上限を10倍程度に設定し、不健康とマークされた9台のサーバーの「負荷分担」を引き受けるよう、どのサーバーにも決して求めないようにするのが合理的かもしれない。これは実現可能ではあるが、望ましいことだろうか?私にはわからない。オーバーエイジの上限を設定する必要があり、その設定は簡単に古くなる(あるいは、トラフィックの少ない時期などには無関係になる)可能性があるという意味で、完全な適応型ではない。
結論
以上のことから、一般的な高可用性環境における負荷分散のための既存のオプションの多くは、正常な状態や一部のエラー状態での負荷分散にはうまく機能する傾向があるものの、その他の状態では、モビング、障害への不十分な対応、相関性のある劣化状態への過剰反応などのために、さまざまな点で不十分であると私は考えている。
理想的な高可用性ロードバランサーは、その通常の動作のためのアクティブなヘルスチェックを避け、代わりに、現在のインフライトリクエストや、レイテンシや故障率の減衰(またはローリング)メトリクスを含む、様々なヘルスメトリクスをパッシブに追跡する。これらのメトリクスを追跡しているクライアントは、定期的なアクティブヘルスチェックの結果だけを観察しているクライアントよりも、はるかに優れた状態で異常検知を行うことができる。
もちろん、本当に理想的なロードバランサーは完璧な効率性を体現しているはずで、リクエストの負荷が増加しても、すべてのリクエストが可能な限り正常かつ迅速に処理される...システムが理論的な限界に達するまでは。私はこの問題を "あったらいいなと思う問題 "に分類したいが、ロードバランサーがサーバーの障害を外部から隠すことに特に長けているのであれば、監視ツールを見直す必要性が浮き彫りになった。
私が考える未解決の主な問題は、これらの健康指標をどのように組み合わせ、カオス的な挙動やこの投稿で言及したその他の問題を最小限に抑え、なおかつ一般的に適用可能な方法でサーバー選択に使用するかということだ。私は現在、多要素加重ランダム選択に賭けているが、それが実際の世界でどのように機能するかはまだわからない。