【実務・中級編】HTTP/2とTCP輻輳制御(Congestion Control)の相互作用 – HTTPプロトコル・通信規格実践ガイド

HTTP/2の「1つのTCP接続」という美徳が、現場のインフラで牙を剥く瞬間:TCP輻輳制御とHead-of-Line Blockingの深い関係

こんにちは。ネットワークの底流で蠢くパケットの息遣いに長年耳を傾けてきたシニアアーキテクトの私です。

Web APIの設計や、大規模なインフラのパフォーマンスチューニングに携わる読者の皆さんなら、日々の業務で「どうすればレイテンシーを極限まで削れるか」に腐心していることでしょう。HTTP/1.1のコネクション枯渇とHOL(Head-of-Line)ブロッキングを過去のものにする救世主として登場したHTTP/2。私たちはその「単一のTCP接続上で複数のストリームを多重化(Multiplexing)する」という鮮烈なアーキテクチャに酔いしれました。

しかし、現場の最前線で数々のパケットキャプチャや障害対応をくぐり抜けてきたエンジニアなら、知っているはずです。「レイヤーの異なるプロトコルを重ね合わせるとき、下のレイヤーのルールを無視した最適化は、必ずどこかで歪みを生む」という鉄則を。

今回は、HTTP/2のマルチプレクシングが、TCPの輻輳制御(CUBICやBBR)とどのように相互作用し、ときには私たちの頭を悩ませる「TCPレイヤーのHead-of-Line Blocking」を引き起こすのか。そのメカニズムと、実務で使える実践的な対策を紐解いていきます。

—

1. HTTP/2のストリーム多重化と、TCPの「単一の巨大パイプ」というジレンマ

HTTP/1.1では、ドメインあたり最大6つ程度のTCPコネクションを並行して張り、ブラウザはリクエストを分散させていました。これはTCPの輻輳ウィンドウ(cwnd)を複数のパイプラインに分散させ、帯域を強引に奪い合うアプローチでした。

一方、HTTP/2は「1つのTCP接続」を極限まで使い倒します。1つのコネクションの中に論理的な「ストリーム(Stream)」が無数に存在し、HEADERSフレームやDATAフレームがインタリーブ(混在)して流れていきます。

[HTTP/2 Layer] Stream 1 (API GET) —–\
Stream 2 (Image) —–> [ 単一のTCP接続 ] -> Internet
Stream 3 (Script) —–/

この設計はアプリケーション層・トランスポート層のコネクション確立オーバーヘッド(TCPハンドシェイクやTLSのフルハンドシェイク)を劇的に削減しました。しかし、ここにTCPの輻輳制御アルゴリズムの基本思想との摩擦が生まれます。

TCPの輻輳制御(CUBIC / BBR)にとってHTTP/2とは何か?

TCPは、ネットワークの混雑状況を推測するために「輻輳ウィンドウ(cwnd)」という概念を持っています。

  • CUBIC: パケットロスを「輻輳のシグナル」と捉え、ロスが起きるまでウィンドウサイズを三次関数的に急拡大させ、ロスが起きるとウィンドウを急激に縮小させる、お馴染みのロスベースのアルゴリズムです。
  • BBR(Bottleneck Bandwidth and RTT): Googleが開発したモデル。パケットロスではなく、ボトルネックの帯域(BDP)と伝搬遅延(RTprop)を継続的に測定し、ネットワークが最も効率よく流れる「パイプの満杯状態」を維持します。

ここで問題になるのは、HTTP/2のすべてのストリームが「単一のTCPの輻輳ウィンドウ」という同じボートに乗っているという事実です。

ある1つの重いストリーム(例えば、数MBのJSONレスポンスや動画セグメント)がパケットロスを引き起こし、TCP層で再送が発生したとしましょう。CUBICであれば、この単一のロスによってcwndは容赦なく半分に絞られます。その結果、同じTCP接続上で何食わぬ顔して流れていたはずの、数バイトの緊急を要するAPIレスポンス(Stream 2)の転送速度まで、もれなく一緒に低下してしまうのです。

