Kubernetes上のコンテナでRDMAを動かして性能計測してみる
Kubernetes上のコンテナでSR-IOVを用いてRDMAを動かしたのでハマった内容を含めて方法をメモしておきます。 ついでにRDMAとTCP/IPで性能比較もしてみました。
何をやるか
- SR-IOVのVFを生やす
- Kubernets上のコンテナにVF (Virtual Function) を割り当てる
- Kubernets上のコンテナにRDMAのdeviceを追加
- RDMAとTCP/IPで性能比較
環境
Mellanox Technologies MT27800 Family [ConnectX-5] (RoCEv2対応の100Gbps Ethernet NIC) で直結された2台のUbuntuマシン (AMD EPYC 7282 16-Core Processor) 上でKubernetesクラスタを構築しています。 Kubernetesはノード間通信にこのNICを使うように設定されています。 コンテナランタイムにはcontainerdを使っています。
SR-IOV
PF (Physical Function)から1個のVFを生やします。公式のドキュメント1に従います。
まずはBIOSの設定を見てSR-IOVとIOMMUが有効になっていることを確認します。 次に以下のようにgrubのパラメータを追加して再起動します。
$ cat /etc/default/grub
...
GRUB_CMDLINE_LINUX="amd_iommu=on iommu=pt pci=realloc"
...
NICのパラメータを変更します。
$ sudo mst start
Starting MST (Mellanox Software Tools) driver set
Loading MST PCI module - Success
Loading MST PCI configuration module - Success
Create devices
Unloading MST PCI module (unused) - Success
$ sudo mst status
MST modules:
------------
MST PCI module is not loaded
MST PCI configuration module loaded
MST devices:
------------
/dev/mst/mt4119_pciconf0 - PCI configuration cycles access.
domain:bus:dev.fn=0000:41:00.0 addr.reg=88 data.reg=92 cr_bar.gw_offset=-1
Chip revision is: 00
$ sudo mlxconfig -d /dev/mst/mt4119_pciconf0 q
Device #1:
----------
Device type: ConnectX5
Name: MCX515A-CCA_Ax_Bx
Description: ConnectX-5 EN network interface card; 100GbE single-port QSFP28; PCIe3.0 x16; tall bracket; ROHS R6
Device: /dev/mst/mt4119_pciconf0
Configurations: Next Boot
...
NUM_OF_VFS 0
SRIOV_EN False(0)
...
$ sudo mlxconfig -d /dev/mst/mt4119_pciconf0 set SRIOV_EN=1 NUM_OF_VFS=1
...
$ sudo mlxconfig -d /dev/mst/mt4119_pciconf0 q
Device #1:
----------
Device type: ConnectX5
Name: MCX515A-CCA_Ax_Bx
Description: ConnectX-5 EN network interface card; 100GbE single-port QSFP28; PCIe3.0 x16; tall bracket; ROHS R6
Device: /dev/mst/mt4119_pciconf0
Configurations: Next Boot
...
NUM_OF_VFS 1
SRIOV_EN True(1)
...
再起動してから以下を実行します。
$ ibv_devices
device node GUID
------ ----------------
mlx5_0 abcdef0300ghijkl
$ echo 1 > /sys/class/net/enp65s0np0/device/sriov_numvfs
$ ibv_devices
device node GUID
------ ----------------
mlx5_0 abcdef0300ghijkl
mlx5_1 0000000000000000
$ ip link
...
4: enp65s0np0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether ab:cd:ef:gh:ij:kl brd ff:ff:ff:ff:ff:ff
vf 0 link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff, spoof checking off, link-state auto, trust off, query_rss off
...
9: enp65s0v0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether lm:no:pq:rs:tu:vw brd ff:ff:ff:ff:ff:ff permaddr 01:23:45:67:89:01
...
この時点でmlx5_1が生えていることが確認できます。 (IPアドレスを割り振れば)VFを使ってTCP/IPでの通信ができるようになっています。 しかし現状はVFのGUIDが0です。この状態だとRDMAが使えないという既知の問題があります。
# https://docs.nvidia.com/networking/display/mlnxofedv451010/known+issues
1047616 | Description: When node GUID of a device is set to zero (0000:0000:0000:0000), RDMA_CM user space application may crash.
Workaround: Set node GUID to a nonzero value.
Keywords: RDMA_CM
GUIDを0以外に設定します。 何の値にするのが正解なのかは分かりませんが、物理NICのGUIDはMACアドレスの間に0300を挟んだものになっていたのでそれに合わせてみます。
$ echo lm:no:pq:03:00:rs:tu:vw | sudo tee /sys/class/net/enp65s0np0/device/sriov/0/node
$ echo 0000:41:00.1 | sudo tee /sys/bus/pci/drivers/mlx5_core/unbind
$ echo 0000:41:00.1 | sudo tee /sys/bus/pci/drivers/mlx5_core/bind
$ ibv_devices
device node GUID
------ ----------------
mlx5_0 abcdef0300ghijkl
mlx5_1 lmnopq0300rstuvw
ここまででSR-IOVのVFを生やしてRDMAが使える状態になりました。
コンテナとSR-IOV
生やしたVFをコンテナに割り当てます。 SR-IOV CNI plugin2とMultus3を使うのが正しい方法だと思いますが、今回はnerdctlとipコマンドを使って手動でやります。
Kubernetesで適当なPodを作っている前提で進めます。 まずはnetwork namespaceの実体を見つけ出してIPコマンドで管理可能な状態にします。
$ NAME=testtest
$ CONTAINERID=$(kubectl get pod $NAME -o json | jq -r '."status"."containerStatuses"[0]."containerID"' | sed 's/containerd:\/\///')
$ PID=$(sudo nerdctl --namespace k8s.io inspect $CONTAINERID --format '{{.State.Pid}}')
$ NETNSNAME=k8s-$NAME
$ sudo ln -s /proc/$PID/ns/net /var/run/netns/$NETNSNAME
/var/run/netns
の配下にファイルを置くことでipコマンドで操作できるようになります。
ホストのnetwork namespaceにあるVFをコンテナのnetwork namespaceに移します。
$ ip netns
k8s-testtest
$ VFLINKNAME=enp65s0v0
$ VFLINKADDR=192.168.0.1/24
$ sudo ip link set dev $VFLINKNAME netns $NETNSNAME
$ sudo ip -n $NETNSNAME link set dev $VFLINKNAME up
$ sudo ip -n $NETNSNAME addr add $VFLINKADDR dev $VFLINKNAME
これでコンテナからVFを直接使えるようになりました。
コンテナとRDMA
コンテナからRDMAを使うためには/dev/infiniband
配下のdeviceをコンテナから見えるようにする必要があります。
コンテナに特権が付いている場合は、Podのmanifestからマウントするだけで使えるようになります4。
特権をつけたくない場合はKubelet Device APIを使ってdeviceをコンテナに割り当てる必要があります。
smarter-device-manager5を使うとdeviceをKubernetes上でCPUやメモリと同様に扱えるようになります。
まず公式のサンプル通りにDaemonSetを追加します。
# https://gitlab.com/arm-research/smarter/smarter-device-manager/-/blob/master/smarter-device-manager-ds.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: smarter-device-manager
namespace: device-manager
labels:
name: smarter-device-manager
role: agent
spec:
selector:
matchLabels:
name: smarter-device-manager
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
name: smarter-device-manager
annotations:
node.kubernetes.io/bootstrap-checkpoint: "true"
spec:
priorityClassName: "system-node-critical"
hostname: smarter-device-management
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: smarter-device-manager
image: registry.gitlab.com/arm-research/smarter/smarter-device-manager:v1.1.2
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
limits:
cpu: 100m
memory: 15Mi
requests:
cpu: 10m
memory: 15Mi
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: dev-dir
mountPath: /dev
- name: sys-dir
mountPath: /sys
- name: config
mountPath: /root/config
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: dev-dir
hostPath:
path: /dev
- name: sys-dir
hostPath:
path: /sys
- name: config
configMap:
name: smarter-device-manager
次にConfigMapで/dev/infiniband
を指定します。
apiVersion: v1
kind: ConfigMap
metadata:
name: smarter-device-manager
namespace: device-manager
data:
conf.yaml: |
- devicematch: infiniband
nummaxdevices: 20
この時点でNodeのmanifestが以下のようになります。
status:
allocatable:
cpu: "32"
ephemeral-storage: "885012522772"
hugepages-1Gi: 8Gi
hugepages-2Mi: "0"
memory: 123272716Ki
pods: "110"
smarter-devices/infiniband: "20"
capacity:
cpu: "32"
ephemeral-storage: 960300048Ki
hugepages-1Gi: 8Gi
hugepages-2Mi: "0"
memory: 131763724Ki
pods: "110"
smarter-devices/infiniband: "20"
あとはPodのmanifestにrequestとして追加します。
spec:
containers:
- ...
resources:
limits:
smarter-devices/infiniband: 1
requests:
smarter-devices/infiniband: 1
これでコンテナからRDMAを使えるようになりました。
ちなみにKubernetesを使わずにnerdctlでコンテナを起動する場合はオプションに--device=/dev/infiniband
と書けばよいです(Dockerも同じはず)。
性能計測
Netperf6を使ってスループット・レイテンシ・CPU時間を計測してみます。 また自分で書いたコードでconnect&closeの時間も計測してみます。 ソケットAPIを用いたコードでRDMAを使うためにrsocket7をLD_PRELOADでロードします。
- host-remote-tcp: ホスト間通信でTCP/IPを使う
- host-remote-roce: ホスト間通信でRoCEv2を使う
- flannel-remote-tcp: CNI PluginとしてFlannelを使って別ホスト上のコンテナ間でTCP/IPを使う
- sriov-remote-roce: SR-IOVのVFを使って別ホスト上のコンテナ間でRoCEv2を使う
その他細かい条件の説明は省略します。
スループット・レイテンシ・CPU時間はTCP/IPよりもRoCEv2の方が優れていますね。 その代わりconnect&closeの時間がかなりかかっていることが分かります。
まとめ
実際に使うには色々と制約があるもののさすがRDMAです。
https://enterprise-support.nvidia.com/s/article/howto-configure-sr-iov-for-connect-ib-connectx-4-with-kvm--infiniband-x#jive_content_id_II_Enable_SRIOV_on_the_MLNX_OFED_driver ↩︎
https://github.com/kubernetes/kubernetes/issues/5607#issuecomment-766089905 ↩︎
https://gitlab.com/arm-research/smarter/smarter-device-manager ↩︎