HTTP/3

20 分で読了
このページの目次

HTTP/3は、HTTPのセマンティクスをQUICにマッピングし、QUICが提供する信頼性の高いストリーム、フロー制御、輻輳制御、およびTLS 1.3のハンドシェイクを使用します。これにより、1つのストリームの伝送ギャップが他のストリームへの順序付き配信の干渉を減らしますが、すべてのリクエスト間の依存関係を解消するわけではありません。

メソッド、ステータスコード、キャッシュなどはHTTPの一般的なセマンティクスを継承しており、HTTP/2の9バイトのフレームヘッダー、ストリームウィンドウ更新、優先度ツリーなどはそのまま持ち越されていません。トラブルシューティングの際は、HTTP層、QUICストリーム、QUICパケット、UDPデータグラムを区別して検討する必要があります。

問題重要な境界
UDPの損失がHTTPコンテンツの損失につながるかQUICが信頼性の高いストリームデータの回復を担当する
ストリームは完全に独立しているか順序付き配信は独立しているが、輻輳、クォータ、上位層の依存関係は共有される
QPACKテーブルの更新が届かない場合はどうするか未到達のテーブルエントリを参照するフィールドブロックがブロッキングの原因となる可能性がある
HTTP/3フレーム内でStream IDはどこにあるか関連付けは所属するQUICストリームで提供されており、HTTPフレームヘッダーには重複してエンコードされない
初回アクセスでは必ずHTTP/2を経由する必要があるかDNSのHTTPSレコードなどを通じてエンドポイントを検出することも可能である
ネットワークの切り替えや0-RTTで必ず高速になるかパス、状態、ポリシー、および実際のリクエスト次第である

ストリームマッピング:リクエスト、制御、圧縮命令の役割分担

HTTP/3 は異なる役割を異なる QUIC ストリームに割り当てる リクエストストリーム:クライアント双方向ストリーム 0、4、8… 各ストリームで1つのリクエストとレスポンス;HEADERS / DATA 制御ストリーム:エンドポイントごとに1つの単方向ストリーム ストリームタイプ 0x00;最初に SETTINGS、その後他の適用可能な制御フレーム QPACKストリーム:エンコーダーとデコーダーはそれぞれ単方向 ストリームタイプ 0x02 / 0x03;テーブル更新と確認命令 QUIC Stream ID と HTTP/3 の単方向ストリームタイプは異なるフィールドです;タイプ 0x00 は QUIC ストリーム番号が 0 であることを意味しません。

リクエストには、クライアントが開始する双方向のQUICストリーム(IDは0、4、8など)が使用され、リクエストとレスポンスはそれぞれそのストリームの2つの方向を占めます。HTTP/2とは異なり、QUICストリーム0はビジネスリクエストを担うことができ、接続制御フレーム専用のストリームではありません。

単方向ストリームの先頭にはストリームタイプが書き込まれ、その後そのタイプで規定された内容が格納されます。各エンドポイントの制御ストリームはSETTINGSを最初のフレームとして持ち、同じ制御ストリームでSETTINGSを再度送信することはできません。HTTP/2のようなSETTINGS ACKは存在しません。QPACKのエンコーダーとデコーダーストリームはそれぞれ独自の命令形式を担い、HTTP HEADERSやDATAフレームに包まれたテーブル更新ではありません。

双方は、相手が少なくとも3つの単方向ストリームを確立できるようにする必要があります。これにより、制御とQPACKのためにスペースとフロー制御の予算を確保します。確立された重要なストリームが予期せず閉じられた場合、接続エラーを引き起こすため、通常のリクエストが完了した後に自然に閉じられるものとして扱うことはできません。RFC 9114:ストリームマッピング

フレームヘッダーは可変長整数であり、固定の9バイトではない

HTTP/3フレームには、Type、Lengthの2つのQUIC可変長整数とペイロードが含まれます。長さはペイロードのみを記述し、ストリームIDは外側のQUICストリームで提供されます。一般的なフレームの整数は、プロトコルで許可されたエンコーディング長を使用できます。この例では最短エンコーディングを選択しています。

