1. 背景:如何在非 K8s 场景下容器化 RDMA 网卡?

在 Kubernetes 生态中,RDMA 网卡的容器化已经有了成熟的工程实践:SR-IOV Device Plugin + SR-IOV CNI + RDMA Shared Device Plugin 构成的方案链已被广泛部署。Kubelet 的 Device Plugin 机制天然支持设备发现、分配与注入的全生命周期。Pod 只需声明资源请求,剩下的工作由调度器和设备插件自动完成。

但并非所有 RDMA 容器化场景都运行在 Kubernetes 上,比如:

场景说明
非 K8s 调度系统使用 CraneSched 等 HPC 调度器通过 CRI 直接调用 containerd 运行容器
开发调试开发者在裸机上用 docker runnerdctl run 快速验证 RDMA 应用
CI/CD 流水线构建管线中需要 RDMA 环境做集成测试,但不值得搭建完整 K8s 集群

在这些场景下,用户通常只能手动挂载 RDMA 字符设备:

docker run --device /dev/infiniband/uverbs0 \
           --device /dev/infiniband/umad0 \
           --device /dev/infiniband/rdma_cm \
           my-rdma-app

这种方式显然有些原始。


2. 方案:CDI + rdma-cdi 工具

CDI 是什么

CDI(Container Device Interface) 是 CNCF 提出的容器设备注入标准。它在 K8s Device Plugin 之外定义了一种运行时级别的设备注入协议:通过一个放在 /etc/cdi/ 下的 JSON/YAML 规范文件(CDI Spec),声明设备注入所需的设备节点、挂载和环境变量。容器运行时在创建容器时读取 CDI Spec 自动完成注入。

CDI 的关键定位是:它工作在容器运行时层(containerd、Docker、CRI-O),不依赖 Kubernetes。这意味着只要容器运行时支持 CDI,无论上层是 K8s 还是直接 docker run,都能使用同一套设备注入机制。

运行时CDI 支持版本
containerd≥ 1.7
Docker≥ 25.0
CRI-O≥ 1.23
Podman≥ 4.1

rdma-cdi

在 K8s 中,新版 Device Plugin 已经在向 CDI 模式迁移。Device Plugin 负责发现和分配,最终通过 CDI Spec 完成注入。而在非 K8s 场景下,设备发现需要由外部工具完成,CDI Spec 需要预先生成。

在非 K8s 环境中,缺少"RDMA 设备发现 + CDI Spec 生成"这个环节。

rdma-cdi 就是解决这个环节的工具。它从 Mellanox/k8s-rdma-shared-dev-plugin 项目中提取 CDI 生成逻辑,去除了所有 Kubernetes 依赖,做成一个独立的命令行工具。

它的定位如下图所示:

┌─── Kubernetes 场景 ───────────────────────────────────┐
│                                                        │
│  SR-IOV Device Plugin  ──→  Kubelet  ──→  containerd   │
│  (设备发现+分配)           (调度)        (CDI 注入)     │
│                                                        │
└────────────────────────────────────────────────────────┘

