- ブラウザURL入力今ここ
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口今ここ
- アプリ処理
- DB保存
前回は、HTTP/2 でも下の TCP の順序は1本のままだと見ました。今回は、並べ直しの順序を両端がどの単位で共有しているかを見ます。
まず結論
HTTP/2 は要求に番号を付けて1本の接続に混ぜましたが、並べ直しの順序は下の TCP 接続1本で両端が共有したままでした。1つのパケットが欠けると、関係の無い要求まで待たされます。
HTTP/3 が載る QUIC は、両端が順序を共有する単位を接続からストリームへ狭めました。欠けたストリームだけが待ちます。ただし輻輳制御は接続全体で共有したままで、「すべてが独立した」わけではありません。
QUIC は UDP の上で動きます。では HTTP/3 で、1つの応答のバイト列はアプリへ順序どおりに渡されるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
並べ直しを待つ範囲を、どこで区切るか
TCP は1本の接続を1列のバイト列として届けます。RFC 9001 §2.1 は TLS の下にある TCP を a reliable transport (typically TCP), which provides sequencing and guaranteed delivery と書きます。1列なので、途中が1つ欠けると、その後ろは届いていても渡せず待ちます。
HTTP/2 はこの1列の上に、番号付きのストリームを混ぜます。しかし番号は HTTP/2 の側にあり、TCP からは見えません。RFC 9114 §1.1 はこう書きます。
However, because the parallel nature of HTTP/2's multiplexing is not visible to TCP's loss recovery mechanisms, a lost or reordered packet causes all active transactions to experience a stall regardless of whether that transaction was directly impacted by the lost packet.
両端の TCP が共有している順序は、接続全体で1つだけです。だから欠けたパケットと無関係な要求まで止まります。
QUIC は順序を共有する単位を変えました。RFC 9000 §2 です。
Streams in QUIC provide a lightweight, ordered byte-stream abstraction to an application.
QUIC does not provide any means of ensuring ordering between bytes on different streams.
各ストリームの中は順序どおり。ストリームの間には順序が無い。並べ直しの待ちはストリームの中に閉じます。HTTP/3 はこれを使い、RFC 9114 §2 は Each request-response pair consumes a single QUIC stream. と定め、こう続けます。
Streams are independent of each other, so one stream that is blocked or suffers packet loss does not prevent progress on other streams.
- ・左が HTTP/2 over TCP、右が HTTP/3 over QUIC
- ・QUIC が分けたのは順序の共有であって、接続まるごとではない
ストリーム番号 0 と 4 を見る
nginx:alpine(nginx/1.31.6)を HTTP/3 で待ち受けさせ、HTTP/3 を話せる curl から2つの要求を並行に送りました。検証環境の macOS 標準 curl 8.7.1 は HTTP/3 を持たないため、Docker の debian:trixie-slim に apt で入る curl 8.14.1 を使っています。証明書は自作の CA で発行したものです。
* Connection #0 is not open enough, cannot reuse* Waiting on connection to negotiate possible multiplexing.* Connected to localhost (172.19.0.2) port 8443* using HTTP/3* [HTTP/3] [0] OPENED stream for https://localhost:8443/a.txt* Re-using existing https: connection with host localhost* [HTTP/3] [4] OPENED stream for https://localhost:8443/b.txt* Connection #0 to host localhost left intact
同じ接続を使い回して(Re-using existing https: connection)、ストリーム 0 と 4 が開いています。2回実行して同じでした。
番号が偶数なのは、HTTP/3 が自前の番号を持たず QUIC のストリーム ID を使うからです。RFC 9000 §2.1 は Client-initiated streams have even-numbered stream IDs と書き、下位2ビットで開いた側と方向を表します。クライアントが開く双方向ストリームは 0, 4, 8 と進みます。HTTP/2 は逆で、RFC 9113 §5.1.1 は Streams initiated by a client MUST use odd-numbered stream identifiers です。並べて読むときは偶奇の向きに注意してください。
この実演では遅延も損失も入れていません。損失が起きたときに他のストリームが止まらないことは実演しておらず、上の RFC 9114 §2 の記述に依っています。
「QUIC は UDP の上で動くので、HTTP/3 を使うアプリは受け取ったデータを自分で並べ直す必要がある」という説明は妥当ですか。
分けたものと、共有したまま残したもの
QUIC はすべてを分けたわけではありません。RFC 9114 §1.2 です。
By providing reliability at the stream level and congestion control across the entire connection, QUIC has the capability to improve the performance of HTTP compared to a TCP mapping.
分けたのは信頼性、つまり欠けを埋めて並べ直す待ちです。輻輳制御は接続全体で1つのままです。「ネットワークの仕組み」コースの『送りすぎない仕組み — フロー制御と輻輳制御の違い』で見たとおり、輻輳制御は途中の道の都合を推測して送る量を抑える仕組みです。
逆に、共有するものを増やして往復を減らした箇所もあります。TCP と TLS は別々に合意します。TCP の3ウェイハンドシェイクで1往復、TLS 1.3 でもう1往復です(RFC 9001 §2.1 は TLS 1.3 を A full 1-RTT handshake, in which the client is able to send application data after one round trip と説明します)。QUIC は2つを1つにしました。
QUIC relies on a combined cryptographic and transport handshake to minimize connection establishment latency.
同コースの『なぜ手を3回握るのか』の区別(送り始められるまで/応答が返るまで)に揃えて数えると、0-RTT と TCP Fast Open を使わない場合、要求を送り始められるまでは TCP + TLS 1.3 が 2 RTT、QUIC が 1 RTT。応答が返るまでは 3 RTT と 2 RTT です。
遅延 50ms を入れて往復を数える
先の nginx コンテナの eth0 に tc qdisc add dev eth0 root netem delay 50ms を入れ、サーバからクライアントへの向きだけを 50ms 遅らせました。RTT はおよそ 50ms です。curl の -w で段階ごとの経過秒を出しています(最初の1回は準備運転として除いた)。
=== run1 --http3-only 02:00:46proto=3 namelookup=0.000018 connect=0.057160 appconnect=0.057156 pretransfer=0.059611 starttransfer=0.121289 total=0.121409=== run1 --http2 02:00:46proto=2 namelookup=0.000019 connect=0.052631 appconnect=0.120827 pretransfer=0.120998 starttransfer=0.174001 total=0.174319=== run2 --http3-only 02:00:46proto=3 namelookup=0.000012 connect=0.056757 appconnect=0.056756 pretransfer=0.058824 starttransfer=0.113046 total=0.113080=== run2 --http2 02:00:46proto=2 namelookup=0.000008 connect=0.052582 appconnect=0.110965 pretransfer=0.111012 starttransfer=0.163608 total=0.163664=== run3 --http3-only 02:00:46proto=3 namelookup=0.000010 connect=0.056028 appconnect=0.056026 pretransfer=0.057156 starttransfer=0.113417 total=0.113491=== run3 --http2 02:00:46proto=2 namelookup=0.000015 connect=0.052027 appconnect=0.110631 pretransfer=0.110786 starttransfer=0.165207 total=0.165475
HTTP/2 は TCP の接続(connect)が約 52ms で1往復、TLS が終わる appconnect が 111〜121ms でもう1往復。要求を送り始める pretransfer は約 2 RTT、応答を受け終える total は 164〜174ms で約 3 RTT です。
HTTP/3 は connect と appconnect がほぼ同じ値(56〜57ms)です。輸送と暗号が別の段階として分かれていません。pretransfer は 57〜60ms で約 1 RTT、total は 113〜121ms で約 2 RTT。どの回でも差はおよそ 1 RTT でした。
条件は、同じホスト上の Docker、損失なし、片方向 50ms の遅延です。この差は往復の数から来るもので、UDP だから速いわけではありません。
「QUIC ではストリームがすべて独立しているので、同じ接続で大きな転送が走っていても、他のストリームが送れる量には一切影響しない」という説明は妥当ですか。
HTTP/3(QUIC のストリーム)
要求1つにストリーム1本。並べ直しの待ちはストリームに閉じ、輻輳制御は接続全体で1つ。RFC 9114 §1.2 の書き方です。
項目 HTTP/3 (QUIC)並行する要求1つに ストリーム1本並べ直しの待ち ストリームごと輻輳制御 接続全体で1つ
HTTP/1.1 で TCP 接続を複数張る
並行したい要求の数だけ TCP 接続を張る。並べ直しの待ちは接続ごとに分かれるが、輻輳制御も接続ごとに別々になる。
項目 HTTP/1.1 の複数接続並行する要求1つに TCP 接続1本並べ直しの待ち 接続ごと輻輳制御 接続ごとに別々
どちらも「1つが欠けても他を待たせない」を実現しています。では何が違うのでしょうか。
RFC 9114 §1.1 が HTTP/1.1 の複数接続の何を問題にしているかを、QUIC の側と照らしてください。
この章のまとめ
- HTTP/2 の番号は TCP から見えない。両端の TCP が共有する順序は 接続全体で1つで、1つの欠けが全ストリームを止める(RFC 9114 §1.1)。
- QUIC のストリームは 中は順序どおり、間は順序なし(RFC 9000 §2)。並べ直すのは QUIC で、「UDP だから順序が無い」は誤り。
- HTTP/3 は要求1つに QUIC のストリーム1本。クライアントが開く番号は 0, 4, 8(偶数)。HTTP/2 は奇数。
- 分けたのは信頼性、輻輳制御は接続全体で共有(RFC 9114 §1.2)。「すべて独立」ではない。
- 輸送と暗号の合意を1つにまとめ、要求を送り始められるまで 2 RTT → 1 RTT。実測でも差は約 1 RTT。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
curl で HTTP/3 の要求を2つ並行に送ると、ストリーム番号は 0 と 4 でした。HTTP/2 では奇数になります。この違いの説明として正しいのはどれですか。
用語集(3語)
- ストリーム(QUIC)
- 1つの QUIC 接続の中に作られる、順序つきのバイト列。RFC 9000 §2 は `ordered byte-stream abstraction` と定め、異なるストリームの間の順序は保証しない。HTTP/3 は要求と応答の1組に1本を使う。
- ストリーム ID
- 接続の中でストリームを識別する番号。QUIC では下位2ビットが開いた側と方向を表し、クライアントが開く双方向ストリームは 0, 4, 8 と進む。HTTP/2 ではクライアントが開くストリームは奇数。
- 輻輳制御
- 経路の混み具合を推測して送る量を抑える仕組み。QUIC は接続全体で1つ持つ(RFC 9114 §1.2)。