リアルタイム通信 — WebSocket・gRPC ストリーミング・WebRTC
ヘッドオブラインブロッキングはどこに残ったか — HTTP/3 と QUIC
一言でいうと
TCPは、順序を守るために、失ったセグメント1つの後ろのバイトをすべて引き留め、QUICはストリームをトランスポート層へ下ろして、失ったストリームだけを待たせます。
なぜ必要なのか
前のモジュールのラボの最後のステップで見た数字を、思い出してみてください。下流方向に20,000バイトが通過したあとで800ms止まる中継器を置くと、1つの接続に載せたHTTP/2の小さな応答は900ms前後で、接続を別々に使ったHTTP/1.1の小さな応答は、数十msで終わりました。中継器が真似たのは、セグメント1つの損失です。TCPはバイトストリームを、順序どおりにしかアプリケーションに渡さないので、途中のセグメントが再送を待つあいだ、その後ろにすでに届いたバイトも、カーネルのバッファーに縛られています。HTTP/2は、アプリケーションレベルのヘッドオブラインブロッキングをなくしましたが、その下のTCPレベルのヘッドオブラインブロッキングは、むしろすべてのストリームがまとめて受けるようになりました。
損失がまれなデータセンターの中では、このコストはほとんど見えません。損失が多いモバイル網やWi-Fi、音声のように数十msが体感されるサービスでは、最も遅い1%のリクエストが、この停止で占められます。
どう動くのか
RFC 9000のQUICは、UDPの上で動きますが、TCPがしていた信頼性のある転送、輻輳制御(RFC 9002)、フロー制御を、すべてやり直します。変わった点は、ストリームがトランスポート層の概念だということです。パケット1つには、複数のストリームのフレームを載せられますが、順序の保証は、ストリームの中だけで行います。あるパケットが消えると、そのパケットにデータがあったストリームだけが再送を待ち、残りのストリームは、届いた順にアプリケーションへ渡されます。
暗号化も、外側に載せずに、内側に入れました。RFC 9001は、TLS 1.3ハンドシェイクをQUICハンドシェイクに溶け込ませて、新しい接続は1-RTTで、以前に会ったサーバーとは0-RTTで、データを送れるようにします。0-RTTデータは、リプレイ攻撃にさらされるので、冪等なリクエストにだけ使う必要があります。接続を、IPとポートではなく接続IDで見分けるので、携帯電話がWi-FiからLTEに切り替わっても、接続を続けられます(接続の移行)。音声通話のように長く続くセッションに、うれしい性質です。
RFC 9114のHTTP/3は、このQUICの上にHTTPの意味を載せます。ヘッダー圧縮も変わりました。HPACKは、ヘッダーブロックが送った順序どおりに届くと仮定して動的テーブルを更新するので、ストリームが別々に届くQUICでは使えません。そのため、RFC 9204のQPACKは、テーブルの更新を、別のエンコーダー・デコーダーストリームで送り、まだ届いていないテーブル項目を指しているために待つリクエストストリームを、いくつまで許可するかを、設定(SETTINGS_QPACK_BLOCKED_STREAMS)で合意します。
| 層 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| リクエストの並行性 | 複数の接続 | 1つの接続のストリーム | 1つの接続のQUICストリーム |
| 損失1つの影響 | その接続だけ | すべてのストリーム | そのストリームだけ |
| ヘッダー圧縮 | なし | HPACK | QPACK |
| 暗号化 | 選択(TLS) | 事実上TLS | 常に(TLS 1.3を内蔵) |
HTTP/3を使えるかどうかは、サーバーがAlt-SvcヘッダーやDNSのHTTPSレコードで知らせ、クライアントはそれを見て、次のリクエストからUDPで試みます。
フロー制御も、2層のままです。QUICは、ストリームごとに、そして接続全体に、受け取れる最大バイト数を通知し(MAX_STREAM_DATA・MAX_DATAフレーム)、同時に開けるストリーム数も、MAX_STREAMSで別に通知します。HTTP/2でSETTINGSとWINDOW_UPDATEがしていたことが、トランスポート層に下りたのです。そのため、前のラボで見た「接続ウィンドウが閉じると、小さな応答も待つ」という現象は、HTTP/3でも同じように起こります。QUICがなくしたのは、損失によるヘッドオブラインブロッキングであり、受け取る側が遅いために生じる塞がりではありません。
現場での姿
HTTP/3がいつも勝つわけではありません。UDP 443を塞ぐ社内網や公衆Wi-Fiが今でもあるので、クライアントはTCPに戻る準備をする必要があり、ユーザー空間で暗号化と輻輳制御を行う分、同じスループットでCPUをより多く使います。ロードバランサーとファイアウォールがQUICを理解できなければ、接続の移行の利点もなくなります。そのため、大規模なサービスは、両方の経路を開けておいて、メトリクスで選びます。
このコースのラボのPodは、NET_ADMIN権限がないので、tc netemで本物のパケット損失を入れられません。そのため、TCP側は、ユーザー空間の中継器で損失の結果(停止)を真似て、QUICはこの読み物で扱います。損失を真似るときに、何を真似て、何を真似られなかったかを書き留めておくことも、測定の一部です。中継器は、再送タイマーと輻輳ウィンドウの縮小までは再現しません。
次のクイズですること
TCPレベルとアプリケーションレベルのヘッドオブラインブロッキングを区別し、QUICがどちら側をどのようになくしたのか、QPACKがなぜ必要だったのかを、確認します。