视频点播平台如何利用香港服务器的GPU虚拟化技术加速多路高清视频编码?

“要不上 GPU?”同事问。
“上。但不是一块卡绑一台机器的老思路了,咱得把卡‘切’开,让每台虚机吃到它该吃的那口饭。”
那一夜,我把两台香港机房里的 GPU 服务器做了 vGPU 虚拟化,把 NVENC 拉满,把延迟打下去。第二天早上,峰值过去了,我们没扩容一台物理机。
下面是完整过程与优化细节。
场景与目标
业务目标:
- 同时转码 40–80 路 1080p H.264/H.265(混合码率),外加 4–8 路 4K HEVC/AV1(可选)。
- 单路端到端延迟(读源→转码→落盘/推 CDN)控制在 500–1200 ms。
- 成本优先:尽量提升“每瓦/每元并发能力”。
- 故障域隔离:不同租户或不同内容池之间相互隔离(VM 级 vGPU)。
技术路径:
- Hypervisor:KVM(CentOS 7.9 / 3.10 内核)+ libvirt。
- GPU:NVIDIA A10 / L40S(机房现货两种都有,用法一致,编码器代际不同:A10=NVENC H264/H265;L40S=新增 AV1)。
- 虚拟化:NVIDIA vGPU(KVM 版 vGPU Manager),每块卡切分多个 vGPU profile。
- 编码:FFmpeg(h264\_nvenc / hevc\_nvenc / av1\_nvenc)、GStreamer(nvcodec + nvv4l2),零拷贝优先。
- 编排:VM 内用 Docker Compose 托管编解码容器,NVIDIA Container Toolkit 透传 vGPU。
- 监控:DCGM + Prometheus + Grafana,外加 ffprobe/tsanalyze 做业务探针。
机房硬件与网络(实配)
| 角色 | 型号/参数 | 数量 | 备注 |
|---|---|---|---|
| GPU 服务器 | 2U 双路 AMD EPYC 7543(32C×2)/ 512GB RAM | 2 | NUMA 亲和,PCIe4 x16 插槽×4 |
| GPU 卡 | A10 24GB 或 L40S 48GB | 各 2 | A10 主打 H264/H265;L40S 额外支持 AV1 |
| 系统盘 | 2× 3.84TB NVMe(RAID1) | 2 套 | 宿主机 |
| 业务盘 | 4× 7.68TB NVMe(RAID10) | 2 套 | VM 共用 NFS/virtiofs scratch |
| 网络 | 25GbE ×2(Bond/active-backup) | 2 套 | 上联 ToR 支持 ECN/RED |
| 虚拟化 | KVM/libvirt + NVIDIA vGPU Manager (KVM) | - | vGPU profiles: A10-4Q/A10-8Q;L40S-6Q/12Q… |
| OS | CentOS 7.9 | - | 统一版控、内核加参数 |
| License | NVIDIA License System (NLS) | 1 VM | 许可证服务,HTTPS |
> 注:A10/L40S 都适合 VOD 编码,后者新增 AV1 能力,码率节省可观(代价是解码端兼容性/功耗)。
方案总览
为什么 vGPU 而不是 PCIe 直通?
直通简单粗暴但隔离粒度粗,一块卡给一台 VM,资源容易“吃不满”。vGPU 可以把一块卡切成多个 profile,比如 A10-4Q(约 4GB 显存份额)×6 个,分别供 6 台轻量转码 VM 使用,让会话密度更高,且故障域隔离更清晰。
两套并行方案(都实践过):
1. NVIDIA vGPU on KVM(本文主线)
2. 备用方案:PCIe 直通**(应急、无许可证时使用,文末附快速开通脚本)
BIOS 与内核准备(宿主机)
1. BIOS 开启 IOMMU(AMD-Vi / Intel VT-d)、SR-IOV(网卡)。
2. GRUB 加内核参数(CentOS 7):
# /etc/default/grub
GRUB_CMDLINE_LINUX="crashkernel=auto rhgb quiet intel_iommu=on amd_iommu=on iommu=pt default_hugepagesz=1G hugepagesz=1G hugepages=64 nohz_full=1-63 rcu_nocbs=1-63"
grub2-mkconfig -o /boot/grub2/grub.cfg && reboot
3. 隔离核给 QEMU/VM:`nohz_full/rcu_nocbs` 根据你的 CPU 拓扑调整。
4. HugePages 预留 64GB;对 QEMU 和容器的内存分配、TLB 命中率有帮助。
5. 安装 KVM/Libvirt:
yum -y groupinstall "Virtualization Host"
systemctl enable --now libvirtd
Step 1:安装 NVIDIA vGPU Manager(宿主机)
vGPU 需要匹配的 **宿主机 vGPU Manager** 与 **Guest vGPU 驱动**,大版本必须一致;并需要 **NVIDIA License**(NLS)。下载与许可证申请按你企业账号流程来。
1. 卸载可能存在的开源 nouveau:
echo -e "blacklist nouveau\noptions nouveau modeset=0" > /etc/modprobe.d/blacklist-nouveau.conf
dracut --force
reboot
2. 安装 vGPU Manager(RPM):
rpm -ivh NVIDIA-vGPU-kepler-<version>.rpm # 示例,按卡型与版本
modprobe nvidia
modprobe nvidia-vgpu-vfio
systemctl enable --now nvidia-vgpu-mgr
nvidia-smi
3. 验证:`nvidia-smi` 能看到物理 GPU,`nvidia-vgpu-mgr` 进程在跑。
Step 2:创建 vGPU 设备 & 绑定到 VM
1. 查询可用 profile(不同卡型有不同名字/显存份额):
ls /sys/class/mdev_bus/*/mdev_supported_types/
cat /sys/class/mdev_bus/0000:65:00.0/mdev_supported_types/*/name
常见例如:`A10-4Q`, `A10-8Q`;`L40S-12Q`, `L40S-24Q` 等(Q = 帧缓冲/图形&编解码能力,转码业务选 Q 或 C 的计算/编解码型,视版本而定)。
2. 用 mdev 创建 vGPU:
# 以 A10 第一块卡为例,创建 4 个 A10-4Q
GPU_PCI=0000:65:00.0
TYPE_UUID=$(ls /sys/class/mdev_bus/${GPU_PCI}/mdev_supported_types | grep A10-4Q)
for i in {1..4}; do
UUID=$(uuidgen)
echo $UUID > /sys/class/mdev_bus/${GPU_PCI}/mdev_supported_types/${TYPE_UUID}/create
echo "created vGPU $UUID"
done
3. libvirt XML 绑定 vGPU 到 VM(节选):
<hostdev mode='subsystem' type='mdev' model='vfio-pci' display='on'>
<source>
<address uuid='550e8400-e29b-41d4-a716-446655440000'/>
</source>
</hostdev>
4. vCPU/NUMA 亲和(关键):把绑定 vGPU 的 VM 固定在与该物理 GPU 同 NUMA 节点的 CPU 上:
<cputune>
<vcpupin vcpu='0' cpuset='0-7'/>
<emulatorpin cpuset='0-1'/>
</cputune>
<numatune>
<memory mode='strict' nodeset='0'/>
</numatune>
Step 3:NVIDIA License System(NLS)部署
1. 启一台 NLS VM(2 vCPU / 4GB RAM 即可),绑定固定 IP 与 FQDN。
2. 安装 NLS(容器或原生皆可),开放管理端口(通常 443)。
3. 在 宿主机与Guest VM 的 `client-config-token` 中填入 NLS 地址与证书。
4. 未授权的经典报错:FFmpeg 调用 `*nvenc` 会失败,`nvidia-smi` 里 vGPU 状态显示 Unlicensed。先把 NLS 通了!
Step 4:Guest VM 内安装 vGPU 驱动 & CUDA
1. 禁用 nouveau & 装 vGPU Guest Driver**(与宿主 Manager 匹配版本):
yum groupinstall -y "Development Tools"
# 同宿主类似的黑名单步骤……
rpm -ivh NVIDIA-Linux-x86_64-vGPU-<version>.run
reboot
nvidia-smi
`nvidia-smi` 能看到 vGPU(产品名通常带 GRID/vGPU 标识)。
2. 安装 CUDA 11/12 运行时(按编解码容器需求)。
3. NVIDIA Container Toolkit:
distribution=$(. /etc/os-release;echo $ID$VERSION_ID | sed -e 's/\.//g')
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.repo \
| tee /etc/yum.repos.d/nvidia-container-toolkit.repo
yum -y install nvidia-container-toolkit
systemctl restart docker
Step 5:编解码容器与零拷贝流水线
Docker Compose(VM 内):每个服务进程控制 4–8 路会话,利于滚动与隔离。
version: "3.8"
services:
transcode-a:
image: ghcr.io/yourorg/ffmpeg-nvenc:6.1
deploy: { resources: { reservations: { devices: [ { capabilities: ["gpu"] } ] } } }
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
volumes:
- /mnt/media:/media
- /etc/localtime:/etc/localtime:ro
command: >
bash -lc "/opt/pipelines/run-a.sh"
transcode-b:
image: ghcr.io/yourorg/ffmpeg-nvenc:6.1
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
volumes:
- /mnt/media:/media
command: >
bash -lc "/opt/pipelines/run-b.sh"
FFmpeg 多路 H.264/H.265(零拷贝思路):
- 解码:`-hwaccel cuda -hwaccel_output_format cuda`
- 缩放:优先 `scale_npp`(NPP),避免下到系统内存。
- 编码:`h264_nvenc` / `hevc_nvenc` / `av1_nvenc`(L40S)。
单路示例(1080p→多码率 H.264 ABR):
ffmpeg -hide_banner -y \
-vsync 0 -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 8 \
-i "http://ingest/asset01.mp4" \
-filter_hw_device cuda \
-filter_complex "
[0:v]scale_npp=1280:720:interp_algo=lanczos,split=3[v720a][v720b][v720c];
[0:v]scale_npp=1920:1080:interp_algo=lanczos[v1080]
" \
-map "[v720a]" -c:v h264_nvenc -preset p5 -tune hq -rc vbr -b:v 1800k -maxrate 2800k -bufsize 3600k -g 120 -bf 3 -spatial-aq 1 -temporal-aq 1 \
-map "[v720b]" -c:v h264_nvenc -preset p5 -tune hq -rc vbr -b:v 1200k -maxrate 2000k -bufsize 2400k -g 120 -bf 3 -spatial-aq 1 -temporal-aq 1 \
-map "[v720c]" -c:v h264_nvenc -preset p5 -tune hq -rc vbr -b:v 800k -maxrate 1200k -bufsize 1600k -g 120 -bf 3 -spatial-aq 1 -temporal-aq 1 \
-map "[v1080]" -c:v h264_nvenc -preset p5 -tune hq -rc vbr -b:v 3500k -maxrate 5500k -bufsize 7000k -g 120 -bf 3 -spatial-aq 1 -temporal-aq 1 \
-map 0:a -c:a aac -b:a 128k -ac 2 -ar 48000 -movflags +faststart \
-f segment -segment_time 6 -reset_timestamps 1 "/media/out/asset01_%v_%03d.mp4"
4K→HEVC(HDR→SDR 可加 `zscale_cuda + tonemap`,此处略):
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 16 \
-i "s3://bucket/asset4k.mkv" \
-filter_complex "[0:v]scale_npp=3840:2160:interp_algo=lanczos[v4k]" -map "[v4k]" \
-c:v hevc_nvenc -preset p6 -tune hq -profile main10 -pix_fmt p010le \
-rc vbr -b:v 12000k -maxrate 20000k -bufsize 24000k -g 120 -bf 4 \
-spatial-aq 1 -temporal-aq 1 -aq-strength 10 \
-map 0:a -c:a aac -b:a 192k \
-f mp4 "/media/out/asset4k_hevc.mp4"
AV1(L40S 可选):
ffmpeg -hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 16 \
-i input.mp4 -c:v av1_nvenc -preset p5 -rc vbr -b:v 2800k -g 240 -bf 3 \
-spatial-aq 1 -temporal-aq 1 -map 0:a -c:a aac -b:a 128k out_av1.mp4
Step 6:并发与会话容量规划
经验表(基于我们机房样本,给出“保守并发”建议,非极限):
| vGPU Profile | 显存配额 | 建议并发:H.264 1080p@3.5Mbps | 建议并发:HEVC 4K@12Mbps | 备注 |
|---|---|---|---|---|
| A10-4Q | ~4GB | 8–12 路 | 1 路 | 负载稳定在 70–80% |
| A10-8Q | ~8GB | 16–20 路 | 2 路 | 余量更充足 |
| L40S-12Q | ~12GB | 20–28 路 | 3–4 路 | 可加少量 AV1 |
| L40S-24Q | ~24GB | 40–50 路 | 6–8 路 | 高密度 VM |
> 注:NVENC 是固定功能单元,**解码/缩放**对 CUDA 核心与显存也有占用;多码率分叉时显存增长明显。表中是我线上稳态的“保守值”,留有 15–30% 余量应对码率抖动与 GOP 波峰。
观测指标(Prometheus/DCGM):
- `DCGM_FI_DEV_FB_USED`(显存占用)、`DCGM_FI_DEV_GPU_UTIL`、`DCGM_FI_DEV_NVENC_UTIL`/`NVDEC_UTIL`
- 宿主机:`node_cpu_seconds_total`(绑定核)、`numa_miss`、`irqbalance` 绑核后关闭
Step 7:关键优化开关(把延迟和抖动压下去)
1. 零拷贝:全链路 `hwaccel cuda` + `scale_npp`,避免 `hwdownload`。
2. 码控:VBR + 合理 `bufsize`(通常 2×\~3×目标码率),`-g`(GOP)根据业务延迟目标设定,直播型可以 60/90,VOD 常 120/240。
3. AQ:`-spatial-aq 1 -temporal-aq 1`,尤其面对运动场景/动漫细节。
4. Lookahead:`-rc-lookahead` 过大引入延迟,VOD 批量转码可以适度开 20–32;准实时转码压到 10–16。
5. 多路复用**:不要一个进程塞 30 路,**拆分成 4–8 路一个容器**,便于滚动、限流与崩溃隔离。
6. NUMA:VM vCPU + vGPU 绑在同一 NUMA,容器再 `--cpuset-cpus` 指到该 NUMA 的核。
7. 磁盘/IO:本地 NVMe 落盘中间件,冷数据再异步上 S3/NAS;NFS/virtiofs 仅作元数据/索引。
8. 网卡:virtio-net multiqueue=4/8,宿主机 RPS/RFS 调优,拥塞用 ECN。
9. 稳定性:`-extra_hw_frames` 适当增加;FFmpeg 加 `-vsync 0` 防止时间戳抖动;`-max_muxing_queue_size 1024`。
Step 8:监控与告警(落地)
DCGM Exporter**(VM 内):
docker run -d --gpus all --name=dcgm-exporter -p 9400:9400 nvidia/dcgm-exporter:3.3.5-3.4.0
fprobe 探针:抽样拉播放链路,统计首帧耗时、GOP 完整性、PTS 单调性;异常进 Slack/飞书。
业务看板:卡的 NVENC 利用率>85% 连续 3 分钟 → 触发缩容合并(把低峰 VM 的会话挪过来),反之扩容。
常见坑 & 现场解法
1. Unlicensed vGPU → 编码器不可用**
现象:`[h264_nvenc @ ...] No NVENC capable devices found`;`nvidia-smi` 显示 Unlicensed。
解法:确认 Guest Driver 与 Manager 大版本一致;NLS 地址/证书 OK;Guest 里 `nvidia-smi -q | grep License` 正常后再跑 FFmpeg。
2. 版本不匹配(Manager/Guest/CUDA/FFmpeg)
解法:统一一个“版本矩阵”,比如 vGPU 16.x + Guest 16.x + CUDA 12.2 + FFmpeg 6.1。容器里把 `libnvidia-encode.so` 与驱动匹配。
3. 显存碎片化 / “Out of memory”
现象:多码率分支多时更明显。
解法:缩小 `rc-lookahead`,合并多路分支的共享前处理(先做一次解码/缩放再 split),提高单进程路数上限前先测显存水位。
4. NUMA 跨节点抖动
解法:把 vCPU 亲和到 GPU 对应 NUMA;关 irqbalance;网卡中断固定在该 NUMA 核上。
5. FFmpeg 编译没开 nvenc
现象:`Unknown encoder 'h264_nvenc'`。
解法:确保编译时 `--enable-nonfree --enable-nvenc`,容器用预编译好并和驱动匹配的版本最省事。
6. ABR 码控“飘”
解法:控制 `-maxrate/-bufsize`,对运动场景适当开 `-multipass 1pass` 或 `2pass`(离线批量可用),并限制 `-qpmin/qpmax`。
效果复盘(上线前后对比)
| 指标 | CPU 转码集群(旧) | vGPU 集群(新) | 变化 |
|---|---|---|---|
| 单机 1080p 并发 | ~6–8 路 | 20–50 路(按 profile) | ↑ 3–6× |
| 单路端到端延迟 | 1200–2000 ms | 500–1200 ms | ↓ 40–60% |
| 峰值功耗(机柜 2 台) | 1.5–1.8 kW | 1.1–1.3 kW | ↓ 20–30% |
| 异常率(24h) | 0.8% | 0.2% | ↓ 75% |
| 成本/并发 | 基线 | 下降 ~35–50% | - |
上线那天早高峰我们稳定撑住了,曲线漂亮地压在 70% 上下,我把上一晚剩下的咖啡一口闷掉,去楼下便利店补了两只饭团。
附:应急的 PCIe 直通(无 vGPU 许可证时)
当你手里暂时拿不到 vGPU 许可证,或者某些业务需要整卡性能,**PCIe 直通**是最快的备选:
1. 绑定 VFIO:
lspci -nn | grep -i nvidia
echo "vfio-pci" > /sys/bus/pci/devices/0000:65:00.0/driver_override
echo 0000:65:00.0 > /sys/bus/pci/drivers_probe
2. ibvirt XML:
<hostdev mode='subsystem' type='pci' managed='yes'>
<source>
<address domain='0x0000' bus='0x65' slot='0x00' function='0x0'/>
</source>
</hostdev>
3. Guest 内装普通 Data Center 驱动,后续 FFmpeg/GStreamer 步骤相同。
> 代价:隔离粒度粗、调度不灵活;一张卡只供一个 VM 使用。
我常用的“上线前自查清单”
- 宿主机 `nvidia-vgpu-mgr` 正常;`nvidia-smi` 显示驱动、温度、功耗、风扇。
- NLS 许可证生效;Guest `nvidia-smi -q` 能看到 Licensed。
- vGPU profile 与并发规划一致;NUMA/CPU 亲和配置完成。
- FFmpeg 输出 `--version`:带 `--enable-nvenc`;`ffmpeg -hwaccels` 有 `cuda`。
- 小样本回归:运动/暗场/动画片/低码率边界。
- 监控告警生效:NVENC\_UTIL、FB\_USED、端到端延迟、水位线。
- 回滚预案:容器镜像/配置回滚、VM 快照、PCIe 直通兜底。
那周五的晚上,机房值守的我把空调温度调高了一度。二十多路 1080p、几路 4K 正在安静地跑着,风从冷通道呼呼地穿过机柜。我关了服务器的屏,靠在墙上发了个消息:“不用扩容,先观测一天。”
后来我们把这套方案写进了标准化交付手册:香港节点优先 L40S,国内节点 A10 为主,按租户切 vGPU profile,FFmpeg/GStreamer 双栈,零拷贝为先。每一条项背后,都是我和同事们在凌晨用导轨托住服务器、在串口屏上敲回车时积累下来的小经验。
如果你也正被多路高清视频编码的成本和并发折磨,不妨按这套流程把 GPU 虚拟化先跑起来。只要第一台 VM 平稳落地,后面就是机械重复的快乐。
关键脚本与片段汇总(便于复制)
创建指定数量的 vGPU(按 profile 名称)
#!/usr/bin/env bash
GPU="0000:65:00.0"
PROFILE=$(ls /sys/class/mdev_bus/${GPU}/mdev_supported_types | grep -m1 "A10-4Q")
COUNT=4
for i in $(seq 1 $COUNT); do
UUID=$(uuidgen)
echo $UUID > /sys/class/mdev_bus/${GPU}/mdev_supported_types/${PROFILE}/create
echo "Created: $UUID"
done
libvirt VM 附加 vGPU 的 XML 片段
<devices>
<hostdev mode='subsystem' type='mdev' model='vfio-pci'>
<source>
<address uuid='REPLACE-WITH-YOUR-UUID'/>
</source>
</hostdev>
<controller type='virtio-serial' index='0'/>
<memballoon model='none'/>
</devices>
GStreamer H.264 零拷贝(示例)
gst-launch-1.0 filesrc location=in.mp4 ! qtdemux name=d \
d.video_0 ! h264parse ! nvh264dec ! nvvidconv ! "video/x-raw(memory:NVMM),width=1920,height=1080" \
! nvvideoconvert ! nvh264enc preset=hp rc=vbr bitrate=3500 iframeinterval=120 \
! h264parse ! qtmux ! filesink location=out_1080p.mp4
DCGM Exporter(Prometheus 指标)
docker run -d --gpus all --restart=always -p 9400:9400 nvidia/dcgm-exporter:3.3.5-3.4.0