インフラエンジニアなら気になるQUICのロードバランサ (実装編)

Jun-ya Kato
nttlabs
Published in
12 min readSep 8, 2021

はじめに

前回のQUICのロードバランサの方式解説に続き、今回はQUIC-LBが実際にどのように動くのかOSS実装を紹介したいと思います。

本稿では、前半はQUIC-LBの実装状況と問題点、およびNginxを改変した実装についてコンフィグと動作イメージを紹介します。後半については、ロードバランスの基礎となっている5タプルによるフロー識別について考えます。フロー識別は、ポリシルーティングやper flow ECMPによるIP転送など、より低レイヤに対しても適用される技術です。もし、L3スイッチやLinuxカーネルのIP転送に対してQUIC向けの拡張をするとどうなるか? を考えてみます。

QUIC-LBの実装状況と問題点

QUIC-LBの提案者である、Martin Duke氏のIETF111での発表によると2つのOSS実装が言及されていました。現状では下記の2つ以外の実装を知り得ていません。

  1. Martin Duke氏によるNginxを改造した実装 (nginx-quic-lb)。Webサーバ実装は含みません。
  2. ANT Financial (AliPayの会社)によるNginxを改造した実装。テスト用のWebサーバは、ngtcp2のサンプルサーバを改造したものを使っています。

さらに、ロードバランサとWebサーバが事前に共有した情報に沿って動作するため、Webサーバ側もQUIC-LB方式に対応した実装が必要となります。バックエンドのWebサーバは好きなものを置きたくなりますが、標準化中のQUIC-LBに対応した実装は皆無です。今後、標準化が進めば、Webサーバの実装も進むと思われますが、まだ未知数の領域です。

以下では、1.のQUIC-LBの実装 nginx-quic-lb について、ドキュメントにあるサンプル設定と動作確認の様子にふれます。

nginx-quic-lb の設定例

nginx-quic-lbのコンフィグにコメントをつけます。NginxをL4 TCP/UDPプロキシとして使うとき使う streamモジュールの設定を拡張したものなっており、理解しやすいです。

# nginx.confstream {
upstream server_pool {
# Plain Text 方式で埋め込む場合の設定
# サーバID長は4バイト
quic-lb cr=0 sidl=4;
# Block Cipher CID 方式で埋め込場合の設定
# サーバID長は6バイト
# key=... に事前共有鍵を設定
quic-lb cr=1 sidl=6 key=abcdefabcdefabcdefabcdefabcdefab;
# Stream Cipher CID 方式で埋め込場合の設定
# サーバID長は8バイト
# key=..., nonce_len=... に事前共有鍵を設定
quic-lb cr=2 sidl=4 key=fedcbafedcbafedcbafedcbafedcbafe
nonce_len=8;
# 転送先のWebサーバを2台設定している
# サーバID と IPアドレス(127.0.0.1)&ポート番号 を対応づける
# 1台のWebサーバに複数のサーバIDをつけている
# 動作確認に使うのは sid0, それぞれ下記の値を設定します
# Webサーバ1台目 : 0x41414141 (文字列表記で "AAAA")
# Webサーバ2台目 : 0x42424242 (文字列表記で "BBBB")
server 127.0.0.1:4434 sid0=41414141 sid1=89abcdef0123
sid2=456789ab;
server 127.0.0.1:4435 sid0=42424242 sid1=abcdef012345
sid2=6789abcd;
}

server {
listen 443 udp;
proxy_pass server_pool;
}
}

nginx-quic-lbの動作確認

少々技巧的な方法でのQUICパケットを生成して、ロードバランサに入力する動作確認を行いました。README.mdでは下記のncコマンドを使っていました(動作するように一部修正しています)。

1: # tcpdump -n -i lo udp ← ループバックI/F loに流れるパケットを可視化する
2: tcpdumpは別のターミナルで起動しておく
3: # nc -u localhost 443
4: 00AAAAfooooooooooooo[enter] ←ユーザによる入力。21バイト以上必要です
5: ^C
6: # nc -u localhost 443
7: 00BBBBfooooooooooooo[enter] ←ユーザによる入力。21バイト以上必要です

