なぜHTTP/1.1は「詰まる」のか?―HTTP/2の多重化がもたらした革命をエンジニアの視点で紐解く
ネットワークエンジニアとして現場を渡り歩いていると、「Webサイトが妙に重い」というトラブルに直面することがよくある。ブラウザのデベロッパーツールを開き、ウォーターフォール図を眺めれば、リクエストが階段状に並び、紫色のバー(待機時間)が不自然に長いことに気づくはずだ。
この現象の元凶こそ、HTTP/1.1の限界である。今日は、HTTP/1.1が抱えていた「HOLブロッキング」という呪縛を、HTTP/2がどのように解き放ったのか、その本質を深掘りしていこう。
—
1. HTTP/1.1の悲劇:パイプライニングとHOLブロッキング
HTTP/1.1における通信の基本は「シリアル処理」だ。一つのTCPコネクションに対してリクエストを投げたら、レスポンスが返ってくるまで次のリクエストは待たなければならない(厳密にはパイプライニングという仕様もあったが、実装上のトラブルが多すぎて実質「死に体」だった)。
なぜこれが問題なのか?
これが悪名高いHead-of-Line (HOL) ブロッキングだ。
例えば、巨大な画像ファイルを一つ取得している最中に、その裏で小さなJSやCSSが待機しているとする。もし先頭の画像リクエストがパケットロスで再送処理(TCPの再送制御)に陥ると、後ろに並んでいるすべてのリクエストは、先頭の処理が終わるまでTCPのバッファ内で塩漬けにされる。
いわば、「スーパーのレジで、前の客が小銭を探してモタついている間に、後ろの全員が一切動けない」という状態だ。これがHTTP/1.1がWebのボトルネックと言われ続けた理由である。
—
2. HTTP/2の処方箋:バイナリフレームと多重化
HTTP/2は、この「行列」の概念を根本から覆した。解決策は「バイナリフレーム化」と「ストリーム多重化」だ。
フレームという単位への分解
HTTP/2では、メッセージをそのままTCPに流すのではなく、一度「フレーム」という小さな断片に分割する。
- `HEADERS` フレーム:HTTPヘッダー情報
- `DATA` フレーム:ペイロード(ボディ)情報
これにより、1つのTCPコネクションの中に、複数のストリーム(論理的な通信路)を同時に共存させることが可能になった。これが「多重化(Multiplexing)」だ。
解決のロジック
もし特定のパケットがロスしても、それは「特定のストリーム」のデータフレームが欠けるだけ。他のストリームで送られている別のファイルは、何事もなかったかのように処理を続けられる。レジの例で言えば、「前の客の小銭確認が終わらなくても、別のレジ(ストリーム)で会計を済ませる」ことができるようになったわけだ。
—
3. 実践:HTTP/2の挙動を追う
言葉だけでは実感が湧かないだろう。実際にツールを使って、通信がどう変化しているか確認してみよう。
curlでHTTP/2を確認する
まずは手元の環境から。`curl`で特定のサイトに対してHTTP/2でリクエストを投げるコマンドだ。
-I: ヘッダーのみ取得
–http2: 明示的にHTTP/2を使用
-v: 冗長モードで通信の詳細を表示
curl -I https://www.google.com –http2 -v
出力の中に `Using HTTP/2 over TLS/TCP` や `h2` という文字列が見えるはずだ。もしローカルでWebサーバーを立てて検証するなら、Nginxの設定を以下のように調整して確認してほしい。
Nginxの設定例 (HTTP/2を有効化)
server {
listen 443 ssl http2; # “http2” を指定するだけで多重化が有効になる
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
# HTTP/2が動いているかレスポンスヘッダーで確認できるようにする
add_header X-Protocol $server_protocol;
}
}
Pythonで並列リクエストを考える(Fetch APIのイメージ)
ブラウザのFetch APIでは、HTTP/2であれば同一ドメインへのリクエストは自動的に多重化される。Pythonの`httpx`ライブラリを使うと、この挙動を意識したクライアントが書ける。
import httpx
import asyncio
HTTP/2をサポートしたクライアントの作成
async def fetch_parallel():
async with httpx.AsyncClient(http2=True) as client:
# 同時に複数のリクエストを投げる
# HTTP/2ならこれらは同じTCPコネクションを共有し、多重化されて流れる
tasks = [client.get(“https://example.com/api/data1”),
client.get(“https://example.com/api/data2”)]
responses = await asyncio.gather(tasks)
print([r.status_code for r in responses])
asyncio.run(fetch_parallel())
—
4. エンジニアへの教訓:インフラ運用での注意点
HTTP/2の多重化は万能薬ではない。現場でよくある失敗を一つ挙げておく。
「コネクションの共有しすぎ」には注意が必要だ。
HTTP/2は1つのコネクションを極限まで使い倒す。そのため、もしバックエンドのAPIサーバーでコネクションプールを過剰に制限していると、TCPの輻輳制御が働き、かえってパフォーマンスが落ちることがある。
- デバッグTips: ネットワークのレスポンスが遅いと感じたら、`chrome://net-export/` で出力されたログを「NetLog Viewer」で確認してほしい。リクエストがどのストリームIDで、どの程度待たされているかが可視化できる。
まとめ
HTTP/1.1の「リクエストの直列化」という制約を、HTTP/2は「バイナリフレームによる多重化」で見事に克服した。この進化のおかげで、私たちは単一のTCPコネクションで高速なWeb体験を享受できている。
しかし、TCP自体が持つHOLブロッキング(TCPはパケットの順序性を保証するため、ロスしたパケットがあると後ろが詰まる)という最後の砦は、さらにその先のHTTP/3 (QUIC)によって解決されることになる。
ネットワークの進化は、常に「いかにして詰まり(ブロッキング)を回避するか」の歴史だ。この視点を持ってインフラを設計・運用すれば、きっと君の作るサービスは、より速く、より強靭なものになるはずだ。
コメント