上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-23 08:23 阅读量:790
凌晨两点半,我们部署在香港机房的的 VOD 平台被一部突发爆火的短剧“压”出一波超预期的并发,CPU 转码集群的负载曲线像心电图一样冲上去。隔着玻璃我能看见几台 2U 机器上那盏代表“热”的红灯在喘气。多路 1080p、几路 4K HDR,CPUs 已经顶不住了。

“要不上 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
目录结构
全文