まぎらわしいですが、“00AAAAfooooooooooooo” は文字列です。先頭6バイトを16進数で書くと “0x30 0x30 0x41 0x41 0x41 0x41 ….” となり、ここがUDPペイロード (QUICヘッダ)となります。

1: '0' 0x30 : QUICショートヘッダの1バイト目
2: '0' 0x30 : コネクションIDの1バイト目 (先頭2ビットCR=00)
3: 'A' 0x41 : 2バイト目
4: 'A' 0x41 : 3バイト目
5: 'A' 0x41 : 4バイト目
6: 'A' 0x41 : 5バイト目

1バイト目の0x30を見ると、最初の1ビット目が0なので、QUICショートヘッダと解釈できます。続く7ビットのバージョン依存情報はめちゃくちゃですが、ロードバランサには関係ありません。またドキュメントでは短い文字列 “00AAAAfoo” しか入力していませんが、21バイト未満だと、暗号化ペイロードの最小長を満たさずロードバランサで捨てられます (実際にはワーカプロセスがクラッシュしてしまいました)。

2バイト目以降がコネクションIDです。コンフィグの世代がCR=00なので、その世代に対応するルールの設定quic-lb cr=0 sidl=4;が適用されます。本設定は、サーバIDの埋め込みはPlain Text方式で長さが4バイトであるとしています。

3バイト目以降が、コネクションIDの2バイト目以降となります。ここにサーバIDを示す 4バイト長の “AAAA” を置いています。

以降のデータとあわせて、UDPペイロード全体は、かろうじてQUICショートヘッダの形をなしています。しかし、Webサーバが受信しても捨てるだけでしょう。しかし、QUIC-LBの振り分けテストは可能なパケットです。

# 00AAAAfooooooooooooo[enter] → to port 4434
15:01:51.037642 IP 127.0.0.1.11487 > 127.0.0.1.443: UDP, length 21
15:01:51.037834 IP 127.0.0.1.43776 > 127.0.0.1.4434: UDP, length 21
# 00BBBBfooooooooooooo[enter] → to port 4435
15:01:51.893109 IP 127.0.0.1.10055 > 127.0.0.1.443: UDP, length 21
15:01:51.893259 IP 127.0.0.1.11473 > 127.0.0.1.4435: UDP, length 21

1つ目のncコマンドが発生するQUICパケットに対してQUIC-LBは、サーバIDが 0x41414141 (AAAA)と解釈し、2つ目のncコマンドが発生するQUICパケットはサーバIDが 0x42424242 (BBBB)と解釈します。それぞれ順に宛先ポート4434, 4435へ向けて転送されたUDPパケットが観測でき、ncで入力したコネクションIDと、設定ファイルにある転送先の関係が一致していることがわかります。

        server 127.0.0.1:4434 sid0=41414141 sid1=89abcdef0123
sid2=456789ab;
server 127.0.0.1:4435 sid0=42424242 sid1=abcdef012345
sid2=6789abcd;

per flow ECMPによるIP転送を使ったフロー分散

ロードバランサの基盤となっているのが5タプル情報を使ったフロー認識でしたが、Webより下層のL3ルータでのIP転送にも適用されます。大規模なWebサービスでは図1のように、

図1: L3SWとロードバランサを組み合わせた多段構成による負荷分散
  1. フロントにL3スイッチを入れてper flow ECMPによるIP転送により分散
  2. 中段に、より上位レイヤで分散するロードバランサを置き
  3. バックエンド最後段のWebサーバへ向けてリクエストを転送する

1~3のような多層構成をとることもあるでしょう。L3スイッチやIPルータでもポリシルーティングと呼ばれるL4 TCP/UDPのヘッダを認識したIP転送機能がありますが、QUICを認識して転送できるのか?の観点で考察してみます。

ハードウェアL3スイッチのECMP IP転送

