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です。