HAProxyの役割と動作の仕組み
製品を指す場合は"HAProxy"、実行可能プログラム、ソフトウェアパッケージ、プロセスを指す場合は"haproxy"と表記します。ただし、実際にはどちらの表記も両方の意味でよく使われ、H-A-Proxyと発音します。ごく初期には、“haproxy"は"high availability proxy"の略で、名前も2語に分けて書かれていましたが、現在は単に"HAProxy"という名前です。
3.1. HAProxyが担う役割と担わない役割
HAProxyは、次の役割を担います。
-
TCPプロキシ:リッスンソケットでTCP接続を受け付け、サーバーに接続し、両方のソケットを結び付けて双方向にトラフィックを流せます。どちら側でもIPv4、IPv6、さらにUNIXソケットをサポートするため、異なるアドレスファミリー間の変換を簡単に実現できます。
-
HTTPリバースプロキシ(HTTPの用語では「ゲートウェイ」):自身をサーバーとして公開し、リッスン中のTCPソケットで受け付けた接続を通じてHTTPリクエストを受信し、別の接続を使ってサーバーへ渡します。両側でHTTP/1.xとHTTP/2を任意に組み合わせられ、TLS上でALPNを使う場合は、それぞれの側のプロトコルを自動検出することもできます。
-
SSLの終端・開始・オフロード:クライアントからの接続、サーバーへの接続、またはその両方でSSL/TLSを使えます。多くの設定を名前(SNI)ごとに適用でき、再起動せずに実行時に更新できます。この構成は極めて拡張性が高く、数万から数十万の証明書を扱う導入例が報告されています。
-
TCPの正規化:接続はOSによってローカルで終端され、両側に対応関係がないため、不正なパケット、フラグの組み合わせ、ウィンドウ広告、シーケンス番号、不完全な接続(SYNフラッド)などの異常なトラフィックは反対側へ渡されません。これにより、脆弱なTCPスタックをプロトコル攻撃から保護でき、サーバーのTCPスタック設定を変更せずに、クライアントとの接続パラメーターを最適化することもできます。
-
HTTPの正規化:HTTPトラフィックを処理する設定では、正しく完全なリクエストだけを通します。これにより、多くのプロトコルベースの攻撃を防げます。また、仕様上許容されるプロトコルの逸脱(複数行のヘッダーなど)を修正し、サーバーで問題を起こさないようにします。
-
HTTPの修正:URLや任意のリクエスト・レスポンスヘッダーを変更、修正、追加、削除、書き換えできます。複雑な環境での相互運用性の問題を解決するのに役立ちます。
-
内容に基づく振り分け:リクエストの任意の要素を使って、リクエストや接続を渡すサーバーを決められます。そのため、同じポートで複数のプロトコル(HTTP、HTTPS、SSHなど)を扱えます。
-
サーバーの負荷分散:TCP接続とHTTPリクエストを負荷分散できます。TCPモードでは接続全体に対して、HTTPモードではリクエストごとに振り分け先を決定します。
-
トラフィックの調整:さまざまな箇所でレートを制限し、サーバーを過負荷から保護し、内容に応じてトラフィックの優先順位を調整できます。また、パケットにマークを付けることで、その情報を下位層や外部のネットワークコンポーネントへ渡すこともできます。
-
DDoSやサービスの不正利用からの保護:IPアドレス、URL、Cookieなどごとに多くの統計を保持し、不正利用の発生を検出して対処できます(攻撃元の速度制限、ブロック、古い内容への誘導など)。
-
ネットワークのトラブルシューティングの観測点:ログに記録される情報が正確なため、ネットワークに関する問題を絞り込むためによく使われます。
-
HTTP圧縮のオフロード:サーバーが圧縮しなかったレスポンスを圧縮でき、接続品質が低いクライアントや、レイテンシーの高いモバイルネットワークを使うクライアントのページ読み込み時間を短縮します。
-
キャッシュプロキシ:レスポンスをRAMにキャッシュでき、同じオブジェクトへの後続リクエストでは、そのオブジェクトが存在して有効な間、サーバーから再度ネットワーク転送するコストを避けられます。ただし、永続ストレージにはオブジェクトを保存しません。このキャッシュ機能はメンテナンス不要となるよう設計されており、サーバーのリソースではなく、haproxyの貴重なリソースの節約だけを目的としています。サーバーを最適化するためのキャッシュには、はるかに多くの調整と柔軟性が必要です。そのような高度なキャッシュが必要な場合は、Varnish Cacheを使ってください。特にいずれかの側でSSL/TLSが必要な場合、haproxyと非常によく連携します。
-
FastCGIゲートウェイ:FastCGIはHTTPの別の表現と見なせるため、HAProxyは、任意の組み合わせのFastCGIアプリケーションサーバーから成るサーバーファームを、その間に別のゲートウェイ層を挟まずに直接負荷分散できます。これにより、リソースを節約し、保守コストを削減できます。
HAProxyは、次の役割は担いません。
-
明示的なHTTPプロキシ、つまりブラウザーがインターネットに到達するために使うプロキシ。この用途には、Squidなどの優れた専用オープンソースソフトウェアがあります。ただし、そのようなプロキシの前段にHAProxyを配置して、負荷分散と高可用性を提供できます。
-
データの加工:リクエストやレスポンスのボディーは変更しません。
-
静的Webサーバー:起動時にchroot jail内に自身を隔離して権限を落とすため、起動後はファイルシステムへ一切アクセスしません。そのため、静的Webサーバーにはできません(ただし、動的サーバーはFastCGI経由でサポートします)。この用途にはApacheやNginxなどの優れたオープンソースソフトウェアがあり、その前段にHAProxyを簡単に配置して、負荷分散、高可用性、高速化を提供できます。
-
パケットベースのロードバランサー:IPパケットやUDPデータグラムを扱わず、NATもDSRも行いません。これらは下位層の仕事です。IPVS(Linux Virtual Server)などのカーネルベースのコンポーネントがすでにこれらを十分に実現しており、HAProxyと非常によく補完し合います。
3.2. HAProxyの動作の仕組み
HAProxyは、非常に高速なI/O層と、優先順位に基づくマルチスレッドスケジューラーを組み合わせた、イベント駆動のノンブロッキングエンジンです。データ転送を目的に設計されているため、最小限の操作で、できるだけ速くデータを移動できるようアーキテクチャーを最適化しています。接続を可能な限り同じCPUに割り当て続けることで、CPUキャッシュの効率を高めることに重点を置いています。そのため、各層にバイパス機構を備えた階層モデルを実装し、必要な場合以外はデータが上位層へ進まないようにしています。処理の大半はカーネルで行われ、HAProxyはヒントを与えたり、後でまとめられると判断した操作を避けたりして、カーネルができるだけ速く処理できるよう支援します。その結果、典型的には、TCPまたはHTTPのcloseモードでは処理時間の15%がHAProxy、85%がカーネルで費やされ、HTTPのkeep-aliveモードでは約30%がHAProxy、70%がカーネルで費やされます。
1つのプロセスで多数のプロキシインスタンスを動かせます。1プロセス内に300000個もの個別のプロキシを設定しても、正常に動作した例が報告されています。99%を超えるユーザーにとって、1コア・1 CPUの構成で十分以上です。そのため、コンテナーや仮想マシンを使う場合は、運用コストを抑え、トラブルシューティングを容易にするため、入手できる最小のイメージを使うことを推奨します。ただし、HAProxyが動作するマシンは決してスワップしてはなりません。また、そのCPUに人為的な制限をかけたり(ハイパーバイザーでCPUの一部だけを割り当てるなど)、コンテキストスイッチのレイテンシーを大きくする計算負荷の高いプロセスとCPUを共有したりしてはなりません。
スレッドを使うと、CPUコアごとに1スレッドを割り当てて、利用可能な処理能力をすべて活用できます。これは主にSSLや、40 Gbpsを超えるデータ転送速度が必要な場合に有効です。この場合、複数の物理CPU間での通信を避けることが極めて重要です。ネットワークスタックとHAProxy自体の両方で、大きなボトルネックになり得るためです。直感に反すると思う方もいるかもしれませんが、性能上の問題が発生した際、最初に行うべきことは、HAProxyが使うCPUの数を減らすことである場合がよくあります。
HAProxyの実行に必要なのは、haproxyの実行可能ファイルと設定ファイルだけです。ログについては、syslogデーモンを正しく設定し、ログローテーションを用意することを強く推奨します。ログはstdout/stderrにも送信でき、コンテナー内では便利な場合があります。起動前に設定ファイルを解析し、すべてのリッスンソケットへのバインドを試み、何かに失敗すれば起動を拒否します。この段階を過ぎれば、失敗することはありません。つまり、実行時の失敗はなく、起動できれば停止されるまで動作し続けます。
起動後、HAProxyが行うことは、次の3つだけです。
-
着信接続を処理する。
-
サーバーの状態を定期的に確認する(ヘルスチェック)。
-
ほかのhaproxyノードと情報を交換する。
着信接続の処理は、多くの設定によって動作が変わるため、最も複雑なタスクです。ただし、次の9つの手順にまとめられます。
-
1つ以上のリッスンアドレスを参照する"frontend"という設定要素に属するリッスンソケットで、着信接続を受け付ける。
-
これらの接続に、フロントエンド固有の処理ルールを適用する。接続のブロック、ヘッダーの変更、統計ページやCLIなどの内部アプレットを実行するための捕捉が行われる場合がある。
-
接続を、サーバーファームを表す"backend"という別の設定要素へ渡す。この要素には、サーバーのリストと、そのサーバーファームの負荷分散方式が含まれる。
-
これらの接続に、バックエンド固有の処理ルールを適用する。
-
負荷分散方式に従って、接続を転送するサーバーを決定する。
-
レスポンスデータに、バックエンド固有の処理ルールを適用する。
-
レスポンスデータに、フロントエンド固有の処理ルールを適用する。
-
起きたことを詳しく記録するログを出力する。
-
HTTPでは2番目の手順に戻って次のリクエストを待ち、それ以外では接続を閉じる。
フロントエンドとバックエンドは、エンドツーエンド接続の片側だけを見るため、ハーフプロキシと考えられることがあります。フロントエンドはクライアント側だけ、バックエンドはサーバー側だけを扱います。HAProxyは、フロントエンドとバックエンドをそのまま合わせたフルプロキシもサポートします。HTTPを処理する場合は、一般に設定をフロントエンドとバックエンドに分けます。任意のフロントエンドから任意のバックエンドへ接続を渡せるため、多くの構成が可能になるからです。TCP専用のプロキシでは、フロントエンドとバックエンドに分ける利点は少なく、フルプロキシのほうが設定を読みやすくできる場合があります。