スイッチシリコンを使って実装したL3スイッチにおいて、フロー識別とIP転送をハードウェアで処理する能力はチップに依存します。現在のところQUICコネクションをフローとして認識する機能は見つけることができません。

しかし、もしプログラミング可能なスイッチシリコンを使ってQUIC-LBを実装するならば、QUICコネクションと下回りのUDPレイヤの4タプルのフロー情報を結びつけて、U-Plane (FIB)に反映すれば良さそうです。U-Planeはスイッチシリコンの機能をそのまま活かしつつ、C-Planeに機能を追加すれば実現できそうな気がします。今後、QUIC対応をうたうネットワークOSが出現するかもしれませんし、SONiC + SAIのようなオープンなネットワークOS上に実装してみたくなります。

ちなみに、既存のハードウェアL3スイッチがQUICで使えないわけではありません、図1の構成では、フロントのL3スイッチは同一QUICコネクションを同一のQUIC-LBに転送することを保証しませんが、QUIC-LBが適切なWebサーバへ転送します。フロントから中間のQUIC-LBへの負荷に対して、ある程度フローのまとまりを保ったまま分散するので、L4(UDPレイヤ)に対するper flow ECMPは有効と思われます。ただし、QUIC-LBとWebサーバ間のIP接続は論理的には確保しなければならないため、負荷に応じたファブリック設計が必要です。

ソフトウェア(Linuxカーネル)のECMP IP転送

LinuxカーネルにもECMPによるIP転送の機能があり、5タプルを参照するper flow ECMPもサポートしています。下記の設定で、同一宛先プレフィックス(2001:db8:443::/64)に対して複数のネクストホップを指定すると、L4 (TCP/UDP)フローを識別した上で入力パケットの振り分けを行います。

# sysctl -w net.ipv6.fib_multipath_hash_policy=1 (for IPv6)
# sysctl -w net.ipv4.fib_multipath_hash_policy=1 (for IPv4)
# ip -6 route add 2001:db8:443::/64 proto static \
nexthop via 2001:db8:1234::1 dev ens1f0 weight 1 \
nexthop via 2001:db8:abcd::1 dev ens1f1 weight 1

fib_multipath_hash_policy=1はTCP/UDPの識別のみですが、QUICパケットのコネクションIDを参照する能力を加えれば、かなり手数は少なくカーネル内QUIC-LBが実装できそうです。

カーネル内のプロトコルスタックのIP転送機能の直接改造は避けたいので、XDPを使って自作のIP転送カーネル内に差し込むことになる思います。コネクションIDからサーバIDを取り出して目的のWebサーバへ転送(XDP_REDIRECT)する機能や、ebpf_fib_lookup()でプロトコルスタックの経路表を参照する機能を組み合わせれば、高性能なカーネル内QUIC-LBを実現に近づきそうです。QUIC-LB方式と同一ではありませんが、類似の機能を搭載している先行OSSとしては、facebook katranがあります。katranでは、XDPを使ったQUICコネクションの振り分け機能の開発が進行中です。

性能はほどほどのノーマルパスでよいので、カーネルを直接改造することなく、かつお手軽にQUIC-LBを実現したいと思いました。しかし、eBPFのヘルパーライブラリをみてもカーネルプロトコルスタックのFORWARD内部を拡張することは難しい様子でした。本稿をお読みいただいた方で、ご存じの方がいればおしえてください。

実装編のまとめ

QUIC-LBに対応したロードバランサ、Webサーバの実装は限られる状況です。しかし、HTTP/3によるWebサービスの普及とともに負荷分散の必要性も高まることは間違いありません。L3スイッチ、L7ロードバランサなどさまざまなレイヤの装置がQUICを意識した機能を載せたり、デバイスのプログラマビリティを生かしてQUIC向けの拡張がされていくのではないかと思っています。

最後に

NTT研究所では超高速なWebサービスと、それ支えるバックエンドインフラの研究開発に取り組んでいます。本分野に関心のある方、ご連絡をお待ちしています。

--

--