これが、HTTP/2における「TCPレイヤーのHead-of-Line Blocking(HOLB)」の本質です。

—

2. パケットの挙動:何が起きているのか?

実際の通信フローをシーケンスで追ってみましょう。ブラウザ(クライアント)が1つのHTTP/2コネクション上で、軽量なAPIリクエストと重量級のデータ取得リクエストを同時に投げたケースを想定します。

Client (HTTP/2) Server
| |
|— [Stream 1: API Get] (Headers/Data) —->|
|— [Stream 2: Large Asset] (Data x 10) —>|
| |
| <--- [TCP ACK 遅延 / パケットロス発生] ---| | | | [TCP層: Stream 2のセグメントでロス検知] | | [TCP層: 選択的確認応答(SACK)と再送待ち] | | | | ❌ 【ここで全ストリームが足止め】 | | [Stream 1のデータもTCPバッファで待機状態] | | | |--- [TCP Retransmit (Stream 2 Segment)] --->|
| |
| <--- [Stream 1のデータが流れてくる] ------| アプリケーション層(HTTP/2)からは「ストリームは完全に独立している」ように見えますが、下層のTCPはストリームの境界など知りません。TCPにとっては、単なる「バイトストリームの連続」に過ぎないため、途中のシーケンス番号が欠損すると、順序を保証(Ordered Delivery)するために、後続のデータ(たとえ別のHTTP/2ストリームに属するものであっても)をアプリケーション層に引き渡すことができなくなるのです。 このジレンマこそが、次世代プロトコルであるHTTP/3(QUIC)が、TCPを捨ててUDPベースのトランスポート層を採用した最大の理由です。

—

3. 実務での影響とデバッグ・検証手法

では、この現象に直面したとき、インフラエンジニアやバックエンドエンジニアはどのように検知し、向き合うべきでしょうか。実務で使える具体的な検証手法と設定アプローチを見ていきましょう。

検証:curlを用いたHTTP/2の挙動確認

まずは、手元の環境やステージング環境でサーバーがしっかりとHTTP/2で応答しているか、そしてどのようなコネクションが張られているかを確かめる基本のコマンドです。

HTTP/2での通信を指定し、詳細なネゴシエーションログを出力する
curl -Iv –http2 https://api.yourdomain.com/v1/health

