在AI计算集群等场景中,系统级容器的稳定部署、GPU资源高效透传及多容器运行时兼容是核心需求,传统容器方案要么无法良好支持systemd服务管理,要么GPU透传配置复杂、需在容器内重复安装驱动,且难以兼顾Rootless Podman与Docker的二级容器运行需求。本文针对这一现状,详细阐述一套集成Nvidia GPU透传、自动驱动注入、systemd-nspawn容器管理及多二级容器运行时的完整方案,该方案已在某高校AI计算集群中稳定运行数月,可有效降低集群容器化部署的复杂度,提升资源利用率。
针对传统容器部署存在GPU透传配置繁琐、需在容器内额外安装驱动导致环境冗余、systemd支持不足、二级容器运行时兼容性差及Rootless模式适配困难等痛点,本方案核心优势在于无需在容器内安装GPU驱动,可动态扫描并挂载NVIDIA库和工具,完美支持GPU透传与CDI驱动注入,兼容Docker与Rootless Podman两种二级容器运行时,同时通过脚本实现systemd-nspawn容器的自动监控与管理,简化集群部署与运维成本。
功能特性:
- ✅ 多级容器的 NVIDIA GPU 透传支持,支持 DinD/PinD 模式
- ✅ 动态扫描 NVIDIA 库和工具,不需要在容器内安装 GPU 驱动,容器驱动自动跟随宿主机更新
- ✅ 真正的系统级容器,完美支持 systemd 服务管理,同时无Docker一级容器方案的存储冗余、可靠性差等诸多弊端。
- ✅ 支持 Docker 和 Podman 二级容器运行时,容器内可以再运行容器,支持 CDI 方式的 NVIDIA GPU 驱动注入
- ✅ 多节点集群支持,自动配置跨节点路由,支持宿主集群NFS。
- ✅ 支持 Rootless Podman 容器运行时,安全性风险低。
- ✅ 自动监控和管理 systemd-nspawn 容器。
宿主环境准备
宿主机需要安装好 NVIDIA GPU 驱动和 NVIDIA Container Toolkit,具体请参考,对于 Ubuntu 22.04,安装步骤如下:
apt update
apt install systemd-container debootstrap tmux
apt install nvidia-container-toolkit nvidia-container-toolkit-base libnvidia-container-tools
systemd-nspawn --version
machinectl --version
由于 Nspawn 的 NAT 存在诸多 Bug,不推荐使用,因此我们还需要宿主机存在一个网络提供者给一级容器使用,这里我直接借用了宿主机的 Docker 网络,为此也需要在宿主机上安装 Docker,后续的脚本会自动创建 Docker network :
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
由于 Nspawn 的端口转发(-p 参数)也存在诸多 Bug,我们使用 rinetd 提供端口转发功能,因此还需要安装 rinetd :
sudo apt install rinetd
本文后续的宿主和容器配置均基于 Ubuntu 22.04 操作系统。其他发行版请参考对应的官方文档进行安装,也欢迎在评论区进行补充。
一级容器 systemd-nspawn 容器支持配置
首先安装一级容器的操作系统,我这里安装到了 /var/lib/machines/inais :
sudo mkdir -p /var/lib/machines/inais
sudo debootstrap \
--include=systemd,dbus \
jammy \
/var/lib/machines/ubuntu22 \
http://archive.ubuntu.com/ubuntu
mkdir -p /var/lib/machines/_Scripts
cd /var/lib/machines/_Scripts
请将我附件提供的 Shell 脚本(点击下载) 上传到 /var/lib/machines/_Scripts 目录下,脚本用于管理基于 systemd-nspawn 的 INAIS 容器环境,支持多节点存储绑定、NVIDIA GPU 透传、自动网络配置和端口转发。
使用方法:
./run.sh daemon
以守护进程模式运行,自动监控和管理容器。
脚本会自动加载同目录下的配置文件 config-{脚本名}.sh,例如 run.sh 会加载 config-run.sh。
配置项说明
必需配置项
| 变量名 | 类型 | 说明 | 示例值 |
|---|---|---|---|
INAIS_HOST_ACCOUNT |
string | 主机账户名,用于构建存储路径 | "Kenvix" |
NODE_ID |
integer | 当前节点ID(1~MAX_NODE_ID) | 15 |
CONTAINER_NAME |
string | 容器名称,用于 machinectl 管理 | "inais" |
CONTAINER_ROOT |
string | 容器根文件系统路径 | "/var/lib/machines/inais" |
CONTAINER_INDEX |
integer | 容器索引,用于计算容器IP地址 | 0 |
INAIS_NETWORK_MODE |
string | 网络模式:"docker" 或 "veth" |
"docker" |
USE_RINETD_PORT_FORWARDING |
boolean | 端口转发方式:true=rinetd, false=systemd-nspawn内置 |
false |
可选配置项
| 变量名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
MAX_NODE_ID |
integer | 21 |
最大节点ID,决定节点范围 1~N |
DISABLE_LOCAL_STORAGE_BINDINGS |
boolean | false |
设为 true 禁用本地存储挂载(当前节点的 /data/inXX) |
DISABLE_NVIDIA_BINDINGS |
boolean | false |
设为 true 禁用所有 NVIDIA 设备和库文件挂载 |
LOCAL_DATA_PREFIX |
string | /data |
本地数据目录前缀 |
NFS_STORAGE_PREFIX |
string | /storagenfs/nfs |
NFS 存储路径前缀(不含节点编号) |
PORT_FORWARD_HOST_START |
integer | 30600 |
主机端起始端口号 |
PORT_FORWARD_CONTAINER_START |
integer | 30000 |
容器端起始端口号 |
PORT_FORWARD_COUNT |
integer | 20 |
端口映射数量 |
配置详解
网络模式 (INAIS_NETWORK_MODE)
docker 模式
- 使用 Docker 创建的桥接网络
inais0 - 容器 IP 格式:
253.{NODE_ID}.{CONTAINER_INDEX/256+1}.{CONTAINER_INDEX%256} - 子网:
253.{NODE_ID}.0.0/16 - 网关:
253.{NODE_ID}.0.1
我这里使用了 IPv4 Class-E 类IP这种比较邪门的地址,以避免和现有网络冲突。也可以修改脚本使用其他私有地址段,例如 10.{NODE_ID}.0.0/16 。需注意 Windows 只对 E 类地址提供有限支持,即可以连接但无法监听;Linux 则完整支持 E 类地址作为单播地址。
veth 模式
- 使用 veth pair 直连
- 主机端 IP:
253.{NODE_ID}.{CONTAINER_INDEX}.1/24 - 容器端 IP:
253.{NODE_ID}.{CONTAINER_INDEX}.2/24 - 自动配置 NAT MASQUERADE
端口映射配置
端口映射范围由三个变量控制:
主机端口范围: PORT_FORWARD_HOST_START ~ (PORT_FORWARD_HOST_START + PORT_FORWARD_COUNT - 1)
容器端口范围: PORT_FORWARD_CONTAINER_START ~ (PORT_FORWARD_CONTAINER_START + PORT_FORWARD_COUNT - 1)
默认配置:30600-30619 → 30000-30019(共 20 个端口)
端口转发方式:
USE_RINETD_PORT_FORWARDING=false:使用 systemd-nspawn 内置的--port参数USE_RINETD_PORT_FORWARDING=true:使用外部 rinetd 工具,支持 TCP 和 UDP
存储绑定
脚本支持多个节点组成集群,组成集群时,编号必须是连续的整数,从 1 开始,到 MAX_NODE_ID 结束。
存储绑定是可选的,可以通过上面的选项禁用。
存储路径前缀
注意:为了提高效率,容器和脚本本身并不直接操作 NFS 存储,而是通过宿主机的 NFS 挂载路径进行绑定。因此请确保宿主机已经正确挂载了 NFS。
可通过以下变量自定义存储路径:
| 变量名 | 默认值 | 说明 |
|---|---|---|
LOCAL_DATA_PREFIX |
/data |
本地数据目录前缀 |
NFS_STORAGE_PREFIX |
/storagenfs/nfs |
NFS 存储路径前缀(不含节点编号) |
节点存储
脚本会自动为所有节点(1 到 MAX_NODE_ID)创建存储绑定:
| 节点类型 | 主机路径 | 容器路径 |
|---|---|---|
| 当前节点 | {LOCAL_DATA_PREFIX}/{INAIS_HOST_ACCOUNT} |
/data/in{NODE_ID:02d} |
| 远程节点 | {NFS_STORAGE_PREFIX}{XX}/{INAIS_HOST_ACCOUNT} |
/data/in{XX} |
静态绑定路径
| 主机路径 | 容器路径 | 说明 |
|---|---|---|
{LOCAL_DATA_PREFIX}/{INAIS_HOST_ACCOUNT}/home |
/home |
用户主目录 |
{LOCAL_DATA_PREFIX}/{INAIS_HOST_ACCOUNT} |
/data/this |
当前节点数据(别名) |
/var/lib/machines/_Data/inais |
/ssd |
本地 SSD 存储 |
NVIDIA GPU 支持
当 DISABLE_NVIDIA_BINDINGS=false(默认)时,脚本会自动挂载:
设备文件(读写)
/dev/nvidia0~/dev/nvidia7/dev/nvidiactl/dev/nvidia-modeset/dev/nvidia-uvm/dev/nvidia-uvm-tools/dev/nvidia-caps/dev/nvidia-nvswitchctl/dev/dri
因为几乎不存在大于8卡的情况,因此这里我写死了最多支持 8 块 GPU(/dev/nvidia0 ~ /dev/nvidia7),如果需要更多,请自行修改脚本。如果少于8卡,则多余的设备文件不会被挂载,不必修改。
系统路径(只读)
/proc/driver/nvidia/sys/devices/pci0000:00/sys/class/drm/sys/module/nvidia*
动态扫描的文件
脚本会自动扫描并挂载以下文件:
/usr/bin/nvidia-*- NVIDIA 可执行文件/usr/bin/nvcc*- CUDA 编译器/usr/bin/nsight*- NVIDIA 分析工具/usr/lib/x86_64-linux-gnu/libnvidia*- NVIDIA 库文件/usr/lib/x86_64-linux-gnu/libcuda*- CUDA 库文件
虽然会挂载宿主的 CUDA 工具包和库文件,但容器内最好还是另外安装一套 CUDA。驱动是前向兼容 CUDA 的,因此容器内的 CUDA 版本可以低于宿主机的驱动版本,按照个人需求决定CUDA版本即可1。
派生配置(自动计算)
以下变量由脚本自动计算,无需手动配置:
| 变量名 | 计算方式 | 示例值 |
|---|---|---|
NODE_ID_STR |
printf '%02d' $NODE_ID |
"15" |
INAIS_HOSTNAME |
"IN$NODE_ID_STR" |
"IN15" |
RINETD_CONFIG |
/var/run/rinetd.conf.$CONTAINER_NAME |
/var/run/rinetd.conf.inais |
RINETD_PID |
/var/run/rinetd.pid.$CONTAINER_NAME |
/var/run/rinetd.pid.inais |
配置文件示例
#!/bin/bash
# === 必需配置 ===
export INAIS_HOST_ACCOUNT="Kenvix"
export NODE_ID=15
export CONTAINER_NAME="inais"
export CONTAINER_ROOT="/var/lib/machines/$CONTAINER_NAME"
export CONTAINER_INDEX=0
export INAIS_NETWORK_MODE="docker" # docker 或 veth
export USE_RINETD_PORT_FORWARDING=false
# === 可选配置 ===
# 节点范围配置
export MAX_NODE_ID=21
# 禁用本地存储挂载
export DISABLE_LOCAL_STORAGE_BINDINGS=false
# 禁用 NVIDIA 挂载(无 GPU 环境设为 true)
#export DISABLE_NVIDIA_BINDINGS=true
# 存储路径前缀配置
export LOCAL_DATA_PREFIX="/data" # 本地数据目录前缀
export NFS_STORAGE_PREFIX="/storagenfs/nfs" # NFS 存储路径前缀
# 端口映射配置
export PORT_FORWARD_HOST_START=30600
export PORT_FORWARD_CONTAINER_START=30000
export PORT_FORWARD_COUNT=20
脚本信号支持
| 信号 | 行为 |
|---|---|
SIGHUP |
立即重新执行监控检查。脚本会作为一个 Daemon 监控容器的状态。 |
SIGTERM SIGINT |
优雅退出(停止容器后退出) |
ℹ️ 注意:该脚本是为多服务器集群设计的,因此假设你有很多服务器需要配置相同的环境。如果你只是单机使用,可以忽略脚本中的集群相关配置,或者直接设置数量为1。
ℹ️ 提示:虽然已经稳定运行一段时间,但此脚本的 Vibe coding 成分较高,请自行进行代码审查,如果存在漏洞请告知我。
赋予可执行权限:
chmod +x run.sh
chmod +x config-run.sh
通过下面的命令,测试启动一级容器:
./run.sh daemon
sleep 2s
tmux attach -t inais
nvidia-smi
如果看到 GPU 信息,说明一级容器的 NVIDIA GPU 透传配置成功。此时可以创建一个 systemd 服务,实现在系统启动时自动启动一级容器:
[Unit]
Description=Run Kenvix Dev Containe at boot
Documentation=Kenvix Dev Container monitoring and management daemon
After=network.target docker.service systemd-machined.service systemd-resolved.service modprobe@tun.service modprobe@loop.service modprobe@dm-mod.service
Wants=network.target systemd-machined.service
Requires=docker.service systemd-machined.service
[Service]
Type=simple
ExecStart=/var/lib/machines/_Scripts/run.sh daemon
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=50
StartLimitInterval=100
[Install]
WantedBy=multi-user.target
保存为 /etc/systemd/system/kenvix-container.service ,然后启用并启动:
sudo systemctl daemon-reload
sudo systemctl enable --now kenvix-container.service
通过下面的命令进入一级容器 Shell:
machinectl shell inais
此时可以在容器内配置 SSH 等常用服务。
二级容器环境配置
二级容器指的是在 systemd-nspawn 容器内运行的 Docker 或 Podman 容器。类似于 DinD(Docker in Docker)或 PinD(Podman in Docker/Podman in Podman),只不过我们这里的一级容器是 systemd-nspawn 容器。
同样的,对于二级容器,切勿使用带有 GPU 驱动的镜像,也不要在容器内安装 GPU 驱动!容器内只应该安装 CUDA 工具包和相关库,GPU 驱动由一级容器通过 CDI 注入。
二级容器 Docker 支持配置
通过 Docker 官方安装最新版本的 Docker-CE 引擎(具体可参见官方文档)。不要使用从 Ubuntu 官方源安装的 docker,否则缺少 CDI 支持将无法使用 GPU:
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
使用 Docker 时,只支持 CDI(Container Device Interface) 方式的GPU驱动注入,因此不能用 Docker 旧版的 --gpus all 参数,需改参数为 --device nvidia.com/gpu=all 即可。
# 例如原命令为
docker run --rm -it --gpus all nvidia/cuda:12.3.1-base-ubuntu22.04 bash
# 则须改为
docker run --rm -it --device nvidia.com/gpu=all nvidia/cuda:12.3.1-base-ubuntu22.04 bash
然后,在二级容器内使用 nvidia-smi 查看 GPU 信息。
二级容器 Podman 支持配置
对于 Ubuntu 22.04,官方源的 Podman 版本非常陈旧,Podman 3 不支持 CDI 方式的GPU驱动注入,文件系统支持也有问题,需要从 OpenSUSE 源安装较新版本的 Podman 4+(或自行编译最新版本):
ubuntu_version='22.04'
key_url="https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_${ubuntu_version}/Release.key"
sources_url="https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_${ubuntu_version}"
echo "deb $sources_url/ /" | tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list
curl -fsSL $key_url | gpg --dearmor | tee /etc/apt/trusted.gpg.d/devel_kubic_libcontainers_unstable.gpg > /dev/null
apt update
apt install podman
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
安装后,需配置使用 cgroupfs,编辑 /etc/containers/containers.conf :
[containers]
cgroup_manager = "cgroupfs"
[engine]
cgroup_manager = "cgroupfs"
编辑 /etc/subuid 和 /etc/subgid ,为需要运行 Podman 的用户分配子 UID/GID 范围,例如:
kenvix:100000:65536
然后,切换到此用户,通过以下命令测试:
# 注意:不能使用 Root 用户运行 Podman GPU 容器!请先切换到非 Root 普通用户
podman run --rm -it --device nvidia.com/gpu=all nvidia/cuda:12.3.1-base-ubuntu22.04 bash
然后,在二级容器内使用 nvidia-smi 查看 GPU 信息。
常见问题
强烈建议也阅读完本章节!在实际使用中几乎必然遇到下面的问题
宿主机 GPU 驱动更新后,一级容器正常但二级容器无法启动
这是由于 CDI 描述文件未更新所致。请运行下面的脚本,自动在 systemd 的开机早期阶段生成 NVIDIA CDI 描述文件。
sudo tee /etc/systemd/system/nvidia-cdi-generate.service >/dev/null <<'EOF'
[Unit]
Description=Generate NVIDIA CDI specification early
Documentation=https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/
DefaultDependencies=no
Before=docker.service podman.service containerd.service crio.service
After=local-fs.target systemd-udev-settle.service
ConditionPathExists=/proc/driver/nvidia/version
[Service]
Type=oneshot
ExecStartPre=/usr/bin/mkdir -p /etc/cdi
ExecStart=/usr/bin/nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nvidia-cdi-generate.service
此脚本在一级容器运行。
Root 用户无法使用 Podman GPU 容器
此问题尚未解决,目前的解决方案是:
- Root 用户一律使用 Docker 运行 GPU 容器。
- 使用非 Root 用户运行 Podman 容器。
为何一级容器选择冷门的 systemd-nspawn 而不是更流行的其他虚拟化?
- Docker/Podman:不支持 systemd,而且 Docker 是应用级别的容器,而非系统级别容器!Docker 设计用来运行单个应用服务,不适合用作完整的操作系统环境。强行使用 Docker 会遇到相当多麻烦事和效率问题。虽然运行 NVIDIA GPU 是简单了,但失去了系统级容器的优势。
- LXC/LXD:LXC/LXD 一样对 GPU 支持不佳,不支持 CDI 方式的驱动注入。而且不支持 systemd。
为何一级容器使用 Ubuntu 24.04 时,二级容器无法使用?
这是一个已知问题,Ubuntu 24.04 作为一级容器操作系统时,Cgroup 存在兼容性问题,导致无法正确创建 Docker 所需的命名空间。目前解决方案仍在探索中,建议暂时使用 Ubuntu 22.04 作为一级容器操作系统。
为何二级容器试图加载不存在的旧显卡驱动?
- 如果之前在一级容器安装过驱动,确保一级容器已经完全卸载了任何显卡驱动。可以临时设置一级容器的启动参数禁用显卡驱动绑定来卸载驱动。(启用绑定时,无法卸载驱动,会导致 apt 直接出错)
- 确保二级容器使用的镜像也不包含任何显卡驱动
- 确保二级容器使用的是 CDI 文件已更新(命令见上方)
有没有你直接配好的镜像?
有的,你可以在评论区催一下我发布