Tech Learning Daily

2026-09-02 (Wed) — 第 48 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Network 🌳 応用 ⏱ 約 8 分

HTTP/2 でストリームをまとめたのに、なぜ HTTP/3 は TCP を手放したのか?

先週の記事で、HTTP/2 のストリーム多重化が「1 リクエストが 1 TCP 接続を占有する」問題を解決したと書いた。だが実はその多重化、TCP の内側では別の詰まりを生んでいた。HTTP/3 はその詰まりを消すために、信頼性の代名詞だった TCP そのものを手放すという大胆な選択をした。何が起きていたのか。

🎯 3 行まとめ
  • HTTP/2 が解決したのはアプリ層の HOL ブロッキングだけで、複数ストリームを 1 本の TCP に流し込む限り、パケットロス時は無関係なストリームまで TCP 側で足止めされる(トランスポート層の HOL ブロッキング)。
  • QUIC(RFC 9000)は UDP の上に自前の信頼性のある配送をストリームごとに実装し、ロストの影響を該当ストリームだけに閉じ込める。
  • QUIC は接続を IP・ポートではなくコネクション ID で識別する(RFC 9000 5 章・9 章)ため、Wi-Fi からモバイル回線への切り替えでも同じ接続を維持できる。

復習:HTTP/2 が解決したのは「アプリ層」の詰まりだけ

HTTP/1.1 は 1 本の TCP 接続で一度に処理できるリクエストが実質 1 つだったため、ブラウザは同一オリジンへの接続を複数本張って並列化していた。HTTP/2 はストリームという概念を導入し、1 本の TCP 接続の中で複数のリクエスト・レスポンスを並行してやり取りできるようにした。これでアプリケーション層では、あるリクエストの応答待ちが他のリクエストをブロックしなくなった。

ただし HTTP/2 のストリームは、あくまで 1 本の TCP 接続というパイプの中に多重化されている。TCP は「送った順にバイトを届ける」ことを保証する単一の順序付きバイト列であり、途中のセグメントが 1 つ失われると、それより後ろのバイトはたとえ別のストリームに属していても、再送が届いてカーネルのバッファが埋まるまで受信側に渡されない。ストリーム A のデータしか失っていなくても、ストリーム B・C まで同じ TCP 接続に乗っている以上、道連れで止まる。これが「トランスポート層の HOL ブロッキング」だ。

QUIC はどうやってこの道連れを消したか

「TCP をやめて UDP に乗せる」と聞くと、信頼性を諦めたように思えるかもしれない。しかし実際には QUIC(RFC 9000)は UDP の上に、TCP に相当する再送・順序制御・輻輳制御を自前で実装している。違いは、その信頼性のある配送を「接続 1 本」単位ではなく「ストリームごと」に独立させたことだ。QUIC のストリームはそれぞれが独立したバイト列として扱われ、あるストリームのパケットが失われても、他のストリームは待たされずに配送され続ける。

HTTP/2 over TCP(1本の順序列)
┌───┬───┬───┬───┬───┐
│ A │ B │ A │ C │ B │
└───┴───┴───┴───┴───┘
     ✕ ここでロスト
  A/B/C 全員がこの隙間で足止め

HTTP/3 over QUIC(ストリーム単位で独立)
┌───┐   ┌───┐   ┌───┐
│ A │   │ B │   │ C │
└───┘   └───┘   └───┘
 ✕ロスト   影響なし   影響なし
Aだけ再送待ち Bは継続  Cも継続

ハンドシェイクの統合と 0-RTT のトレードオフ

TCP 上に TLS を張る従来の構成では、TCP の 3 ウェイハンドシェイクで 1 RTT、その上で TLS 1.3 のハンドシェイクにさらに通常 1 RTT かかってから、ようやく最初のアプリケーションデータを送れる。QUIC はトランスポートの接続確立と TLS 1.3 のハンドシェイクを 1 つに統合しており、初回接続でもこれより少ない往復で完了する設計になっている。

さらに、以前接続したことがあるサーバーに対しては、QUIC はキャッシュした TLS セッション情報を使って最初のパケットにアプリケーションデータを乗せる 0-RTT を利用できる。ただし RFC 9000 の 5 章は「0-RTT はリプレイ攻撃に対する保護を一切提供しない」と明記している。0-RTT のパケットは通信路上でそのままコピーされて再送されても正当なリクエストと区別できないため、サーバー側は 0-RTT で送られてよいリクエストを冪等なものに限定し、そうでないと判断した場合は 425 Too Early を返して通常のハンドシェイクへのやり直しを強制する設計になっている。

