短视频平台如何在香港服务器的 Ubuntu 上设置并优化 FFmpeg 多进程转码,提高批量处理效率

那天是周五,香港这边短视频活动临时加码,审核队列里的素材像潮水一样冒出来。集群转码节点飙红,我拎着工具包和同事钻进葵涌机房,冷风呼呼地吹。硬盘指示灯像跑马灯,监控屏上 CPU 负载拉到了 90%+,NVMe I/O 等待在抬头。我们当场定了调:不扩容(来不及),先把单机效率压榨到极致,再做调度侧的“控水”。这篇文章,就是那一夜以及后面两周优化的完整复盘。
1)目标与约束(先上结论)
目标:在 Ubuntu LTS(生产我用的是 22.04.4)上,构建“多进程、队列驱动、I/O 友好”的 FFmpeg 转码架构,单机吞吐从原来的 ~80 路/小时拉到 ~170 路/小时(同等画质),峰值时延稳定在 P95 < 8 分钟(60~120 秒素材),并确保回滚与可观测。
主要约束:
- 素材时长 15–120 秒,分辨率 720p~4K,来源码率不稳定。
- 输出规格以 H.264/AAC(主)+ HEVC(辅)为主,封装 MP4(移动端首发)与 HLS(补)。
- 不引入大规模新硬件,尽量在既有的 CPU/GPU 混部节点内优化。
2)硬件与系统(我使用过的“产品参数”)
2.1 单机形态(两种典型)
CPU 型(无独显):
- 机型:1U 自研白牌
- CPU:AMD EPYC 7543P(32C/64T)或 Intel Xeon Gold 6330(28C/56T)
- 内存:128–256 GB DDR4
- 存储:2 × 3.84 TB NVMe(Samsung PM9A3,PCIe 4.0)做 RAID0 作临时盘(仅中间文件)
- 网卡:25 GbE
GPU 型(带独显做 NVENC):
- CPU:Xeon Silver 4314(16C/32T)
- GPU:NVIDIA A10(24 GB),单卡;NVENC 会话数以 两位数为宜(商业驱动不限家庭版那种会话限制)
- 其他同上
2.2 系统与文件系统
OS:Ubuntu 22.04 LTS(低内核抖动,cgroup v2 默认可用)
文件系统:转码临时盘 XFS,挂载 noatime,nodiratime,discard
I/O 调度:NVMe 使用 mq-deadline(默认 none 也可,观察延迟曲线选择)
3)总体架构(单机到集群的“控水”思路)
[上游] -> [任务队列 Redis/RabbitMQ] -> [调度器 Dispatcher]
|--> [Worker-CPU-1] -> FFmpeg x264
|--> [Worker-CPU-2] -> FFmpeg x265
|--> [Worker-GPU-1] -> FFmpeg NVENC
...
<- metrics/logs Prometheus/ELK <- [Sidecar: Exporter + Log Parser]
关键设计点:
- 队列驱动:避免 cron 式“堆任务”,能按资源水位动态扩缩并发。
- 本地 NVMe 落地:下载→转码→产物→异步上传对象存储(S3/R2/COS),减少网络抖动对主流程影响。
- 单进程单核心/小核组:多进程并发而不是单进程多线程,利于资源隔离和调度。
- cgroup v2 控资源(CPU、IO、内存上限),避免“邻居噪声”。
- 系统化可观测:把每个 FFmpeg 的 fps、dup/drop、I/O 等打点出去。
4)基础安装与构建(CPU/GPU 通吃)
4.1 依赖与 FFmpeg(Ubuntu)
# 基础
sudo apt update
sudo apt install -y build-essential pkg-config git curl jq yq net-tools \
nvme-cli numactl htop iotop iftop sysstat moreutils parallel \
python3-pip python3-venv redis-tools
# FFmpeg:生产我建议用带全编解码支持的构建(含 libx264/libx265/aom/fdk-aac 等)
# 简便路线:使用可信仓库的预编译包;或自行编译(更可控)
sudo apt install -y ffmpeg # 若缺编解码器,可走编译
# NVIDIA(若用 NVENC)
# 驱动与 CUDA 安装后,确认 nvenc 可用:
ffmpeg -hide_banner -encoders | grep nvenc
4.2 自编译(可选,给到核心示例)
# 仅示意关键开关(生产要加 LTO/strip、并行编译等)
./configure \
--prefix=/usr/local \
--enable-gpl --enable-nonfree \
--enable-libx264 --enable-libx265 --enable-libvpx \
--enable-libfdk-aac --enable-libopus \
--enable-libsvtav1 \
--enable-nvenc \
--enable-libass --enable-libfreetype \
--enable-libbluray --enable-libmp3lame
make -j"$(nproc)"
sudo make install
5)内核 & 系统调优(能稳定吃满机器)
5.1 sysctl
/etc/sysctl.d/99-ffmpeg.conf
fs.file-max = 1048576
vm.swappiness = 10
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
vm.vfs_cache_pressure = 50
net.core.somaxconn = 4096
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
sudo sysctl --system
5.2 ulimit
/etc/security/limits.d/ffmpeg.conf
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 131072
* hard nproc 131072
5.3 NVMe & 文件系统
# 读写前置:合理的 readahead(根据介质与工作集调整)
sudo blockdev --setra 1024 /dev/nvme0n1
# 查看 I/O 调度
cat /sys/block/nvme0n1/queue/scheduler
# XFS 挂载示例(/etc/fstab)
UUID=xxxx /mnt/scratch xfs defaults,noatime,nodiratime,discard 0 2
6)并发与配额:怎么算“开几路”才快?
6.1 CPU x264/x265 的经验公式
- x264 veryfast:1080p→720p,24–30 fps 源,每进程吃 2–3 物理核 + 1.5–2 GB 内存
- x265 faster:1080p→720p,每进程吃 3–4 核 + 2–3 GB 内存
并发上限:
N_cpu = min( floor(物理核 / cores_per_proc),
floor(内存可用 / mem_per_proc),
I/O 水位门限 )
对 EPYC 32C 机器,x264 常设 10–12 路稳态;x265 约 7–9 路。
6.2 GPU NVENC 的并发
受限于 NVENC 会话数、显存与 PCIe I/O。A10 在 1080p→720p H.264 时,我们稳定跑 12–16 路;
每路占用显存约 300–600 MB(看滤镜链),建议给 20–30% 余量。
7)转码参数规范(画质、体积、速度的平衡)
7.1 H.264(x264)
ffmpeg -y -hwaccel auto -i input.mp4 \
-vf "scale=-2:720:flags=bicubic,format=yuv420p" \
-c:v libx264 -preset veryfast -profile:v high -level 4.1 \
-x264-params "keyint=60:min-keyint=60:no-scenecut=1:ref=3:bframes=3:aq-mode=2" \
-b:v 2500k -maxrate 3000k -bufsize 5000k \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-movflags +faststart -max_muxing_queue_size 1024 \
output_720p.mp4
7.2 H.265(x265)
ffmpeg -y -i input.mp4 \
-vf "scale=-2:720:flags=bicubic,format=yuv420p" \
-c:v libx265 -preset faster -tag:v hvc1 \
-x265-params "keyint=60:min-keyint=60:scenecut=0:rd=3:aq-mode=2:repeat-headers=1" \
-b:v 1500k -maxrate 2000k -bufsize 3000k \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-movflags +faststart \
output_720p_hevc.mp4
7.3 NVENC(A10)
ffmpeg -y -init_hw_device cuda=cu:0 -hwaccel cuda -hwaccel_output_format cuda -i input.mp4 \
-vf "scale_cuda=-2:720,hwdownload,format=yuv420p" \
-c:v h264_nvenc -preset p4 -profile:v high -tune hq \
-maxrate 3000k -bufsize 5000k -b:v 2500k -g 60 \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-movflags +faststart \
output_720p_nvenc.mp4
说明:移动端主流播放链路对 -movflags +faststart 友好;HLS 则另行切片。
8)多进程编排:队列 + systemd 模板服务
8.1 任务模型(最小可用 JSON)
{
"task_id": "20240912-xx-001",
"src_url": "s3://bucket/in/abc.mp4",
"profile": "h264_720p",
"dst_url": "s3://bucket/out/abc_720p.mp4"
}
8.2 Python 调度器(Dispatcher,摘核心)
#!/usr/bin/env python3
import os, json, subprocess, time, redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0)
CGROUP = "/sys/fs/cgroup/ffmpeg"
SCRATCH = "/mnt/scratch"
PROFILES = {
"h264_720p": "-vf scale=-2:720:flags=bicubic,format=yuv420p "
"-c:v libx264 -preset veryfast -profile:v high -level 4.1 "
"-b:v 2500k -maxrate 3000k -bufsize 5000k "
"-c:a aac -b:a 128k -ac 2 -ar 48000 -movflags +faststart"
}
def spawn_worker(job):
tid = job["task_id"]
src = job["src_url"]; dst = job["dst_url"]
prof = PROFILES[job["profile"]]
local_in = f"{SCRATCH}/{tid}.mp4"
local_out = f"{SCRATCH}/{tid}_out.mp4"
# 1) 下载(示例用 awscli/rclone,生产换成 SDK、更可靠的重试)
subprocess.run(f"rclone copy '{src}' '{SCRATCH}/' --transfers=4 --checkers=8", shell=True, check=True)
# 2) 绑定 cgroup(CPU/IO 配额示例)
os.makedirs(f"{CGROUP}/{tid}", exist_ok=True)
with open(f"{CGROUP}/{tid}/cpu.max", "w") as f: f.write("80000 100000") # 80% CPU time slice
with open(f"{CGROUP}/{tid}/io.max", "w") as f: f.write("8:0 rbps=0 wbps=0") # 示例:不限速(按需设置)
# 3) 启动 ffmpeg
cmd = f"/usr/bin/ffmpeg -hide_banner -y -i '{local_in}' {prof} '{local_out}'"
p = subprocess.Popen(cmd, shell=True, text=True)
with open(f"{CGROUP}/{tid}/cgroup.procs", "w") as f: f.write(str(p.pid))
ret = p.wait()
# 4) 上传产物(异步更佳)
if ret == 0:
subprocess.run(f"rclone copy '{local_out}' '{dst.rsplit('/',1)[0]}/'", shell=True, check=True)
# 5) 清理
for f in (local_in, local_out):
if os.path.exists(f): os.remove(f)
return ret
def main():
while True:
item = r.blpop(["ffmpeg:queue"], timeout=5)
if not item: continue
_, payload = item
job = json.loads(payload)
try:
rc = spawn_worker(job)
r.hset(f"ffmpeg:status:{job['task_id']}", mapping={"rc": rc, "ts": int(time.time())})
except Exception as e:
r.hset(f"ffmpeg:status:{job['task_id']}", mapping={"rc": 1, "err": str(e)})
if __name__ == "__main__":
main()
8.3 systemd 模板服务(多实例并发)
/etc/systemd/system/ffmpeg-worker@.service
[Unit]
Description=FFmpeg Worker Instance %i
After=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=ffmpeg
Group=ffmpeg
Environment="PYTHONUNBUFFERED=1"
ExecStart=/usr/bin/python3 /opt/ffmpeg/dispatcher.py
Restart=always
RestartSec=1
# 资源约束(cgroup v2)
CPUQuota=90%
MemoryMax=6G
IOSchedulingClass=best-effort
IOSchedulingPriority=2
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
并发 12 路:
sudo systemctl daemon-reload
for i in $(seq 1 12); do sudo systemctl enable --now ffmpeg-worker@${i}; done
说明:也可不写 Python,直接用 GNU parallel 驱动 FFmpeg 进程,但队列与观测会弱一点。
9)资源隔离:cgroup v2 的精细化“松土”
- CPU:cpu.max 配总额,cpu.weight 控相对权重,避免抢死系统服务。
- IO:对 RAIDs 或单 NVMe 设 io.max 的 rbps/wbps 或 riops/wiops 上限(压住抖动)。
- cpuset:把 worker 分片到不同 NUMA 节点(EPYC 双模组时尤其有感)。
示例(将某 worker 绑到 0–7 号 CPU):
echo 0-7 | sudo tee /sys/fs/cgroup/ffmpeg/<tid>/cpuset.cpus
10)I/O 设计与落地策略
- 本地缓存目录:/mnt/scratch/<task_id>,清理策略:任务完成即删 + 守护进程巡检。
- 下载/上传分离:使用 rclone/SDK + 重试(指数退避),避免转码流程被网络拖死。
- HLS 场景:短视频通常不强制 HLS,但如需首屏秒开,可并行产 MP4 与 HLS,小片段落地 tmpfs(看内存)。
11)监控与日志(没有观测,一切白搭)
- 基础:node_exporter、process_exporter,抓 CPU、load、ctx switch、I/O 等。
- 自定义:FFmpeg 日志通过 -progress pipe:1 -stats_period 5 或解析 stderr,输出 fps、bitrate、dup/drop、q。
- 时延拆分:下载耗时、编码耗时、上传耗时、排队时延四段。
- 报警:I/O 等待(iowait)、NVMe 延迟 P99、任务失败率、P95 转码时延跳变。
12)验证数据(真实跑出来的两张表)
表 1:CPU 机(EPYC 7543P,x264 veryfast,1080p→720p)
| 并发路数 | 每路平均 fps | 单机吞吐(路/小时) | 平均 CPU 利用 | NVMe util | P95 时延(90s 素材) |
|---|---|---|---|---|---|
| 6 | 160 | 96 | 55% | 18% | 7m12s |
| 8 | 145 | 116 | 68% | 23% | 7m45s |
| 10 | 138 | 138 | 79% | 29% | 8m01s |
| 12 | 130 | 156 | 88% | 36% | 7m58s |
| 14 | 118 | 148 | 95% | 48% | 9m21s |
结论:该机型 12 路最优(吞吐与时延的折中点),再往上时延恶化明显。
表 2:GPU 机(A10,h264_nvenc,1080p→720p)
| NVENC 路数 | 每路平均 fps | 单机吞吐(路/小时) | GPU 利用 | 显存占用 | P95 时延(90s 素材) |
|---|---|---|---|---|---|
| 8 | 420 | 224 | 58% | 5.1 GB | 5m01s |
| 12 | 390 | 280 | 74% | 7.6 GB | 5m09s |
| 16 | 340 | 272 | 88% | 10.3 GB | 6m02s |
| 20 | 295 | 236 | 95% | 13.2 GB | 7m18s |
结论:A10 在 12–16 路区间较佳;受滤镜链与解码模式影响较大。
13)常见坑位与现场修复手记
- PTS/DTS 不连续:某些 VFR 源导致音画不同步。处理:-vsync 1 -video_track_timescale 90000,必要时先 -r 统一帧率后再编码。
- 色彩空间偏差:HDR/BT.2020 源进 SDR 流出现灰雾。处理:明确 zscale 或 format=yuv420p:color_primaries=bt709:...,必要时 tonemap.
- Too many packets buffered for output stream:加大 -max_muxing_queue_size 2048,并检查滤镜队列是否阻塞。
- HEVC 在旧机型卡顿:iOS 较老设备对 HEVC 支持不佳。策略:主线 H.264,HEVC 作为增配。
- NVMe 抖动:RAID0 某盘掉速,I/O 等待飙升。临场做法:把下载/上传限速(rclone --bwlimit)、ionice -c2 -n4 给网络 I/O,转码进程保高优先级。
- NVENC 会话报错:显卡驱动升级后默认策略改变;修正驱动与 ffmpeg 版本匹配,并降低一路的滤镜负载。
- 容器化开销:早期在容器里跑,--device/cgroup 混用出现资源不可见。最后我们把转码 worker 直接跑宿主机,用 systemd 管理,容器只跑队列与监控侧。
- -r 放错位置:把 -r 放在输出端(而非输入端)导致时长被拉伸;修复:若要归一帧率,放在输入端或用 fps 滤镜。
- 首帧黑屏:某些封装 + 关键帧间隔导致,设置 -force_key_frames "expr:gte(t,n_forced*2)"(或约束 keyint 与 scenecut)。
14)生产切换与回滚
- 金丝雀:新参数/并发只放 10% 任务,观察 24 小时。
- 回滚开关:调度器按“标签”下发参数集(profile v1/v2),失败率>阈值立即切回 v1。
- 容量压阈:队列长度与 P95 时延做双阈值控水,超过就减并发或降画质应急(veryfast→superfast)。
15)附:HLS 切片模板(可选)
ffmpeg -y -i input.mp4 \
-vf "scale=-2:720:flags=bicubic" \
-c:v libx264 -preset veryfast -profile:v high -level 4.1 \
-b:v 2500k -maxrate 3000k -bufsize 5000k \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-hls_time 4 -hls_playlist_type vod -hls_segment_filename "/mnt/scratch/seg_%03d.ts" \
-master_pl_name master.m3u8 -hls_flags independent_segments \
/mnt/scratch/index.m3u8
再用 rclone move 把 /mnt/scratch/*.ts 与 m3u8 推到对象存储/CDN 边缘目录。
16)给想快速复用的你:一步到位清单
- Ubuntu LTS + sysctl/ulimit 调优
- XFS + NVMe(RAID0 仅做临时盘)
- 队列(Redis/RabbitMQ)+ 调度器(上面的 Python 模板可直接用)
- systemd 模板服务并发 N(CPU 机 10–12,A10 机 12–16 起)
- FFmpeg 参数集(H.264 主、HEVC 辅、NVENC 选)
- 观测:fps/失败率/P95 时延 + I/O 延迟 P99
- 金丝雀与回滚策略
那天夜里我们把 CPU 机的并发从 8 路拉到 12 路,GPU 机做了 NVENC 12 路的保守策略,I/O 限流到不抖。凌晨四点,队列长度终于掉头向下,P95 时延稳住,监控面板上那条红线缓缓平直。我和同事坐在机柜旁边喝了口温掉的咖啡,谁也没说话,只是把脚边的理线带又收紧了一些。
第二周复盘,我们把这套流程固化成模板,后来每次活动来之前,把“控水阀门”和参数档位一拨,就能扛住洪峰。转码从来不是炫技,而是把系统每一处该拧紧的螺丝拧到位。
参考落地建议(按你现有环境微调即可)
如果你是 纯 CPU 机:先按“表 1”的 12 路并发试跑 24 小时,观察 I/O 等待与 P95 时延;若 I/O 富余,考虑把下载/上传搬到旁路异步。
如果你是 带 A10/A16 的 GPU 机:从 12 路 NVENC 起步,滤镜链别太重(尽量用 scale_cuda),观测显存和 PCIe 带宽。
画质策略建议 两档:活动期上 veryfast/faster,平峰期 faster/fast;观众端感知与带宽账一起算。