【論文メモ】Resource Multiplexing and Prioritization in HTTP/2 over TCP and HTTP/3 over QUIC 2章 QUIC ResourceMultiplexing

QUICの多重化によって起きる課題2つについて議論している。

①ストリームの多重化によって起こる優先度の決定。QUIC自体は個別のストリームに関連付けられたセマンティクスは認識できないので、相対的なストリーム間の優先度はQUIC側からはわからない。FIFOみたいな送り方をするのか、Round-Robinやそのバリエーション(例えば、1つのQUICパケット毎に送信すするストリームを変えるのか、複数のQUICパケットで送ってから別のストリームにスイッチするのか)がいいのか。

②再送のスケジューリング ロスしたデータと普通のデータ間の優先度をどうするのか。 3つのアプローチが実際の実装から観測された。

  • ロスしたデータも普通のデータも同じ優先度として扱う
  • ロスしたデータは優先度を高くする。ただしロスしたデータがあるストリーム間の優先度は考慮しない。
  • ロスしたデータに高い優先度を設定する。またそれらの中でさらにスケジューリングを設定する。

このセクションでは、これら二つについて、各種実装が現在どういう実装をしているかを評価している。

内容はかなり重たいですが学びは多い。

2.1 Background: Resource Streams

QUICは独立した多重化されたバイトストリームを一つのコネクション上で実現する。 これらのストリームは、QUIC自体ではかなり抽象的な概念ですが、うまくHTTPリクエストとレスポンスにマッピングされている。 これは、実際にH2などのプロトコルの複数のファイルからデータを伝送している場合でも、転送するすべてのデータを単一の不透明なバイトストリームの一部とみなすTCPとは全く対照的です。

これは内部のプロトコルのメカニズムを見ると簡単に理解できます。TCPはシーケンス番号をパケットヘッダーに含めます。それは、大きなストリーム内での現在のパケットの位置を表します。 例えば、1400バイトのTCPパケットがシーケンス番号10を持つ場合、10バイトまで運ぶことができ、1409のペイロードを含みます。(シーケンス番号10から1400バイト読まれた1410を確認応答で返す) QUICは異なるアプローチをとる。ペイロードメタデータをヘッダには含まない。その代わりに追加のフレーミングレイヤーを使う。 QUICのストリームフレームはストリームID(そのバイトストリームが属しているフレーム)とオフセットと長さのフィールドを使う。 しかしこれらのオフセットはストリーム毎に異なる。 もしあるストリームAがオフセット10で長さ1400を持っているときに、同様に続くストリームフレームBがオフセット10で長さ1400の全く異なるデータを持つことができる。 TCPでは、Bのデータは1410のオフセットを与えられる。

柔軟性が追加されたが、QUICのセットアップは新しい課題を提供(Provide)している。 TCPは単にアプリケーションレイヤーから受信したデータをシングルバイトストリームでトランスポートすればよかったが、QUICは、複数の進行中のストリームのデータをQUICパケットに入れて送るかを決めなければならない。 言い換えると、多重化とスケジューリングを決めなければいけない。 Section4で見るように、これはアプリケーションレイヤー、例えばH2やH3の優先度制御のシステムで決められる。 しかし、QUICはスタンドアロンなトランスポートポートプロトコルであり、HTTP以外でも使用することを目的としているため、これらのメカニズムをトランスポートレイヤーだけで考慮することも有用です。 QUICはストリームの抽象的な概念を含むが、個々のストリームに関連付けられたセマンティクスを認識していないため、相対的なストリーム間の優先度を引き出すことができません 言い換えると、QUICはすべてのストリームを同じ重要度として扱います。

☆ここ重要 二つのストリームAとBに対して考えられるいくつかのアプローチを検討します。 一つの可能性はsequentialの方法です。これは、StreamBのデータを送信する前に、StreamAのデータをすべて送信します。 他の方法として、Round-Robinが考えられます。Bにスイッチングする前にAの限定的なデータを送ります。 たくさんのRRのvariantが存在する。スイッチする前にどのくらいのデータを送信するかに依存する。 例えば、あるスケジューラーは40パケット送った後にスイッチしたり、1パケット送った後にスイッチしたり、あるいは1つのパケットに複数のストリームを入れて送信します。 質問は次の通りです。どの方法、variantがどういう状況でベストな性能を出すのでしょうか?