値の範囲エンコーディングバイト数上位2ビット
0 〜 63100
64 〜 16,383201
16,384 〜 1,073,741,823410
さらに 2^62−1 まで811

5バイトのDATAを持つHTTP/3フレームは、Type 1バイト + Length 1バイト + コンテンツ5バイトで、合計7バイトです。1000バイトのDATAの場合、1+2+1000=1003バイトとなります。これらの数字にはQUIC STREAMフレーム、パケット保護、UDP/IPヘッダーは含まれておらず、エンドツーエンドで固定の割合を節約したと主張するために直接使用することはできません。

def varint(value):
    if not 0 <= value < 2**62:
        raise ValueError('out of range')
    for size, prefix in [(1, 0), (2, 1), (4, 2), (8, 3)]:
        if value < 2**(8 * size - 2):
            return (value | (prefix << (8 * size - 2))).to_bytes(size, 'big')

def decode(data):
    size = 1 << (data[0] >> 6)
    if len(data) < size:
        raise ValueError('truncated')
    return int.from_bytes(data[:size], 'big') & ((1 << (8 * size - 2)) - 1), size

for n in [0, 63, 64, 16383, 16384, 2**30 - 1, 2**30, 2**62 - 1]:
    value, used = decode(varint(n))
    assert value == n and used == len(varint(n))
assert varint(64).hex() == '4040'
small = varint(0) + varint(5) + b'hello'
large = varint(0) + varint(1000) + b'x' * 1000
assert small.hex() == '000568656c6c6f' and len(small) == 7
assert len(large) == 1003
try:
    decode(bytes([0x40]))
except ValueError:
    pass
else:
    raise AssertionError('accepted truncated integer')
print('HTTP/3 DATA 完全フレームのバイト数:', len(small), len(large))

リクエストまたはレスポンスにはHEADERS、オプションのDATA、トレーラーHEADERSが使用され、方向の終了はQUICストリームの終了によって表現され、HTTP/2のEND_STREAMフラグは存在しません。HEADERSフレームは複数のQUIC STREAMフレームやパケットにまたがる可能性があり、CONTINUATIONは必要ありません。パケットの境界はフィールドブロックの境界ではありません。

TCPのキューヘッドブロッキングは減ったが、共有リソース仍需直面する

ストリーム0にコンテンツの欠落があり、ストリーム4の完全なデータが既に到着している場合、QUICはストリーム4の配信を続けることができます。ストリーム0のギャップを待つ必要はありません。ただし、同じ損失パケットに複数のストリームのデータが含まれている可能性があり、損失による輻輳ウィンドウの調整は接続全体の送信に影響を与える可能性があります。

接続レベルのMAX_DATA、単一ストリームのMAX_STREAM_DATA、オープン可能なストリームの数、CPUおよびサーバー側のキューイングも進行を制限する可能性があります。HTTP/3では、フロー制御をQUICに委譲し、HTTP/2のWINDOW_UPDATEを送信しません。HTTP/2がDATAペイロードに対してのみ課金するのとは異なり、QUICストリームのクォータは、HTTPフレームヘッダーやHEADERSデータを含むそのストリームのバイト全体をカバーします。

したがって、「あるリクエストが別のストリームのバイトギャップのために待たされることはない」というのは具体的な保証ですが、「リクエスト間の完全な独立性」はそれを過度に拡大したものです。QUIC ストリームとフロー制御

QPACK:圧縮の利点とブロッキングリスクの間の選択

QPACK は依存関係の選択を許可し、依存関係による有限のブロッキングも許可する エンコーダーストリーム:6番目、7番目のエントリを挿入 途中での遅延;受信側は一時的に5エントリのみを挿入 リクエストA:Required Insert Count=7 HEADERS は到着したが、一時的にデコードできない リクエストB:動的テーブル参照なし Required Insert Count=0 テーブル更新到着後 累積挿入数が7に達し、Aは継続可能 B が独立してデコードできるからといって、共有への影響が全くないわけではありません:接続の輻輳、フロー制御、CPU、およびアプリケーションリソースは依然として共有されます。

