1. HTTPの基礎を振り返る
この文書は、上記バージョンに実装されている設定言語を扱います。ヒント、例、助言は提供しません。そのような文書については、リファレンスマニュアルまたはアーキテクチャマニュアルを参照してください。番号付きの章は、直接移動できるようにHAProxyのフラットなサイドバーに順番に並んでいます。
HAProxyをHTTPモードで実行すると、リクエストとレスポンスの両方を完全に解析してインデックスを作成するため、その内容に含まれるほぼあらゆる要素についてマッチ条件を組み立てられます。
ただし、HTTPのリクエストとレスポンスがどのように構成され、HAProxyがそれらをどのように分解するかを理解することが重要です。これを理解すれば、正しいルールの記述や既存設定のデバッグが容易になります。
まず、HTTPは一連のRFCで標準化されており、HAProxyはそれらにできる限り忠実に従います。
- RFC 9110: HTTP Semantics(プロトコル要素の意味を説明)
- RFC 9111: HTTP Caching(HTTPキャッシュが従うべきルールを説明)
- RFC 9112: HTTP/1.1(表現、相互運用性のルール、セキュリティ)
- RFC 9113: HTTP/2(表現、相互運用性のルール、セキュリティ)
- RFC 9114: HTTP/3(表現、相互運用性のルール、セキュリティ)
これらに加えて、RFC 8999から9002は、HTTP/3プロトコルが使用するQUICトランスポート層を規定しています。
1.1. HTTPトランザクションモデル
HTTPプロトコルはトランザクションに基づきます。つまり、各リクエストに対してレスポンスは必ず一つだけ返されます。当初のプロトコルバージョン1.0では、接続ごとにリクエストは一つでした。クライアントからサーバーへTCP接続を確立し、クライアントがその接続を通じてリクエストを送り、サーバーが応答すると、接続を閉じます。したがって、新しいリクエストには新しい接続が必要です。
このモードはしばしば"HTTP close"モードと呼ばれ、HTTPトランザクションの数だけ接続が確立されます。レスポンスの後にサーバーが接続を閉じるので、クライアントはコンテンツ長を知る必要がなく、接続が閉じられた時点でレスポンスが完了したと判断します。これは、ネットワークエラーによってレスポンスの一部が切り詰められた場合でも、クライアントがレスポンスを完全だと誤認しうることを意味します。かつてはこのため、途中までしかない画像が画面に表示されることがありました。
プロトコルがトランザクションを単位としていることを利用し、連続する二つのトランザクションの間で接続を閉じないように改良できました。ただし、このモードではクライアントが無期限に待ち続けないよう、サーバーは各レスポンスのコンテンツ長を必ず示さなければなりません。そのために、特別なヘッダー"Content-length"を使用します。このモードは"keep-alive"モードと呼ばれ、HTTP/1.1で登場しました(一部のHTTP/1.0エージェントも対応しています)。複数のリクエストで再利用される接続は「持続的接続」と呼ばれます。
このモードの利点は、トランザクション間の遅延を減らせること、サーバー側で必要な処理能力が少なくなること、切り詰められたレスポンスを検出できることです。通常はcloseモードより高速ですが、常にそうとは限りません。一部のクライアントは同時接続数を少なく制限することが多く、その分、ネットワーク接続の悪さを補いにくくなるためです。また、一部のサーバーは次のリクエストが来る可能性を考えて長時間接続を維持する必要があり、接続数の多さからメモリ使用量が増えることがあります。一方、早く閉じすぎると、ちょうど接続を閉じた時点で到着したリクエストが失敗することがあります。
このモードではレスポンスのサイズを事前に知る必要があるため、動的に生成したコンテンツや圧縮したコンテンツでは常に利用できるわけではありません。このため、別のモードとして"chunked mode"が実装されました。送信側は全体のサイズを一度に通知する代わりに、すでにバッファーにあるレスポンスの次の「チャンク」のサイズだけを通知し、サイズがゼロのチャンクを使って任意の時点で終了できます。このモードではContent-Lengthヘッダーを使用しません。
通信に関するもう一つの改良はパイプラインモードです。引き続きkeep-aliveを使用しますが、クライアントは最初のレスポンスを待たずに二番目のリクエストを送信します。これは、ページを構成する多数の画像を取得する場合に役立ちます。
連続するリクエスト間のネットワーク遅延をなくせるため、性能に非常に大きな利点をもたらすことは明らかです。しかし、HTTPではレスポンスを対応するリクエストに関連付ける方法がないため、多くのHTTPエージェントはパイプラインを正しくサポートしていません。このため、サーバーはリクエストを受信した順序とまったく同じ順序で応答しなければなりません。実際には、さまざまなクライアントが何度か導入を試みた後、一部のサーバーでの信頼性の低さから完全に使われなくなりました。ただし、サーバーにはこの機能のサポートが義務付けられています。
次の改良は、HTTP/2とHTTP/3に実装されている多重化モードです。このモードでは、複数のトランザクション(リクエストとレスポンスの組)を単一の接続上で並列に送信し、それぞれが互いに独立して自身の速度で進行します。多重化プロトコルでは、同じ接続上で並行して行われる通信を表すため、「ストリーム」という新しい概念が導入されました。通常、各ストリームには接続内で一意の識別子を割り当て、両端がこれを使ってデータの受け渡し先を判断します。クライアントが同じ接続上で多数(最大100、場合によってはそれ以上)のストリームを並行して開始し、サーバーに処理を振り分けさせ、準備できたレスポンスから任意の順序で応答させることはよくあります。多重化モードの主な利点は、ラウンドトリップ数を大幅に減らし、遅延の大きいネットワークでページの読み込み時間を短縮することです。多数の画像を使うサイトで、すべての画像が並行して読み込まれるように見えることがあります。
これらのプロトコルは、回線上のバイト数を減らすためにヘッダーフィールドを圧縮する仕組みを採用し、効率も高めています。そのため、適切なツールなしでは、HTTP/1のように手で操作したり目で読んだりすることは現実的ではありません。この理由により、文献(この文書を含む)にあるHTTPメッセージの各種例では、新しいバージョンのプロトコルでも引き続きHTTP/1の構文を使って表現しています。
HTTP/2にはいくつかの設計上の制約があります。たとえば、パケットの損失がすべてのストリームに同時に影響します。また、クライアントがオブジェクトの取得に時間をかけすぎると(たとえばディスクへの保存が必要な場合)、その取得が遅くなり、その間は後ろで待機するデータにアクセスできなくなることがあります。これは"head of line blocking"、“HoL blocking”、または単に"HoL"と呼ばれます。
HTTP/3はQUIC上に実装され、QUIC自体はUDP上に実装されています。QUICはストリームを独立して処理することで、トランスポート層のhead of line blockingを解決します。実際、損失が発生しても、影響を受けたストリームが他のストリームに影響することはなく、すべてに並行してアクセスできます。QUICは接続マイグレーションもサポートしますが、現在haproxyはこれをサポートしていません。
HAProxyは、持続的接続についてデフォルトでkeep-aliveモードで動作します。接続ごとに各リクエストとレスポンスを処理し、レスポンスの終了から新しいリクエストの開始までの間、両側の接続をアイドル状態に保ちます。クライアントからHTTP/2接続を受け取ると、すべてのリクエストを並行して処理し、keep-aliveのHTTP接続と同じように、接続をアイドル状態にして新しいリクエストを待ちます。
HAProxyは基本的に3つの接続モードをサポートします。
-
keep alive:すべてのリクエストとレスポンスを処理し、クライアント側とサーバー側の接続を新しいリクエストに備えて維持します。これがデフォルトで、現在のWebとプロトコル(HTTP/2およびHTTP/3)に適しています。
-
server close:レスポンスの後にサーバー側の接続を閉じます。
-
close:レスポンスの終了後、両側の接続を能動的に閉じます。
これに加えて、HTTPプロトコル仕様で要求されるとおり、デフォルトでは、サーバー側の接続はどのクライアントのどのリクエストでも再利用できます。そのため、特定のクライアントに関する情報が必要な場合は、各リクエストと一緒に渡さなければなりません(クライアントの送信元アドレスなど)。サーバーとの通信でHTTP/2を使用する場合、HAProxyはデフォルトでその接続を同じクライアント専用にし、クライアント間でhead of line blockingが起こる危険を避けます。
1.2. 用語
HAProxy内の用語は、HTTPプロトコルとその用途の変化に合わせ、長年の間に少し変化してきました。当初は接続、セッション、ストリーム、トランザクションに大きな違いはありませんでしたが、現代のHTTPプロトコルに存在するものに近づけるよう、徐々に明確になりました。ただし、過去との互換性を保つため、一部の用語は設定やコマンドラインインターフェースに残っています。
以下は現在のHAProxyバージョンに適用される定義です。
-
connection(接続):接続は、リモートエージェント(クライアントまたはサーバー)とhaproxyの間にある、可能な限り低い層の単一の双方向通信チャネルです。通常は、IPとポートの組の間で確立されたTCPソケットに相当します。クライアント側では、クライアントがhaproxyに接続したときに最初に作成されるエンティティが接続であり、接続レベルのルールが最も早い段階で適用されます。
-
session(セッション):セッションは、接続に関連するコンテキスト情報を加えます。これには、トランスポート層固有の情報(TLSキーなど)や変数が含まれます。この用語はHAProxy内で長く、両端間のエンドツーエンドのHTTP/1.0通信を指すために使われてきました。そのため、現在はストリームを表していても、一部のCLIコマンドや統計の名前にはこの用語が残っていますが、ヘルプメッセージや説明では曖昧にならないようにしています。ネットワークレベルの用語(OS内のTCPセッションや、ファイアウォールを通過するTCPセッションなど)や、HTTP以外のユーザーレベルのアプリケーション(telnetセッションやSSHセッションなど)では、今でも適切な用語です。Cookieにユーザーの完全なコンテキストを保存し、同じサーバーへ送信する必要がある「アプリケーションセッション」と混同してはいけません。
-
stream(ストリーム):ストリームは、解析や変換を適用できるアプリケーションレベルのエンドツーエンドの双方向通信に厳密に対応します。HTTPでは、一つのリクエストと対応するレスポンスを含み、リクエストの到着時に作成され、レスポンスの配信終了時に完了します。この文脈では、このストリームと多重化プロトコルのストリームの間には1:1の関係があります。TCP通信では、接続ごとにストリームは一つです。
-
transaction(トランザクション):トランザクションは、リクエストとそれに対応するレスポンスの組だけを指します。ストリーム以前はセッションと組み合わせて使われていましたが、現在はトランザクションとストリームの間に1:1の関係があります。主に変数のスコープ"txn"に現れ、このスコープはトランザクション全体、したがってストリーム全体で有効です。
-
request(リクエスト):クライアントからサーバーへ流れるトラフィックを指します。主にHTTPで、操作を行う場所を示すために使います。この用語はTCPの操作でも、データを処理する場所を示すために使われます。リクエストは、トラフィックや活動の単位としてカウンターによく現れます。常にレスポンスを伴うとは限りませんが(エラーなどのため)、リクエストなしにレスポンスが自発的に発生することはないため、全体の活動を示す指標として有用です。TCPでは、リクエストの数は接続数と同じです。
-
response(レスポンス):サーバーからクライアントへ流れるトラフィックを指します。HAProxy自身がレスポンスを生成する場合(HTTPリダイレクトなど)には、HAProxyからクライアントへ流れるトラフィックを指すこともあります。
-
service(サービス):通常、統計ページ、キャッシュ、小さなアプリケーションを実装するLuaコードなど、サーバーを必要としないHAProxy内部の処理を指します。サービスは通常、リクエストを読み取り、何らかの操作を行い、レスポンスを生成します。
1.3. HTTPリクエスト
まず、このHTTPリクエストを考えます。
1.3.1. リクエスト行
1行目が「リクエスト行」です。常に3つのフィールドで構成されます。
- METHOD:GET
- URI:/serv/login.php?lang=en&profile=2
- バージョンタグ:HTTP/1.1
これらはすべて、標準でLWS(linear white spaces)と呼ばれる文字で区切ります。通常はスペースですが、タブ、または改行/復帰の後にスペース/タブが続くものでも構いません。メソッド自体にコロン(’:’)を含めることはできず、英字だけに制限されます。こうしたさまざまな組み合わせがあるため、複雑な、あるいは不正確な正規表現をユーザーに書かせるより、HAProxy自身で分割することが望まれます。
URI自体にはいくつかの形式があります。
- 「相対URI」:
- 「絶対URI」、別名「URL」:
-
アスタリスク(’*’):この形式はOPTIONSメソッドと組み合わせた場合にのみ受け入れられ、中継できません。次のホップの機能を問い合わせるために使います。
-
アドレス:ポートの組み合わせ:192.168.0.12:80。これはCONNECTメソッドで使います。このメソッドはHTTPプロキシ経由でTCPトンネルを確立するためのもので、通常はHTTPSに使いますが、他のプロトコルに使うこともあります。
相対URIには二つの部分があります。疑問符の前の部分を「パス」と呼びます。通常、サーバー上の静的オブジェクトへの相対パスです。疑問符の後の部分を「クエリ文字列」と呼びます。主に動的なスクリプトへ送るGETリクエストで使われ、使用する言語、フレームワーク、アプリケーションに大きく依存します。
HTTP/2とHTTP/3はリクエストにバージョン情報を含めないため、バージョンは下位のプロトコルと同じもの(つまり"HTTP/2")とみなします。また、これらのプロトコルはリクエスト行をひとまとまりで送信せず、名前がコロンで始まる「疑似ヘッダー」と呼ばれる個々のフィールドに分割します。HAProxyは、扱いやすいようにこれらを同等のリクエスト行へ再構成します。このため、ログ内のリクエスト行はHTTP/1.xとHTTP/2またはHTTP/3で少し異なる場合があります。
1.3.2. リクエストヘッダー
ヘッダーは二行目から始まります。行頭の名前と、その直後のコロン(’:’)で構成されます。慣例ではコロンの後にLWSを置きますが、必須ではありません。その後に値が続きます。同じヘッダーが複数ある場合は、値の順序を保つ限り、カンマで区切って一行にまとめられます。これは"Cookie:“フィールドでよく見られます。後続行がLWSで始まる場合、ヘッダーを複数行にまたがらせることもできます。1.3の例では、4行目と5行目が"Accept:“ヘッダーの合計3つの値を定義しています。最後に、仕様どおり、ヘッダーの先頭または末尾にあるLWSはすべて無視され、値には含まれません。
よくある誤解とは異なり、ヘッダー名は大文字と小文字を区別しません。他のヘッダー名を参照する値(“Connection:“ヘッダーなど)も同様です。HTTP/2とHTTP/3では、デバッグモードで実行すると確認できるように、ヘッダー名は常に小文字で送信されます。内部ではすべてのヘッダー名を小文字に正規化し、HTTP/1.xとHTTP/2またはHTTP/3でまったく同じ表現を使用し、反対側にはそのまま送信します。このため、キャメルケースで入力したHTTP/1.xリクエストも小文字で届きます。
ヘッダーの終わりは、最初の空行で示します。よく二重改行と呼ばれますが、二重改行は空行の有効な形式の一つにすぎず、厳密な表現ではありません。
幸い、HAProxyはヘッダーのインデックス作成、値の検査、個数のカウントの際に、これらの複雑な組み合わせをすべて処理します。そのため、どのような書き方ができるかを心配する必要はありません。ただし、珍しくても正当な動作をするアプリケーションを、バグがあると決めつけないことが重要です。
重要な注意:
1.4. HTTPレスポンス
HTTPレスポンスはHTTPリクエストと非常によく似ています。どちらもHTTPメッセージと呼びます。このHTTPレスポンスを考えます。
特別なケースとして、HTTPはステータスコード1xxの、いわゆる「情報レスポンス」をサポートします。これらはレスポンスの内容を一切運ばず、たとえばクライアントにリクエストの送信継続を求める、一種の通知メッセージとしてのみ使われる点が特殊です。ステータス100のレスポンスの場合、要求された情報は、その情報レスポンスの後に続く、次の100以外のレスポンスメッセージで運ばれます。つまり、一つのリクエストに複数のレスポンスを送信でき、これはkeep-aliveを有効にした場合にのみ機能します(1xxメッセージはHTTP/1.1で登場しました)。HAProxyはこれらのメッセージを処理し、正しく転送して読み飛ばし、次の100以外のレスポンスだけを処理できます。そのため、明示的に別の指定がない限り、これらのメッセージはログに記録されず、変換もされません。ステータス101のメッセージは、同じ接続上でプロトコルが変わり、CONNECTが発生した場合と同じようにHAProxyがトンネルモードへ切り替える必要があることを示します。この場合、Upgradeヘッダーには切り替え先のプロトコルの種類に関する追加情報が含まれます。
1.4.1. レスポンス行
1行目が「レスポンス行」です。常に3つのフィールドで構成されます。
- バージョンタグ:HTTP/1.1
- ステータスコード:200
- 理由:OK
ステータスコードは常に3桁です。最初の桁は全般的な状態を示します。
- 1xx = 読み飛ばす情報メッセージ(例:100、101)
- 2xx = OK、後にコンテンツが続く(例:200、206)
- 3xx = OK、後にコンテンツは続かない(例:302、304)
- 4xx = クライアントに起因するエラー(例:401、403、404)
- 5xx = サーバーに起因するエラー(例:500、502、503)
一部のエージェントが内部状態を報告するためにログへ出力することはありますが、通信では599を超えるステータスコードを送信してはいけません。各コードの詳しい意味はRFC9110を参照してください。HTTP/2以降にはバージョンタグがなく、":status"疑似ヘッダーでステータスコードを伝えます。
“reason"フィールドは単なる手掛かりであり、クライアントは解析しません。何が入っていても構いませんが、広く定着したメッセージに従うのが一般的です。“OK”、“Found”、“Authentication Required"など、一語または複数の語で構成できます。HTTP/2以降には存在せず、送信されません。HTTP/2以降のレスポンスをHTTP/1クライアントに送信する場合、HAProxyはステータスコードに対応する、このような一般的なreasonフィールドを生成します。
HAProxy自身が出力することのあるステータスコードは次のとおりです。
上記のエラーコード4xxと5xxはカスタマイズできます(セクション4.2 の"errorloc"を参照)。特定のアクションによって、別のステータスコードを意図的に出力することもできます(たとえば、セクション4.3 の"deny”、“return”、“redirect"アクションを参照)。
1.4.2. レスポンスヘッダー
レスポンスヘッダーはリクエストヘッダーとまったく同じように機能するため、HAProxyは両方に同じ解析関数を使います。詳しくは1.3.2を参照してください。