HTTP/2の裏側:単一コネクションの甘い罠と、TCP輻輳制御が引き起こす「本当のボトルネック」
現場のインフラエンジニアやWeb APIをバリバリ開発している皆さん、こんにちは。
日々のアプリケーションの高速化、お疲れ様です。「HTTP/2を導入したから、とりあえずサイトが速くなった」「マルチプレクシング(多重化)のおかげで、HTTP/1.1のドメインシャーディングなんて過去の遺物さ」——そう思っていませんか?
確かに、ブラウザのネットワークタブを見れば、1本のTCPコネクション上で何十ものリクエストが並行して美しく流れていく様子を確認できます。しかし、ネットワークの最下層、すなわちLinuxカーネルのTCPスタックの視点に立ってみると、そこには「HTTP/2のマルチプレクシングゆえの、避けられないトレードオフ」が潜んでいます。
今回は、数々の現場で深夜のパケットキャプチャと格闘してきたシニアネットワークエンジニアの視点から、HTTP/2におけるTCP輻輳制御(Congestion Control)のリアルな挙動と、私たちが直面する「真のヘッド・オブ・ライン(HOL)ブロッキング」の正体に迫ります。
—
1. HTTP/2マルチプレクシングとTCPの構造的矛盾
HTTP/1.1では、同時並行でリクエストを捌くためにブラウザは複数のTCPコネクション(通常は同一オリジンあたり6本程度)を張っていました。これは「1本のコネクションが詰まっても、別のコネクションが生きていれば何とかなる」という冗長性(あるいは力技)でもありました。
これに対し、HTTP/2は「単一のTCPコネクションを極限まで使い倒す」アーキテクチャを採用しています。
[HTTP/1.1の時代]
コネクション1 ─── [リクエストA]
コネクション2 ─── [リクエストB]
コネクション3 ─── [リクエストC] ← 1本がロスしても他は進む
[HTTP/2の世界]
単一のTCPコネクション ─── [ストリーム1: API取得]
├─── [ストリーム2: 画像A]
└─── [ストリーム3: 画像B] ← ここが詰まると全部止まる!
この設計により、TCPハンドシェイクのオーバーヘッドやスロースタートのペナルティを最初に1回払うだけで済むという絶大なメリットを得ました。しかし、ここでTCPの基本原則を思い出してください。TCPは「バイトストリームの順序保証」を厳格に行うプロトコルです。
—
2. TCP輻輳ウィンドウ(CWND)とスロースタートの挙動
単一コネクション上で複数のHTTP/2ストリームが相乗りするということは、すべてのリクエスト/レスポンスが「単一の輻輳ウィンドウ(CWND: Congestion Window)」の枠を奪い合うことを意味します。
スロースタートの恩恵と足枷
新しいTCPコネクションが確立された直後、CWNDは非常に小さな値(初期値は通常10セグメント程度、Linuxの近年のカーネルでは`initcwnd = 10`が標準)からスタートします。
1. 初期状態: サーバーは小さなCWNDの範囲内でしかパケットを送信できません。
2. ACK受信: クライアントからのACKを受け取るたびに、CWNDは指数関数的に拡大します(スロースタート)。
3. 帯域飽和: やっと帯域幅の限界(BDP: Bandwidth-Delay Product)に到達し、高速にデータが流れるようになります。
HTTP/2では、この「ウォームアップ(暖気運転)」のコストを最初のコネクション確立時の一回だけで済ませられるため、小規模なアセットを大量に取得する際には圧倒的なパフォーマンスを発揮します。
—
3. パケットロスがもたらす致命傷:TCP層のHOLブロッキング
しかし、この「相乗り構造」は、ひとたびパケットロスが発生した瞬間に牙を剥きます。
回線の揺らぎや無線環境の不安定さによって、パケットが1つドロップしたとしましょう。TCPは信頼性を担保するため、ロスしたパケットが再送され、正常に受信側で順序が復元されるまで、それ以降のすべての順序待ちデータをアプリケーション層(この場合はHTTP/2レイヤー)へ渡すことを停止します。
これが、HTTP/2におけるTCPレイヤーのヘッド・オブ・ライン(HOL: Head-of-Line)ブロッキングです。
- ストリーム1(重要度の高いAPIレスポンス) のパケットがロスした。
- ストリーム2(どうでもいい装飾用アイコン) のデータは届いているのに、TCPバッファで足止めを食らう。
- 結果として、画面全体の描画がストップする。
HTTP/1.1であれば、ストリーム2用の別コネクションはスイスイ流れていたはずの状況でも、HTTP/2では1本の運命共同体であるがゆえに全体が巻き添えを食うのです。これが、悪条件のモバイル回線などでHTTP/2が予期せぬレイテンシーの悪化を引き起こす原因となります。
—
4. 実務での検証・デバッグ手法
机上の空論を語っても仕方がありません。実際に私たちのアプリケーションやインフラがこの影響を受けていないか、あるいはどのように挙動しているかを検証するための実務的なアプローチを見ていきましょう。
① curlを用いたHTTP/2コネクションとストリームの確認
まずは、手元の環境でターゲットサーバーが正しくHTTP/2で通信し、どのようなタイミングでデータを返しているかを`curl`でプロファイルします。
HTTP/2での通信詳細(タイミングやプロトコルバージョン)をデバッグ出力する
curl -Iv –http2 https://api.example.com/v1/users \
-w “HTTP Version: %{http_version}\nTime Connect: %{time_connect}s\nTime Start Transfer: %{time_starttransfer}s\n”
- 実務Tips: `-w` オプションで `time_starttransfer`(最初のバイトを受け取るまでの時間)を計測することで、TCPのスロースタートや初期ハンドシェイクの遅延を定量的に評価できます。
② Python (requests / httpx) による多重化挙動のシミュレーション
PythonでAPIクライアントを実装する際、`requests` はデフォルトでHTTP/1.1ですが、モダンな `httpx` を使えばHTTP/2(Multiplexing)の恩恵を受けたクライアントを簡単に記述できます。
import httpx
import time
HTTP/2を有効にしたクライアントを作成
注意: サーバー側がHTTP/2 (ALPN) に対応している必要があります
with httpx.Client(http2=True) as client:
start_time = time.time()
# 複数のエンドポイントへ同時にリクエストを投げる(同一コネクションのマルチプレクシング)
urls = [
“https://httpbin.org/delay/1”,
“https://httpbin.org/json”,
“https://httpbin.org/uuid”
]
# 内部的に単一のTCPコネクション上でストリームが多重化される
responses = [client.get(url) for url in urls]
for resp in responses:
print(f”URL: {resp.url}, Status: {resp.status_code}, Time: {resp.elapsed.total_seconds()}s”)
print(f”総所要時間: {time.time() – start_time:.2f}秒”)
—
5. インフラ・サーバー側でのチューニング指針
このTCP輻輳制御とHOLブロッキングのジレンマに対し、インフラエンジニアとしてどのように立ち向かうべきでしょうか。現場で使える具体的なチューニング指針を整理します。
① BBR輻輳制御アルゴリズムの採用(Linuxカーネル 4.9以降)
従来のTCP輻輳制御(CUBICなど)は、「パケットロス=輻輳(混雑)」と検知するため、無線環境などで突発的なロスが発生した際にCWNDを急激に縮小させ、スループットが大幅に低下していました。
Googleが開発した BBR (Bottleneck Bandwidth and Round-trip propagation time) は、パケットロスではなく「帯域とRTT(往復遅延時間)」をベースに制御するため、ロスに強く、HTTP/2のパフォーマンスを劇的に安定させます。
設定例(Linuxカーネルパラメータ `/etc/sysctl.conf`):
利用可能な輻輳制御アルゴリズムに bbr が含まれているか確認した上で適用
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
※適用後は sudo sysctl -p で反映させてください
② 適切な初期輻輳ウィンドウ(initcwnd)の維持
クラウド環境(AWSのALBやNginxコンテナなど)において、初期パケット数がデフォルトで小さく絞られている場合があります。適切な帯域があることが分かっている環境であれば、初期ウィンドウを広げることでスロースタートのペナルティを軽減できます。
③ 次世代プロトコル「HTTP/3 (QUIC)」への移行という選択肢
HTTP/2が抱える「TCP層のHOLブロッキング」という根本的な構造欠陥を解決するために生まれたのが、UDPベースのトランスポート層プロトコルである QUIC(HTTP/3) です。
QUICはコネクション内でストリームが完全に独立しており、あるストリームでパケットロスが起きても、他のストリームのデータは一切ブロックされません。インフラのモダン化を進める上での究極のカードとなります。
—
まとめ
HTTP/2は魔法の杖ではありません。「1本のコネクションで効率よく通信できる」というメリットの裏側で、TCPの輻輳制御と順序保証という物理法則の制約を一身に背負っていることを忘れてはならないのです。
- モバイルや劣悪な回線環境では、TCPパケットロスによるHOLブロッキングが全体をスローダウンさせる。
- 対策として、LinuxカーネルのBBR化や、将来的にはHTTP/3 (QUIC) への移行を視野に入れる。
ネットワークの挙動原理を深く理解し、表面的ではない「本当の高速化」を一緒にデザインしていきましょう。現場からは以上です!
コメント