【実務・中級編】HTTP/2におけるDATAフレームの構造とペイロードの断片化 – HTTPプロトコル・通信規格実践ガイド

パケットが泣くぞ!HTTP/2の「DATAフレーム」と断片化の深層を読み解く

ネットワークエンジニアとして現場に立っていると、「HTTP/1.1の限界を迎えてHTTP/2へ移行したはいいが、なぜか特定の巨大なJSONレスポンスの転送時に思わぬレイテンシやプロトコルエラー(RST_STREAM)が発生する」といった相談をよく受ける。

ブラウザのDevToolsや`curl`の出力結果を眺めて「速くなったね」と満足しているうちはアマチュアだ。プロのインフラエンジニアやWeb APIアーキテクトであれば、TCPのその上を走るHTTP/2のバイナリフレーム、特にアプリケーションの本体を運ぶ「DATAフレーム」が、パケット上でどのように呼吸し、分割(断片化)されているのかを解像度高くイメージできなければならない。

今回は、HTTP/2の心臓部であるDATAフレームの構造と、最大フレームサイズ (`SETTINGS_MAX_FRAME_SIZE`) による分割制御のメカニズムについて、実務の現場で即使える知見を交えて徹底解説しよう。

—

1. HTTP/2フレームの基本構造とDATAフレームの位置づけ

HTTP/1.1では、テキストベースのリクエスト/レスポンスがTCPストリーム上にダラダラと流れていた。そのため、1つのコネクションで同時に複数のリクエストを処理しようとすると、ヘッド・オブ・ライン・ブロッキング(HoLブロック)の呪縛に苦しめられることになった。

HTTP/2はこの問題を解決するために、通信を「ストリーム(Stream)」という仮想的な双方向チャネルに分割し、その上を「フレーム(Frame)」というバイナリの小包が飛び交うアーキテクチャを採用した。

すべてのHTTP/2フレームは、ペイロードのサイズに関わらず、必ず9オクテット(バイト)の固定長ヘッダーから始まる。

0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (24) |
+—————+—————+—————+
| Type (8) | Flags (8) |
+-+-+-+-+-+-+-+-+—————+——————————-+
|R| Stream Identifier (31) |
+-+————————————————————-+
|D… |
+—————+

  • Length (24ビット): ペイロード(データ本体)の長さを表す。デフォルトの上限は2^14(16,384バイト / 16KB)だが、後述する `SETTINGS_MAX_FRAME_SIZE` で拡張可能。
  • Type (8ビット): フレームの種類。DATAなら `0x0`、HEADERSなら `0x1`、SETTINGSなら `0x4` など。
  • Flags (8ビット): フレーム固有のフラグ。DATAフレームで最も重要なのは `END_STREAM (0x1)` だ。これが立つと、「このストリームで送るデータはこれで最後だ」という合図になる。
  • Stream Identifier (31ビット): このフレームがどのストリームに属しているかを示すID。奇数はクライアント発、偶数はサーバー発。

この世界において、DATAフレーム(Type = 0x0)は、Web APIのレスポンスボディやファイルといった生データを運ぶ唯一無二の存在である。

—

2. なぜDATAフレームは「断片化(Fragment)」されるのか?

ここで実務的な疑問が生じる。
「もしサーバーが10MBの巨大なJSONデータを返そうとしたとき、10MB丸ごとのDATAフレームを1つ作って流せばいいのではないか?」

答えは「絶対にノー」だ。ここにHTTP/2の美しさと、ネットワークエンジニアの腕の見せ所がある。

マルチプレクシングを守るための分割制御

HTTP/2の最大の武器は「マルチプレクシング(多重化)」だ。1つのTCPコネクション上で、複数のストリームが同時にデータを送り合う。

もし、あるストリームが10MBのDATAフレームを独占して送り始めたらどうなるか?その間、同じコネクション上で緊急度の高い別のAPIリクエスト(例えば、UIを描画するための小さなCSSやAPIレスポンス)のDATAフレームが送信待ち(ブロック)させられてしまう。これではHTTP/1.1のピペライニングと変わらない。