この問題は信頼性を考えるとさらに複雑になります。TCPのようにQUICはACKとタイムアウトベースの再送によってデータの信頼性を担保します。 TCPと異なり、これらの再送はストリームベースで行われます。QUICはパケット全体をACKしますが、エンドポイントはどのストリームがどのパケットに含まれていたか情報を維持することを期待されます。 だから、QUICはパケット全体を再送する必要はなく、個々のストリームのフレームのみを再送すればよいのです。 実際、それすら必要ありません。QUICは、ACKされていないバイトストリームの範囲(offset+length)のみを記録しておけばよいのです。もしパケットロスが検出されたら、失われた範囲を再度新しいストリームフレームにパッキングします。 (別のQUICパケットに含まれて送られた2つの異なるストリームの情報を一つのQUICパケットに結合して再送することができます。) なので、QUICの実装は普通のデータと比較して、どのように再送をスケジューリングするかを決める必要があります。 ここでは3つの再送のアプローチを考えます。これらは実験の結果見えてきたアプローチです。 この後の例では、ストリームABCDに対して、Round-Robinのスケジューリングを想定します。また、AとBで送る情報のうち一部はロスしたものとします。

RA #1 Default Scheduling: 再送データは普通のデータと同じに扱われます。スケジューラーによって、普通の手続きと同様に選ばれます。再送データは特別な扱いを受けません。 例えば、ABCDABCDというようになります。

RA #2 Priority retransmission with default scheduling: ロスしたデータには高い優先度を与えます。またロスしたデータ間の優先度は同じものとします。 例えば、Round-Robinのスケジューラーは、最初にロスが発生したストリームの間でRound-Robinの処理を行い、それから、すべてのストリームに対してRound-Robinの処理を行います。例えば、ABABCDCDというようになります。

RA #3: Priority retransmissions with special scheduling ロスしたデータに高い優先度を設定します。またそれらの中でさらにスケジューリングを設定します。 例として、Round-Robinスケジューラーは、ロスしたデータに対してはsequentialなスケジューリングを行います。 例えば、AABBCDCDというようになります。 逆にデフォルトがsequentialなスケジューラーでロスしたパケットに対してRound-Robinを行うのであれば、ABABCCDDというようになります。

これらのスケジューラーとRAの間に直接的な明確な勝者がいないことは明らかです。 それぞれがトレードオフがあり、性能が良いか悪いかはユースケースと特定の型 (specific types ?これなんだろう)とロスの頻度に依存します(burstyなのかsingle packet lossなのか)。 この大きなパラメータ空間を考慮して、現在のQUIC実装によって行われたデフォルトの選択を評価することにしました。 これらは、外部の優先順位付けのシグナリングを使用しない場合に、QUICスタックから見られる既成の動作とパフォーマンスを決定します。

2.2 Experimental Setup

2020 1月時点で、20の公開されているQUICの実装がある。 ここではIETFQUICのみを評価する。また、関連した研究で複数のQUICの実装を比較しているものは今のところない。

多くのIETF QUICの実装は、公開されているエンドポイントまたはオープンソースを用意している。 壊れていたり、更新が行われていないものや、パブリックなエンドポイント無しで実装が非公開なものを除いて、以下の10を選んだ aioquic quiche (Google) lsquic (LiteSpeed) msquic (Microsoft) mvfst (Facebook) ngtcp2 picoquic quiche (Cloudflare) ats (Apache Foundation) quicly (Fastly) 以降は、Googleのquicheはgoogleと呼ぶ

実験を行うために、すべてのQUICのfeatureが実装されており、他の実装との相互接続性も高く、pythonで実装されており簡単に調整ができるので、aioquicをクライアントとして選択した。 このクライアントをdebian のDockerコンテナ上で走らせて、公開されているエンドポイントに対して同時に10個のファイルのリクエストを送信するようにした。ファイルのサイズを1KBと10MBの間でいくつか試している。 H3をリクエストのために使っているが、H3のレイヤーからQUICのレイヤーに対して優先度の情報は与えていない。これによってトランスポートのデフォルトの挙動を見ることができる。

