リバースプロキシとロードバランシング

23 分で読了
このページの目次

リバースプロキシはサービスに代わってリクエストを受領し、バックエンドに対して交換を行います。ロードバランシングは、接続やリクエストをどの利用可能なターゲットに割り当てるかを決定します。これら2つは1つのコンポーネントで実装されることが多いですが、同じ概念ではありません。バックエンドが1つしかなければリバースプロキシが必要になる場合もありますし、レイヤー4のロードバランサがHTTPを解析しない場合もあります。

https://api.example.com/orders を例に挙げると、ユーザーリクエストはまずエッジエントリポイントを経て、リージョンプロキシを経て、最終的に注文インスタンスに到達します。各ホップには独立した接続、タイムアウト、アイデンティティの境界があります。クライアントが502エラーとして目にするものは、バックエンドで既にコミット済みであり、単にレスポンスの経路で失敗しただけである可能性があります。

解決したい問題先に明確にするべきこと
ホストまたはパスによるルーティングどのレイヤーでTLSを解除し、HTTPを解析できるか
バックエンドで誤ったドメイン名やプロトコルが見える元のHost、アップストリームTLSのServerName、転送メタ情報
インスタンス間のリクエスト数は均等だが、負荷が偏っている割り当ての単位と、リクエストごとのコスト
連続リクエストがブロックされる、またはSSEがバッチ処理されるバッファリング、フラッシュ、アイドルタイムアウト
自動リトライにより障害が拡大する試行予算、並列数、冪等性の契約
停止後も古いトラフィックが残る接続の排空(drain)、コネクションプーリング、長接続の状態

2つの接続、3つの混同しやすいアイデンティティ

1つのビジネスリクエストは、通常、2つの独立したトランスポート接続を跨ぐ クライアント HTTPS / HTTP/2 または HTTP/3 リバースプロキシ 終端、ルーティング、プーリング、ポリシー バックエンドインスタンス HTTP/1.1 または他のアップストリームプロトコル ダウンストリームID、アップストリームID、ビジネスIDをそれぞれ検証する フロントエンドでh3をネゴシエートしても、プロキシからバックエンドへの通信がh3であるとは限らない L4透通、NAT、ダイレクトルーティングなどのアーキテクチャでは接続の境界が異なります。本図はHTTPを終端するレイヤー7のリバースプロキシを示しています。

レイヤー7のプロキシは通常、ダウンストリームのTLSを終端し、独立したアップストリーム接続を確立します。クライアントはプロキシが提示する公開サービス証明書を検証し、プロキシがHTTPSでバックエンドにアクセスする場合は、バックエンドのアイデンティティも検証する必要があります。バックエンドが見るTLSのクライアントは、プロキシである可能性が高く、最終ユーザーではありません。

また、以下をそれぞれ記録する必要があります。

  1. クライアントの元のリクエストのauthority
  2. プロキシがアップストリームに接続する際のTLS ServerName
  3. アプリケーションが絶対URLを生成するために使用する外部サービス名

これらは同じである場合もあれば、異なる場合もあります。Hostを一律に127.0.0.1:3000に変更すると、一部のアプリケーションで誤ったリダイレクトが発生する可能性があります。一方、外部のHostを保持し続けると、ホストされているアップストリームのバーチャルホスト要件に適合しない可能性があります。

フォワードプロキシはクライアントがターゲットを選択し、リバースプロキシはサービスがエントリポイントを提供します。「アドレスを隠せる」ことは結果として生じうるものであり、定義ではありません。プロキシは元のアドレスを積極的に渡すこともあるため、両者を単に「どちらが匿名化されるか」で定義することはできません。

L4 / L7:速度のラベルよりも、単位を選択することが重要

方式通常、何に基づいて割り当てるか可視情報と限界
L4接続転送新規TCP接続などHTTPパスを理解しないことが多い。既存の接続はターゲットに固定される
TLS透通および可視ハンドシェイク情報の読み取り接続および適用されるSNIなどHTTPを復号しない。ECHなどは可視情報を変更する
L7 HTTPプロキシリクエスト、ストリーム、および実装のプーリングポリシーメソッド、Host、パス、または認証結果に基づいてルーティング可能

HTTP/2では、1つの接続内で多数のリクエストを処理できます。L4でこの接続を1つのバックエンドに選択しても、その中の各リクエストが個別にロードバランシングされるわけではありません。L7ではリクエストごとにアップストリームを選択できますが、接続プール、ストリームの並列数、およびアフィニティポリシーの制約を受けます。