┌─── Docker / Nerdctl 场景 ─────────────────────────────┐
│                                                        │
│  rdma-cdi generate  ──→  /etc/cdi/*.yaml               │
│  (设备发现+Spec生成)      (CDI Spec)                    │
│                                                        │
│  docker/nerdctl run --device <cdi-name>                │
│                        ──→  containerd / Docker        │
│                             (CDI 注入)                  │
│                                                        │
└────────────────────────────────────────────────────────┘

功能概览

子命令功能
discover发现主机上所有 RDMA 设备及其字符设备映射
generate为指定 RDMA 设备生成 CDI Spec 文件
doctor环境诊断:检查内核模块、设备可见性、链路状态
cleanup安全清理本工具创建的 CDI Spec 文件

工作原理

PCI 地址 / 网卡名
       │
       ▼
  ┌──────────────┐     读取 sysfs
  │ RDMA 设备发现 │ ──────────────→ /sys/bus/pci/devices/<BDF>/
  └──────┬───────┘                 /sys/class/net/<ifname>/
         │
         ▼
  ┌──────────────┐     调用 rdmamap 库
  │ 字符设备枚举  │ ──────────────→ /dev/infiniband/uverbs*
  └──────┬───────┘                 /dev/infiniband/umad*
         │                         /dev/infiniband/rdma_cm
         ▼
  ┌──────────────┐
  │ CDI Spec 生成 │ ──→ /etc/cdi/rdma-cdi_rdma_<name>.yaml
  └──────────────┘

底层调用 Mellanox rdmamap 库,通过 sysfs 和 PCI 总线拓扑自动枚举 RDMA 字符设备,然后生成符合 CDI 标准的 Spec 文件。

安装

git clone https://github.com/Nativu5/rdma-cdi.git
cd rdma-cdi
make install   # 默认安装到 /usr/local/bin

# 或指定安装路径
make install PREFIX=~/.local/bin
编译要求:Go 1.24+、Linux 系统

3. 环境检查与设备发现

在生成 CDI Spec 之前,先确认 RDMA 环境就绪。

3.1 环境诊断

rdma-cdi 内置诊断功能,一键检查内核模块、字符设备、链路状态和 netns 模式:

rdma-cdi doctor                        # 检查所有 RDMA 设备
rdma-cdi doctor --pci 0000:17:00.0     # 检查指定设备
rdma-cdi doctor --strict               # 严格模式:告警也视为失败

输出示例:

+----------+------------------+----------------+-----------------------------------------------+
|  STATUS  |      CHECK       |     DEVICE     |                    MESSAGE                    |
+----------+------------------+----------------+-----------------------------------------------+
| ✓ PASS   | rdma_devices     | 0000:17:00.0   | All required RDMA devices present (3):        |
|          |                  |                | /dev/infiniband/uverbs0, rdma_cm, umad0       |
| ✓ PASS   | kernel_modules   | (host)         | All required kernel modules loaded             |
| ✓ PASS   | net_interface    | 0000:17:00.0   | Interface: ib0                                |
| ✓ PASS   | link_state       | 0000:17:00.0   | Link ib0 is up (encap: infiniband, MTU: 2044) |
| ✓ PASS   | rdma_netns_mode  | 0000:17:00.0   | RDMA netns mode: exclusive (1)                |
+----------+------------------+----------------+-----------------------------------------------+

如果 kernel_modules 检查失败,手动加载缺失模块:

modprobe ib_core ib_uverbs ib_umad rdma_cm rdma_ucm

如果 rdma_netns_mode 报告为 shared,建议切换为 exclusive(容器间 RDMA 设备隔离需要此模式):

rdma system set netns exclusive

3.2 设备发现

确认诊断通过后,使用 discover 查看可用设备及其字符设备映射:

rdma-cdi discover
+----------------+-----------+-----------+-----------+-----------------------------------------------+
|  PCI ADDRESS   | INTERFACE |  DRIVER   | LINK TYPE |                   DEVICES                     |
+----------------+-----------+-----------+-----------+-----------------------------------------------+
| 0000:17:00.0   | ib0       | mlx5_core | infiniband| /dev/infiniband/uverbs0, umad0, rdma_cm       |
| 0000:17:00.1   | ib1       | mlx5_core | infiniband| /dev/infiniband/uverbs1, umad1, rdma_cm       |
+----------------+-----------+-----------+-----------+-----------------------------------------------+

也可按 PCI 地址或接口名查询单个设备,--output json 可输出 JSON 格式便于脚本解析:

rdma-cdi discover --pci 0000:17:00.0
rdma-cdi discover --ifname ib0 --output json

4. 生成 CDI Spec

4.1 单个设备 / 批量生成

# 按 PCI 地址生成(默认 YAML 格式,输出到 /etc/cdi/)
rdma-cdi generate --pci 0000:17:00.0

# 按网卡接口名生成
rdma-cdi generate --ifname ib0

# 为所有 RDMA 设备批量生成
rdma-cdi generate --all

# 指定 JSON 格式、自定义输出目录、自定义前缀和名称
rdma-cdi generate --pci 0000:17:00.0 --format json --output-dir /var/run/cdi --prefix myvendor --name hca0

4.2 生成的 CDI Spec 示例

rdma-cdi generate --pci 0000:17:00.0 为例:

cdiVersion: "0.6.0"
kind: "rdma/pci-0000-17-00-0"          # <prefix>/<name>,后续启动容器时引用
devices:
  - name: "0000:17:00.0"               # 设备标识
    containerEdits:
      deviceNodes:
        - path: /dev/infiniband/uverbs0
          hostPath: /dev/infiniband/uverbs0
          permissions: rw
        - path: /dev/infiniband/umad0
          hostPath: /dev/infiniband/umad0
          permissions: rw
        - path: /dev/infiniband/rdma_cm
          hostPath: /dev/infiniband/rdma_cm
          permissions: rw

文件写入路径为 /etc/cdi/rdma-cdi_rdma_pci-0000-17-00-0.yaml


5. 在 Docker / Nerdctl 中使用

生成 CDI Spec 后,需要在容器运行时侧启用 CDI,然后就可以用 --device 指定 CDI 设备名启动容器。

5.1 启用 CDI 支持

对于 Docker(≥ 25.0):

编辑 `/etc/docker/daemon.json`:

```json
{
  "features": {
    "cdi": true
  },
  "cdi-spec-dirs": ["/etc/cdi", "/var/run/cdi"]
}
```

```bash
systemctl restart docker
```

对于 Nerdctl + containerd(≥ 1.7):

编辑 `/etc/containerd/config.toml`:

```toml
[plugins.'io.containerd.grpc.v1.cri']
  enable_cdi = true
  cdi_spec_dirs = ["/etc/cdi", "/var/run/cdi"]
```

```bash
systemctl restart containerd
```

5.2 启动容器

Docker 和 Nerdctl 的命令完全一致,--device 参数格式为 <kind>=<device-name>

docker run --rm -it \
  --device rdma/pci-0000-17-00-0=0000:17:00.0 \
  ubuntu:22.04 \
  ls -la /dev/infiniband/
nerdctl run --rm -it \
  --device rdma/pci-0000-17-00-0=0000:17:00.0 \
  ubuntu:22.04 \
  ls -la /dev/infiniband/

其中 rdma/pci-0000-17-00-0 是 CDI Spec 中的 kind0000:17:00.0devices[].name

预期输出:

crw-rw---- 1 root root 231, 0 ... /dev/infiniband/uverbs0
crw-rw---- 1 root root 231, 1 ... /dev/infiniband/umad0
crw-rw---- 1 root root 231, 2 ... /dev/infiniband/rdma_cm

6. 示例:容器内运行 RDMA perftest

假设两个节点各有一张 RDMA 网卡,已完成前述第 3-5 节的环境检查、Spec 生成和运行时配置。

Server 端(PCI 地址 0000:17:00.0):

docker run --rm -it --network host \
  --device rdma/pci-0000-17-00-0=0000:17:00.0 \
  rdma-perftest:latest \
  ib_write_bw

Client 端(PCI 地址 0000:86:00.0,Server IP 10.0.1.1):

docker run --rm -it --network host \
  --device rdma/pci-0000-86-00-0=0000:86:00.0 \
  rdma-perftest:latest \
  ib_write_bw 10.0.1.1

注意:可能需要使用 --net=host 或搭配使用 RDMA 相关 CNI 实现组网。


7. 清理

rdma-cdi 仅清理自身创建的文件(文件名以 rdma-cdi_ 开头),不会影响其他工具生成的 CDI Spec:

rdma-cdi cleanup --dry-run             # 预览将要删除的文件
rdma-cdi cleanup                       # 执行清理
rdma-cdi cleanup --prefix rdma --name pci-0000-17-00-0  # 仅清理指定设备

总结

在 Kubernetes 场景中,SR-IOV Device Plugin 和 RDMA Shared Device Plugin 已经解决了 RDMA 设备的容器化问题。但在 K8s 之外,对于 Docker 直接使用、HPC 调度器、开发调试等需求,这个问题一直缺少标准化的解决方案。

CDI 标准把设备注入能力下沉到容器运行时层,不再依赖 Kubernetes。rdma-cdi 工具则承担了 K8s Device Plugin 在非 K8s 环境下的角色:自动发现 RDMA 设备、生成 CDI Spec。两者结合,让 docker run --device <cdi-name> 成为可能,消除了手动挂载 RDMA 字符设备的痛点。


参考链接