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

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

发布人:Minchunlin 发布时间:2025-09-12 09:53 阅读量:1115


那天是周五,香港这边短视频活动临时加码,审核队列里的素材像潮水一样冒出来。集群转码节点飙红,我拎着工具包和同事钻进葵涌机房,冷风呼呼地吹。硬盘指示灯像跑马灯,监控屏上 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)常见坑位与现场修复手记

  1. PTS/DTS 不连续:某些 VFR 源导致音画不同步。处理:-vsync 1 -video_track_timescale 90000,必要时先 -r 统一帧率后再编码。
  2. 色彩空间偏差:HDR/BT.2020 源进 SDR 流出现灰雾。处理:明确 zscale 或 format=yuv420p:color_primaries=bt709:...,必要时 tonemap.
  3. Too many packets buffered for output stream:加大 -max_muxing_queue_size 2048,并检查滤镜队列是否阻塞。
  4. HEVC 在旧机型卡顿:iOS 较老设备对 HEVC 支持不佳。策略:主线 H.264,HEVC 作为增配。
  5. NVMe 抖动:RAID0 某盘掉速,I/O 等待飙升。临场做法:把下载/上传限速(rclone --bwlimit)、ionice -c2 -n4 给网络 I/O,转码进程保高优先级。
  6. NVENC 会话报错:显卡驱动升级后默认策略改变;修正驱动与 ffmpeg 版本匹配,并降低一路的滤镜负载。
  7. 容器化开销:早期在容器里跑,--device/cgroup 混用出现资源不可见。最后我们把转码 worker 直接跑宿主机,用 systemd 管理,容器只跑队列与监控侧。
  8. -r 放错位置:把 -r 放在输出端(而非输入端)导致时长被拉伸;修复:若要归一帧率,放在输入端或用 fps 滤镜。
  9. 首帧黑屏:某些封装 + 关键帧间隔导致,设置 -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;观众端感知与带宽账一起算。

目录结构
全文