最少接続(Least Connections)はL7固有のものではなく、例えばNGINX stream upstreamでもleast_connがサポートされています。逆に、接続数が少なくてもビジネス負荷が低いとは限りません。1つの接続が多数のHTTP/2ストリームや、高コストな長時間タスクを担っている可能性があります。NGINX stream upstream

L4がHTTPを解析しないことは、一部の処理負荷を削減しますが、TLS、コピー、キャッシュヒット、ハードウェアパスを離れて、「すべてのL4デプロイメントがL7より速い」と宣言することはできません。WebSocketもL4専用に要求されるものではありません。ハンドシェイクとトンネリング転送をサポートするHTTPプロキシでも、WebSocketを処理できます。

均衡アルゴリズム:何の均衡を取っているかをまず問う

アルゴリズム適用される理解典型的なバイアス
ラウンドロビン / 加重ラウンドロビンスケジューリングの選択回数を順次または重みで分配リクエストコストが異なる場合、CPU使用量やバイト数は同比例ではない
最少接続 / 最少アクティブリクエスト観測された負荷が小さいターゲットを選択指標の定義、長時間タスクとマルチプレクシングが効果に影響する
ランダム / 2つのランダム選択集中意思決定のコストを低減結果はサンプルとローカルビューの影響を受ける
IPまたはセッションアフィニティ同じキーを可能な限り同一ターゲットに配置NATによる大規模クライアントのホットスポット、アドレス変更、障害移行
一貫性ハッシュ / HRWメンバー変更時にキーの移行を最小限に抑える毎回正確に1/Nの移行を保証するものではなく、ホットスポットを消除するものでもない

例えば、Aに100個の2msリクエストが割り当てられ、Bに100個の200msリクエストが割り当てられた場合、スケジューリング回数は完全に均等ですが、直列作業の負荷は100倍異なります。重み3:1は、その具体的な選択モデル下での選択比率を制約するだけで、3倍のバイト数や3倍のCPU使用を保証するものではありません。

ヘルスなインスタンスの合計平均処理能力が1000リクエスト/秒で、エントリポイントに1200リクエスト/秒が継続的に到達している場合、均衡アルゴリズムを変えても、継続的に増加するキューは解消されません。キューの上限、バックプレッシャ、レートリミット、またはスケールアウトが必要であり、リトライに予算を確保する必要があります。NGINX HTTP upstream

クライアントアドレスと転送ヘッダー:信頼できるソースがフィールド値に優先する

Forwarded、X-Forwarded-For、X-Forwarded-Protoなどは、前回のホップで観察された情報を表現できますが、外部クライアントも同様の名前のフィールドを構築できます。バックエンドは、監査、レートリミット、またはアクセス制御のために、左端のアドレスを無条件で取得してはいけません。

どの直接接続元が信頼できるプロキシとみなされるか、および各レイヤーでのクリア、追加、上書きのルールを定義する必要があります。マルチホップチェーンでは、既知の信頼できる末端から前方へ解析し、信頼できない境界に到達したら停止します。プライベートネットワークセグメントが自動的に制御されたプロキシを意味するわけではありません。Forwarded自体も暗号的な署名を提供しません。RFC 7239

PROXYプロトコルは、接続の先頭にメタ情報を渡す別のメカニズムであり、HTTPヘッダーではなく、送信者を自動的に認証するものでもありません。限定されたエントリポイントでのみ、双方が一致して有効にする必要があります。これを通常のTLSやHTTPを期待するリスナーに送信すると、解析エラーが発生します。

最小限のCaddy例:デフォルトの転送ルールを保持

以下はオフラインでの理解のための設定です。ローカルの8080エントリポイントでパスに基づき2つのローカルインスタンスに転送し、/api/プレフィックスを保持します。パブリックな証明書申請はなく、実際のドメインにも依存しません。