すべての実験は2つのWAN環境で行った。 下り1Gbps、上り10Mbpsを持つHsselt大学のネットワークと、下り35Mbps、上り2Mbpsの住宅で使うようなWi-Fiネットワークを使っている。 最初のネットワークでは、最適な環境での多重化の振る舞いをみることができる。二つ目のネットワークは、パケットロス率が一つ目のネットワークより高いので、再送に対する洞察を行うことができる。 すべてのテストは少なくとも10回計測を行っている。 解析のために、aioquicでqlogを使っている。qlogは構造化されたjsonベースのフォーマットで、詳細なイベントのログを含んでいる。 qlogをカスタムスクリプトで処理して、qvisで可視化を行った。 このために、multiplexing graphという新しい機能のcontributionを行った。

2.3 Multiplexing Results and Discussion

主要な結果はFigure 1にまとめられている。ここでは、1MBのファイルのリクエストを10個同時に行う実験の結果を示している。 エンドポイントごとに1つずつ結果をのせている。 picoquicサーバー側の大規模の変更前後の結果を含めている(before: picoquic、after: picoquic_alt) mvfstは、クライアント側のパラメータを調整した結果(before:mvfst, after: mvfst_alt) を含めている。 エンドポイントは、大まかに似た挙動をするものをグループ化している。

f:id:neko--suki:20200215145842p:plain
Figure 1 (論文からの引用)

Figure1は、QUICのSTREAMフレームの受信を、ストリームごとに色分けして載せている。そのためパケット間の到着時刻のギャップについては隠蔽されている。 いくつかのコンテキスト依存の情報が隠蔽されるが、全体の多重化の振る舞いについて見ることが可能になる。 同じ色のブロックが大きく続いている状態は、そのストリームがより連続的に送られている(sequential)であることを意味する。 一方で、色香すぐに変わる場合は、Round-Robinのスケジューラーが使われていることを意味する。 黒いブロックは、再送データを意味している。 再送は、それぞれのストリームのバイトの範囲を継続してトラッキングすることで判断している。(送信側がストリーム内では常に順番に送ってきていると仮定する) もし新しいフレームが十分に前に作られたバイト範囲のギャップを埋めるのであれば、それは再送のフレームと判断する。 もし、少し前に作られたギャップを埋めた場合、それはネットワークのジッターによってフレームの順序が逆転して届いたものとして判断する。 なので、黒が下についてないブロックからはエンドポイントのデフォルトの多重化の振る舞いを見ることができて、黒が下についている場所からは再送の振る舞いを見ることができる。 Figure1はそれぞれのエンドポイントを示す結果だが、ここからの議論は特に言及がない限りはすべての結果が考慮されている。

(ここが最初に) Figure1から一般的な傾向を見ることができる。多くの実装がRound-Robin方式でスケジューリングを指定要る。2つのスタックのみがSequentialなアプローチをとっている。 Round-Robinのグループの中で、多くの実装がパケット事にストリームをスイッチングする細かい粒度の方法を使っている。 msquicとgoogleのみがより大きなブロックでのスイッチング(それぞれ4と14 QUIC パケット)を行っている。 lsquicは最初おかしく見えたので実装者に意図を確認した。実装の容易化とサーバーの性能向上のために、ブロックサイズを輻輳制御ウィンドウのサイズをもとに選んでいる。データを送ることができる場合、次のストリームが選ばれて、輻輳制御ウィンドウを埋めることができるすべてのデータを送信するようになっている。 この実装意図から、最初は粒度が細かかったものがだんだんと粒度が荒くなっていき、ロスが検出されると再度粒度が細かくなるというトレースの結果を説明することができる。

次に、再送のアプローチ(Retransmission Approach)は、大きくRA#1とRA#2に分けられる。 これら2つの見た目上の大きな違いは、#1の場合黒い領域は小さな白いギャップでインターリブされており、ロスしていない(再送ではない)ストリームからのデータが送信されます。これは再送信が絶対的な優先順位を受けないことを意味増します。quiclyのトレースが良い例である。 逆に#2の場合は、黒いエリアはほぼ連続している。これは、実装者が再送データに高い優先度を割り当てていることを意味する。 lsquicの最初の黒いブロックが分かりやすい例である。同様なふるまいは、atsはpicoquicの右側でも見ることができる。つまり再送の紫のデータが送信中の黄色のデータに割り込まれる形で送信されている。 唯一、mvfstのみが、#3の方法を採用している。普段はパケット毎のRound-Robinであるが、データがロスした場合は、Sequentialなふるまいになっている。 これらの発見のインパクトについては、Section3とSection6で議論する。

