ACM 論文「QUIC is not Quick Enough over Fast Internet」を読んだ

アメリカの学会 ACM で発表された論文「QUIC is not Quick Enough over Fast Internet」を読んだので、ブログ記事にしておきます。

論文の結論を先に書いておくと、

  • 500Mbps を越える高速かつ高品質なネットワーク接続環境での話(有線接続とか WiFi 7 とか 5G モバイルとかを想定)。
  • 大きなファイル (~1GB) のダウンロードに要する時間とかで、体感できる差が観測できる(9秒 vs 18秒)。
  • クライアント側の OS (Linux/macOS/Windows) やソフトウェア (cURL/Chromium/Firefox) に関わらず同様に差が観測できる。
  • サーバ側のソフトウェア (OpenLiteSpeed/Nginx-quic) に関わらず同様に差が観測できる(OS は Ubuntu 18.04)。
  • QUIC が遅い原因は、「UDP であること」「プロトコルスタックがユーザ空間で実装されていること」である。つまり、QUIC を QUIC たらしめている特徴がそのまま原因になっている。

論文の概要

論文の概要(グラフとか)は、Gigazine さんの記事を参照してください(手抜きですみません):

論文では、Ubuntu 18.04 のデスクトップ PC を2台用意して、ほぼ Ethernet 直結、という構成で、いろいろ評価しています。モバイル回線などを想定した帯域制限は、Linux の tc コマンドを使って模擬しています。モバイル回線の模擬では、歩行パターンと自動車走行パターンの2パターンを用いています。

論文では、いろいろなクライアントプログラム(=ユーザ空間のプロトコルスタックの実装)を使って評価しています:

  • cURL
  • quic_client by Chromium Project
  • Chromium
  • Google Chrome
  • Microsoft Edge
  • Opera
  • Firfox

いずれのクライアントプログラムでも、「CPU 負荷が高い」「ファイルダウンロードが遅い」という傾向は変わらず。

また、Linux (Ubuntu) だけでなく、macOS や Windows でも評価してみたが、傾向は変わらず。

以下、原因の解析です。

パケットトレース (tcpdump) を見てみると、以下のことがわかった:

  • Linux kernel で処理された「受信パケット数」が一桁多い (744K vs 58K)。これは、NIC の TCP オフロード機能で複数の TCP セグメントが1回にまとめられた影響。
  • ACK が返ってくるまでの round-trip time (RTT) が一桁遅い (16.2ms vs 1.9ms)。ping RTT は 0.23ms なので、クライアント側での ACK 送信処理の時間が支配的な要因。

クライアント側での CPU 負荷が高い原因について、Linux kernel のプロファイリングを解析したところ、以下のことがわかった:

  • Linux カーネル内部でのパケット受信処理 netif_receive_skb の呼び出し回数が一桁多い (231K vs 15K)
  • システムコールの回数が一桁多い (17K vs 4K)
  • カーネル空間からユーザ空間にデータをコピーする処理 copy_user_enhanced_fast_string の呼び出し回数が多い (4K vs 3K)

クライアントプログラム (Chromium) のプロファイリングを解析したところ、以下のことがわかった:

  • パケットデータをソケットから読み出す処理が重い (0.248sec vs 0.037sec)
  • パケットのペイロードを解析する処理が重い (0.310sec vs 0.084sec)
  • ACK を返す処理が重い (2.972sec vs 0sec)

このような傾向が出る原因は、ひとつは、「TCP ではなく UDP を使っている」からで、

  • TCP は単純な byte stream なので複数の TPC セグメントをまとめて処理するのが簡単だが、QUIC のように multiplex している UDP ではそもそも困難。
  • したがって、TCP のように NIC のプロトコルオフロード機能が効かない。すべてのパケットを1個ずつ処理しないといけない。

もうひとつの原因は、「プロトコルスタックがユーザ空間で実装されている」からで、

  • 1パケットごとにカーネル空間からユーザ空間にデータコピーを行っているので、重い。
  • ACK を返す処理は、TCP ならカーネル内で効率的に折り返せるが、QUIC ではユーザ空間で行うので、重い。

というストーリーになっています。

以上の結果から、論文では、以下のような改善策を提案しています:

  • NIC のプロトコルオフロード機能(複数の UDP パケットをまとめる処理)を活用する。ただし、このオフロード機能が使えるかはクライアントのプラットホームに依るので、一般化は困難。
  • QUIC の UDP パケットに適した NIC プロトコルオフロード機能を実装する(受信側で複数パケットをまとめる処理とか、送信レート制御とか)。
  • クライアント側での QUIC 実装ロジックを工夫する(“delayed QUIC ACK” を実装するとか、recvmsg の代わりに recvmmsg を呼ぶとか)。
  • クライアント側をマルチスレッド処理にして、複数の QUIC 接続を使って並列ダウンロードする。

感想

QUIC が遅い原因は、「UDP であること」「プロトコルスタックがユーザ空間で実装されていること」ということで、つまり、QUIC を QUIC たらしめている特徴がそのまま原因になっています。したがって、今回の測定結果を劇的に改善するのは、難しそうです。

UDP パケットの処理を高速化するには NIC のオフロード機能が有効と言っていますが、そもそも UDP の場合はオフロード機能を実装するのが難しいとも言っていて、まぁ、一般的な NIC に実装されそうにはありません。

プロトコルスタックがユーザ空間で実装されているのでオーバヘッドが大きい、という点については、改善できる点はあるにしても、本質的にはどうにもならなさそうです。個人的には、HPC 向け高速ネットワークの InfiniBand などで採用されている User-level 通信が使えれば、カーネルを介すオーバヘッドはなくなるので、思想的には最適解な気がします。ただ、一般的な Ethernet NIC に実装されるのは難しそうですが・・・。

あと、1点気になるところとして、「サーバとクライアントとのネットワーク距離が遠い場合には、また違った傾向が出てくるのでは?」と思いました。論文では言及がありませんでしたが、ACK が返ってくるタイミングや、送信レート制御処理などに影響があるので、もしかしたら別の知見が得られるかも??

いずれにしても、QUIC プロトコルに関する新しい知見で、おもしろい論文でした。

p.s. この論文、arXiv でもダウンロードできちゃうみたいですが、読むならちゃんと ACM で買いましょう。無料ユーザ登録して、15ドルで買えます。

Previous: Rust で「まぎらわしいアルファベット」を検出したい

Home

Next: Rust で SSL/TLS 通信をしてみる