出力のチェックポイント:

  • Connected to api.yourdomain.com (192.0.2.1) port 443 (#0)
  • Using HTTP/2, server supports multiplexing
  • Connection state changed (HTTP/2 confirmed)

…
< HTTP/2 200 < content-type: application/json `Using HTTP/2, server supports multiplexing` が確認できれば、単一のTCP接続上での多重化が有効になっています。

インフラ設定:LinuxカーネルのTCP輻輳制御の確認と変更

もし、大規模なAPIサーバーを運用しており、パケットロス率が無視できないモバイル回線からのアクセスが多い場合、デフォルトの `cubic` から `bbr` への移行を強く推奨します。BBRはパケットロスに対する耐性が高く、CUBICのようにロス=即減速とならないため、HTTP/2の多重化環境下でもスループットが落ちにくい特性があります。

現在使用している輻輳制御アルゴリズムの確認:

sysctl net.ipv4.tcp_congestion_control
出力例: net.ipv4.tcp_congestion_control = cubic

一時的にBBRに変更する(要 root 権限 / Linux Kernel 4.9以降):

sudo sysctl -w net.ipv4.tcp_congestion_control=bbr

永続化設定(`/etc/sysctl.conf` や `/etc/sysctl.d/99-tcp-tuning.conf` 等に記述):

BBR輻輳制御アルゴリズムの有効化
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

(※BBRを有効にする際は、キューイング規律(qdisc)に `fq` (Fair Queueing)を指定することが公式に推奨されています。)

—

4. アプリケーション設計・API設計における実践的対策

OSのチューニングやトランスポート層の変更以外にも、アプリケーションの設計思想を工夫することで、HTTP/2とTCP輻輳制御の摩擦を回避・軽減することができます。

対策1: 「重いレスポンス」と「リアルタイム性が必要なAPI」のドメイン分離

もしあなたのシステムに、数メガバイトの巨大なバイナリデータを返すエンドポイントと、数ミリ秒のレイテンシーが命である軽量なAPIエンドポイントが混在しているなら、それらを同一のHTTP/2コネクション(同一オリジン)で処理させるべきではありません。

HTTP/2の多重化の恩恵を受けたいからといって、すべてを1つのドメインに集約すると、前述した「1つの重いストリームによる全ストリームの巻き添え事故」の餌食になります。

  • API用ドメイン: `api.yourdomain.com` (軽量・高速なJSONレスポンス専用)
  • アセット/メディア用ドメイン: `cdn.yourdomain.com` (大容量データ専用)

ブラウザは異なるドメインに対しては「別々のTCP接続」を確立するため、重いアセットの転送でTCPのウィンドウが絞られても、API用のTCPコネクションは健全な状態を保つことができます。

対策2: 実装コードにおけるタイムアウトとプログレス制御(Pythonの例)

バックエンドのマイクロサービス間通信や、外部APIクライアントを実装する際も、HTTP/2の特性を意識したタイムアウト設定が不可欠です。Pythonの `httpx` ライブラリ(HTTP/2をネイティブサポート)を用いた堅牢なクライアント設定の例を示します。

import httpx
import sys

def fetch_critical_data():
# HTTP/2を有効化したクライアントの生成
# 接続プールやタイムアウトを適切に管理する
limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
timeout = httpx.Timeout(connect=2.0, read=5.0, write=2.0, pool=5.0)

# Note: httpxでHTTP/2を使うには `h2` パッケージのインストールが必要 (pip install httpx[http2])
with httpx.Client(http2=True, limits=limits, timeout=timeout) as client:
try:
# 軽量で重要なAPIリクエスト
response = client.get(“https://api.yourdomain.com/v1/critical-status”)
response.raise_for_status()

print(f”Status Code: {response.status_code}”)
print(f”Payload: {response.json()}”)

except httpx.TimeoutException as e:
# TCPのHOLブロッキングやネットワーク輻輳により読み込みが遅延した場合のフォールバック
print(f”[警告] ネットワークの輻輳またはタイムアウトが発生しました: {e}”, file=sys.stderr)
# ここでサーキットブレーカーの発動やキャッシュからのフォールバック処理を実装する

except httpx.HTTPStatusError as e:
print(f”HTTPエラー: {e.response.status_code}”, file=sys.stderr)

if __name__ == “__main__”:
fetch_critical_data()

このような実装では、単にエラーをキャッチするだけでなく、「ネットワーク層の遅延がアプリケーションの致命傷にならないようにする」ための防衛策(タイムアウトの厳格化)が組み込まれています。

—

まとめ

HTTP/2のマルチプレクシングは、Webのパフォーマンスを次のステージへと押し上げた偉大な規格です。しかし、「単一のTCP接続」という仕組みは、下層にあるTCPの輻輳制御(CUBICやBBR)やパケットロス時の順序保証(HOLブロッキング)という物理的な制約から私たちを完全に解放してくれたわけではありません。

現場のインフラエンジニアやアーキテクトに求められるのは、プロトコルの美辞麗句に惑わされず、「パケットが今、どのレイヤーで、どのような渋滞を起こしているか」を想像する力です。

  • 重いデータと軽いデータでドメインを適切に分離する。
  • カーネルの輻輳制御アルゴリズム(BBRなど)をユースケースに合わせて最適化する。
  • アプリケーション層で適切なタイムアウトとエラーハンドリングを設計する。

これらを組み合わせることで、HTTP/2のポテンシャルを最大限に引き出しつつ、現場のトラブルを未然に防ぐ堅牢なシステムを構築できるはずです。さあ、今すぐあなたのシステムのTCP統計やドメイン設計を見直してみませんか?

コメント

タイトルとURLをコピーしました