【テクニカル・上級編】HTTP/2におけるストリーム状態遷移図(Stream States) – HTTPプロトコル・通信規格実践ガイド

パケットの海を支配する:HTTP/2ストリーム状態遷移の深淵と極限チューニング

ネットワークの歴史を振り返るとき、私たちは常に「レイテンシー」という名の物理法則と戦ってきた。1つのTCPコネクション上で1つのリクエストしか処理できず、HOL(Head-of-Line)ブロックに泣かされたHTTP/1.1の時代から、1つのTCPパイプライン上に仮想的なマルチプレクス空間を作り上げたHTTP/2への進化は、まさにパラダイムシフトだった。

しかし、この美しく複雑な多重化の裏側で、パケットは何を考え、ストリームはどう生きて死んでいくのだろうか?

今回は、HTTP/2の心臓部である「ストリーム状態遷移(Stream States)」にスポットを当て、パケットレベルの挙動からLinuxカーネルのチューニング、さらにはセキュリティの急所まで、現場のトレンチコートを着たまま深く潜ってみたい。

—

1. HTTP/2ストリームという名の「儚い命」

HTTP/1.1における通信は、いわば一方向の道路を往来するダンプカーのようなものだった。しかしHTTP/2では、単一のTCPコネクションという「大動脈」の中に、独立した双方向の仮想チャネルである「ストリーム(Stream)」が無数に奔流する。

各ストリームには `1` から始まる奇数(クライアント起因)または偶数(サーバー起因)の識別子(Stream ID)が割り振られ、完全に独立してライフサイクルを駆け抜ける。このライフサイクルを規定するのが、RFC 7540が定める厳格な状態遷移(State Machine)だ。

+——–+

  • send | | recv

on | idle | on
——-| |——-
+——–+
| |
| |
send / | | / recv
recv | | HEADERS
HEADERS | |
v v
+——–+
| open |
+——–+
| |
send / | | / recv
recv | | END_STREAM
END_ | |
STREAM v v
+——–+ +——–+
recv | half- | | half- | send
on | closed | | closed | on
— | (local)| | (remote)| —
+——–+ +——–+
| |
| |
v v
+———-+
| closed |
+———-+

この状態機械のルールを誤解すると、トラフィックが急増した瞬間に `STREAM_CLOSED` エラーやプロトコル違反による `RST_STREAM` が連鎖し、アプリケーションのパフォーマンスは地に落ちる。インフラエンジニアであれば、この遷移図を脳内に焼き付けておく必要がある。

—

2. 状態の解剖学:idle から closed への軌跡

それぞれの状態がパケットの送受信に対してどのような意味を持つのか、リアルな挙動を追ってみよう。

`idle`(アイドル)

すべてのストリームの出発点だ。まだパケットは流れていないが、メモリ上にはその存在が予約されている。

  • できること: クライアント(またはサーバー)が `HEADERS` フレームを送信(または受信)することで、この状態を脱する。
  • 注意点: `idle` 状態のストリームに対して、いきなり `DATA` フレームを送ることは許されない。そんな暴挙に出れば、即座にサーバーから `PROTOCOL_ERROR` の `RST_STREAM` が返される。

`open`(オープン)

双方向で自由にフレームのやり取りができる全二重通信の状態だ。

  • できること: クライアントはリクエストボディの `DATA` を送り続け、サーバーはそれを受け取りながら、先行してレスポンスの `HEADERS` を返すことができる。
  • 挙動: `END_STREAM` フラグが立ったフレームが送受信されるまで、この状態は維持される。

`half-closed (remote)` / `half-closed (local)`(半閉塞)

HTTP/2の非対称な美しさが現れるのがここだ。TCPの `FIN` とは異なり、HTTP/2では「自分からの送信は終わったが、相手からの受信はまだ続ける」という片肺飛行が可能になる。

  • half-closed (local): 自側が `END_STREAM` を送信した状態。これ以降、自側から `DATA` フレームを送ることはできないが、相手からの `DATA` は受け付けられる。
  • half-closed (remote): 相手から `END_STREAM` を受信した状態。相手からの入力はもう来ないが、こちらからレスポンスの残りを叩き返すことはできる。

`closed`(クローズ)

ライフサイクルの終着点。この状態に達したストリームIDを再利用することはできない(クライアントは単調増加のIDを使い続ける)。

  • パケットレベルの現実: `closed` になったあとも、TCP層のバッファやHTTP/2のフロー制御ウィンドウ(Flow Control Window)の計算上、一定の配慮が必要となる。特に `RST_STREAM` を送信/受信した直後は、ゴーストパケット(遅延到着したデータ)の処理に注意を払わなければならない。

—

各状態におけるフレーム送信の可否(チートシート)

| 状態 | HEADERS送信 | DATA送信 | RST_STREAM送信 | WINDOW_UPDATE送信 |
| :— | :—: | :—: | :—: | :—: |
| idle | ⭕ (開始) | ❌ | ❌ | ❌ |
| open | ❌ | ⭕ | ⭕ | ⭕ |
| half-closed (local) | ❌ | ❌ | ⭕ | ⭕ |
| half-closed (remote)| ❌ | ⭕ | ⭕ | ⭕ |
| closed | ❌ | ❌ | ❌ | ❌ (一部例外あり) |

—

3. トランスポート層の罠:TCPバッファとHTTP/2フロー制御のジレンマ

HTTP/2はアプリケーション層で完璧なマルチプレクシングを実現したが、その足元を支えるのは相変わらず信頼性トランスポート層の重鎮、TCPだ。ここに重大な落とし穴がある。