HPACKは順序付き接続における圧縮状態の進化に依存しています。HTTP/3ストリームは独立して配信できるため、QPACKはテーブルの更新と確認を専用ストリームに分離します。フィールドブロックは、まだ受信していない動的エントリを参照する可能性があり、Required Insert Count を通じてデコード依存関係を表現します。

図ではデコーダの累積挿入数が5エントリのみですが、リクエストAは7エントリを要求しているため、更新を待つ必要があります。リクエストBは静的エントリまたはリテラルのみを使用するため、Required Insert Count=0でデコード可能です。ここでは復元された挿入カウントを示していますが、実際のフィールドには規定されたカウントエンコーディングやBase/相対インデックスも関与しており、単純な整数比較を完全なデコーダとして扱うことはできません。

QPACKの静的テーブルには99エントリがあり、インデックスはHPACKと共通ではありません。動的テーブルは依然として接続のエンコーディング方向ごとに個別に維持されます。デコーダは、Section Acknowledgment、Stream Cancellation、Insert Count Incrementなどの情報を返し、エンコーダーが依存関係と解放可能な状態を追跡するのに役立ちます。これらの命令の単位と解放条件は異なるため、「テーブル同期」として一括して呼ぶことはできません。

SETTINGS_QPACK_MAX_TABLE_CAPACITY と SETTINGS_QPACK_BLOCKED_STREAMS のデフォルト値はどちらも0です。許可されるブロッキングストリーム数は、「現在リクエスト中のストリームの総数」ではありません。エンコーダーは、潜在的なブロッキングを増やさないために、既知の安全な参照またはリテラルを選択して、より低い圧縮率で対応することもできます。Never Indexed は暗号化されたフィールドとは等しくありません。RFC 9204:QPACK

ストリームのバイトは揃っているのに、なぜフィールドがデコードできないのか?

AのHEADERSをすべて配信しても、まだ6番目と7番目の挿入を待っています。Bを配信するとすぐにデコード可能です。6に補完してもAの依存関係は満たされず、7に補完されて初めてロックが解除されます。この待機はQPACKの依存関係で発生しており、Aのストリームバイトの欠落に起因するものではありません。

図を準備しています
ストリームのバイトは揃っているのに、なぜフィールドがデコードできないのか?

信頼性の高いストリーム配信の完了は、フィールドのデコード完了を意味しません。動的エントリを参照するブロックのみが対応する挿入を待ち、静的/リテラルブロックは先に完了できます。

エンドポイントの検出:Alt-Svc と DNS HTTPS

Alt-Svc: h3=":443"; ma=86400 は、元のサービスが同じホストの443ポートでHTTP/3の代替サービスを提供することを示し、max-ageは代替サービス情報のキャッシュ時間を記述します。これはHTTPコンテンツのキャッシュ時間ではなく、クライアントが既存の接続をすぐに移行する必要があることも意味しません。

代替サービスは元のoriginを変更しません。他のホスト名やポートに接続する場合でも、認証は元のサービスを代表する権限があることを証明する必要があります。この位置指示を、新しい身份を自動的に信頼する権限として扱うことはできません。RFC 7838:代替サービス

DNSのHTTPS/SVCBレコードも、alpn、portなどのサービスパラメータを提供し、クライアントがアクセスする前にh3を検出するのに役立ちます。これにより、毎回h1/h2経由でAlt-Svcを受信する必要がなくなります。レコードは単なる検出材料であり、クライアントの機能、証明書検証、および接続結果は依然として要件を満たす必要があります。RFC 9460

