QUIC と TCP の比較 : どちらが優れているのか?
Fastly の QUIC に対する期待</u> (そして、quicly</u> と呼ばれる独自の実装を構築している理由) については、これまで何度もご紹介してきました。QUIC のメリットとして、レイテンシの短縮</u>、スループットの向上、クライアントの移動に対するレジリエンス、プライバシーとセキュリティの強化が挙げられます。IETF の QUIC ワーキンググループ</u>は現在、QUIC の最初のバージョンを完成させ、インターネット全体でデプロイ可能にする準備を進めています。QUIC を使用するシステムを構築している、または今後使用を計画している開発者やチームの多くが、QUIC の幅広い普及を切望していますが、一つの懸念が浮かび上がっています。QUIC に実装されている新機能や保護対策によって計算コストがどの程度影響を受けるのかということです。QUIC は TCP に代わる技術として注目されていますが、著しく高い計算能力が求められる場合、広く受け入れられることは可能でしょうか?
私たちはその答えを見つけるため、テストを実施しました。結論を言うと、QUIC の計算効率は TCP に匹敵するものでした。
<p></p>
ただし、今回のテストだけでは QUIC の計算効率について断言することはできません。私たちのテストで使用された設定とベンチマークはシンプルであり、より現実的で典型的なハードウェアとトラフィックのシナリオを使用してテストを重ねる必要があります。重要な点は、TCP と QUIC のいずれの場合でもハードウェアのオフロードが有効になっていなかったということです。今回のテストでは、シンセティックなトラフィックの単純なシナリオを使用して明白な計算上のボトルネックを排除しながら、QUIC のコストを削減する方法についてインサイトを得ることを目的としていたためです。
とはいえ、今回のシンプルなシナリオでも QUIC が TCP と同レベルのパフォーマンスを発揮したのは意外でした。
このブログ記事でご紹介するテストは、フェラーリ相手に自社製の車でレースに出場し、互角に戦おうとするようなものだったと言えるかもしれません。サーキットは非常に人工的な環境であり、サーキットでの車の運転は、(レースドライバーでない限り) 日常的な運転体験とはまったく異なります。しかし、サーキットを高速で駆け抜ける際に直面する問題を考えてみると、ボトルネックを解明できるはずです。これを今回のテストに当てはめてみると、重要なのは、このボトルネックを取り除くための対策です。これが、今回の記事のテーマです。
背景
長年 Web の主力プロトコルとして利用されてきた TCP には、実装を最適化し、計算効率を高めるための努力と工夫がつぎ込まれてきました。一方、QUIC は発展途上のプロトコルであり、まだ広く普及しておらず、計算効率を高めるための調整はされていません。このような新しいプロトコルは、実績ある TCP と張り合うことができるのでしょうか?QUIC は近い将来、TCP と同等の効率を実現できるのか、という点が重要なポイントとなります。
QUIC の計算コストは、大きく分けて2種類の要素で構成されていると考えられます。
確認応答 (ACK) 処理 : 典型的な TCP 接続のパケットでは、ほとんどが確認応答 (ACK) のみを転送します。TCP の確認応答は、送信側と受信側の両方で、カーネル内で処理されます。QUIC はユーザー空間でこれを処理するため、ユーザーとカーネルの境界をまたぐデータコピーが増え、コンテキストスイッチも多くなります。さらに、TCP の確認応答はプレーンテキストですが、QUIC の場合は暗号化されているため、確認応答の送受信コストが増大します。
パケットごとの送信オーバーヘッド : カーネルは、TCP 接続を認識し、変更がないことが予想されるステートを記憶し、接続内で送信されるすべてのパケットに対して再利用します。例えば通常、宛先アドレスのルートの検索やファイアウォールのルールの適用は、接続開始時に一度するだけで済みます。一方、QUIC 接続では、カーネルに接続ステートは記憶されないため、QUIC のすべての送信パケットに対して上記のようなカーネル動作が発生します。
QUIC はユーザー空間で実行されるため、この場合のコストは TCP と比較すると高くなります。QUIC によって送受信されるすべてのパケットが、ユーザーとカーネルの境界をまたぐためです。この動作は、コンテキストスイッチ</u>と呼ばれます。
テストと第一印象
先ほど提示した疑問に答えるため、シンプルなベンチマークを設定しました。QUIC サーバーと QUIC クライアントとして、quicly</u> を使用しました。QUIC パケットは常に TLS 1.3 で暗号化され、quicly は H2O</u> の TLS ライブラリである picotls</u> を使用します。比較用の TCP セットアップでは、QUIC セットアップとの差異を最小限に抑えるため、picotls とネイティブの Linux TCP を使用しました。undefinedundefinedundefined
計算効率の測定には、ネットワークを飽和させるために必要な計算リソースの量を測定する方法と、利用可能なすべての計算能力を使用して持続可能なスループットを測定する方法の2種類があります。ネットワークを飽和させると、パケット損失や、その後の損失回復、輻輳制御による変動が発生します。こうした要素を含めてパフォーマンスを測定することは重要ですが、今回のテストではこの変動を避けたいと考え、後者のメソッドを選択しました。送信者がアイドル状態の高帯域幅ネットワークで、すべての計算能力をもって維持できるスループットとして、計算効率を測定しました。
送信側の計算効率が重要な理由は、他に2つ挙げられます。まず送信者は、トランスポートプロトコルの計算コストの大部分を負担する傾向があります。これは、ネットワークにドロップされたパケットを検出するタイマーの実行、それらのパケットの再送信、ネットワークの往復時間のモニタリング、ネットワークの輻輳を防ぐ帯域推定ツールの実行など、計算コストのかかるトランスポート機能のほとんどを送信者が行うためです。次に、サーバーは一般的に送信者であり、プロトコル処理の計算効率の向上は、サーバーにとって重要だからです (これは、クライアント側の計算効率が重要ではないという意味ではありませんが、プロトコル処理がクライアント側の主要なボトルネックになることは通常ありません)。
結果を発表する前に、テスト環境の設定を簡単に説明します。送信側は、シングルコアとシングルスレッドに制限し、Intel Core m3-6Y30 で Ubuntu 19.10 (Linux カーネルバージョン 5.3.0) を使用しました。送信側は、ASIX AX88179 コントローラーを使用する USB ギガビットイーサネットアダプター経由でローカルネットワークに接続しました。チェックサムオフロードは、TCP と UDP のいずれでも有効でした。TCP セグメンテーションオフロード (TSO)</u> など、その他の TCP 用ハードウェアの最適化は使用しませんでしたが、今後の比較で UDP 用の類似ハードウェアの最適化</u>とともに使用する予定です。受信側は 2.5 GHz で動作するクアッドコアの MacBook Pro で、市販のギガビットイーサネットスイッチを介して、送信側と接続しました。重要なのは、送信側が計算能力をすべて使用してもネットワークが飽和しないように、CPU のクロックを 2.2 GHz から 400 MHz に制限したことです。テストでは、ネットワークに損失がないことを確認しました。
ここでは送信側にかなりローエンドのハードウェアを使用していますが、先ほど述べたように、この段階ではボトルネックを排除するための対策と他の環境への移行性に重点を置いているためです。今後のステップでは、サーバーグレードのハードウェアを検討します。
最初の参考ベンチマークとして、iperf</u> を使用して暗号化前の (RAW) TCP スループットの最大値を測定しました。このスループットは 708 Mbps でした。
2つ目の参照ベンチマークとして、AES128-GCM を暗号に使用する picotlsundefined を使い、TLS 1.3 over TCP によって達成可能な持続スループットを測定しました。このスループットは 461 Mbps で、暗号化されていない TCP の場合の約65%です。このパフォーマンスの低下は、暗号化やノンブロッキングソケットの使用、確認応答の処理のためにユーザー空間の実行を中断したコストによるものです。ここで注目したいのは、これらのコストは TLS over TCP と QUIC の両方に適用されるため、ここで調査している問題にはオーバーヘッドの影響はないという点です。
最後に、既製の quicly を使用した場合の持続スループットを測定したところ、196 Mbps という結果でした。QUIC のこの結果は TLS 1.3 over TCP の場合の約40%でした。予想していた通り、QUIC はプロセスの負荷がかかり、初回の数値は厳しいものでした。
<p></p>
このテストでは、QUIC を使用した場合のスループットコストを測定するだけでなく、この値を削減するためにできる対策を確認したいと考えました。また、Fastly の実装方法に限定したくなかったので、プロトコルに対する変更や微調整も検討しました。これは3つのステップで行いましたので、以下では各ステップについて詳しく説明します。
確認応答の頻度を削減
TCP と同様、QUIC 仕様</u>では、受信側が受信した2つのパケットごとに確認応答を送信するよう推奨されています。合理的なデフォルト設定ですが、確認応答の受信と処理は、データ送信側にとって計算コストが上昇する原因になります。受信側は単純に確認応答の送信数を減らすこともできますが、これによって、特に接続の初期段階で接続のスループットが低下する可能性があります。
これを理解するには、送信側が確認応答を受け取るたびに、送信レートが上がると考えてください。確認応答の受信が早ければ早いほど、送信レートが上昇するのも速くなります。レートが低い場合、往復ごとに受信側に送信されるパケットが少なくなるため、受信側から戻ってくる確認応答の数も少なくなります。このような接続で確認応答の数を減らすと、送信側の送信レートと全体的なパフォーマンスが明らかに低下します。
接続のスループットが高い場合、確認応答の数が減っても、送信者のレートに大きな影響を与えない程度の確認応答が返されるということになります (これはやや簡略化して説明しており、他の検討事項については、こちら</u>で詳しく説明しています)。今回のテスト設定では、送信者のスループットに影響を与えることなく、確認応答の頻度を減らすことができます。
確認応答の頻度を2パケットに1回から10パケットに1回に減らした結果、quiclyundefined は 240 Mbps のスループットを維持することができました。こうすることで、ネットワーク上の確認応答パケットの数を減らすことができ、このテストでは、計算オーバーヘッドの削減が送信者にメリットをもたらすことが証明されました。この結果、QUIC の Delayed ACK 拡張</u>の提案を実装するべきだと確信しました。これについては、後ほど説明します。
<p></p>
汎用セグメンテーションオフロード (GSO) によるパケットの結合
次に、「Optimizing UDP for content delivery: GSO, pacing and zerocopy</u> (コンテンツ配信向けの UDP の最適化 : GSO、ペーシング、ゼロコピー)」の Willem de Bruijn 氏と Eric Dumazet 氏の提言に従い、UDP GSO がシングルパケットの書き込みとパケットごとのコンテキストスイッチによるオーバーヘッドを減らすのに役立つかどうかを確認しました。汎用セグメンテーションオフロード (GSO)</u> は Linux の機能で、ユーザー空間のアプリケーションが連続したパケットを単一のユニットとしてカーネルに提供できるようにします。このユニットは、ひとつの大きな仮想パケットとしてカーネルを通過します。つまり、小さなパケットごとに1回ずつ判断するのではなく、大きな仮想パケットに対して少ない回数で判断するようになります。TCP のセグメンテーションオフロードとは異なり、Linux の GSO は必ずしもハードウェアオフロードとは限りません。ハードウェアが UDP パケットのセグメンテーションオフロードをサポートしていない場合、大きな仮想パケットはネットワークカードドライバーで複数の小さな UDP パケットに分割されます。今回のテストではこのプロセスを使用しました。
最大10個の UDP パケットをひとつのオブジェクトにまとめ、GSO を使用して送信したところ、QUIC のスループットは 240 Mbps から 348 Mbps になり、45%の向上を実現しました。さらに多くのパケットを結合させることでパフォーマンスが向上するかどうかを確認するため、GSO を使用して最大20個の UDP パケットを結合させました。これにより、スループットはさらに45%向上し、QUIC は 431 Mbps の速さで動作するようになりました。
<p></p>
これは非常に大きな発見でした。QUIC のパケットごとのコストは明らかに重大なボトルネックであり、GSO で対処することで大きな改善が見られました。GSO のサイズを選択する必要がありましたが、これについては後で説明します。次に、あまり注目されていないもうひとつのパラメーター、パケットサイズに着目しました。
パケットサイズの拡張
QUIC 仕様では、最小 QUIC パケットサイズのデフォルトとして1,200バイトという比較的小さめなサイズを推奨しており、quiclyundefined では1,280バイトに設定されていました。何らかの根拠によりパスがこれより大きなサイズのパケットをサポートする可能性が高いと考えられる場合は、実装するパケットサイズを拡張することができます。このパスが1,472バイトの QUIC パケットをサポートでき、TCP が1,460バイトのパケットを使用していたことを考えると、QUIC で使用するパケットサイズを拡張するのは合理的です。最大パケットサイズを拡張することで、一定量のデータを転送するために必要なパケット数を減らすことができるため、計算コストが削減されます。また、送信側と受信側の両方でパケットごとの処理コストが固定されるため、計算の非効率性も軽減できます。
そこで、QUIC のパケットサイズを1,280バイトから1,460バイトに変更し、TCP のペイロードサイズと同等に設定しました。この変更により、quiclyundefined は 466 Mbps のスループットを維持することができ、スループットが8%向上しました。
QUIC は TLS over TCP より高速になったのです!undefined
<p></p>
本番環境での活用
このテストの結果、quicly の効率性を改善する方法が明確になりました。確認応答の頻度を減らし、GSO でパケットを結合させ、使用するパケットサイズをできるだけ大きくすることです。そこで、さまざまな環境で動作するように、これらの最適化を一般化し、副作用が発生するリスクを最小限に抑える方法について考えてみましょう。
確認応答の頻度を削減
確認応答の頻度を10パケットに1回の割合に固定した場合、いくつかの問題が発生します。まず、上述のように、接続のスループットがもともと低い場合、スループットを著しく低下させる可能性があります。次に、このテストでのクライアントは quicly ですが、本番環境ではブラウザがクライアントになるため、Fastly ではコントロールできなくなります。
これらの問題は QUIC の Delayed ACK 拡張機能</u>で解決できます。これにより、送信側は現在の送信レートに基づいて、受信側の確認応答の頻度を動的にコントロールできます。
このテストの実施後、Delayed ACK 拡張を実装し、送信側が確認応答の頻度を輻輳ウィンドウの8分の1 (往復時間の約8分の1) にコントロールするようにしました。上記のテスト環境の設定で、確認応答の頻度が60パケットに1回に減少し、テスト時よりも大幅に削減されました。undefined
GSO によるパケットの結合
GSO のテストでは、結合するパケットの数が増えると、QUIC の効率が向上することがわかりましたが、これにはデメリットもあります。GSO による結合の結果、送信側からすべてのパケットがネットワークに送り出され、一時的にネットワークのバッファへの負荷が増大し、パケット損失の確率が高くなるためです。
では、このトレードオフを考えた場合、quicly が結合する最適なパケット数はいくつになるでしょうか?QUIC の送信側の許容バーストサイズ</u>は10に指定されていますが、ここでもこの推奨値を適用できそうです。quicly では現在、GSO によって10パケットを結合して送信するオプションを実装しています。undefinedundefined
現在、送信側のカーネルはこれらのパケットのペースを調整できない</u>、つまり GSO で結合されたパケットを、ネットワークがバーストを吸収しなくても済むレートで送信できないため、注意が必要です。今後 Linux カーネルに、こうした機能が実装された場合には、試してみたいと思います。
パケットサイズの選択
パケットサイズの大きさに比例して、パフォーマンスは向上します。ただし、パケットが大きくなると、一部のネットワークパスでドロップされるリスクが発生します。Path MTU Discovery</u> などのメカニズムを実装して、接続で利用できる最大パケットサイズを検出することもできますが、実質的に長期間の接続でのみ有用です。ほとんどの接続において、またすべての接続の開始時において、送信側が適切なパケットサイズを決定する必要があります。
現在 quicly undefinedサーバーは、固定されたパケットサイズを使用する代わりに、クライアントから最初に受信したパケットのサイズに基づいて独自のパケットサイズを決定しています。ネットワークを通じて quicly サーバーに届いたパケットと同じサイズのパケットなら、ネットワークを通じてクライアントに届く可能性が高いと考えるのは妥当であると言えます。
まとめと次のステップ
これらの変更により、quicly は、クライアントが最初に送信する QUIC パケットが1,460バイトの場合は 464 Mbps (TLS 1.3 over TCP より1%速い)、クライアントが最初に送信する QUIC パケットが Chrome で採用されているデフォルトパケットサイズの1,350バイトの場合は 425 Mbps (TLS 1.3 over TCP よりわずか8%遅い) を実現できるようになりました。
このテストで使用されたワークロードと環境は、ワークロードと環境の広大な空間におけるひとつのポイントに過ぎず、今回のテストの目的は、この特殊なマイクロベンチマークにおいて、QUIC が TCP と同レベルのスループットを達成できるかどうかを確認することにありました。さらにテストを重ね、他の標準的な設定やシナリオにおける QUIC のパフォーマンスを調査し、改善を続けていく予定です。
システムの最適化とプロトコルの仕組みを賢く活用することで、QUIC が TCP と同等の計算効率を実現できることが今回のテストを通じて分かったのは大きな成果でした。
今後、さらにテストとデプロイを重ね、quicly 実装のパフォーマンスを調整し、その結果をご報告します。ご期待ください。