次に発見された実装毎の癖について議論する 最初に、atsとpicoquicはsequentialな方式をとっている。しかし、LIFOで処理を行っている。言い換えると、10番目にリクエストしたリソースが最初に送信される。これは一般的には最適ではないが、いくつかのユースケースでは理にかなったやり方である。実装者にこの情報を提供したところ、atsはこの振る舞いに気づいていた。ただし、彼らは、H3の新しい優先度制御のスキームが決まるまで方式の変更を待っている。 一方で、picoquicはこの状況を予期していなかった。メンテナーはこの振る舞いを修正して、FIFOになるようにした。その結果が、picoquic_altに示されている。 同様に、ngtcp2も最初はRA#3の振る舞いのように、再送をsequentialに行っているように見えた。実装者にこの情報を伝えたところ、これは、公平なキューイングの実装のバグだということが発覚した。これは修正され現在はRA#2のようになっている。

次に、mvfstは、一貫して、大きなsequentialなデータ送信を最初のリクエストで行って、そのあとでRound-Robinにスイッチングしているように見える。実装者の助けをもとに分析したところ、これはQUICのフローコントロールのメカニズムが原因であるということを追跡できた。 QUICにはTCPのように、受信側のバッファを上回るペースでのデータ送信を防ぐために、動的なフロー制御機能が実装されている。 TCPでは一つのフロー制御ウィンドウしか存在しないが、QUICは複数のフロー制御方法が存在している。一つ目はTCPのようにコネクション全体のフロー制御で、二つ目はストリーム毎のフロー制御である。 この特徴が、複雑なinterblockingの振る舞いにつながった。 ステップ毎にこれを確認していく。 1. aioquicは、コネクションレベルのフロー制御の上限を1048576に設定する(1Mib)に設定した。しかし同様にすべてのストリーム毎の上限も同じ設定を行った。これらをハンドシェイクの時に設定している。 2. クライアントは、10個のリクエストを同時に発行する。それぞれは同じファイルでちょうど1,000,000 MB (1MB)のサイズである。 3. mvfstのサーバーはすべてのリクエストを来た淳に順番に、現在のフロー制御の上限を埋めるような複雑な内部のバッファリングシステムによって処理する。 4. サーバーは最初のリクエストに対する処理を行い、バッファにファイル全体を入れる。なぜなら、ストリームレベルのフローの上限が1,408,576で、これはファイルサイズの上限より大きいからである。これによって、48576バイトがコネクションレベルのフロー制御として残る。 5. サーバーは次のリクエストを処理し、48576バイトをバッファに入れる。これによって、コネクションレベルのフロー制御の上限に到達する 6. サーバーは残りの8個のファイルについて、リクエストを順番に処理する。しかしコネクションレベルでのフロー制御が上限に到達しているので、それらのデータはバッファリングできない。 7. サーバーは、最初の二つのデータをRound-Robin方式で送り始める。2つ目のリクエストでバッファリングされているデータはすぐに送信されつくすので、1つ目のストリームのデータのみが送信されている状況になる。(Figure1 mvfstの最初のほうにある黄色一色に染まっている部分) 8. しばらくすると、サーバーはコネクションレベルでのフロー制御情報の更新をクライアントから受け取る。その地点から、内部のバッファを再度埋めるようになる。この時には、10個のストリームの情報をすべて知っているので、それらすべてに対して、Round-Robinのスケジューリング方式を適用し始める。 これは、最初のストリーム毎のフロー制御の上限を、コネクションレベルのフロー制御の上限の1/4にすることで、簡単に確認することができた。その結果は、mvfst_altに示されている。 コネクションレベルのフロー制御の上限とストリームレベルの制御フローの上限を同じ値にするのは、他のエンドポイントに対しても行っていたがmvfstのみがこのような結果になった。 実装者が意図するに、プロダクションでのデプロイメントでもしRound-Robin方式のスケジューリングを行いたいのであれば、ストリーム毎のフローには上限を持たせたほうが良い(例えば64KB)ということを言っていた。 例えば、Round-Robinのサーバーは、もしクライアントが1ストリームのフロー制御のみしか許さ胃ないのであれば、sequentialなふるまいを強要することができる。

