最近、OVS(Open vSwitch)がAF_XDPに対応したとの話を聞いたのでどういう背景があったのか、そしてどうのように使えば良いのか調べてみた。 OVSは、カーネルモジュールとユーザスペースプロセスから構成されている。 その構成の部分で、以下のような課題が見えてきた1ので、最近AF_XDPを使った実装に置き換えが進められているようだ。

  • カーネル本体の更新やシステム全体のリスタートを要求する修正がある
  • カーネル開発者の方針や実装に影響を受ける
  • DPDKで速度面で劣る
  • バックポートが多すぎる
  • ディストリビューションのサポートが受けられなくなることがある

どれも構成変更を推し進めるには妥当な理由に思う。 下図は、バックポートと新規機能それぞれに起因する差分をコード行数によって比較したもの。 バックポートにかかるコストが読み取れる。 ちなみに、カーネルモジュールを使った実装は2022年4月にリリース予定のOVS 2.18で廃止される予定2になっている。

ovs_code_changed

出典:Revisiting the Open vSwitch Dataplane Ten Years Later

それならユーザスペースでデータプレーンを実装したDPDK(Data Plane Development Kit)で良いじゃないかいうと、 それはそれで課題がある。

  • ipコマンドなどカーネルネットワークスタック向けのツールと相性が良くない
  • 特定のNICやCPUを占有してしまう

この辺りの課題を解決するアプローチとしてAF_XDPの導入が進められている。AF_XDPを使うと、XDPのフックポイントに小さなeBPFプログラムを仕組んでおき、カーネルネットワークスタックをバイパスした上で、ユーザプロセスへとパケットを転送することができる。 安定した仕様を持つので、将来のカーネルリリースでも継続して使えるはず。 既存ツールとも相性が良い。DPDKからAF_XDPを使おうという話 3 もあるが、DPDKとOVSの間のメンテナンスコストが残る。というわけで、OVS本体でAF_XDPをサポートしようということになったようだ。

ところでAF_XDPを使うとどのようにパケットが転送されるのだろうか。 AF_XDP は fill リングと competion リングの2つのリングを持つ。 その各要素はディスクリプタとなっていて、umem 領域を指している。 パケット受信時の流れを図中の番号に沿って見ていく。

  1. まずアプリケーションはfillリングに空きディスクリプタを登録する
  2. カーネルは fill リングからそのディスクリプタを取り出す
  3. umem領域にパケット本体を書き込む
  4. そのumem領域を指すようなディスクリプタを completion リングに登録する
  5. アプリケーションは completion リングからディスクリプタを取り出す
  6. そのディスクリプタの指す umem 領域からパケット本体を取り出す

ovs_afxdp

出典:Revisiting the Open vSwitch Dataplane Ten Years Later

性能はどうだろう。25G NICで64バイトショートパケットを流した時のスループットとCPU使用率についてのデータがあった。 仮想マシンを経由するPVPのシナリオでは、vhostuser の採用によってCPU使用率は差し置いたままで、 スループットが向上する。 ただ、DPDKには届かない。コンテナを経由するPCPのシナリオでは、ユーザ・カーネル間のデータコピーを省略できるため、AF_XDPがベストチョイスかと思う。 向き不向きがあるので、シナリオによって選び方が変わるだろう。

ovs_perf

出典:Revisiting the Open vSwitch Dataplane Ten Years Later

さて、続いて実際に利用するにはどのようなコマンドを使うのかについて見ていく。 カーネルは CONFIG_BPFCONFIG_BPF_SYSCALLCONFIG_XDP_SOCKETS が有効になった状態でビルドしておく必要があるので注意する。 今回はUbuntu 20.04にlinux-image-5.11.0-41-genericパッケージを入れることにした。

libbpf に依存するので、先立って入れておく。

git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git -b v5.15 --depth 1
pushd ./bpf-next/tools/lib/bpf
sudo make install
sudo sh -c "echo /usr/local/lib64 >> /etc/ld.so.conf"
sudo ldconfig
popd

ovsのビルド時には --enable-afxdp を指定する。

git clone https://github.com/openvswitch/ovs.git -b v2.16.1 --depth 1
pushd ovs
./boot.sh
./configure --enable-afxdp
make -j $(nproc)
sudo make install
popd

ovsの起動はいつもどおり。 ブリッジ生成時に datapath_type=netdev として、ユーザスペースのデータパスを使うよう指示する。 この辺りはDPDKを使う時のオプションと同じ。

sudo ovs-ctl start --system-id=random
sudo ovs-vsctl -- add-br br0 -- set Bridge br0 datapath_type=netdev

今回は2つのnetnsを作り、その間で疎通させたい。

sudo ip netns add at_ns0
sudo ip link add p0 type veth peer name afxdp-p0
sudo ip link set p0 netns at_ns0
sudo ip link set dev afxdp-p0 up
 
sudo ip netns add at_ns1
sudo ip link add p1 type veth peer name afxdp-p1
sudo ip link set p1 netns at_ns1
sudo ip link set dev afxdp-p1 up

ポートを作る時にはtype="afxdp"オプションを付与する。

sudo ovs-vsctl add-port br0 afxdp-p0 -- set interface afxdp-p0 type="afxdp"
sudo ovs-vsctl add-port br0 afxdp-p1 -- set interface afxdp-p1 type="afxdp"

それぞれの netns でリンクをあげてIPアドレスを付与する。

sudo ip netns exec at_ns0 sh << NS_EXEC_HEREDOC
sudo ip addr add "10.1.1.1/24" dev p0
sudo ip link set dev p0 up
NS_EXEC_HEREDOC

sudo ip netns exec at_ns1 sh << NS_EXEC_HEREDOC
sudo ip addr add "10.1.1.2/24" dev p1
sudo ip link set dev p1 up
NS_EXEC_HEREDOC

最後にactions=normalのようなフロールールを投入すると、疎通が取れるようになる。  

sudo ovs-ofctl add-flow br0 actions=normal
sudo ip netns exec at_ns0 ping -i .2 10.1.1.2
# PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
# 64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.128 ms
# 64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.069 ms
# 64 bytes from 10.1.1.2: icmp_seq=3 ttl=64 time=0.066 ms
# 
# --- 10.1.1.2 ping statistics ---
# 3 packets transmitted, 3 received, 0% packet loss, time 2028ms
# rtt min/avg/max/mdev = 0.066/0.087/0.128/0.028 ms

オフィシャルなドキュメント 4 で “The AF_XDP support of Open vSwitch is considered ’experimental’, and it is not compiled in by default.” とあるように、実験的な位置付けのようだ。実際まだ使えない機能もある。 一旦これにて終わり。