HTTP/2 Multiplexing:パケットの渋滞を解消する「並列処理」の魔法
ネットワークエンジニアとして現場を渡り歩いていると、いまだに「HTTP/1.1の呪縛」から抜け出せずに苦しんでいる設計に遭遇することがあります。クライアントとサーバーの間に無数のTCP接続を張り、キープアライブのタイムアウト値と睨めっこし、さらには「ドメインシャーディング(サブドメインを分けて接続数を稼ぐ技)」で無理やり並列化を図る……。
これら全て、HTTP/2の登場とともに過去の遺物となりました。今回は、Web APIのパフォーマンスを劇的に変える「HTTP/2 Multiplexing」の深淵に迫ります。
—
1. HTTP/1.1の限界:ヘッド・オブ・ライン・ブロッキング(HOLB)
HTTP/1.1の通信において、ブラウザやクライアントは「リクエストした順番通りにレスポンスが返ってくる」ことを期待しています。これが不幸の始まりです。
もし最初の大きな画像ファイルの読み込みに時間がかかると、その後に続く軽量なAPIレスポンスのパケットが、TCPバッファの先頭で立ち往生してしまう。これが「HTTP層におけるヘッド・オブ・ライン・ブロッキング(HOLB)」です。たとえネットワーク帯域に余裕があっても、前のリクエストが完了しない限り、後ろは一切動けない。まるで、先頭の客が会計でモタついているせいで、全員の列が止まってしまうスーパーのレジと同じです。
—
2. Multiplexingの仕組み:ストリームという名の「高速道路」
HTTP/2は、この問題を「ストリーム(Stream)」という概念で解決しました。
HTTP/2では、単一のTCP接続の中で、複数の独立したストリームを同時に扱います。各ストリームには個別のIDが割り振られ、リクエストとレスポンスの断片(フレーム)が、まるでパズルのピースのように混ぜ合わされて送信されます。
なぜこれが速いのか?
- 非同期通信: レスポンスが順番通りに返ってくる必要がないため、サーバーは準備ができたものから順にフレームを投げればよい。
- 効率的なTCP利用: 1つのTCP接続を使い切るため、TCPの「スロースタート」問題(接続初期の通信速度が遅い現象)を一度のハンドシェイクで済ませられる。
—
3. 実務で見る「Multiplexing」の観測方法
現場で「本当にMultiplexingできているのか?」を確認する際、私はよく curl を使います。HTTP/2通信をデバッグするための強力なツールです。
# -v で詳細なやり取りを表示
# --http2 でHTTP/2プロトコルを強制的に使用
curl -I -v --http2 https://api.example.com
実行結果の中に以下のような記述があれば、それは正常にHTTP/2でネゴシエーションが完了している証拠です。
# 接続時にHTTP/2プロトコルが選択されたことを示す
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
また、ブラウザの「ネットワーク」タブを確認する際も、Protocol カラムが h2 になっているか必ずチェックしてください。ここが http/1.1 のままだと、どんなに美しいAPIを設計しても、HOLBの罠からは逃げられません。
—
4. Web API設計における注意点:やりすぎは禁物
Multiplexingは魔法ではありません。特に、API設計者が陥りがちな罠が「過度なリクエストの分割」です。
HTTP/2が使えるからといって、1画面で数百個の小さなGETリクエストを投げるような設計は避けるべきです。TCPの単一接続を使っている以上、ネットワーク上のパケットロス(TCPパケットの欠落)が発生すると、結局そのコネクション上の全ストリームがTCPレベルで再送待ち(TCP HOLB)に巻き込まれます。
現場のTips:
- リソースの統合: API設計の基本である「リソースの粒度」は守りましょう。1つの画面に多くの情報が必要なら、GraphQLや、適切なクエリパラメータによる「フィルタリング/プロジェクション」を活用し、リクエスト数を適正に保つことが、結果として最も高いパフォーマンスを生みます。
—
5. Pythonによる確認:ストリームの挙動を体感する
最後に、httpx ライブラリを使って、実際に並列リクエストが効率的に処理されているかを確認する簡単なスクリプトを紹介します。
import httpx
import asyncio
# 非同期で複数のAPIリクエストを投げる
async def fetch_api(client, url):
resp = await client.get(url)
print(f"URL: {url}, Status: {resp.status_code}, HTTP Version: {resp.http_version}")
async def main():
# http2=True を明示的に指定
async with httpx.AsyncClient(http2=True) as client:
tasks = [fetch_api(client, "https://api.example.com/data") for _ in range(5)]
await asyncio.gather(*tasks)
# HTTP/2が有効であれば、単一のコネクションで並列処理が行われる
asyncio.run(main())
このコードを実行し、Wireshark等でパケットをキャプチャすると、単一のTCPストリームの中で HEADERS フレームと DATA フレームが交互に流れている様子が確認できるはずです。
—
まとめ:アーキテクトとしての視点
HTTP/2 Multiplexingは、インフラエンジニアにとっては「帯域をいかに効率よく使い切るか」の回答であり、API設計者にとっては「リクエストのオーバーヘッドをいかに削るか」の武器です。
しかし、プロトコルがどれだけ進化しても、「そもそも、その通信は本当に必要なのか?」という問いかけを忘れてはいけません。不必要なリクエストを減らし、必要なデータだけを確実に届ける。この泥臭い努力の土台に、HTTP/2の恩恵が乗ることで初めて、真に高速なWeb APIは完成するのです。
皆さんの設計が、この美しいプロトコルの恩恵を最大限に引き出せることを願っています。それでは、また次回の深淵でお会いしましょう。
コメント