DPDK と AF_XDP は、ユーザー空間で動作するプログラムが NIC を介した I/O を行う際に、カーネルの通信に関連する実装の大部分を迂回すること(カーネルバイパス)を可能にするもので、通信性能の向上に寄与することが知られています。
これは物理 NIC だけではなく、コンテナ環境で利用される仮想 NIC においても同様の効果が期待できます。
この記事では、DPDK と AF_XDP を使ってカーネルバイパスを行うように実装されたアプリケーションが Docker 環境で仮想 NIC を扱う場合の性能について簡単な計測を通して調べます。
具体的な仮想 NIC を扱う設定方法と性能計測方法は全て記事の末尾のコマンドリストとDockerfile および compose.yaml に記載します。
計測内容は Docker の標準的な設定で実行可能かつ一台のマシンで完結するものに限定したため、ノートパソコン上の Linux をインストールした仮想マシンなどでも実行可能です。
ソフトウェアの組み合わせ
仮想 NIC
DPDK と AF_XDP では扱いやすいコンテナ環境での利用が想定された仮想 NIC が異なります。
具体的には、DPDK は virtio に関連した実装が充実している(8. Virtio_user for Container Networking — Data Plane Development Kit 25.11.0 documentation)一方で、AF_XDP は veth との親和性が高いようです。
仮想スイッチ
Linux bridge(Linux カーネルの bridge 実装)は Linux が提供する機能である veth と組み合わせやすいですが、DPDK が扱う virtio との接続には Linux カーネルの作成する tap デバイスを挟む(9. Virtio_user as Exception Path — Data Plane Development Kit 25.11.0 documentation)ことによるコストがあります。
一方で、DPDK へ対応している Open vSwitch は DPDK を利用するアプリと virtio を通した接続が容易です(DPDK vHost User Ports — Open vSwitch 3.6.90 documentation)。
これらのことから、Linux bridge を利用する場合は veth を通して AF_XDP を使いやすく、Open vSwitch を利用する場合は virtio 経由で DPDK を使うアプリと組み合わせやすいと言えると思います。
組み合わせ
上記のことから性能計測には、DPDK と AF_XDP について、それぞれ以下の組み合わせを利用します。
| フレームワーク | 仮想 NIC | 仮想スイッチ |
|---|---|---|
| DPDK | virtio | Open vSwitch |
| AF_XDP | veth | Linux bridge |
コンテナ用ネットワーク構成ソフトウェア
veth などの仮想 NIC や bridge との紐付けはコンテナ用ネットワーク構成機能を実装したソフトウェアによって行われます。
今回は Docker の標準的な設定を利用するため、各コンテナに Linux bridge と紐付けられた veth が割り当てられる構成を利用します。
Docker はそのままでは Open vSwitch の立ち上げと仮想 NIC の紐付けを行わないため、今回の計測では Open vSwitch を実行する場合には compose.yaml から専用のコンテナを起動するとともに、プログラム実行コマンド側で仮想 NIC の紐付けのための調整を行うようにします。
性能計測
ワークロード
計測に利用するソフトウェア
DPDK の場合
- 仮想スイッチ:Open vSwitch(Open vSwitch)
Open vSwitch は DPDK と組み合わせて利用できるようにコンパイルします(Open vSwitch with DPDK — Open vSwitch 3.6.90 documentation)。コンパイルの方法は Dockerfile.dpdk に記載されています。
DPDK と AF_XDP の場合の両方
- TCP/IP スタック:iip(DPDK と AF_XDP と組み合わせて利用可能な TCP/IP スタック)
- サーバー:mimicached(iip と組み合わせて利用可能な memcached 互換サーバー実装)
- クライアント:bench-iip(iip と組み合わせて利用可能なベンチマークツール)
これらをインストールした Docker イメージの作成方法は記事の末尾の Dockerfile.dpdk と Dockerfile.af_xdp に記載されています。
カーネルをバイパスしない場合
カーネルをバイパスしない一般的な構成の場合についての参考値を得るために、以下のプログラムを利用して計測を行います。
- サーバー:公式の memcached 実装(Docker Hub で配布されている公式イメージ)
- クライアント:memtier_benchmark(Docker Hub 上で RedisdLabs が配布しているイメージ)
基本的に各プログラムは一つのスレッドで実行して最大1CPU コアを利用するようにしますが、memtier_benchmark を利用する場合は十分な負荷をかけられるように4スレッドを利用することで最大4つの CPU コアを利用できるようにします。
性能計測に利用した環境
ノートパソコン上で動作する仮想マシンを利用しました。
計測結果
以下が計測の結果です。テーブル全体を見るには画面のサイズによっては横スクロールが必要かもしれません。
| 通し番号 | サーバー | クライアント | 仮想スイッチ | 利用 CPU コア数 | スループット(リクエスト毎秒) | 記事末尾の compose.yaml へのリンク |
|---|---|---|---|---|---|---|
| 1 | memcached (1 core) | memtier_benchmark (4 cores) | Linux bridge | 5 | 436073 | compose.1.yaml |
| 2 | memcached (1 core) | bench-iip on AF_XDP (1 core) | Linux bridge | 2 | 612398 | compose.2.yaml |
| 3 | mimicached on DPDK (1 core) | bench-iip on DPDK (1 core) | Open vSwitch (1 core) | 3 | 3112032 | compose.3.yaml |
| 4 | mimicached on AF_XDP (1 core) | memtier_benchmark (4 cores) | Linux bridge | 5 | 1072698 | compose.4.yaml |
| 5 | mimicached on AF_XDP (1 core) | bench-iip on AF_XDP (1 core) | Linux bridge | 2 | 1646433 | compose.5.yaml |
- 通し番号1と2の比較:サーバーがカーネルをバイパスしない公式の memcached 実装でも、クライアント側が AF_XDP を利用すると、少ない CPU コア数で高い性能が達成できる様子が見られました。
- 通し番号3:DPDK を利用している場合に5つの計測の中で最大の性能が見られました。
- 通し番号3と5の比較:注意点として、AF_XDP の最大性能よりも DPDK の場合の方が最大性能が高くなっていますが、AF_XDP の場合は CPU コアを2つしか利用しないのに対して、DPDK の場合は Open vSwitch のために CPU コアを1つ多く利用しているので、消費されている CPU リソースで換算すると、少なくともこのワークロードにおいては必ずしも AF_XDP が DPDK と比べて大幅に非効率とは言い難いと思われます。
- 通し番号1と4の比較:サーバーが AF_XDP を利用すると、クライアントがカーネルをバイパスしない memtier_bechmark の場合にも性能が2倍以上になる様子が見られました。
- 通し番号1と5の比較:サーバーとクライアント両方が AF_XDP を利用した構成では、一般的なカーネルをバイパスしない構成と比較して、3.7 倍以上の性能が見られました。
性能計測に利用したコマンドと Docker 関連ファイル
性能計測に利用したコマンドと Docker 関連のファイルを記載します。
なお、再現実験をされる場合には自己責任でお願いいたします。root 権限が必要なコマンドも含まれておりますので、ご注意ください。
この記事の作者は、この記事によって引き起こされた全ての問題についての責任を負いません。
下準備用コマンド
DPDK と Open vSwitch 利用のためのコマンド
sudo sh -c "echo 2048 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages"
sudo modprobe openvswitch
Dockerfile
Dockerfile.dpdk
以下のソフトウェアをダウンロードしてコンパイルおよびインストールします。
特に DPDK はコンパイル時にマシンごとに利用可能な CPU 命令などに合わせて最適化を行うため、コンパイル済みのプログラムをダウンロードするのではなく、ソースコードからビルドします。
FROM ubuntu:24.04 ENV TOPDIR=/ WORKDIR ${TOPDIR} # DPDK ENV DPDK_VER=25.11 ENV DPDK_DIR=${TOPDIR}/dpdk ENV DPDK_SRC_DIR=${DPDK_DIR}/dpdk-${DPDK_VER} ENV DPDK_BUILD_DIR=${DPDK_SRC_DIR}/build ENV DPDK_INSTALL_DIR=${DPDK_DIR}/install ENV DPDK_MESON_LIBDIR=lib/dpdk RUN apt update; apt install -y gcc make meson python3-pyelftools libnuma-dev ADD https://fast.dpdk.org/rel/dpdk-${DPDK_VER}.tar.xz ${DPDK_DIR}/ RUN tar xvf ${DPDK_SRC_DIR}.tar.xz -C ${DPDK_DIR} RUN if [ "`uname -m`x" = "aarch64x" ]; then meson -Dplatform=generic --prefix=${DPDK_INSTALL_DIR} --libdir=${DPDK_MESON_LIBDIR} ${DPDK_BUILD_DIR} ${DPDK_SRC_DIR}; elif [ "`uname -m`x" = "x86_64x" ]; then meson --prefix=${DPDK_INSTALL_DIR} --libdir=${DPDK_MESON_LIBDIR} ${DPDK_BUILD_DIR} ${DPDK_SRC_DIR}; else echo "unhandled CPU type: `uname -m`"; exit 1; fi RUN ninja -C ${DPDK_BUILD_DIR} RUN ninja -C ${DPDK_BUILD_DIR} install # DPDK-compatible Open vSwitch ENV OVS_VER=3.6.1 ENV OVS_DIR=${TOPDIR}/ovs ENV OVS_SRC_DIR=${OVS_DIR}/ovs-${OVS_VER} ENV OVS_INSTALL_DIR=${OVS_DIR}/install RUN apt update; apt install -y autoconf libtool-bin pkg-config ADD https://github.com/openvswitch/ovs/archive/refs/tags/v${OVS_VER}.tar.gz ${OVS_DIR}/ RUN tar xvf ${OVS_DIR}/v${OVS_VER}.tar.gz -C ${OVS_DIR} RUN cd ${OVS_SRC_DIR}; ./boot.sh; export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}/pkgconfig; export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}; ./configure --with-dpdk=static CFLAGS="-g -O3 -march=native" --prefix=${OVS_INSTALL_DIR} --localstatedir=${OVS_INSTALL_DIR}/var --sysconfdir=${OVS_INSTALL_DIR}/etc; make; make install # benchmark tool using DPDK RUN apt update; apt install -y pkg-config unzip ADD https://github.com/yasukata/bench-iip/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv bench-iip-master bench-iip; rm master.zip ADD https://github.com/yasukata/iip/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv iip-master ./bench-iip/iip; rm master.zip ADD https://github.com/yasukata/iip-dpdk/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv iip-dpdk-master ./bench-iip/iip-dpdk; rm master.zip RUN echo "" > ./bench-iip/iip-dpdk/build.mk RUN cd bench-iip; CFLAGS=`PKG_CONFIG_PATH=${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}/pkgconfig pkg-config --cflags libdpdk` LDFLAGS=`PKG_CONFIG_PATH=${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}/pkgconfig pkg-config --libs libdpdk` IOSUB_DIR=./iip-dpdk make # memcached-compatible server using DPDK ADD https://github.com/yasukata/mimicached/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv mimicached-master mimicached; rm master.zip ADD https://github.com/yasukata/memcached-protocol-parser/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv memcached-protocol-parser-master mimicached/memcached-protocol-parser; rm master.zip RUN ln -s /bench-iip/iip /mimicached; cd mimicached; CFLAGS=`PKG_CONFIG_PATH=${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}/pkgconfig pkg-config --cflags libdpdk` LDFLAGS=`PKG_CONFIG_PATH=${DPDK_INSTALL_DIR}/${DPDK_MESON_LIBDIR}/pkgconfig pkg-config --libs libdpdk` IOSUB_DIR=../bench-iip/iip-dpdk make
以下のコマンドでビルドします。
docker build -f Dockerfile.dpdk -t experiment-dpdk:1.0 .
Dockerfile.af_xdp
以下のソフトウェアをダウンロードしてコンパイルおよびインストールします。
FROM ubuntu:24.04 WORKDIR / # benchmark tool using AF_XDP RUN apt update; apt install -y unzip gcc make libnuma-dev libxdp-dev ADD https://github.com/yasukata/bench-iip/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv bench-iip-master bench-iip; rm master.zip ADD https://github.com/yasukata/iip/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv iip-master ./bench-iip/iip; rm master.zip ADD https://github.com/yasukata/iip-af_xdp/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv iip-af_xdp-master ./bench-iip/iip-af_xdp; rm master.zip RUN mv /bench-iip/iip-af_xdp/main.c /bench-iip/iip-af_xdp/_main.c; echo "#define helper_ip4_get_connection_affinity __helper_ip4_get_connection_affinity" > /bench-iip/iip-af_xdp/main.c; echo "#include \"_main.c\"" >> /bench-iip/iip-af_xdp/main.c; echo "#undef helper_ip4_get_connection_affinity" >> /bench-iip/iip-af_xdp/main.c; echo "static uint16_t helper_ip4_get_connection_affinity(uint16_t protocol, uint32_t local_ip4_be, uint16_t local_port_be, uint32_t peer_ip4_be, uint16_t peer_port_be, void *opaque) { return 0; (void) protocol; (void) local_ip4_be; (void) local_port_be; (void) peer_ip4_be; (void) peer_port_be; (void) opaque; (void) __helper_ip4_get_connection_affinity; }" >> /bench-iip/iip-af_xdp/main.c RUN cd bench-iip; IOSUB_DIR=./iip-af_xdp make # memcached-compatible server using AF_XDP ADD https://github.com/yasukata/mimicached/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv mimicached-master mimicached; rm master.zip ADD https://github.com/yasukata/memcached-protocol-parser/archive/refs/heads/master.zip ./ RUN unzip ./master.zip; mv memcached-protocol-parser-master mimicached/memcached-protocol-parser; rm master.zip RUN ln -s /bench-iip/iip /mimicached; ln -s /bench-iip/iip-af_xdp /mimicached; cd mimicached; IOSUB_DIR=./iip-af_xdp make
以下のコマンドでビルドします。
docker build -f Dockerfile.af_xdp -t experiment-af_xdp:1.0 .
compose.yaml
compose.1.yaml
services: memcached: image: memcached:1.6.40-alpine3.23 command: - --memory-limit=1024 - --threads=1 memtier_benchmark: depends_on: - memcached image: redislabs/memtier_benchmark:2.2.0 command: -h memcached -p 11211 -P memcache_text -t 4 -c 64 --key-maximum 1 --ratio=0:1 --test-time=10
compose.2.yaml
services: memcached: image: memcached:1.6.40-alpine3.23 command: - --memory-limit=1024 - --threads=1 client: depends_on: - memcached image: experiment-af_xdp:1.0 cap_add: - SYS_NICE - SYS_ADMIN - NET_ADMIN - IPC_LOCK - BPF command: sh -c "/bench-iip/a.out -l 1 -i eth0 -- -s `getent hosts memcached | awk '{ print $1 }'` -p 11211 -m \"```echo -e 'get a\\r\\n\0'```\" -c 64"
compose.3.yaml
services: ovs: image: experiment-dpdk:1.0 cap_add: - SYS_NICE - NET_ADMIN devices: - /dev/net/tun:/dev/net/tun volumes: - /dev/hugepages:/dev/hugepages - ./tmp-sock:/ovs/install/var/run/openvswitch command: sh -c "rm /ovs/install/var/run/openvswitch/br0.*; rm /ovs/install/var/run/openvswitch/db.sock; rm /ovs/install/var/run/openvswitch/ovs-vswitchd.*; rm /ovs/install/var/run/openvswitch/ovsdb-server.*; rm /ovs/install/var/run/openvswitch/vhost*; /ovs/install/share/openvswitch/scripts/ovs-ctl start; /ovs/install/bin/ovs-vsctl --no-wait set Open_vSwitch . other_config:dpdk-init=true; /ovs/install/share/openvswitch/scripts/ovs-ctl stop; /ovs/install/share/openvswitch/scripts/ovs-ctl start; /ovs/install/bin/ovs-vsctl del-br br0; ovs/install/bin/ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev; /ovs/install/bin/ovs-vsctl add-port br0 dport0 -- set Interface dport0 type=dpdkvhostuserclient options:vhost-server-path=/ovs/install/var/run/openvswitch/vhost0; /ovs/install/bin/ovs-vsctl add-port br0 dport1 -- set Interface dport1 type=dpdkvhostuserclient options:vhost-server-path=/ovs/install/var/run/openvswitch/vhost1; /ovs/install/bin/ovs-vsctl show; tail -f /dev/null" mimicached: depends_on: - ovs image: experiment-dpdk:1.0 cap_add: - SYS_NICE - IPC_LOCK volumes: - /dev/hugepages:/dev/hugepages - ./tmp-sock:/var/run/dpdk-app command: sh -c "sleep 2; LD_LIBRARY_PATH=/dpdk/install/lib/dpdk /mimicached/a.out -l 1 --proc-type=primary --file-prefix=pmd1 --vdev=net_virtio_user0,path=/var/run/dpdk-app/vhost0,server=1 --no-pci --single-file-segments -- -a 0,10.100.0.20 -e 0 -- -p 11211 -m 10000 -z 100000" client: depends_on: - ovs image: experiment-dpdk:1.0 cap_add: - SYS_NICE - IPC_LOCK volumes: - /dev/hugepages:/dev/hugepages - ./tmp-sock:/var/run/dpdk-app command: sh -c "sleep 2; LD_LIBRARY_PATH=/dpdk/install/lib/dpdk /bench-iip/a.out -l 2 --proc-type=primary --file-prefix=pmd2 --vdev=net_virtio_user1,path=/var/run/dpdk-app/vhost1,server=1 --no-pci --single-file-segments -- -a 0,10.100.0.10 -e 0 -- -s 10.100.0.20 -p 11211 -m \"```echo -e 'get a\\r\\n\0'```\" -c 64" tty: true
compose.4.yaml
services: mimicached: image: experiment-af_xdp:1.0 cap_add: - SYS_NICE - SYS_ADMIN - NET_ADMIN - IPC_LOCK - BPF command: sh -c "/mimicached/a.out -l 0 -i eth0 -- -p 11211 -m 1024 -z 100000" memtier_benchmark: depends_on: - mimicached image: redislabs/memtier_benchmark:2.2.0 command: -h mimicached -p 11211 -P memcache_text -t 4 -c 64 --key-maximum 1 --ratio=0:1 --test-time=10
compose.5.yaml
services: mimicached: image: experiment-af_xdp:1.0 cap_add: - SYS_NICE - SYS_ADMIN - NET_ADMIN - IPC_LOCK - BPF command: sh -c "/mimicached/a.out -l 0 -i eth0 -- -p 11211 -m 1024 -z 100000" client: depends_on: - mimicached image: experiment-af_xdp:1.0 cap_add: - SYS_NICE - SYS_ADMIN - NET_ADMIN - IPC_LOCK - BPF command: sh -c "/bench-iip/a.out -l 1 -i eth0 -- -s `getent hosts mimicached | awk '{ print $1 }'` -p 11211 -m \"```echo -e 'get a\\r\\n\0'```\" -c 64"
実行は以下のコマンドで行います。実行時には以下の FILE の箇所を compose.1.yaml のようにファイル名と置き換えます。
docker compose -f FILE up
まとめ
Docker 環境で、標準的なコンテナ用ネットワーク構成を大幅に変更しないまま、DPDK と AF_XDP を利用したアプリケーションが仮想 NIC を扱う場合の性能について簡単な計測を行いました。