負荷分散の基礎
このドキュメントは、HAProxyを知らない方と、古いバージョンを知っていて改めて理解したい方のための入門です。主な目的は、HAProxyが探している製品かどうかを判断するために必要な情報を提供することです。上級ユーザーにも、知らなかった新機能によって、考えていた構想の実現につながる情報が見つかるかもしれません。サイジングに関する情報、製品のライフサイクルの説明、一部の機能が重なる製品との比較も提供します。
このドキュメントには設定方法の説明やヒントはありませんが、関連ドキュメントの場所を説明します。ガイドは、HAProxyのサイドバーで、トピックごとのページを単一階層で順に並べています。
負荷分散とは、複数のコンポーネントをまとめ、エンドユーザーの介入なしに、拡張可能な方法で、各コンポーネント単体を上回る総処理能力を得ることです。これにより、1つのコンポーネントが1つの操作を実行する時間内に、より多くの操作を同時に実行できます。ただし、個々の操作は依然として一度に1つのコンポーネントで処理されるため、負荷分散なしの場合より速くはなりません。すべてのコンポーネントを活用して負荷分散の利点を十分に得るには、少なくとも利用可能なコンポーネント数と同じ数の操作と、効率的な負荷分散の仕組みが常に必要です。よい例が高速道路の車線数です。個々の車の速度を上げなくても、同じ時間内に通過できる車の数を増やせます。
負荷分散の例:
- マルチプロセッサーシステムのプロセススケジューリング
- リンクの負荷分散(EtherChannel、Bondingなど)
- IPアドレスの負荷分散(ECMP、DNSラウンドロビンなど)
- サーバーの負荷分散(ロードバランサーによるもの)
負荷分散を行う仕組みやコンポーネントをロードバランサーと呼びます。Web環境では「ネットワークロードバランサー」と呼ばれますが、この用途が負荷分散として最も広く知られているため、単に「ロードバランサー」と呼ぶほうが一般的です。
ロードバランサーは、次のレベルで動作できます。
-
リンクレベル:リンクの負荷分散と呼び、パケットを送るネットワークリンクを選択します。
-
ネットワークレベル:ネットワークの負荷分散と呼び、一連のパケットが通る経路を選択します。
-
サーバーレベル:サーバーの負荷分散と呼び、接続やリクエストを処理するサーバーを決定します。
異なるニーズに対応する2つの技術があり、一部は重なります。どちらの場合も、負荷分散はトラフィックを本来の流れから別の経路へ振り向ける操作であり、すべてのルーティング判断の間で必要な整合性を保つため、常に最低限の注意が必要であることを忘れないでください。
第1の技術はパケットレベルで動作し、パケットをおおむね個別に処理します。入力パケットと出力パケットは1対1で対応するため、通常のネットワークスニファーでロードバランサーの両側のトラフィックを追跡できます。この技術は非常に低コストかつ極めて高速にできます。ECMPを行うスイッチのように、通常はハードウェア(ASIC)で実装され、回線速度に達する性能を得られます。一般にはステートレスですが、パケットが属するセッションを考慮するステートフルな実装もあり、layer4-LBまたはL4と呼ばれます。パケットを変更していなければ、DSR(direct server return、戻りのトラフィックがLBを再び通過しない方式)をサポートする場合もありますが、内容はほぼ認識できません。この技術はネットワークレベルの負荷分散に非常に適していますが、高速でごく基本的なサーバーの負荷分散にも使われることがあります。
第2の技術はセッションの内容を扱います。入力ストリームを再構築し、全体として処理する必要があります。内容を変更することもでき、出力ストリームは新しいパケットへと分割されます。このため、通常はプロキシが処理を行い、レイヤー7ロードバランサー、またはL7と呼ばれることがよくあります。両側に別々の接続が存在し、入力パケットと出力パケットの大きさや個数には対応関係がありません。クライアントとサーバーが同じプロトコルを使う必要もありません(IPv4とIPv6、平文とSSLなど)。処理は常にステートフルで、戻りのトラフィックはロードバランサーを通過しなければなりません。追加処理にはコストがかかるため、特に小さなパケットでは、常に回線速度を達成できるとは限りません。一方、幅広い機能を実現でき、ハードウェアアプライアンスに組み込まれていても、一般には純粋なソフトウェアで実装されます。この技術はサーバーの負荷分散に非常に適しています。
パケットベースのロードバランサーは通常、カットスルーモードで配置されます。つまり、通常のトラフィック経路上に設置し、設定に従ってトラフィックを振り分けます。戻りのトラフィックは、必ずしもロードバランサーを通る必要はありません。正しい宛先へ送るために、ネットワークの宛先アドレスを変更する場合があります。この場合、戻りのトラフィックは必ずロードバランサーを通過しなければなりません。経路の都合でそれができない場合、ロードバランサーはパケットの送信元アドレスも自身のアドレスに置き換え、戻りのトラフィックを強制的に自身へ通すことができます。
プロキシベースのロードバランサーは、独自のIPアドレスとポートを持つサーバーとして、構成の変更なしに配置されます。クライアントがサーバーへ直接接続せず、ロードバランサーのIPアドレスへ正しく接続するように、アプリケーションの調整が必要な場合もあります。そのために、ロードバランサーがサーバーのレスポンスの一部を調整する必要がある場合もあります(HTTPリダイレクトで使われるHTTPのLocationヘッダーフィールドなど)。プロキシベースのロードバランサーの中には、自身が所有しないアドレス宛てのトラフィックを捕捉し、サーバーへの接続時にクライアントのアドレスを送信元として使えるものがあります。これにより、パケットベースのロードバランサーとよく似たカットスルーモードで、通常のルーターやファイアウォールのように配置できます。パケットモードとプロキシモードを併せ持つ製品では、特に重宝されます。ただし、この場合もDSRは不可能であり、戻りのトラフィックはロードバランサーへ戻す必要があります。
拡張性の高い階層構成としては、前段のルーターが負荷分散された複数のリンクからトラフィックを受け取り、ECMPで第1層にある複数のステートフルなパケットベースのロードバランサー(L4)へ分散する方法があります。これらのL4ロードバランサーは、さらに多数のプロキシベースのロードバランサー(L7)へトラフィックを渡します。これらは内容を解析し、最終的にどのサーバーが受信するかを決定する必要があります。
コンポーネント数とトラフィックの経路の選択肢が増えるほど、障害のリスクは高まります。非常に大きな環境では、故障したいくつかのコンポーネントを常に修理・交換していることさえ普通です。スタック全体の健全性を把握せずに負荷分散を行うと、可用性は大きく低下します。そのため、適切なロードバランサーは、トラフィックを送ろうとするコンポーネントが稼働中で到達可能かを確認し、故障したものへの送信を停止します。これを実現するには、いくつかの方法があります。
最も一般的なのは、定期的にプローブを送信して、コンポーネントが動作していることを確認する方法です。このプローブを「ヘルスチェック」と呼びます。チェックは、対処したい種類の障害を検出できるものでなければなりません。たとえば、pingによるチェックでは、Webサーバーがクラッシュしてポートでリッスンしなくなっても検出できません。ポートへの接続なら検出でき、さらに高度なリクエストであれば、サーバーが正常に動作し、依存するデータベースにも到達できることを確認できます。ヘルスチェックでは、偶発的な測定誤差を考慮して数回の再試行を行うことがよくあります。チェック間隔は、エラー発生後に故障したコンポーネントが長時間使われないよう、十分短くしなければなりません。
別の方法は、宛先へ送った本番トラフィックをサンプリングし、正しく処理されるかを観測して、不適切なレスポンスを返すコンポーネントを除外することです。ただし、本番トラフィックの一部を犠牲にする必要があり、常に許容されるわけではありません。この2つを組み合わせると、障害の検出には両方を使い、復旧の検出にはヘルスチェックだけを使うことで、それぞれの利点を得られます。最後の方法は、集中的な状態報告です。中央の監視エージェントが、すべてのコンポーネントの状態をすべてのロードバランサーへ定期的に通知します。これにより、各コンポーネントはインフラストラクチャーの全体像を把握できますが、精度や応答性が低くなる場合もあります。ロードバランサーとサーバーの両方が多い環境に最も適しています。
レイヤー7ロードバランサーには、スティッキネス、または永続性と呼ばれる別の課題もあります。基本となる要件は、同じ送信元(エンドユーザーなど)からの連続した複数のリクエストや接続を、同じ宛先へ送ることです。最もよく知られた例がオンラインストアのショッピングカートです。クリックのたびに新しい接続が作られる場合でも、ユーザーは常に自分のカートを保持するサーバーへ送られなければなりません。内容を認識できれば、リクエスト内の要素から送信先サーバーを特定しやすくなりますが、それだけでは十分でない場合があります。たとえば、送信元アドレスをサーバー選択のキーに使う場合、ハッシュ方式を採用し、アドレスを利用可能なサーバー数で割った結果に基づいて、特定のIPアドレスを常に同じサーバーへ送ることができます。しかし、1台のサーバーが故障すると結果が変わり、すべてのユーザーが突然別のサーバーへ送られてカートを失います。この問題の解決策は、選択した宛先を記憶しておき、同じ訪問者が現れたら、利用可能なサーバー数にかかわらず同じサーバーへ送ることです。情報はロードバランサーのメモリに保存でき、ロードバランサーが複数ある場合はほかのロードバランサーへの複製が必要になることもあります。また、クライアントがリクエストごとにその情報を返せるなら、さまざまな方法でクライアントのメモリに保存することもできます(Cookieの挿入、サブドメインへのリダイレクトなど)。この仕組みには、送信元IPアドレスのように不安定で分布に偏りがある情報に依存せずに済むという利点もあります。実際、これがレイヤー4ではなくレイヤー7のロードバランサーを採用する最も大きな理由です。
Cookie、Hostヘッダーフィールド、URLなどの情報を取り出すため、ロードバランサーはSSL/TLSトラフィックを復号し、場合によってはサーバーへ渡す際に再暗号化する必要があります。この処理は高コストなため、トラフィックの多いインフラストラクチャーでは、ロードバランサーを多数配置する場合があります。
レイヤー7ロードバランサーは、復号、解析、変更、Cookieの照合、送信先サーバーの決定など、トラフィックに複雑な操作をいくつも行うため、実際に問題の原因となる可能性があります。また、単に問題を表面化させただけでも、原因として疑われることがよくあります。調べてみると、サーバーが不安定で定期的に稼働・停止を繰り返していたり、Webページに埋め込まれた固定リンクによってクライアントがロードバランサーを通らず特定のサーバーへ直接接続していたり、高負荷時に応答が極端に遅くなってタイムアウトしていたりすることがよくあります。そのため、ログはレイヤー7の負荷分散で極めて重要です。問題が報告されたら、ロードバランサーが誤った判断をしたか、そうであればなぜかを調べ、再発を防ぐことが重要です。