3つ目に、実装がどのようにSTREAMフレームをパケットに振り分けているかである。ほとんどのスタックがフルサイズのパケット、すべてが一つのSTREAMフレームのフレームを含むように、生成している。 一方で、いくつかの実装は、異なる振る舞いを見せる。 特に、googleとmvfstは、同じストリームの小さいSTREAMフレームがパケットに含まれるというバグを含んでいた。 例えば、1つのQUICパケットが、同じストリームの情報を異なるSTREAMフレームとしてもつ可能性がある。例えば、7, 500, 9, 700バイトのように4つのSTREAMフレームに分割される。 mvfstのバグは修正された。Googleのバグは、HTTP/2のフレーミングのロジックを再利用したことが原因のようで、将来解決される予定です。 関連して、mvfstは時々複数の異なるストリームのSTREAMフレームが一つのQUICパケットに含まれることがある唯一の実装である。 これは再送の時だけ発生する。例えば、再送の時に、一つのSTREAMではQUICパケットを満たせないときに発生する。この時に次のストリームのデータでQUICパケットを満たすようにする。 奇妙なことに、mvfstは時々とても小さいサイズのパケット(例えば40byte)を送信する。 これは、mvfstが厳密に現在の輻輳制御ウィンドウに従っているからである。 いくつかの場合に、輻輳制御ウィンドウが降るパケットを送信するほど大きくないため、小さいパケットが送信される。 これはほかの実装者との興味深い議論を引き起こしました。実装者はすべて、次のパケットで送信することを示しました。 彼らは、このわずかなバッファの使いすぎは、実装の複雑性、正確性、性能のトレードオフであると感じています。 mvfstは予期できない振る舞いを示しているが、これは、主に複雑なロジックを含む進んだ実装をしている結果だからという点には、留意が必要です。 さらに、その実装者は私たちのフィードバックを非常に受け入れてくれました。この作品の著者との継続的な議論により、時間の経過とともに私たちの側からより詳細な調査が行われました。 実際には、quicheの次に、mvfstは広くデプロイされて、facebookのモバイルアプリで使われている実装です。

最後に、テスト済みの実装はすべて進行中の作業であり、これらの結果は必ずしもこれらの企業が提供または展開する最終製品を表すものではないことに注意することが重要だと感じています ただし、これらの実装を早期にテストすることの利点は多岐にわたります。 まず、目立った影響を与える可能性のある微妙なバグや癖を識別するのに役立ちます。 第二に、実装間の違いを示し、実装者がアプローチを再評価できるようにします。 第三に、QUICプロトコルの固有の複雑さと、デバッグと分析を実行するための特殊なツールと視覚化の有用性を強調しています。 第四に、そして最も重要なことは、アプリケーション層がQUICの多重化動作を操作できるように、QUICプロトコルが標準化されたインターフェースを定義する必要があるかどうかという問題です。 現時点では、提案されているQUIC仕様は、実装がそのようなインターフェイスを提供する必要があることを示していますが(should)、そのようなインターフェイスの外観を指定していません。 そのため、異なるデフォルトアプローチを選択する実装だけでなく、異なるAPIAPIを提供する場合)を実装することになるため、アプリケーションレイヤプロトコルの実装が基になるQUICスタックを交換することがより困難になります。 QUICソフトウェアの動作はTCP展開よりもはるかに不均一であることが予想されるため、これは重要なアクションポイントのように感じられ、たとえばHTTP / 3実装の再利用が容易になります。 たとえば、最近の「Pluginized QUIC」の論文は、この目的のための柔軟なAPI統合の出発点となる可能性があります。 どの多重化アプローチがパフォーマンスに最適であるかについての詳細は、セクション5.3および次のヘッドオブラインブロッキングの調査で説明します。