HTTP/1.1の「行列」とHTTP/2の「並走」:パケットレベルで紐解く多重化の真実
ネットワークエンジニアとして現場に立っていると、「HTTP/2にすれば速くなる」という曖昧な言説を耳にする。しかし、なぜ速いのか、その裏側でTCPセッションとTLSレコードがどう悲鳴を上げ、あるいは歓喜しているのかを正確に語れる者は意外と少ない。
今回は、HTTP/1.1のパイプライニングが抱えた「呪い」と、HTTP/2がバイナリフレームという武器を手にしてどうそれを克服したのか、その深淵を覗いていくことにしよう。
—
HTTP/1.1の「HOLブロッキング」という悪夢
HTTP/1.1の最大の問題は、TCPという信頼性の高いトランスポート層の上に「直列」という制約を強いたことにある。
1. パイプライニングの限界
HTTP/1.1のパイプライニングは、リクエストを立て続けに送ることでRTT(Round Trip Time)を稼ごうとした試みだ。しかし、サーバー側の処理が順序通りであることを強制されるため、先頭のリクエストが重い処理(DBのI/O待ちなど)に捕まると、後続のリクエストはすべてTCPバッファの中で塩漬けになる。これがHOL(Head-of-Line)ブロッキングだ。
2. TCPレベルでの悲劇
もし1つのパケットがロスすれば、TCPの再送制御により、後続のパケットはたとえ届いていてもアプリケーション層に渡されない。単一のTCPストリームに複数のHTTPリクエストを詰め込むことは、まさに「一本道での玉突き事故」を待つようなものだった。結果、ブラウザはこれを回避するためにドメインシャーディングを行い、6本ものTCPコネクションを並列させるという非効率な力技で対抗せざるを得なかったのだ。
—
HTTP/2:バイナリフレームによる「革命」
HTTP/2の真骨頂は、ストリームの「多重化(Multiplexing)」にある。これは単純な並列化ではない。データを「フレーム」という単位に切り刻み、インターリーブ(混合)させることで実現される、極めて洗練されたアーキテクチャだ。
バイナリフレーム化の恩恵
HTTP/1.1がテキストベースのヘッダーを解析するのにCPUを浪費していたのに対し、HTTP/2はバイナリフレーム(`HEADERS`, `DATA`, `SETTINGS`など)を採用した。これにより、構文解析は決定論的かつ高速になり、ヘッダー圧縮アルゴリズムであるHPACKが導入された。
HPACKは、静的テーブルと動的テーブルを用いて、HTTPヘッダーの冗長性を排除する。例えば、`User-Agent`や`Cookie`のような巨大な文字列は、一度送信すれば後はインデックス番号に変換される。これにより、パケットサイズは劇的に削減される。
—
パフォーマンスチューニング:カーネルとTCPの最適化
HTTP/2を真に活かすには、アプリケーション層だけではなく、TCP/IPスタックの調整が不可欠だ。以下は、高負荷なHTTP/2サーバーを運用する際のLinuxカーネルパラメータの推奨設定例である。
TCPウィンドウサイズの拡大:高RTT環境でのスループット低下を防ぐ
読み取りバッファの最小/デフォルト/最大値
sysctl -w net.ipv4.tcp_rmem=”4096 87380 16777216″
書き込みバッファの最小/デフォルト/最大値
sysctl -w net.ipv4.tcp_wmem=”4096 65536 16777216″
TCP接続の急増に備えたバックログの拡大
sysctl -w net.core.somaxconn=65535
TCP Fast Openの有効化:TLSハンドシェイク時のRTTを削減する
sysctl -w net.ipv4.tcp_fastopen=3
解説:
- `tcp_rmem/wmem`: 高速なネットワーク環境では、デフォルトのウィンドウサイズでは帯域を使い切れない。最大値を16MB程度まで引き上げることで、TCPの「帯域×遅延積(BDP)」をカバーする。
- `tcp_fastopen`: 3ウェイハンドシェイクの過程でデータを送信可能にする。HTTP/2のTLSハンドシェイクと組み合わせることで、体感速度を劇的に改善する。
—
セキュリティの盲点:HTTP/2における新たな脅威
多重化は万能ではない。HTTP/2の仕様では、クライアントは無制限にストリームを開くことができる。これを悪用したのが「HTTP/2 Rapid Reset」のような攻撃だ。
攻撃者はストリームを作成した直後に`RST_STREAM`フレームを送ることで、サーバーのCPUリソースを枯渇させる。これを防ぐためには、サーバー側でストリームの同時実行数(`SETTINGS_MAX_CONCURRENT_STREAMS`)を適切に制限し、レートリミットを厳格に適用する必要がある。
Nginxの設定例:ストリームの乱用を防ぐ
http {
# 同時ストリーム数を制限
http2_max_concurrent_streams 128;
# リクエストボディのサイズを制限し、メモリ枯渇を防ぐ
client_body_buffer_size 16k;
client_max_body_size 1m;
}
—
結びに代えて:プロトコルの進化を理解するということ
HTTP/1.1からHTTP/2への進化は、単なる「速くなった」という結果論ではなく、「いかにして限られた帯域を効率的に使い、TCPの物理的な制約を論理層でバイパスするか」という、極めてエンジニアリング的な知恵の結晶である。
そして現在、HTTP/3(QUIC)がその先の地平を見据えている。UDPベースのQUICがなぜHOLブロッキングを「トランスポート層そのもの」から排除できたのか。その議論はまた別の機会に譲るとしよう。
ネットワークは生き物だ。プロトコルの挙動をパケットレベルで可視化し、カーネルの奥底にあるバッファの状態を想像できる者にのみ、極限のパフォーマンスは微笑むのである。
コメント