TCPのHOLブロック vs HTTP/2のマルチプレクシング

HTTP/2は1本のTCPコネクション上で数百のストリームを多重化する。しかし、TCP自体はストリームの概念を知らない。単なる「バイトストリームの連続」としてパケットを運ぶ。
そのため、もし回線上で単一のパケットロスが発生すると、そのパケットが回収されるまで、同一TCPコネクション上のすべてのHTTP/2ストリームが完全にフリーズする。HTTP/2のマルチプレクシングは、トランスポート層のHOLブロックの前には無力なのだ。

この悪夢を回避するため、インフラエンジニアはLinuxカーネルのパラメータを極限までチューニングする必要がある。

実践:Linuxカーネルチューニング(`sysctl.conf`)

パケットロス時の影響を最小限に抑え、BDP(Bandwidth-Delay Product)を最大化するための設定例を挙げる。

/etc/sysctl.d/99-http2-tcp-tuning.conf

TCP BBR混雑制御アルゴリズムの有効化(パケットロスに強く、高スループットを実現)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

TCP送受信バッファの動的チューニング上限を拡大(高BDP回線対策)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

ウィンドウ スケーリングの有効化(必須)
net.ipv4.tcp_window_scaling = 1

TIME_WAITソケットの高速リサイクルと再利用
net.ipv4.tcp_tw_reuse = 1

早期再送(Early Retransmit)の最適化による再送遅延の削減
net.ipv4.tcp_early_retrans = 3

さらに、HTTP/2自体のフロー制御(Flow Control)も忘れてはならない。
TCPのウィンドウサイズとは別に、HTTP/2はストリーム単位およびコネクション単位で `WINDOW_UPDATE` フレームを送り合い、メモリの枯渇を防ぎつつスループットを制御する。
サーバーサイド(Nginx, Envoy, Goなど)の実装において、初期ウィンドウサイズ(初期値は65,535バイト)が小さすぎると、広帯域・高遅延なネットワーク(大陸間通信など)でパイプラインが遊んでしまう。
Nginxであれば `http2_max_field_size` やバッファサイズの調整、Envoyであれば `initial_stream_window_size` を適切に引き上げることが、真のパフォーマンスを引き出す鍵となる。

—

4. セキュリティの急所:ストリーム濫用とリソース枯渇攻撃

マルチプレクシングの恩恵は、そのまま攻撃者にとっての「武器」にもなり得る。HTTP/2の仕様を悪用したDDoS攻撃は、ステートフルなインフラにとって悪夢だ。

1. HTTP/2 Rapid Reset Attack (CVE-2023-44487)

記憶に新しいこの脆弱性は、まさにストリーム状態遷移の盲点を突き刺したものだった。
攻撃者は `HEADERS` フレームでストリームを `open` 状態にした直後、自ら `RST_STREAM` フレームを送り、即座にそのストリームを `closed` に遷移させる。これを数千、数万のストリームで超高速に繰り返す。

  • 内部挙動: サーバー側はリクエストを受け付け、ストリームの領域を確保し、処理の準備(あるいはキャンセル処理)を強いられる。しかし、クライアント側はすでにそのストリームを放棄しているため、サーバーのCPUとメモリは「幻影のキャンセル処理」で飽和し、正当なトラフィックが処理できなくなる(CPU使用率が100%に張り付く)。

2. Stream Multiplexing Abuse(スロローリスのHTTP/2版)

数千のストリームを同時に `open` 状態にし、それぞれのストリームで極めて低速に(例えば数秒に1バイトずつ)データを送り続けることで、サーバーの同時接続数制限やメモリを食いつぶす手口。

対策:硬いインフラストラクチャの構築

アプリケーションサーバーやリバースプロキシのパラメータを絞り込み、異常なストリームのライフサイクルを検知・切断する必要がある。

  • 同時オープンストリーム数の制限: コネクションあたりの最大アクティブストリーム数(`SETTINGS_MAX_CONCURRENT_STREAMS`)を適切に制限する(デフォルトの無制限は論外であり、通常は `100`〜`250` 程度に絞る)。
  • タイムアウトの厳格化: アイドル状態のストリームや、ヘッダー送信が完了しないストリームに対するタイマー(Read/Write Timeout)を短く設定する。

例えば、Nginxでの堅牢な設定例:

http {
# HTTP/2の設定
http2 on;

# 同時ストリーム数の上限を厳しく制限し、リソース枯渇を防ぐ
http2_max_concurrent_streams 128;

# クライアントからのリクエストボディ受信タイムアウト
client_body_timeout 10s;
client_header_timeout 10s;

# バッファサイズの適正化
client_header_buffer_size 4k;
large_client_header_buffers 4 16k;
}

—

5. まとめ:パケットの息吹を感じ取れ

HTTP/2のストリーム状態遷移は、単なる仕様書のフローチャートではない。それは、クライアントとサーバーがひとたび握手をかわした後に繰り広げられる、ダイナミックで繊細な「パケットのダンス」そのものだ。

`idle` で芽生え、`open` で激しく交錯し、`half-closed` で片方が静寂を迎え、最後に `closed` で綺麗に消えていく。この一連のライフサイクルを深く理解し、LinuxカーネルのTCPバッファからNginxのディレクティブ、そして昨今のセキュリティ脆弱性に至るまで網羅的にチューニングして初めて、私たちは真に「速く、かつ堅牢な」ネットワークアーキテクチャを構築することができる。

パケットは嘘をつかない。挙動の裏にある状態遷移の妙を愛し、極限のパフォーマンスをエンジニアリングしよう。

コメント

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