したがって、HTTP/2では大きなアプリケーションデータを小さなDATAフレームに意図的に断片化(Fragment)し、他のストリームのフレームとインタリーブ(交互に配置)させながら送信する。

控制パラメータ:`SETTINGS_MAX_FRAME_SIZE`

この断片化のサイズを規定するのが、HTTP/2の接続確立時にネゴシエーションされるパラメータ `SETTINGS_MAX_FRAME_SIZE` だ。

  • RFC 7540のデフォルト値: `16,384バイト (16KB)`
  • 取りうる最大値: `16,777,215バイト (16MB – 1バイト)` (24ビットのLengthフィールドの限界)

サーバー(nginx、Envoy、Goの`net/http`など)は、クライアントから通知された、あるいは自らが定めたこの `SETTINGS_MAX_FRAME_SIZE` を超えないように、アプリケーション層から渡されたデータを切り刻んで次々とDATAフレームを生成・送信する。

—

3. 通信フロー:DATAフレームが流れる舞台裏

実際にWebブラウザやクライアントがAPIを叩き、サーバーから巨大なデータを受け取るまでのシーケンスを追ってみよう。

Client Server
| |
|— HEADERS (Stream ID: 1, GET /api/v1/big-data) ——>|
| | (データ準備 &
| | 16KBごとに分割)
|<-- HEADERS (Stream ID: 1, 200 OK) ---------------------| |<-- DATA (Stream ID: 1, Length: 16384, Flags: 0x0) -----| (1個目の断片) |<-- DATA (Stream ID: 1, Length: 16384, Flags: 0x0) -----| (2個目の断片) | ... (中略) ... | |<-- DATA (Stream ID: 1, Length: 4096, Flags: 0x1) ------| (最後の断片 + END_STREAM) | | ここで注目してほしいのは、最後のDATAフレームに付与される `END_STREAM` フラグ(Flags: 0x1) だ。
受信側のTCP/HTTPスタックは、このフラグを受け取ることで「このストリームのレスポンスは正常に完了した」と判断し、アプリケーション層へデータを引き渡す。もし途中でコネクションが切断されたり、フレームが欠損すれば、ブラウザ側は `net::ERR_HTTP2_PROTOCOL_ERROR` などのエラーを吐き出すことになる。

—

4. 実務で役立つ!HTTP/2通信のデバッグと検証手法

現場で「APIのレスポンスが途中で途切れる」「プロトコルエラーで落ちる」というトラブルに直面したとき、どのようにDATAフレームの断片化やサイズを確認すべきか。実務で使えるアプローチをいくつか紹介しよう。

① `curl` を使ったフレーム単位の可視化

現代の `curl` は非常に強力だ。`–http2` オプションに加え、詳細なトレースを出力させることで、フレームのやり取りを覗き見ることができる。

HTTP/2でリクエストを送り、stderrに通信の詳細(フレーム単位の挙動含む)を出力させる
curl -v –http2 https://api.example.com/v1/heavy-payload

さらに深くパケットレベルで解析したい場合は、Wiresharkを用いる。TLSの鍵(SSLKEYLOGFILE)をキャプチャツールに食わせることで、暗号化されたHTTP/2のバイナリフレーム(DATAフレームの中身やサイズ、フラグ)を完全に復号して目視確認できるようになる。インフラエンジニアの必須スキルだ。

② Pythonと`h2`ライブラリによるカスタムクライアントでの検証

アプリケーション側からあえて `SETTINGS_MAX_FRAME_SIZE` を調整したり、受信するDATAフレームの断片化をプログラマティックに観察したい場合は、Pythonの `h2` ライブラリ(HTTP/2ステートマシン実装)が便利だ。

以下は、低レイヤーでHTTP/2通信を制御し、受信したDATAフレームのサイズをログに出力するスクリプトの例である。

import h2.connection
import h2.events
import socket
import ssl

接続先の定義
HOST = “api.example.com”
PORT = 443

