<p><!--META
{
"title": "QPACKヘッダー圧縮 - HTTP/3における革新的なヘッダー処理",
"primary_category": "ネットワーク",
"secondary_categories": ["HTTP","QUIC"],
"tags": ["HTTP/3","QPACK","ヘッダー圧縮","RFC9204","QUIC","HPACK"],
"summary": "HTTP/3で導入されたQPACKヘッダー圧縮は、HTTP/2 HPACKの課題を解決し、ストリームごとのHOLブロッキングを回避しながら高い圧縮効率を維持します。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"HTTP/3のQPACKヘッダー圧縮について徹底解説!HPACKの課題をどう解決し、HOLブロッキングを回避するのか?主要な仕様、HTTP/2との比較、セキュリティ、実装の注意点までを網羅。#HTTP3 #QPACK #ネットワーク"},
"link_hints": ["https://www.rfc-editor.org/rfc/rfc9114.html","https://www.rfc-editor.org/rfc/rfc9204.html"]
}
-->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">QPACKヘッダー圧縮 – HTTP/3における革新的なヘッダー処理</h1>
<h2 class="wp-block-heading">背景</h2>
<p>HTTP/3は、トランスポート層にQUICを採用することで、HTTP/2が抱えていた複数の問題を解決しました。特に、HTTP/2のヘッダー圧縮メカニズムであるHPACK (RFC 7541) は、異なるHTTPストリーム間で共有される圧縮コンテキストに依存していたため、単一のヘッダーブロックのパケットロスが、そのストリームだけでなく、他のすべてのストリームのヘッダーデコードを遅延させる「ヘッダー圧縮におけるHOL(Head-of-Line)ブロッキング」を引き起こす可能性がありました。</p>
<p>QUICは、TCPのHOLブロッキングを回避するために、多重化されたストリームを独立して転送する機能を備えています。このQUICの利点を最大限に活かすためには、ヘッダー圧縮メカニズムもストリーム間の依存関係を極力排除する必要があります。この課題を解決するために、HTTP/3 (RFC 9114) では、QPACK (RFC 9204) という新しいヘッダー圧縮方式が導入されました。</p>
<h2 class="wp-block-heading">設計目標</h2>
<p>QPACKは、HPACKの課題を解決し、HTTP/3の性能を最大限に引き出すために以下の設計目標を掲げました [1, 2]。</p>
<ul class="wp-block-list">
<li><p><strong>HOLブロッキングの回避</strong>: ヘッダー圧縮コンテキストの変更が、他のストリームのヘッダーデコードをブロックしないようにする。</p></li>
<li><p><strong>高い圧縮効率の維持</strong>: HPACKが提供していた高い圧縮率を維持、あるいは改善する。</p></li>
<li><p><strong>インクリメンタルな状態更新のサポート</strong>: 動的テーブルの更新を効率的に管理し、変更を徐々に適用できるようにする。</p></li>
<li><p><strong>QUICの0-RTTハンドシェイクとの互換性</strong>: 0-RTTデータ送信時に、既知のヘッダーを効率的に圧縮・伸長できるメカニズムを提供する。</p></li>
<li><p><strong>デコーダの複雑さの最小化</strong>: エンコーダ側の責任を増やし、デコーダ側での状態管理の複雑さを軽減する。</p></li>
</ul>
<h2 class="wp-block-heading">詳細</h2>
<p>QPACK (RFC 9204) は、HTTPヘッダーフィールドを効率的に圧縮するためのメカニズムであり、以下の主要な要素で構成されます。</p>
<h3 class="wp-block-heading">主要な概念</h3>
<ol class="wp-block-list">
<li><p><strong>静的テーブル (Static Table)</strong>:</p>
<ul>
<li><p>HTTP/2のHPACKと同様に、よく使われるHTTPヘッダーフィールド名と値のペアが事前に定義された不変のテーブルです。QPACKとHPACKで一部内容が異なります [2]。</p></li>
<li><p>エンコーダとデコーダは、このテーブルを共有し、インデックス参照でヘッダーを圧縮します。</p></li>
</ul></li>
<li><p><strong>動的テーブル (Dynamic Table)</strong>:</p>
<ul>
<li><p>セッション中に動的に追加されるヘッダーフィールド(例:Cookie、User-Agentなど)を格納するテーブルです。</p></li>
<li><p>HPACKでは、この動的テーブルがすべてのストリームで共有されていたため、HOLブロッキングの原因となりました。</p></li>
<li><p>QPACKでは、動的テーブルは<strong>エンコーダとデコーダで共有されるグローバルなコンテキスト</strong>として扱われますが、その更新と利用方法に工夫が凝らされています。</p></li>
</ul></li>
<li><p><strong>制御ストリーム (Control Streams)</strong>:</p>
<ul>
<li><p>QPACKは、ヘッダーデータを運ぶ通常のHTTPストリームとは別に、動的テーブルの更新指示や確認応答を運ぶための特別な<strong>単方向制御ストリーム</strong>を使用します [2]。</p></li>
<li><p><strong>Encoder Instruction Stream (エンコーダ指示ストリーム)</strong>: エンコーダからデコーダへ、動的テーブルへのエントリ追加や容量変更の指示を送信します。</p></li>
<li><p><strong>Decoder Instruction Stream (デコーダ指示ストリーム)</strong>: デコーダからエンコーダへ、動的テーブルの更新が正常に処理されたことの確認応答 (ACK) を送信します。これにより、エンコーダはデコーダがどの状態までテーブルを処理したかを把握できます。</p></li>
</ul></li>
</ol>
<h3 class="wp-block-heading">QPACKヘッダーブロックの構造</h3>
<p>QPACKで圧縮されたヘッダーブロックは、HTTP/3の<code>HEADERS</code>フレームや<code>PUSH_PROMISE</code>フレームのペイロードとして転送されます。ヘッダーブロックは以下の構造を持ちます [2]。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">Header Block Prefix:
Required Insert Count: variable-length integer (0-7 bits)
Delta Base: variable-length integer (0-7 bits)
Encoded Field Section:
... (Encoded Header Field representations) ...
</pre>
</div>
<ul class="wp-block-list">
<li><p><strong>Required Insert Count (RIC)</strong>: ヘッダーブロックを完全にデコードするためにデコーダが<strong>処理済みである必要がある</strong>動的テーブルエントリの数をエンコーダが示します。これにより、デコーダは未処理の動的テーブル更新がある場合でも、その更新がRICを満たすまで待機することで、HOLブロッキングを回避しつつ整合性を保てます。</p></li>
<li><p><strong>Delta Base (DB)</strong>: ヘッダーブロック内で使用される「Post-Baseインデックス」の基準となる動的テーブルインデックスからの差分を示します。</p></li>
</ul>
<h3 class="wp-block-heading">圧縮方式</h3>
<p>QPACKでは、以下の方法でヘッダーフィールドを表現します [2]。</p>
<ul class="wp-block-list">
<li><p><strong>Static Table Index</strong>: 静的テーブルのインデックスを直接参照。最も効率的。</p></li>
<li><p><strong>Dynamic Table Index</strong>: 動的テーブルのインデックスを直接参照。</p></li>
<li><p><strong>Post-Base Index</strong>: <code>Delta Base</code>からのオフセットで動的テーブルエントリを参照。0-RTTシナリオで特に有用。</p></li>
<li><p><strong>Literal Field</strong>: 圧縮せずにヘッダー名と値をそのまま送信。</p>
<ul>
<li><p><code>Literal With Static Name Reference</code>: 静的テーブルのインデックスでヘッダー名を指定し、値はリテラル。</p></li>
<li><p><code>Literal With Dynamic Name Reference</code>: 動的テーブルのインデックスでヘッダー名を指定し、値はリテラル。</p></li>
<li><p><code>Literal With Post-Base Name Reference</code>: <code>Delta Base</code>からのオフセットで動的テーブルエントリのヘッダー名を指定し、値はリテラル。</p></li>
<li><p><code>Literal With Literal Name</code>: ヘッダー名も値もリテラル。</p></li>
</ul></li>
</ul>
<h3 class="wp-block-heading">動的テーブルの同期プロセス</h3>
<p>QPACKの大きな特徴は、動的テーブルの更新をヘッダーデータとは<strong>非同期</strong>かつ<strong>アウトオブバンド</strong>で同期する点です。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
sequenceDiagram
participant "Client as エンコーダ (送信側)"
participant "Server as デコーダ (受信側)"
Client ->> Server: QUIC接続確立 / HTTP/3設定交換 (ALPN)
activate Server
Server ->> Client: HTTP/3設定交換完了 (SETTING_QPACK_MAX_TABLE_CAPACITYなど)
deactivate Server
Note over Client: 動的テーブルは初期状態で空
Client ->> Server: QPACK Encoder Instruction Stream: |SET_DYNAMIC_TABLE_CAPACITY| (MAX_CAPACITY)
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |DYNAMIC_TABLE_CAPACITY_ACK| (ACK_VAL)
deactivate Server
Client ->> Server: HTTP/3 HEADERSフレーム (Stream A) | Header Block Prefix: ΔBase=0, RIC=0, Fields: (literal)User-Agent:Chrome
Note over Client: このヘッダーは動的テーブルに<br/>依存しないため、ACK不要で送信可能。
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |HEADER_ACK| (Stream A processed)
deactivate Server
Note over Server: User-Agentを動的テーブルに<br/>追加 (例: Index 62)。
Client ->> Server: HTTP/3 HEADERSフレーム (Stream B) | Header Block Prefix: ΔBase=1, RIC=1, Fields: (static):method:GET, (dynamic)User-Agent (idx:62)
Note over Client: Stream Bは動的テーブル項目を参照。<br/>RIC=1 は、デコーダがUser-Agentを<br/>テーブルに追加済みであることを要求。
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |HEADER_ACK| (Stream B processed)
deactivate Server
</pre></div>
<p>動的テーブルの更新は、エンコーダ指示ストリームを通じてデコーダに送信されます。デコーダがその更新を処理し、動的テーブルに反映した後、デコーダ指示ストリームを通じてエンコーダに<code>HEADER_ACK</code>を返します。これにより、エンコーダはデコーダがどの動的テーブルエントリまで認識しているかを把握し、それ以降のヘッダーブロックでそのエントリを参照できるようになります。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
flowchart TD
subgraph QPACKエンコーダの状態管理
ENC_INIT["QPACKエンコーダ初期化"] --> ENC_DT_EMPTY{"動的テーブル: 空"}
subgraph ヘッダーエンコード
ENC_HEADERS["HEADERSフレームエンコード"]
ENC_DT_EMPTY --> ENC_HEADERS
ENC_DT_STATE{"動的テーブルの状態"} -- |利用可能な参照| --> ENC_HEADERS
ENC_HEADERS -- |新規エントリ追加| --> ENC_ADD_DT_ENTRY["動的テーブル更新指示作成"]
ENC_ADD_DT_ENTRY --> ENC_DT_STATE
ENC_ADD_DT_ENTRY --> ENC_SEND_ENC_INS["エンコーダ指示ストリーム送信"]
end
ENC_SEND_ENC_INS --> ENC_RECV_DEC_ACK["デコーダACK受信待機"]
ENC_RECV_DEC_ACK -- |HEADER_ACKを受信| --> ENC_DT_STATE
ENC_RECV_DEC_ACK -- |DYNAMIC_TABLE_CAPACITY_ACKを受信| --> ENC_DT_STATE
end
subgraph QPACKデコーダの状態管理
DEC_INIT["QPACKデコーダ初期化"] --> DEC_DT_EMPTY{"動的テーブル: 空"}
subgraph ヘッダーデコード
DEC_HEADERS["HEADERSフレームデコード"]
DEC_DT_EMPTY --> DEC_HEADERS
DEC_DT_STATE{"動的テーブルの状態"} -- |参照解決| --> DEC_HEADERS
DEC_HEADERS -- |更新適用| --> DEC_APPLY_DT_UPDATE["動的テーブル更新適用"]
DEC_APPLY_DT_UPDATE --> DEC_DT_STATE
DEC_APPLY_DT_UPDATE --> DEC_SEND_DEC_INS["デコーダ指示ストリーム送信"]
end
DEC_RECV_ENC_INS["エンコーダ指示ストリーム受信"] --> DEC_DT_STATE
DEC_SEND_DEC_INS --> DEC_RECV_ENC_INS
end
ENC_SEND_ENC_INS <--> DEC_RECV_ENC_INS
ENC_RECV_DEC_ACK <--> DEC_SEND_DEC_INS
</pre></div>
<h2 class="wp-block-heading">既存プロトコルとの比較</h2>
<p>HTTP/2のHPACKと比較して、QPACKは以下の点で進化しています。</p>
<ul class="wp-block-list">
<li><p><strong>HOLブロッキングの回避</strong>:</p>
<ul>
<li><p><strong>HPACK</strong>: 動的テーブルは全ストリームで共有される単一のコンテキスト。パケットロスによる動的テーブルの更新遅延が、すべてのストリームのヘッダーデコードをブロックする。</p></li>
<li><p><strong>QPACK</strong>: 動的テーブルの更新は制御ストリームで非同期に処理され、ヘッダーブロックはRICを用いてデコーダの状態要求を示す。これにより、ヘッダー圧縮におけるHOLブロッキングを回避。</p></li>
</ul></li>
<li><p><strong>状態管理</strong>:</p>
<ul>
<li><p><strong>HPACK</strong>: 単一の共有状態。エンコーダとデコーダは常に同期した状態を維持する必要がある。</p></li>
<li><p><strong>QPACK</strong>: グローバルな動的テーブルは存在するが、ヘッダーブロック自体はストリームごとに独立してデコード可能。デコーダが動的テーブルの特定の状態に達していることをRICで保証することで、柔軟なデコードを可能に。</p></li>
</ul></li>
<li><p><strong>制御ストリーム</strong>:</p>
<ul>
<li><p><strong>HPACK</strong>: 専用の制御ストリームはなし。ヘッダーブロック内にテーブル更新情報を含む。</p></li>
<li><p><strong>QPACK</strong>: 動的テーブルの更新指示や確認応答のために専用の単方向制御ストリームを使用。</p></li>
</ul></li>
<li><p><strong>0-RTTサポート</strong>:</p>
<ul>
<li><p><strong>HPACK</strong>: 動的テーブルはセッション固有のため、0-RTTでは利用が困難。</p></li>
<li><p><strong>QPACK</strong>: Post-Baseインデックスなどのメカニズムにより、0-RTTデータ送信時に過去のセッションで学習した動的テーブルエントリの一部を安全に参照できる仕組みを提供。</p></li>
</ul></li>
</ul>
<h2 class="wp-block-heading">相互運用</h2>
<p>QPACKの実装間での相互運用性には、以下の点に注意が必要です。</p>
<ul class="wp-block-list">
<li><p><strong>QPACK設定のネゴシエーション</strong>: HTTP/3の<code>SETTINGS_QPACK_MAX_TABLE_CAPACITY</code>と<code>SETTINGS_QPACK_BLOCKED_STREAMS</code>を通じて、エンコーダとデコーダは動的テーブルの最大容量と、デコーダがブロックできる最大ストリーム数を通知し合います。これらの設定が正しく交換されることが重要です [2]。</p></li>
<li><p><strong>動的テーブルの同期プロトコル</strong>: エンコーダ指示ストリームとデコーダ指示ストリームのメッセージ(例: <code>INSERT_COUNT_INCREMENT</code>, <code>HEADER_ACK</code>)がRFC 9204に厳密に従って実装されている必要があります。</p></li>
<li><p><strong>エラー処理</strong>: 動的テーブルの不一致や容量超過などのエラーシナリオにおいて、プロトコルが定義するエラーコード(例: <code>QPACK_DECOMPRESSION_FAILED</code>)を用いて適切にエラーを通知し、接続を閉じるメカニズムが重要です。</p></li>
</ul>
<h2 class="wp-block-heading">セキュリティ考慮</h2>
<p>QPACKはヘッダー圧縮に特化したプロトコルですが、ネットワーク通信の一部であるため、いくつかのセキュリティ上の考慮事項があります。</p>
<ul class="wp-block-list">
<li><p><strong>リプレイ攻撃</strong>: QPACK自体がリプレイ攻撃から保護するメカニズムは持ちません。しかし、QPACKはQUICとTLS 1.3上で動作するため、QUICのハンドシェイク中に確立されるセッションキーによって、トランスポート層でリプレイ保護が提供されます [1]。QPACKの動的テーブルの内容はセッションごとに確立されるため、過去のセッションのQPACK状態を再利用して攻撃することは困難です。</p></li>
<li><p><strong>ダウングレード攻撃</strong>: HTTP/3プロトコル全体のネゴシエーションはALPN (Application-Layer Protocol Negotiation) によって行われるため、HTTP/3をHTTP/2やHTTP/1.1にダウングレードさせる攻撃は、ALPNのメカニズムによって防止されます。</p></li>
<li><p><strong>キー更新</strong>: QPACKはTLS 1.3のセッションキーに依存するため、キーの更新はQUICとTLS 1.3の機能によって行われます。QPACKはヘッダーデータ自体の暗号化は行わず、QUICの暗号化ペイロードとして転送されます。</p></li>
<li><p><strong>0-RTTの再送リスク</strong>: QPACKは0-RTTをサポートしますが、0-RTTで送信されるヘッダーブロックは、デコーダがまだ認識していない動的テーブルエントリを参照できません。つまり、0-RTTヘッダーは静的テーブルエントリやリテラル、または過去のセッションから永続的にキャッシュされた動的テーブルエントリ(Post-Baseインデックスで参照可能)のみを使用できます。0-RTTデータは冪等であるべきというQUICの原則に従い、QPACKヘッダーブロックも適切に構成される必要があります [1]。</p></li>
<li><p><strong>情報漏洩</strong>: 圧縮辞書(動的テーブル)の内容がサイドチャネル攻撃(例えばCRIME/BREACH攻撃の亜種)に利用される可能性は、HPACKと同様に存在します。しかし、QPACKではストリームごとのHOLブロッキングが回避されるため、攻撃者が特定のヘッダーを挿入してレスポンスを監視するような攻撃の機会は減少します。それでも、機密情報を含むヘッダーを動的テーブルに長期間保持することには注意が必要です。</p></li>
</ul>
<h2 class="wp-block-heading">実装メモ</h2>
<p>QPACKを効率的かつ堅牢に実装するためには、以下の点に留意する必要があります。</p>
<ul class="wp-block-list">
<li><p><strong>MTU/Path MTU (PMTU)</strong>: QUICはPMTU Discoveryをサポートしており、QPACKヘッダーブロックもPMTU内に収まるようにフラグメント化されます。ただし、ヘッダーブロック全体を一つのQUICパケットに収めることが理想的であり、特に動的テーブル更新のACKは迅速に処理されるべきです。フラグメント化されたヘッダーブロックの再構成ロジックを適切に実装する必要があります。</p></li>
<li><p><strong>HOL blocking回避</strong>: QPACKの最も重要な設計目標です。デコーダは、<code>Required Insert Count</code>を満たすまでヘッダーブロックのデコードを遅延させることができますが、これは<strong>ヘッダーブロックごとの遅延</strong>であり、<strong>ストリーム間のHOLブロッキングではない</strong>点に注意が必要です。デコーダがブロックできるストリーム数には<code>SETTINGS_QPACK_BLOCKED_STREAMS</code>で上限があるため、デコーダは過度に多くのストリームをブロックしないよう、動的テーブル更新の処理を優先すべきです。</p></li>
<li><p><strong>キュー制御</strong>: エンコーダは、デコーダが処理を完了した動的テーブルエントリの数を示す<code>HEADER_ACK</code>を定期的に受信することで、自身の動的テーブルの状態を更新します。未確認の動的テーブル更新が多数キューに溜まっている場合、エンコーダは新しいヘッダーを動的テーブルのインデックス参照で圧縮することを避け、リテラルとして送信するなどのフォールバックメカニズムを持つべきです。これにより、デコーダが過負荷になるのを防ぎ、遅延を最小限に抑えられます。</p></li>
<li><p><strong>優先度</strong>: HTTP/3はストリームごとに優先度を設定できます。QPACKのエンコーダとデコーダは、優先度の高いストリームのヘッダーを優先的に処理し、動的テーブルの更新指示やACKも、優先度の高いストリームからのリクエストに対応するものを優先的に送信・処理するよう設計すべきです [3]。</p></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>QPACKヘッダー圧縮 (RFC 9204) は、HTTP/3 (RFC 9114) がQUIC上で動作する上で不可欠な要素です。HTTP/2のHPACKが抱えていたヘッダー圧縮におけるHOLブロッキングの問題を、制御ストリームと非同期な動的テーブル更新メカニズムによって解決しました。これにより、HTTP/3はQUICのストリーム多重化の利点を最大限に活かし、低遅延かつ高効率なWeb通信を実現します。実装においては、動的テーブルの厳密な同期管理、エラー処理、そして0-RTTや優先度といったQUICの特性との連携が重要となります。</p>
<hr/>
<p>[1] Mike Bishop, et al. “HTTP/3”. RFC 9114. IETF. 2022年6月.
[2] Martin Thomson, et al. “QPACK: Header Compression for HTTP/3”. RFC 9204. IETF. 2022年6月.
[3] Mozilla Wiki Contributors. “HTTP3 FAQ”. Mozilla Wiki. 2024年2月14日.</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
QPACKヘッダー圧縮 – HTTP/3における革新的なヘッダー処理
背景
HTTP/3は、トランスポート層にQUICを採用することで、HTTP/2が抱えていた複数の問題を解決しました。特に、HTTP/2のヘッダー圧縮メカニズムであるHPACK (RFC 7541) は、異なるHTTPストリーム間で共有される圧縮コンテキストに依存していたため、単一のヘッダーブロックのパケットロスが、そのストリームだけでなく、他のすべてのストリームのヘッダーデコードを遅延させる「ヘッダー圧縮におけるHOL(Head-of-Line)ブロッキング」を引き起こす可能性がありました。
QUICは、TCPのHOLブロッキングを回避するために、多重化されたストリームを独立して転送する機能を備えています。このQUICの利点を最大限に活かすためには、ヘッダー圧縮メカニズムもストリーム間の依存関係を極力排除する必要があります。この課題を解決するために、HTTP/3 (RFC 9114) では、QPACK (RFC 9204) という新しいヘッダー圧縮方式が導入されました。
設計目標
QPACKは、HPACKの課題を解決し、HTTP/3の性能を最大限に引き出すために以下の設計目標を掲げました [1, 2]。
HOLブロッキングの回避: ヘッダー圧縮コンテキストの変更が、他のストリームのヘッダーデコードをブロックしないようにする。
高い圧縮効率の維持: HPACKが提供していた高い圧縮率を維持、あるいは改善する。
インクリメンタルな状態更新のサポート: 動的テーブルの更新を効率的に管理し、変更を徐々に適用できるようにする。
QUICの0-RTTハンドシェイクとの互換性: 0-RTTデータ送信時に、既知のヘッダーを効率的に圧縮・伸長できるメカニズムを提供する。
デコーダの複雑さの最小化: エンコーダ側の責任を増やし、デコーダ側での状態管理の複雑さを軽減する。
詳細
QPACK (RFC 9204) は、HTTPヘッダーフィールドを効率的に圧縮するためのメカニズムであり、以下の主要な要素で構成されます。
主要な概念
静的テーブル (Static Table):
動的テーブル (Dynamic Table):
セッション中に動的に追加されるヘッダーフィールド(例:Cookie、User-Agentなど)を格納するテーブルです。
HPACKでは、この動的テーブルがすべてのストリームで共有されていたため、HOLブロッキングの原因となりました。
QPACKでは、動的テーブルはエンコーダとデコーダで共有されるグローバルなコンテキストとして扱われますが、その更新と利用方法に工夫が凝らされています。
制御ストリーム (Control Streams):
QPACKは、ヘッダーデータを運ぶ通常のHTTPストリームとは別に、動的テーブルの更新指示や確認応答を運ぶための特別な単方向制御ストリームを使用します [2]。
Encoder Instruction Stream (エンコーダ指示ストリーム): エンコーダからデコーダへ、動的テーブルへのエントリ追加や容量変更の指示を送信します。
Decoder Instruction Stream (デコーダ指示ストリーム): デコーダからエンコーダへ、動的テーブルの更新が正常に処理されたことの確認応答 (ACK) を送信します。これにより、エンコーダはデコーダがどの状態までテーブルを処理したかを把握できます。
QPACKヘッダーブロックの構造
QPACKで圧縮されたヘッダーブロックは、HTTP/3のHEADERSフレームやPUSH_PROMISEフレームのペイロードとして転送されます。ヘッダーブロックは以下の構造を持ちます [2]。
Header Block Prefix:
Required Insert Count: variable-length integer (0-7 bits)
Delta Base: variable-length integer (0-7 bits)
Encoded Field Section:
... (Encoded Header Field representations) ...
Required Insert Count (RIC): ヘッダーブロックを完全にデコードするためにデコーダが処理済みである必要がある動的テーブルエントリの数をエンコーダが示します。これにより、デコーダは未処理の動的テーブル更新がある場合でも、その更新がRICを満たすまで待機することで、HOLブロッキングを回避しつつ整合性を保てます。
Delta Base (DB): ヘッダーブロック内で使用される「Post-Baseインデックス」の基準となる動的テーブルインデックスからの差分を示します。
圧縮方式
QPACKでは、以下の方法でヘッダーフィールドを表現します [2]。
Static Table Index: 静的テーブルのインデックスを直接参照。最も効率的。
Dynamic Table Index: 動的テーブルのインデックスを直接参照。
Post-Base Index: Delta Baseからのオフセットで動的テーブルエントリを参照。0-RTTシナリオで特に有用。
Literal Field: 圧縮せずにヘッダー名と値をそのまま送信。
Literal With Static Name Reference: 静的テーブルのインデックスでヘッダー名を指定し、値はリテラル。
Literal With Dynamic Name Reference: 動的テーブルのインデックスでヘッダー名を指定し、値はリテラル。
Literal With Post-Base Name Reference: Delta Baseからのオフセットで動的テーブルエントリのヘッダー名を指定し、値はリテラル。
Literal With Literal Name: ヘッダー名も値もリテラル。
動的テーブルの同期プロセス
QPACKの大きな特徴は、動的テーブルの更新をヘッダーデータとは非同期かつアウトオブバンドで同期する点です。
sequenceDiagram
participant "Client as エンコーダ (送信側)"
participant "Server as デコーダ (受信側)"
Client ->> Server: QUIC接続確立 / HTTP/3設定交換 (ALPN)
activate Server
Server ->> Client: HTTP/3設定交換完了 (SETTING_QPACK_MAX_TABLE_CAPACITYなど)
deactivate Server
Note over Client: 動的テーブルは初期状態で空
Client ->> Server: QPACK Encoder Instruction Stream: |SET_DYNAMIC_TABLE_CAPACITY| (MAX_CAPACITY)
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |DYNAMIC_TABLE_CAPACITY_ACK| (ACK_VAL)
deactivate Server
Client ->> Server: HTTP/3 HEADERSフレーム (Stream A) | Header Block Prefix: ΔBase=0, RIC=0, Fields: (literal)User-Agent:Chrome
Note over Client: このヘッダーは動的テーブルに
依存しないため、ACK不要で送信可能。
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |HEADER_ACK| (Stream A processed)
deactivate Server
Note over Server: User-Agentを動的テーブルに
追加 (例: Index 62)。
Client ->> Server: HTTP/3 HEADERSフレーム (Stream B) | Header Block Prefix: ΔBase=1, RIC=1, Fields: (static):method:GET, (dynamic)User-Agent (idx:62)
Note over Client: Stream Bは動的テーブル項目を参照。
RIC=1 は、デコーダがUser-Agentを
テーブルに追加済みであることを要求。
activate Server
Server ->> Client: QPACK Decoder Instruction Stream: |HEADER_ACK| (Stream B processed)
deactivate Server
動的テーブルの更新は、エンコーダ指示ストリームを通じてデコーダに送信されます。デコーダがその更新を処理し、動的テーブルに反映した後、デコーダ指示ストリームを通じてエンコーダにHEADER_ACKを返します。これにより、エンコーダはデコーダがどの動的テーブルエントリまで認識しているかを把握し、それ以降のヘッダーブロックでそのエントリを参照できるようになります。
flowchart TD
subgraph QPACKエンコーダの状態管理
ENC_INIT["QPACKエンコーダ初期化"] --> ENC_DT_EMPTY{"動的テーブル: 空"}
subgraph ヘッダーエンコード
ENC_HEADERS["HEADERSフレームエンコード"]
ENC_DT_EMPTY --> ENC_HEADERS
ENC_DT_STATE{"動的テーブルの状態"} -- |利用可能な参照| --> ENC_HEADERS
ENC_HEADERS -- |新規エントリ追加| --> ENC_ADD_DT_ENTRY["動的テーブル更新指示作成"]
ENC_ADD_DT_ENTRY --> ENC_DT_STATE
ENC_ADD_DT_ENTRY --> ENC_SEND_ENC_INS["エンコーダ指示ストリーム送信"]
end
ENC_SEND_ENC_INS --> ENC_RECV_DEC_ACK["デコーダACK受信待機"]
ENC_RECV_DEC_ACK -- |HEADER_ACKを受信| --> ENC_DT_STATE
ENC_RECV_DEC_ACK -- |DYNAMIC_TABLE_CAPACITY_ACKを受信| --> ENC_DT_STATE
end
subgraph QPACKデコーダの状態管理
DEC_INIT["QPACKデコーダ初期化"] --> DEC_DT_EMPTY{"動的テーブル: 空"}
subgraph ヘッダーデコード
DEC_HEADERS["HEADERSフレームデコード"]
DEC_DT_EMPTY --> DEC_HEADERS
DEC_DT_STATE{"動的テーブルの状態"} -- |参照解決| --> DEC_HEADERS
DEC_HEADERS -- |更新適用| --> DEC_APPLY_DT_UPDATE["動的テーブル更新適用"]
DEC_APPLY_DT_UPDATE --> DEC_DT_STATE
DEC_APPLY_DT_UPDATE --> DEC_SEND_DEC_INS["デコーダ指示ストリーム送信"]
end
DEC_RECV_ENC_INS["エンコーダ指示ストリーム受信"] --> DEC_DT_STATE
DEC_SEND_DEC_INS --> DEC_RECV_ENC_INS
end
ENC_SEND_ENC_INS DEC_RECV_ENC_INS
ENC_RECV_DEC_ACK DEC_SEND_DEC_INS
既存プロトコルとの比較
HTTP/2のHPACKと比較して、QPACKは以下の点で進化しています。
HOLブロッキングの回避:
状態管理:
制御ストリーム:
0-RTTサポート:
相互運用
QPACKの実装間での相互運用性には、以下の点に注意が必要です。
QPACK設定のネゴシエーション: HTTP/3のSETTINGS_QPACK_MAX_TABLE_CAPACITYとSETTINGS_QPACK_BLOCKED_STREAMSを通じて、エンコーダとデコーダは動的テーブルの最大容量と、デコーダがブロックできる最大ストリーム数を通知し合います。これらの設定が正しく交換されることが重要です [2]。
動的テーブルの同期プロトコル: エンコーダ指示ストリームとデコーダ指示ストリームのメッセージ(例: INSERT_COUNT_INCREMENT, HEADER_ACK)がRFC 9204に厳密に従って実装されている必要があります。
エラー処理: 動的テーブルの不一致や容量超過などのエラーシナリオにおいて、プロトコルが定義するエラーコード(例: QPACK_DECOMPRESSION_FAILED)を用いて適切にエラーを通知し、接続を閉じるメカニズムが重要です。
セキュリティ考慮
QPACKはヘッダー圧縮に特化したプロトコルですが、ネットワーク通信の一部であるため、いくつかのセキュリティ上の考慮事項があります。
リプレイ攻撃: QPACK自体がリプレイ攻撃から保護するメカニズムは持ちません。しかし、QPACKはQUICとTLS 1.3上で動作するため、QUICのハンドシェイク中に確立されるセッションキーによって、トランスポート層でリプレイ保護が提供されます [1]。QPACKの動的テーブルの内容はセッションごとに確立されるため、過去のセッションのQPACK状態を再利用して攻撃することは困難です。
ダウングレード攻撃: HTTP/3プロトコル全体のネゴシエーションはALPN (Application-Layer Protocol Negotiation) によって行われるため、HTTP/3をHTTP/2やHTTP/1.1にダウングレードさせる攻撃は、ALPNのメカニズムによって防止されます。
キー更新: QPACKはTLS 1.3のセッションキーに依存するため、キーの更新はQUICとTLS 1.3の機能によって行われます。QPACKはヘッダーデータ自体の暗号化は行わず、QUICの暗号化ペイロードとして転送されます。
0-RTTの再送リスク: QPACKは0-RTTをサポートしますが、0-RTTで送信されるヘッダーブロックは、デコーダがまだ認識していない動的テーブルエントリを参照できません。つまり、0-RTTヘッダーは静的テーブルエントリやリテラル、または過去のセッションから永続的にキャッシュされた動的テーブルエントリ(Post-Baseインデックスで参照可能)のみを使用できます。0-RTTデータは冪等であるべきというQUICの原則に従い、QPACKヘッダーブロックも適切に構成される必要があります [1]。
情報漏洩: 圧縮辞書(動的テーブル)の内容がサイドチャネル攻撃(例えばCRIME/BREACH攻撃の亜種)に利用される可能性は、HPACKと同様に存在します。しかし、QPACKではストリームごとのHOLブロッキングが回避されるため、攻撃者が特定のヘッダーを挿入してレスポンスを監視するような攻撃の機会は減少します。それでも、機密情報を含むヘッダーを動的テーブルに長期間保持することには注意が必要です。
実装メモ
QPACKを効率的かつ堅牢に実装するためには、以下の点に留意する必要があります。
MTU/Path MTU (PMTU): QUICはPMTU Discoveryをサポートしており、QPACKヘッダーブロックもPMTU内に収まるようにフラグメント化されます。ただし、ヘッダーブロック全体を一つのQUICパケットに収めることが理想的であり、特に動的テーブル更新のACKは迅速に処理されるべきです。フラグメント化されたヘッダーブロックの再構成ロジックを適切に実装する必要があります。
HOL blocking回避: QPACKの最も重要な設計目標です。デコーダは、Required Insert Countを満たすまでヘッダーブロックのデコードを遅延させることができますが、これはヘッダーブロックごとの遅延であり、ストリーム間のHOLブロッキングではない点に注意が必要です。デコーダがブロックできるストリーム数にはSETTINGS_QPACK_BLOCKED_STREAMSで上限があるため、デコーダは過度に多くのストリームをブロックしないよう、動的テーブル更新の処理を優先すべきです。
キュー制御: エンコーダは、デコーダが処理を完了した動的テーブルエントリの数を示すHEADER_ACKを定期的に受信することで、自身の動的テーブルの状態を更新します。未確認の動的テーブル更新が多数キューに溜まっている場合、エンコーダは新しいヘッダーを動的テーブルのインデックス参照で圧縮することを避け、リテラルとして送信するなどのフォールバックメカニズムを持つべきです。これにより、デコーダが過負荷になるのを防ぎ、遅延を最小限に抑えられます。
優先度: HTTP/3はストリームごとに優先度を設定できます。QPACKのエンコーダとデコーダは、優先度の高いストリームのヘッダーを優先的に処理し、動的テーブルの更新指示やACKも、優先度の高いストリームからのリクエストに対応するものを優先的に送信・処理するよう設計すべきです [3]。
まとめ
QPACKヘッダー圧縮 (RFC 9204) は、HTTP/3 (RFC 9114) がQUIC上で動作する上で不可欠な要素です。HTTP/2のHPACKが抱えていたヘッダー圧縮におけるHOLブロッキングの問題を、制御ストリームと非同期な動的テーブル更新メカニズムによって解決しました。これにより、HTTP/3はQUICのストリーム多重化の利点を最大限に活かし、低遅延かつ高効率なWeb通信を実現します。実装においては、動的テーブルの厳密な同期管理、エラー処理、そして0-RTTや優先度といったQUICの特性との連携が重要となります。
[1] Mike Bishop, et al. “HTTP/3”. RFC 9114. IETF. 2022年6月.
[2] Martin Thomson, et al. “QPACK: Header Compression for HTTP/3”. RFC 9204. IETF. 2022年6月.
[3] Mozilla Wiki Contributors. “HTTP3 FAQ”. Mozilla Wiki. 2024年2月14日.
コメント