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 run 或 nerdctl 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 exclusive3.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 json4. 生成 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 hca04.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 中的 kind,0000:17:00.0 是 devices[].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_cm6. 示例:容器内运行 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_bwClient 端(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 字符设备的痛点。