如何在容器化环境(Docker/K8s)下做GPU资源编排与负载感知调度的实战方案,适用于更大规模的分布式转码集群部署

在最初部署视频转码系统时,我们依赖的是香港 MEGA-i 机房内的几台 Dell R750xa 裸金属服务器,采用 GPU + FFmpeg 本地运行脚本调度,配合 Redis 队列消费任务。这种方式在小规模(3 台内)部署时还算可控,但当我们的 GPU 节点扩展到 10+ 台、任务量突破每日 30,000 条时,以下问题接踵而至:
- 任务调度混乱:节点资源状态全靠 nvidia-smi + 脚本轮询,调度不智能,抢占常见。
- 跨节点资源浪费:某些 GPU 长期空闲,其他节点却排队堆积。
- 负载感知缺失:系统无感知 GPU 温度、编码 session、显存压力等负载指标。
- 上线/下线新节点需要手工配置:维护成本高,出错概率大。
那时候我下定决心:必须把整个 GPU 编码集群容器化,并接入调度系统自动感知资源状态,实现弹性与高效的任务调度。
于是,我们基于 Docker + Kubernetes + NVIDIA GPU Operator + 自研调度器 构建了一套可扩展、高可用、智能调度的视频转码平台。以下是整个部署与演进过程的完整实操记录。
一、香港机房环境与基础设施构建
1. 硬件部署情况
机房位置:MEGA-i 香港 8/F 区域
- 服务器数量:12 台 GPU 节点(R740xa + A5000),3 台控制节点
网络配置:
- 内部网络使用 VXLAN overlay,10GbE 交换网络
- 公网采用 BGP 多线冗余出网
- K8s pod 网络使用 Calico(BGP 模式)联通多个机房 VLAN
2. Kubernetes 集群环境
| 组件 | 版本 |
|---|---|
| Kubernetes | 1.26 |
| Containerd | 1.6.x |
| NVIDIA GPU Operator | 23.3.x |
| Helm | 3.12 |
| Calico | 3.25 |
3. GPU 节点配置
每台 GPU 节点规格:
- CPU:Intel Xeon Gold 6338 ×2
- GPU:NVIDIA A5000 ×2(或 A30 ×4)
- 内存:256GB DDR4
- 存储:1TB NVMe 本地缓存 + Ceph RBD 回源
二、Docker + NVIDIA Container Toolkit 的 GPU 基础打通
虽然最终部署用的是 K8s,但一开始我们在单节点上使用 Docker 原型验证 GPU 编码容器。
1. 安装 NVIDIA Container Toolkit
sudo apt install nvidia-container-toolkit
sudo systemctl restart containerd
验证容器能访问 GPU:
docker run --gpus all nvidia/cuda:12.2-base nvidia-smi
2. 构建 GPU 编码用 FFmpeg 镜像
我们基于 CUDA 11.8 + FFmpeg 编译了支持 h264_nvenc 的镜像:
FROM nvidia/cuda:11.8-runtime-ubuntu22.04
RUN apt update && apt install -y \
ffmpeg \
libnpp-dev libx264-dev libx265-dev libfdk-aac-dev
COPY entrypoint.sh /usr/local/bin/
CMD ["ffmpeg"]
最终镜像打包后传入私有 Harbor 仓库(可在多节点复用)。
三、在 Kubernetes 上实现 GPU 编排与调度
1. 安装 NVIDIA GPU Operator
GPU Operator 是我们集群最关键的基础模块,它负责自动在 GPU 节点上安装驱动、运行 device-plugin 并暴露资源给调度器。
使用 Helm 部署:
helm repo add nvidia https://nvidia.github.io/gpu-operator
helm upgrade --install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator \
--create-namespace
部署后,所有 GPU 节点都会自动暴露资源:
kubectl describe node <node-name> | grep nvidia.com/gpu
2. 编排 GPU 容器任务
部署一个使用 GPU + FFmpeg 的 Job:
apiVersion: batch/v1
kind: Job
metadata:
name: transcode-job
spec:
template:
spec:
containers:
- name: transcoder
image: my-registry/transcoder:latest
resources:
limits:
nvidia.com/gpu: 1
command: ["ffmpeg", "-hwaccel", "cuda", "-i", "input.mp4", "-c:v", "h264_nvenc", "-b:v", "5M", "output.mp4"]
restartPolicy: Never
backoffLimit: 2
每个 Job 会调度到有空闲 GPU 的节点上运行,实现并发视频转码。
3. 使用自研调度器进行负载感知调度
K8s 默认的调度器并不能精确理解 GPU 的负载(比如 session 数、功耗、温度)。为了解决这个问题,我们:
- 使用 DCGM Exporter 暴露 GPU 指标到 Prometheus
- 编写了一个 K8s Scheduling Extender(调度扩展器),根据 GPU 当前 session 数、温度、显存使用情况,对所有候选节点进行打分
- 在节点压力大的时候,打分降低,优先调度到空闲 GPU 节点
调度打分逻辑示意:
score = 100
score -= (session_count * 10)
score -= (gpu_temp - 50) * 0.5
score -= (memory_used_pct * 0.5)
调度器使用 HTTP 扩展 API 接入 kube-scheduler,实现动态排序。
四、实战中的问题与解决方案
问题 1:GPU 资源浪费严重
表现:K8s 只根据 nvidia.com/gpu: 1 做粗粒度调度,导致每块 GPU 只能跑一个任务,利用率低。
解决方案:
使用 NVIDIA MPS(Multi-Process Service) 启用多进程共享 GPU
修改容器并发数逻辑,控制每个容器可在一块 GPU 上跑多个 FFmpeg 实例(我们限制为 3~4 个)
结合调度器逻辑动态感知 session 状态
问题 2:转码任务卡死,Pod 不释放 GPU
表现:有时 FFmpeg 异常退出,Pod 处于 CrashLoopBackOff 状态,但 GPU 仍被 nvidia-smi 显示占用
解决方案:
设置 terminationGracePeriodSeconds,在容器退出时主动 kill GPU 编码进程
使用 nvidia-smi pmon 配合 preStop hook 强制清理进程
问题 3:跨节点文件同步慢,转码卡顿
表现:CephFS 拉原始视频到 GPU 节点时出现延迟,转码容器卡在 I/O 阶段
解决方案:
改为 Sidecar 模式:每个 GPU Pod 中添加一个缓存 sidecar,用于从中央对象存储拉文件到本地 SSD
使用 InitContainer 先完成数据拉取,然后再启动转码容器
输出文件在本地缓存盘落地后,通过异步同步推送到 OSS
五、最终效果与可视化监控
通过这套 K8s 容器化调度系统,我们实现了:
- 每张 GPU 平均可并发处理 3~4 个 1080p 任务
- 整体 GPU 利用率由原先的 45% 提升至 80% 以上
- 支持横向扩展,新增 GPU 节点只需打个 Label 自动加入调度池
- 通过 Grafana 实时查看每个 GPU 节点的 session 数、温度、功耗等指标
总结:容器化不是目标,而是持续优化的开始
这次将 GPU 转码系统容器化、调度系统智能化的实践,让我们从“小作坊式”的裸跑任务,过渡到了一个能够 支撑弹性扩展、高负载感知、GPU 高效利用的生产级平台。
我特别有感触的一点是:K8s 本身并不能解决 GPU 的智能调度问题,关键还是你是否设计了足够精细的调度逻辑、负载采集与业务协调机制。