UDPがブロックされ、バージョンが互換性がない、またはパスが到達不能な場合、クライアントは競合またはTCP上のHTTPへのフォールバックを行う可能性があります。具体的な待機時間は実装によって決定され、「QUICの試行失敗に遅延が全くない」と約束することはできません。指標は、初回検出、接続確立、フォールバック、およびビジネス処理の時間に分解する必要があります。

0-RTT と移行:以前の状態とパス検証への依存

0-RTTには、利用可能な回復状態が必要であり、サーバーによって拒否される可能性があります。早期リクエストを送信する際には、記憶された適切なHTTP/3設定と転送パラメータを使用する必要があります。拒否された後の再送信のタイミングや再送信の可否は、プロトコルおよびビジネスの規約に従う必要があります。

早期データにはリプレイのリスクがあります。GETは形式的に安全ですが、実際のアプリケーションに副作用が全くないことを証明するものではありません。引き落としや注文の提出などの操作は、遅延が低いからといって早期データに含めることはできません。HTTPのEarly-Dataフラグと425 Too Earlyは、ミドルボックスとオリジンサーバーの調整に役立ちます。425後の再送信は、早期データに再度含めるべきではありません。RFC 8470

接続の移行は、アドレス変更後も接続身份を維持できますが、新しいパスの利用可能性、検証、および接続状態が維持されていることが必要であり、エンドポイントのポリシーによって制限されます。Wi-Fiからモバイルへの切り替え時には、プロービング、輻輳状態の調整、または失敗を経験する可能性があり、切り替えが常に中断なく行われることを保証するものではありません。サーバー側のロードバランシングも、適切なCIDを接続状態を保持しているインスタンスにルーティングする必要があります。QUIC 移行

キャンセルと GOAWAY:境界とHTTP/2との違い

QUICのRESET_STREAM、STOP_SENDINGなどはリクエストの終了に関与しますが、「ストリームのキャンセル」はビジネストランザクションのロールバックとは等しくありません。レスポンスがない場合、リクエストがすでに処理された可能性を判断し、ビジネスの再送信を決定する必要があります。

サーバー側のHTTP/3 GOAWAY内のリクエストストリームIDは、「受け入れない」境界です。これより大きいまたは等しいIDのリクエストは処理されず、これより小さいIDのリクエストはすでに処理されている可能性があります。境界が8の場合、ストリーム8、12は他の接続で再送信できますが、ストリーム0、4はこれだけを実行されていないと認定することはできません。HTTP/2のLast-Stream-IDは、すでに処理されている可能性を含む上限値であり、同じ比較演算としてコピーしないでください。

複数のGOAWAYの境界値が増加することはありません。クライアントが発行するGOAWAYはプッシュIDを使用しており、これを同じクライアントリクエストストリームIDとして直接解釈することはできません。RFC 9114:退出

プッシュ、DATAGRAM と観測

HTTP/3の基本仕様は依然としてプッシュと対応する許可制御を定義していますが、ブラウザやサーバー製品がサポートしているかどうかは別途確認する必要があります。プロトコルがプッシュ全体を廃棄したと直接記述することはできません。通常のHTTP DATAは依然として信頼性の高いストリームを介して送信され、QUICがDATAGRAMをサポートしていても、すべてのHTTP/3レスポンスが自動的に信頼できない転送になるわけではありません。

パケットキャプチャでは、まずUDP上のQUIC、バージョン、および接続確立を確認し、利用可能な復号材料または実装イベントトレースを使用してHTTPフレーム、QPACKブロッキング、およびフロー制御を参照します。UDPポート443だけではリクエストの成功を判断できません。アプリケーションのステータスコードとQUIC転送の終了エラーは別々に記録し、ビジネスによる拒否と接続障害を区別する必要があります。

継続的な参照

  • HTTP/2:フレーム、マルチプレクシング、HPACK、および退出境界の対照。
  • UDP と QUIC:パケット回復、CID、およびフロー制御。
  • TLS 1.3:ハンドシェイクと早期データのセキュリティ属性。