TLSコンテキストの設定(HTTP/2ではALPNによる “h2” のネゴシエーションが必須)
ctx = ssl.create_default_context()
ctx.set_alpn_protocols([“h2”])

ソケット接続の確立
sock = socket.create_connection((HOST, PORT))
conn = ssl.wrap_socket(sock, server_hostname=HOST)

HTTP/2接続インスタンスの初期化
http2_conn = h2.connection.H2Connection()
http2_conn.initiate_connection()
conn.sendall(http2_conn.data_to_send())

リクエストの送信(HEADERSフレームの構築)
headers = [
(“:method”, “GET”),
(“:path”, “/v1/heavy-payload”),
(“:authority”, HOST),
(“:scheme”, “https”),
]
http2_conn.headers_received = False
http2_conn.send_headers(stream_id=1, headers=headers, end_stream=True)
conn.sendall(http2_conn.data_to_send())

サーバーからのレスポンス(フレーム)をループで処理
while True:
data = conn.recv(65535)
if not data:
break

events = http2_conn.receive_data(data)
for event in events:
# 受信したイベントがDATAフレームの場合の処理
if isinstance(event, h2.events.DataReceived):
print(f”[DEBUG] DATAフレーム受信: Stream ID={event.stream_id}, ”
f”サイズ={len(event.data)}バイト, ”
f”END_STREAM={event.flow_controlled_length == 0}”) # 簡略化

# ウィンドウ更新通知を返す(フロー制御の維持に必須)
http2_conn.increment_flow_control_window(
increment=event.flow_controlled_length,
stream_id=event.stream_id
)

elif isinstance(event, h2.events.StreamEnded):
print(f”[INFO] ストリーム {event.stream_id} が正常に終了しました。”)
break

to_send = http2_conn.data_to_send()
if to_send:
conn.sendall(to_send)

conn.close()

このコードを動かすと、サーバーから送られてくる巨大なデータが、きれいに16KB(あるいはサーバー側の設定サイズ)刻みのDATAフレームに断片化されて到着する様子が生々しく確認できるはずだ。

—

5. インフラ運用上の注意点とチューニングの勘所

最後に、実務でWebサーバー(NginxやEnvoyなど)を運用するエンジニアに向けて、DATAフレームと断片化にまつわる実践的なTipsを残しておこう。

1. `http2_max_field_size` やバッファサイズの誤解
Nginxなどの設定で調整する `http2_chunk_size` やバッファ設定は、裏を返せばこのDATAフレームの断片化サイズに直結している。不必要にチャンクサイズを大きくすると、マルチプレクシングの恩恵(公平な帯域共有)が薄れ、他の軽量リクエストのレイテンシが跳ね上がる原因になる。基本的にはデフォルトの挙動を信用し、特殊な大容量ファイル配信サーバーなどでない限り無理にいじるべきではない。
2. フロー制御(Flow Control)との組み合わせ
HTTP/2のDATAフレームは、TCPのウィンドウ制御とは別に、ストリーム単位およびコネクション単位のアプリケーション層フロー制御を持っている。受信側がデータの処理に追いつき、ウィンドウサイズを回復させる(`WINDOW_UPDATE` フレームを送る)までの間、送信側はDATAフレームの送信を一時停止する。もしAPIサーバー側で「データが途中で止まる」現象が起きたら、大抵はこのフロー制御のクレジット枯渇か、アプリケーションのバッファ詰まりが原因だ。

—

まとめ

HTTP/2のDATAフレームと断片化の仕組みは、一見するとただの「データの切り刻み」に思えるかもしれない。しかしその裏では、公平なマルチプレクシングを実現し、Web全体のパフォーマンスを底上げするための洗練されたバイナリ制御がリアルタイムで行われている。

パケットがどのように分割され、どのフラグと共に流れていくのか。その構造を頭の中に描き出せるようになってこそ、真のネットワーク・インフラストラクチャーを語る資格が生まれる。日々の開発やインフラ運用の現場で、ぜひこの視点を活かしてほしい。

コメント

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