リバースプロキシはサービスに代わってリクエストを受領し、バックエンドに対して交換を行います。ロードバランシングは、接続やリクエストをどの利用可能なターゲットに割り当てるかを決定します。これら2つは1つのコンポーネントで実装されることが多いですが、同じ概念ではありません。バックエンドが1つしかなければリバースプロキシが必要になる場合もありますし、レイヤー4のロードバランサがHTTPを解析しない場合もあります。
https://api.example.com/orders を例に挙げると、ユーザーリクエストはまずエッジエントリポイントを経て、リージョンプロキシを経て、最終的に注文インスタンスに到達します。各ホップには独立した接続、タイムアウト、アイデンティティの境界があります。クライアントが502エラーとして目にするものは、バックエンドで既にコミット済みであり、単にレスポンスの経路で失敗しただけである可能性があります。
| 解決したい問題 | 先に明確にするべきこと |
| ホストまたはパスによるルーティング | どのレイヤーでTLSを解除し、HTTPを解析できるか |
| バックエンドで誤ったドメイン名やプロトコルが見える | 元のHost、アップストリームTLSのServerName、転送メタ情報 |
| インスタンス間のリクエスト数は均等だが、負荷が偏っている | 割り当ての単位と、リクエストごとのコスト |
| 連続リクエストがブロックされる、またはSSEがバッチ処理される | バッファリング、フラッシュ、アイドルタイムアウト |
| 自動リトライにより障害が拡大する | 試行予算、並列数、冪等性の契約 |
| 停止後も古いトラフィックが残る | 接続の排空(drain)、コネクションプーリング、長接続の状態 |
2つの接続、3つの混同しやすいアイデンティティ
レイヤー7のプロキシは通常、ダウンストリームのTLSを終端し、独立したアップストリーム接続を確立します。クライアントはプロキシが提示する公開サービス証明書を検証し、プロキシがHTTPSでバックエンドにアクセスする場合は、バックエンドのアイデンティティも検証する必要があります。バックエンドが見るTLSのクライアントは、プロキシである可能性が高く、最終ユーザーではありません。
また、以下をそれぞれ記録する必要があります。
- クライアントの元のリクエストのauthority
- プロキシがアップストリームに接続する際のTLS ServerName
- アプリケーションが絶対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つのビジネスデッドラインで全ての試行を制約する
接続タイムアウトは接続の確立を制限し、レスポンスヘッダータイムアウトはヘッダーの待機を制限し、読み取りアイドルタイムアウトは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 = 0.2
expected_attempts = 1 + p + p**2
assert abs(expected_attempts - 1.24) < 1e-12
assert abs(1000 * expected_attempts - 1240) < 1e-9
assert 3**3 == 27
print('残りミリ秒 / 秒あたりの期待試行数 / 再帰的な最悪試行数:', remaining, 1000 * expected_attempts, 3**3)
上記の独立した失敗は教育上の仮定です。実際の障害は関連していることが多く、リトライは依存関係をさらに圧迫する可能性があります。試行数、総時間、および追加のリトライトラフィックを制限し、適切なバックオフとジッターを使用することで、階層的なリトライの増幅を防ぐことができます。
各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?
時間条は同じ起点と截止線(デッドライン)を共有しており、初回の失敗によってそれらがシフトすることはありません。ここでの「利用可能な予算」は、まだ間に合うかどうかだけを答え、ビジネス操作を安全に繰り返し実行できるかどうかは答えません。これら2つの判断が両方とも満たされて初めて、リトライを行うべきです。
図を準備しています
各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?リトライ予算は、総デッドラインの残高であり、完全な時窗を新たに開始するものではない。残高がゼロの場合、どれだけリトライ回数を設定しても、時間は生じない。
{"id":"proxy-retry-deadline","title":"各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?","summary":"リトライ予算は、総デッドラインの残高であり、完全な時窗を新たに開始するものではない。残高がゼロの場合、どれだけリトライ回数を設定しても、時間は生じない。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"prior\" data-i18n=\"prior\"></label><output for=\"prior\" id=\"prior-value\"></output><input id=\"prior\" type=\"range\" min=\"0\" max=\"500\" value=\"140\" step=\"10\"></div><div class=\"control\"><label for=\"first\" data-i18n=\"first\"></label><output for=\"first\" id=\"first-value\"></output><input id=\"first\" type=\"range\" min=\"0\" max=\"850\" value=\"220\" step=\"10\"></div><div class=\"control\"><label for=\"reserve\" data-i18n=\"reserve\"></label><output for=\"reserve\" id=\"reserve-value\"></output><input id=\"reserve\" type=\"range\" min=\"0\" max=\"300\" value=\"100\" step=\"10\"></div><div id=\"timeline\" class=\"intervals\"></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"wrong\"></span><strong id=\"wrong\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){const p=+$('#prior').value,f=+$('#first').value,r=+$('#reserve').value,left=Math.max(0,850-p-f-r);for(const id of ['prior','first','reserve'])set(id+'-value',$('#'+id).value);intervals('timeline',[{start:0,end:850,label:'deadline 850 ms'},{start:0,end:p+f,label:'elapsed '+(p+f),cls:'gold'},{start:p+f,end:p+f+left,label:'retry '+left}],1650);set('remaining',left);set('wrong',p+f+850+r);set('equation','remaining = max(0,850 − '+p+' − '+f+' − '+r+') = '+left);$('#remaining').dataset.value=left;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"first":"初回試行の所要時間(ms)","heading":"各リトライでタイムアウトを完全にリセットすると、どれくらい待たされるのか?","intro":"経過時間、初回試行、返却予約を調整し、共有デッドラインとエラーリセット後の2回目の予算を見比べる。","prior":"試行前の経過時間(ms)","remaining":"2回目で利用可能な予算(ms)","reserve":"返却予約(ms)","reset":"リセット","scope":"総予算850 ms、直列2回試行;時間的許容度のみをチェックし、ビジネスの冪等性、失敗フェーズ、キャンセル、およびリクエストの再プレイ可能性は独立して満たす必要がある。","scopeLabel":"モデルの範囲と規約","wrong":"850msを再設定した場合の総消費時間上限"}}
バッファリング、ストリーミング、およびバックプレッシャ
レスポンスのバッファリングにより、プロキシはアップストリームからより速く読み取り、内容を一時保存してからクライアントにゆっくりと送信できるため、アップストリームがスロークライアントによって長時間占有されるのを防ぎます。その代償として、メモリ/ディスクの予算と、最初のバイトまたはチャンクごとのレイテンシの増加があります。リクエストのバッファリングは、スローなアップロードがバックエンドをどのように占有するか、およびリクエストボディが失敗後に再プレイ可能かどうかに影響します。
SSE、モデルのストリーミング出力、および大規模なダウンロードは、配信の要件に応じてフラッシュを設定する必要があります。アプリケーションがflushを呼び出しても、すべてのプロキシ層や圧縮器が即座に出力するとは限りません。すべてのバッファリングを無効にすることも、無料の最適化ではありません。スロークライアントはバックプレッシャをアップストリームに伝播させ、リソースを長期間占有する可能性があります。NGINX proxy buffering
コネクションプールの再利用はハンドシェイクを削減しますが、アップストリームのアイドル接続の寿命と調整する必要があります。バックエンドが既に閉じているが、プロキシがまだ認識していないプールの接続は、再利用時に失敗を引き起こします。リトライが可能かどうかは、依然としてリクエストのセマンティクスと送信フェーズに戻り、無限のリトライでプール設定の問題を隠してはいけません。
ヘルスチェックとリリース時の排空(drain)
アクティブなチェックは定期的にプローブを送信し、パッシブなチェックは実際のリクエストの失敗を観察します。これらは異なる問題を解決します。/readyzが200を返すことは、このインスタンスが新しい作業を受信する準備ができていることを示しますが、すべての共有依存関係の瞬間的なジッターを、インスタンスプール全体の同時削除に無条件に拡散してはいけません。
検出周期、失敗閾値、およびプローブのタイムアウトは、検出遅延を共同で決定します。10秒ごとにプローブし、連続して2回の失敗後にのみ削除する場合でも、障害発生後ちょうど20秒で切り替わるわけではありません。発生位相、プローブの所要時間、およびスケジューリングの影響を受けます。復旧時には、トラフィックを段階的に増加させ、キャッシュや接続をウォームアップする必要があります。
停止する際は、まず新しい作業の受信を停止し、次に受信済みのリクエストの終了を待ち、最後に期限までに残余の接続を閉じます。HTTP/2およびHTTP/3では、それぞれGOAWAYを使用できます。WebSocketやSSEなどの長接続では、最大の排空時間とクライアントのリトライ動作を明確にする必要があります。インスタンスをディスカバリリストから削除しただけでは、既存のすべての接続が即座に消失することはありません。
ブルーグリーンデプロイメントや重み付きグレーデッドリリースでは、セッションアフィニティ、リクエストコスト、およびサンプル分布も考慮する必要があります。1%のリクエストが必ずしも1%のユーザーをカバーするわけではなく、大規模テナントや特定のパスを欠落させる可能性があります。指標はバージョン、ルーティング、およびエラータイプ別に同時に観察する必要があります。
観測:1つの失敗を具体的な試行に対応させる
アクセスログには、少なくともリクエストID、ルーティング、選択されたアップストリーム、試行回数、ステータス、および重要な所要時間を関連付ける必要があります。クライアントの総レイテンシをキューイング、接続確立、ヘッダー待機、およびレスポンス転送に分解することで、「アップストリームが応答しない」「クライアントが読み取るのが遅い」「プロキシのプール待機」といった状況を区別できます。
キャッシュ、レートリミット、および認証も、どのレイヤーで実行されたかを明記する必要があります。キャッシュヒットによってバックエンドが迂回された場合、バックエンドのログにリクエストがないのは正常な現象です。ユーザートークン、元の証明書秘密鍵、または機密情報をデバッグログに混入しないでください。診断は、構造化された結果と必要な識別子に依存すべきです。
継続的な参照