http://127.0.0.1:8080 {
    handle /api/* {
        reverse_proxy 127.0.0.1:9001 127.0.0.1:9002 {
            lb_policy round_robin
            health_uri /readyz
            health_interval 10s
            health_timeout 2s
            health_fails 2
            health_passes 2
            transport http {
                dial_timeout 2s
                response_header_timeout 5s
            }
        }
    }
    handle {
        respond "not found" 404
    }
}

handleは一致したプレフィックスを剥離しません。handle_pathに変更する場合は、バックエンドが期待するURIがそれに伴って変化するかどうかを確認する必要があります。/api/*は厳密に/apiのパスを自動的に含みません。エントリポイントの契約は明確にする必要があります。

CaddyはデフォルトでX-Forwarded-For/Proto/Hostを処理し、信頼できないエントリポイントから渡された対応する値を無視します。追加で設定が必要なのは、信頼できるプロキシの境界であり、すべての転送フィールドを無条件で上書きすることではありません。ここではHTTPアップストリームを使用しています。HTTPSアップストリームのHostの動作にはバージョン差があり、Caddy 2.11.0以降ではアップストリームのHostを自動的に一致させることができますが、旧バージョンでは実際の名前に基づいて明示的に設定する必要があります。Caddy reverse_proxy

本例はCaddy 2.10.2でオフライン適応チェック済みです。設定構文が通ったからといって、バックエンドの/readyz実装が正しいことを意味するわけでも、ネットワークトラフィックの検証が完了したことを意味するわけでもありません。自動HTTPS、TLSの信頼性、および本番公開アドレスは、デプロイに合わせて個別に設定する必要があります。

タイムアウトとリトライ:1つのビジネスデッドラインで全ての試行を制約する

1つのリクエストの時間予算は、各リトライのたびにゼロからリセットして計測してはならない 経過時間 140 ms エントリポイントのキューイングと処理 初回試行 220 ms 接続確立 / 待機 / 失敗 残り最大 390 ms 850−140−220−100 総予算 850 ms;レスポンス返却用に 100 ms を確保 再試行するには、メソッド、リクエストボディ、および実行状態の条件を満たす必要がある 数値は教育用の予算です。接続、ヘッダー、アイドル読み取り、およびエンドツーエンドのデッドラインは、異なる種類のタイムアウトです。

接続タイムアウトは接続の確立を制限し、レスポンスヘッダータイムアウトはヘッダーの待機を制限し、読み取りアイドルタイムアウトは2回の進行間の間隔を制限し、エンドツーエンドのデッドラインはビジネスの総消費時間を制限します。これらは互いに代替できません。例えば、NGINXのproxy_read_timeoutは連続した読み取り間の待機を制約するものであり、レスポンス全体がこれ以上長く続くことはできないという意味ではありません。NGINX proxy timeout

初回試行で220msを消費した後、2回目の試行で850msの完全な予算をリセットすることはできません。プロキシのキューイング、レスポンスの返却、およびクライアントの残り待機時間も考慮する必要があります。タイムアウト後、バックエンドのタスクがキャンセルされるかどうかは、キャンセルシグナルがアプリケーションおよびダウンストリームに継続して伝達されるかどうかにかかっています。

リトライ可能であるためには、以下の条件を同時に満たす必要があります。

  • リクエストボディが再プレイ可能であること
  • 失敗したフェーズがリトライを許可していること
  • ビジネス実行の不確実性が許容できること
  • 残りの予算が十分であること

クライアントに部分的なレスポンスが既に送信された後、通常、バックエンドを透明に切り替えてレスポンス全体を再実行することはできません。POSTは明示的な冪等キーに依存する場合があります。「502を受信したからといって、安全に再実行できることを自動的に証明するものではありません。」

budget, elapsed, first_attempt, reserve = 850, 140, 220, 100
remaining = max(0, budget - elapsed - first_attempt - reserve)
assert remaining == 390
# 教育モデル:各試行の失敗確率が独立して p、最大3回試行。
p = 0.2
expected_attempts = 1 + p + p**2
assert abs(expected_attempts - 1.24) < 1e-12
assert abs(1000 * expected_attempts - 1240) < 1e-9
# 3層それぞれで最大3回、最悪ケースでは27回のダウンストリーム試行に拡大する。
assert 3**3 == 27
print('残りミリ秒 / 秒あたりの期待試行数 / 再帰的な最悪試行数:', remaining, 1000 * expected_attempts, 3**3)

上記の独立した失敗は教育上の仮定です。実際の障害は関連していることが多く、リトライは依存関係をさらに圧迫する可能性があります。試行数、総時間、および追加のリトライトラフィックを制限し、適切なバックオフとジッターを使用することで、階層的なリトライの増幅を防ぐことができます。

各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?

時間条は同じ起点と截止線(デッドライン)を共有しており、初回の失敗によってそれらがシフトすることはありません。ここでの「利用可能な予算」は、まだ間に合うかどうかだけを答え、ビジネス操作を安全に繰り返し実行できるかどうかは答えません。これら2つの判断が両方とも満たされて初めて、リトライを行うべきです。

図を準備しています
各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?

リトライ予算は、総デッドラインの残高であり、完全な時窗を新たに開始するものではない。残高がゼロの場合、どれだけリトライ回数を設定しても、時間は生じない。

バッファリング、ストリーミング、およびバックプレッシャ

レスポンスのバッファリングにより、プロキシはアップストリームからより速く読み取り、内容を一時保存してからクライアントにゆっくりと送信できるため、アップストリームがスロークライアントによって長時間占有されるのを防ぎます。その代償として、メモリ/ディスクの予算と、最初のバイトまたはチャンクごとのレイテンシの増加があります。リクエストのバッファリングは、スローなアップロードがバックエンドをどのように占有するか、およびリクエストボディが失敗後に再プレイ可能かどうかに影響します。

SSE、モデルのストリーミング出力、および大規模なダウンロードは、配信の要件に応じてフラッシュを設定する必要があります。アプリケーションがflushを呼び出しても、すべてのプロキシ層や圧縮器が即座に出力するとは限りません。すべてのバッファリングを無効にすることも、無料の最適化ではありません。スロークライアントはバックプレッシャをアップストリームに伝播させ、リソースを長期間占有する可能性があります。NGINX proxy buffering

コネクションプールの再利用はハンドシェイクを削減しますが、アップストリームのアイドル接続の寿命と調整する必要があります。バックエンドが既に閉じているが、プロキシがまだ認識していないプールの接続は、再利用時に失敗を引き起こします。リトライが可能かどうかは、依然としてリクエストのセマンティクスと送信フェーズに戻り、無限のリトライでプール設定の問題を隠してはいけません。

ヘルスチェックとリリース時の排空(drain)

アクティブなチェックは定期的にプローブを送信し、パッシブなチェックは実際のリクエストの失敗を観察します。これらは異なる問題を解決します。/readyzが200を返すことは、このインスタンスが新しい作業を受信する準備ができていることを示しますが、すべての共有依存関係の瞬間的なジッターを、インスタンスプール全体の同時削除に無条件に拡散してはいけません。

検出周期、失敗閾値、およびプローブのタイムアウトは、検出遅延を共同で決定します。10秒ごとにプローブし、連続して2回の失敗後にのみ削除する場合でも、障害発生後ちょうど20秒で切り替わるわけではありません。発生位相、プローブの所要時間、およびスケジューリングの影響を受けます。復旧時には、トラフィックを段階的に増加させ、キャッシュや接続をウォームアップする必要があります。

停止する際は、まず新しい作業の受信を停止し、次に受信済みのリクエストの終了を待ち、最後に期限までに残余の接続を閉じます。HTTP/2およびHTTP/3では、それぞれGOAWAYを使用できます。WebSocketやSSEなどの長接続では、最大の排空時間とクライアントのリトライ動作を明確にする必要があります。インスタンスをディスカバリリストから削除しただけでは、既存のすべての接続が即座に消失することはありません。

ブルーグリーンデプロイメントや重み付きグレーデッドリリースでは、セッションアフィニティ、リクエストコスト、およびサンプル分布も考慮する必要があります。1%のリクエストが必ずしも1%のユーザーをカバーするわけではなく、大規模テナントや特定のパスを欠落させる可能性があります。指標はバージョン、ルーティング、およびエラータイプ別に同時に観察する必要があります。

観測:1つの失敗を具体的な試行に対応させる

アクセスログには、少なくともリクエストID、ルーティング、選択されたアップストリーム、試行回数、ステータス、および重要な所要時間を関連付ける必要があります。クライアントの総レイテンシをキューイング、接続確立、ヘッダー待機、およびレスポンス転送に分解することで、「アップストリームが応答しない」「クライアントが読み取るのが遅い」「プロキシのプール待機」といった状況を区別できます。

キャッシュ、レートリミット、および認証も、どのレイヤーで実行されたかを明記する必要があります。キャッシュヒットによってバックエンドが迂回された場合、バックエンドのログにリクエストがないのは正常な現象です。ユーザートークン、元の証明書秘密鍵、または機密情報をデバッグログに混入しないでください。診断は、構造化された結果と必要な識別子に依存すべきです。

継続的な参照

  • mTLS:プロキシ終端後のアイデンティティの境界。
  • HTTP/2、HTTP/3:マルチプレクシング、終了、およびフロー制御。
  • REST と API 設計:リトライとビジネスの冪等性。
  • WebSocket 与 SSE:長接続およびストリーミング配信。