QUIC は UDP の上に成り立つため、初めて訪れたクライアントはそのサーバーが対応しているかをそもそも知らない。そこで登場するのが Alt-Svc という応答ヘッダーで、HTTP/1.1 や HTTP/2 の応答に混ぜて「自分は h3 でも応答できる」とブラウザに予告する役割を持つ。

# サーバーが HTTP/3 を提供していることを HTTP/2 応答で予告する
Alt-Svc: h3=":443"

# ヘッダーを送らないと、ブラウザは QUIC が使えることを知る手段が
# なく、サーバーが対応済みでも HTTP/2 のまま通信し続ける

コネクション ID で接続を「生かし続ける」

TCP は接続を送信元 IP・送信元ポート・宛先 IP・宛先ポートの組で識別する。スマホが Wi-Fi からモバイル回線に切り替わると送信元 IP が変わるため、この組が変わってしまい、古い TCP 接続は継続できず、ハンドシェイクからやり直しになる。

QUIC は各エンドポイントが独自に選ぶコネクション ID で接続を識別する(RFC 9000 5.1 節)。IP アドレスやポートが下位層で変わっても、同じコネクション ID を名乗るパケットは同じ論理接続として扱われる。RFC 9000 の 9 章(Connection Migration)はこの仕組みを使い、クライアントがネットワークパスを切り替えても接続を維持できることを定めている。

🍱 たとえるなら

TCP は、荷物 A・B・C を 1 本の紐に順番通りに結わえて 1 人の配達員に運ばせるようなものだ。紐の途中で B が枝に引っかかって止まると、紐でつながっている以上 A も C も配達員も一緒に立ち往生する。QUIC は荷物ごとに別々の配達員を割り当てる。B の配達員が足止めされても、A と C の配達員はそのまま進み続ける。さらに QUIC の配達員は「配達員 ID」という名札で管理されているので、依頼主の家(IP アドレス)が引っ越しても、同じ ID の配達員が来た荷物だと分かれば配達を続けられる。

💼 実務でどう出会うか

Chrome / Firefox の DevTools の Network パネルで Protocol 列を表示すると、対応済みのサイトへのリクエストは h3 と表示される。Cloudflare や Amazon CloudFront のような CDN はエッジで HTTP/3 に対応しており、オリジンサーバー側のコードを変えなくても、CDN の前段だけで有効化できることが多い。自前でオリジンに導入する場合は nginx 1.25 系以降の QUIC モジュールのように、対応する Web サーバーソフトウェアが UDP のリスンと Alt-Svc の送出を別途行う必要がある。

⌨️ 手を動かす(5 分)

実際に HTTP/3 対応サイトが返す Alt-Svc ヘッダーを覗いて、ブラウザがどうやって QUIC の存在を知るのかを確認する。

curl -sI https://www.cloudflare.com | grep -i alt-svc

h3 を含む値(例えば alt-svc: h3=":443"; ...)が返ってくるはずだ。これは今まさに使った HTTP/2 の応答ヘッダーに埋め込まれた「予告」で、この応答を受け取ったブラウザは次回以降そのオリジンへの接続を UDP/443 の QUIC で試みるようになる。

🙅 よくある誤解
  • UDP を使う時点で QUIC は信頼性を犠牲にしている — UDP 自体は順序保証も再送もしないが、QUIC はその上に独自の再送・順序制御・輻輳制御をストリームごとに実装しており、信頼性を失っているわけではない。
  • HTTP/3 に切り替えれば常に HTTP/2 よりはっきり速くなる — 効果が大きいのはパケットロスが起きやすいモバイル回線やネットワーク切り替えが多い環境で、安定した低遅延の回線では体感できる差は小さいことが多い。
  • 0-RTT は再接続を無条件に安全に高速化できる — 0-RTT で送られたデータはリプレイ攻撃から保護されないため、サーバー側は送ってよいリクエストを冪等なものに制限し、必要なら 425 Too Early でやり直しを強制する。
📖 用語ミニ辞典
HOL ブロッキング(Head-of-Line Blocking)
先頭のデータが詰まることで、後続の無関係なデータまで処理を待たされる現象。層ごとに別々に起こりうる。
QUIC
UDP 上に構築された、ストリーム単位の信頼性と TLS 1.3 を統合したトランスポートプロトコル(RFC 9000)。
コネクション ID
QUIC が接続を識別するために各エンドポイントが選ぶ ID。IP・ポートが変わっても接続を同一視できる。
0-RTT
以前接続したサーバーに対し、ハンドシェイク完了前の最初のパケットにアプリケーションデータを乗せる機能。リプレイ攻撃への保護はない。
Alt-Svc
サーバーが別プロトコル(例: h3)で応答可能なことをクライアントに予告する応答ヘッダー。
🔗 もっと深く