视频转码是「计算密集型 + 长任务 + 多产物」的典型系统:一个 2 小时 4K 视频要转出 6 档码率、每档切成几百个分片,单机跑要几小时,必须拆成成千上万个可并行的子任务分给一个转码集群。它和普通任务调度的区别在于——任务之间有时间轴依赖(必须按分片顺序合并)、资源是昂贵的 GPU、产物要立刻分发到全球 CDN。本文按照系统设计面试的标准答题结构,设计一个生产级的视频转码平台。
一句话:转码平台的核心是「把一条长视频切成可并行的小段,用 DAG 编排它们,再把结果拼回去」——切得够细才能横向扩展,DAG 管得住依赖才不会乱序。
一、需求澄清与量级估算
1.1 需求澄清
面试官给出题目「设计一个视频转码平台」后,先通过提问明确边界:
- 输入形态:用户上传的原始视频(各种容器/编码)还是平台内部产出的源文件?输入格式越杂,预处理(探测、修复、标准化)越重。
- 输出规格:需要几档分辨率/码率?是否需要 HLS/DASH 自适应流?是否需要 DRM 加密?
- 时效要求:是「上传后异步转码,几分钟可看」(点播)还是「必须秒级出流」(直播)?本文以点播异步转码为主。
- 规模:日均上传时长多少?峰值并发转码任务多少?这决定集群规模与成本模型。
- 成本约束:GPU 转码贵、CPU 转码慢,是否允许「热门视频用 GPU 快速转、冷门视频用 CPU 慢速转」的混合策略?
- 产物生命周期:转码产物保留多久?是否需要多版本(不同清晰度)按需生成、按需清理?
- 容错要求:一个分片转码失败如何处理?整个任务失败如何重试而不从头再来?
1.2 量级估算
以一个中等规模 UGC 平台为例:
日均上传: 50 万条视频,平均时长 3 分钟 → 150 万分钟/天
平均转码倍率: 转码耗时 ≈ 视频时长 × 2(CPU)/ × 0.3(GPU)
日转码计算量: 150 万分钟 × 2 = 300 万 CPU 分钟 ≈ 5 万 CPU 小时/天
GPU 分摊: 假设 60% 走 GPU,则 GPU 需 150 万 × 0.3 / 60% ≈ 75 万 GPU 秒 ≈ 208 GPU 小时/天
峰值按 3 倍算,需要约 30 张 GPU 卡持续满载(含余量)
输出产物: 每条视频 6 档码率 × 平均 3 分钟 → 约 18 分钟产物
存储增量: 50 万 × 6 档 × 平均 20MB ≈ 60 TB/天(需分级存储与清理)
分片规模: 按 6 秒一片,3 分钟视频 = 30 片;6 档 = 180 个子任务/条
子任务总量: 50 万 × 180 = 9000 万个子任务/天 ≈ 1000 子任务/秒(峰值 3000/秒)
关键结论:子任务数量(每秒数千)远大于视频条数(每秒几条),所以调度器的核心指标是「子任务吞吐」而不是「视频吞吐」。同时 GPU 是稀缺资源,调度必须支持优先级与抢占,否则一条冷门长视频会阻塞热门短视频的转码。
二、高层架构设计
上传 ──▶ ┌──────────────┐
│ 对象存储 (源) │
└──────┬───────┘
│ 上传完成事件
▼
┌──────────────┐
│ 探测服务 │ ffprobe:时长/分辨率/编码/音轨
│ (probe) │ 生成转码规格(码率阶梯)
└──────┬───────┘
▼
┌──────────────┐ ┌─────────────────┐
│ 编排器 (DAG) │─────▶│ 任务队列 │
│ 切片/依赖/合并 │ │ (优先级 + 分片) │
└──────┬───────┘ └────────┬────────┘
│ ▼
│ ┌─────────────────┐
│ │ 转码 Worker 池 │
│ │ (GPU/CPU 混合) │
│ └────────┬────────┘
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ 合并/封装 │◀─────│ 分片产物 (临时) │
│ (HLS/DASH) │ └─────────────────┘
└──────┬───────┘
▼
┌──────────────┐ ┌─────────────────┐
│ 产物存储 + CDN│─────▶│ 播放器 │
└──────────────┘ └─────────────────┘
五条关键链路:
- 探测:源文件落对象存储后触发
ffprobe,拿到时长、分辨率、帧率、音轨等元数据,据此生成转码规格(几档码率、用什么编码器)。 - 切片:把源视频按关键帧(GOP 边界)切成固定时长的分片,关键帧对齐是后续能无缝合并的前提。
- 转码:每个分片 × 每档码率 = 一个独立子任务,投递到任务队列,由 Worker 拉取执行。
- 合并封装:所有分片转码完成后,按顺序拼接成完整码流,再封装成 HLS(
.m3u8+.ts/.m4s)或 DASH(.mpd)清单。 - 分发:产物写入对象存储,预热到 CDN,播放器通过清单文件按网络状况切换码率。
三、核心组件设计
3.1 分片转码与任务编排
为什么要分片:一条 2 小时 4K 视频,单机转码要几小时。切成 1200 个 6 秒分片后,理论上可以用 1200 个 Worker 并行,把时间压到分钟级。分片转码是整个平台可扩展性的根基。
切片的关键约束:
# 必须按关键帧对齐切片,否则拼接处会出现花屏/音画不同步
ffmpeg -i source.mp4 -c copy -f segment \
-segment_time 6 \
-segment_format mp4 \
-reset_timestamps 1 \
-force_key_frames "expr:gte(t,n_forced*6)" \
-map 0:v -map 0:a \
chunk_%05d.mp4
-force_key_frames强制每 6 秒一个关键帧,保证切点落在 GOP 边界上,转码后能无损拼接。-reset_timestamps 1让每个分片的时间戳从 0 开始,否则合并时时间戳会错乱。-c copy是流拷贝不重编码,切片本身极快。
转码单个分片:
# 把 6 秒分片转成 720p、H.264、目标码率 2500kbps
ffmpeg -i chunk_00042.mp4 \
-vf "scale=-2:720" \
-c:v libx264 -preset veryfast -b:v 2500k -maxrate 2675k -bufsize 5000k \
-c:a aac -b:a 128k -ac 2 \
-g 48 -keyint_min 48 -sc_threshold 0 \
-f mp4 out_720p_00042.mp4
-g 48 -keyint_min 48 -sc_threshold 0 保证每个分片内部 GOP 长度固定且不因场景切换插入额外关键帧——这既利于自适应码率切换,也保证所有码率档的切片边界一致。
DAG 任务编排:转码不是一条直线,而是一张有向无环图。
┌──────────┐
│ probe │
└────┬─────┘
▼
┌──────────┐
│ split │ ← 切片
└────┬─────┘
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ transcode│ │ transcode│ │ transcode│ ← 360p/720p/1080p ...
│ chunk 1 │ │ chunk 2 │ │ chunk N │
└────┬─────┘ └────┬─────┘ └────┬─────┘
└─────────────┼─────────────┘
▼
┌──────────┐
│ merge │ ← 按序拼接每档码流
└────┬─────┘
▼
┌──────────┐
│ package │ ← 生成 m3u8/mpd + 加密
└────┬─────┘
▼
┌──────────┐
│ publish │ ← 写产物存储 + CDN 预热
└──────────┘
编排器的核心职责是依赖管理 + 状态追踪:只有当一个视频的所有 transcode 子任务都成功,才触发 merge;任一子任务永久失败,则整个任务标记失败并告警。这种「DAG 依赖 + 失败重试」的调度模型与 分布式任务调度系统设计
里讲的 DAG 编排完全同构,区别是这里的节点数量级大得多(每条视频数百个节点)。
子任务状态表:
CREATE TABLE transcode_task (
task_id BIGINT PRIMARY KEY,
video_id BIGINT NOT NULL,
chunk_index INT NOT NULL,
profile VARCHAR(32) NOT NULL, -- 360p/720p/1080p/...
status TINYINT NOT NULL, -- 0待处理 1执行中 2成功 3失败 4已取消
retry_count INT NOT NULL DEFAULT 0,
worker_id VARCHAR(64),
input_url VARCHAR(512),
output_url VARCHAR(512),
duration_ms INT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_video_chunk_profile (video_id, chunk_index, profile),
INDEX idx_status_created (status, created_at)
);
UNIQUE KEY (video_id, chunk_index, profile) 保证同一分片同一档不会被重复调度(幂等调度),Worker 重复拉取时以唯一键做去重。
3.2 码率阶梯与自适应
码率阶梯(Bitrate Ladder) 决定了「观众能用多好的画质看,平台要付多少带宽」。经典的固定阶梯:
档位 分辨率 视频码率 音频码率 适用网络
240p 426x240 300 kbps 64 kbps 2G/弱网
360p 640x360 800 kbps 96 kbps 3G
480p 854x480 1400 kbps 128 kbps 4G 一般
720p 1280x720 2500 kbps 128 kbps 4G 良好/WiFi
1080p 1920x1080 5000 kbps 192 kbps WiFi
1440p 2560x1440 9000 kbps 192 kbps 宽带
2160p 3840x2160 16000 kbps 256 kbps 高速宽带
固定阶梯的问题:对所有视频用同一套码率是浪费——一个纯色动画的 1080p 只需要 1Mbps 就无损,一个高速运动场景的 1080p 要 8Mbps 才不糊。用统一码率要么浪费带宽,要么画质不够。
Per-Title Encoding(逐视频编码) 是工业界的主流优化:先用低成本快速试编几段,测量「码率-质量」曲线(用 VMAF/PSNR 量化),再为该视频定制阶梯。
def build_ladder(video_id, probe_meta):
# 1. 抽取 3 段代表片段(低/中/高复杂度)
samples = extract_representative_samples(video_id, probe_meta, n=3)
# 2. 每段用几档码率试编,测 VMAF
curve = {}
for sample in samples:
for br in [300, 600, 1200, 2400, 4800, 9000]:
out = encode(sample, bitrate=br)
curve.setdefault(br, []).append(measure_vmaf(sample, out))
# 3. 拟合曲线,找每个分辨率下达到目标 VMAF(如 93)的最低码率
ladder = []
for res, target_vmaf in [(360, 90), (720, 93), (1080, 93)]:
br = min_bitrate_for_vmaf(curve, res, target_vmaf)
ladder.append({"resolution": res, "bitrate": round(br)})
return ladder
收益可观:整体带宽可下降 20%~40%,同时画质不降。代价是每次转码前多一轮「试编探测」,增加约 10%~15% 的计算开销——用低成本档位试编即可,不必全规格试。
自适应流(ABR)的清单生成:HLS 的主清单列出所有码率档,播放器按带宽自动切换。
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.4d401e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
编码器选择:H.264 兼容性最好(几乎所有设备都支持);H.265/HEVC 同画质省 30%~50% 带宽但专利与兼容性差;AV1 省得更多但编码慢、解码支持仍在普及。工程上常见做法是「主用 H.264 保证兼容,对支持的终端额外提供 H.265/AV1 档」。
3.3 任务队列与优先级
队列分层:不同视频的时效要求差别极大,用单一队列会让长视频阻塞短视频。
优先级队列设计:
P0(实时): 直播录制回放、短视频首发 —— 期望 1 分钟内出流
P1(高): 热门创作者/大 V 上传 —— 期望 5 分钟内
P2(普通): 普通 UGC —— 期望 30 分钟内
P3(低): 历史视频重转、归档 —— 可以跑几小时
队列实现:
每个优先级一个队列(Redis List / Kafka topic / RabbitMQ priority queue)
Worker 按 P0 → P1 → P2 → P3 顺序拉取,但设置「低优先级保底配额」防止饿死
低优先级饿死问题:如果 P0 永远有任务,P3 就永远排不上。解决办法是加权公平:每处理 10 个 P0 任务,强制处理 1 个 P3 任务;或给 P3 预留固定比例的 Worker。
GPU 调度与抢占:
- GPU 稀缺:一张 GPU 同一时刻通常只能跑一个转码进程(多进程共享会互相拖慢)。所以 GPU 是最小分配单元。
- 亲和性调度:分片产物在本地磁盘时,尽量把「同一视频的相邻分片」调度到同一台机器,减少网络传输。但过度亲和会降低负载均衡,需要权衡。
- 抢占:P0 任务到达且无空闲 GPU 时,可以抢占 P3 任务——把 P3 任务中断、保存已完成分片进度、重新入队。抢占的成本是浪费已消耗的计算,所以只对「进度低、剩余多」的 P3 任务抢占。
def schedule(subtask, cluster):
# 1. 优先找有空闲 GPU 的机器
node = cluster.find_idle_gpu(require=subtask.needs_gpu)
if node:
return node
# 2. 无空闲,高优先级任务可抢占低优先级
if subtask.priority <= P1:
victim = cluster.find_preemptible(max_priority=P3, max_progress=0.3)
if victim:
cluster.preempt(victim) # 中断并重新入队
return victim.node
# 3. 排队等待
return queue.put(subtask, priority=subtask.priority)
成本控制:GPU 转码单位成本是 CPU 的 5~10 倍。生产策略是「分层路由」:P0/P1 走 GPU 求快,P2 按集群负载在 GPU/CPU 间动态路由,P3 一律走 CPU 或干脆夜间低电价时段跑。这在 短视频系统设计 的转码子系统里有更完整的成本模型讨论。
Worker 的容错:
- 心跳与租约:Worker 拉取任务时获得一个带 TTL 的租约(如 5 分钟),需定期续租;租约过期说明 Worker 挂了,任务重新入队。
- 幂等执行:任务重跑时先检查产物是否已存在且完整(校验文件大小/时长),存在则直接标记成功,避免重复计算。
- 失败分类:源文件损坏(不可重试)vs 临时故障(可重试)。可重试任务用指数退避重试,超过 3 次转人工排查。
3.4 产物分发与回源
产物组织:转码完成后的产物要按「便于 CDN 缓存、便于按需清理」的目录结构存放。
s3://media-bucket/videos/{video_id}/
├── source/original.mp4 # 原始上传(长期保留)
├── hls/
│ ├── master.m3u8 # 主清单
│ ├── 360p/index.m3u8 + seg_*.ts
│ ├── 720p/index.m3u8 + seg_*.ts
│ └── 1080p/index.m3u8 + seg_*.ts
├── dash/manifest.mpd + seg_*.m4s
├── poster/cover.jpg # 封面(首帧或用户指定)
└── meta.json # 时长/码率阶梯/编码信息
CDN 分发策略:
- 预热(Pre-warm):热门视频转码完成后主动把清单文件推送到 CDN 边缘,避免第一个用户回源慢。
- 缓存键设计:清单文件(
.m3u8/.mpd)短缓存(如 60 秒,便于更新),分片文件(.ts/.m4s)长缓存(如 30 天,内容不可变)。这是关键——分片一旦生成就不再变化,用「内容哈希命名 + 长缓存」可让 CDN 命中率接近 100%。 - 回源保护:冷启动或缓存击穿时大量请求会打到源站。用「源站限流 + 请求合并(同一分片的并发回源合并成一个)」保护源站。CDN 缓存规则的完整设计可参考 视频流媒体平台 与 媒体转码管道实践 。
- 多版本清理:旧版本产物(如用户重新上传了同一视频)要清理,避免存储无限膨胀。用生命周期规则:超过 N 天未被访问的分片降级到冷存储,超过 M 天删除低清晰度档(保留 360p 与 1080p 两端)。
回源与播放的配合:播放器拿到 master.m3u8 后,先按初始带宽选一档拉取,随后根据下载速度动态升降档。清单里的 BANDWIDTH 与 RESOLUTION 是播放器决策的依据,所以转码时要保证声明的码率与实际码率接近(不能声明 2.5Mbps 实际产出 5Mbps,否则播放器会误判)。
四、深入权衡
1. 分片粒度的取舍:分片越小,并行度越高、负载越均衡,但切片/合并的元数据开销越大,且过小的分片会降低压缩效率(每个分片独立编码,无法跨片参考)。6 秒是行业常见折中:既能切出足够多并行单元,压缩损失又可接受。
2. GPU vs CPU:GPU 转码快 3~7 倍但成本高、画质在同码率下略逊(硬件编码器的率失真优化不如软件 x264/x265)。策略是「时效敏感 + 高码率档用 GPU,画质敏感 + 低码率档用 CPU」,或用 GPU 做初转、CPU 做精修。
3. 转码时机:上传即全量转码(用户第一次播放就很快,但可能为没人看的视频浪费算力)vs 按需转码(省算力,但首次播放要等)。折中是「上传后先转 360p 与 720p 两档快速可用,其余档位按需或按播放量触发」。
4. 一致性与幂等:转码任务必须能安全重试,所以要保证「同一分片同一档位重复执行结果一致」。做法是输出到固定路径({video_id}/{profile}/seg_{index}.ts),重跑覆盖同名文件,合并阶段只认最终产物。
5. 成本与体验:转码是重资产投入,平台常在「多转档位提升体验」和「少转档位节省成本」之间摇摆。数据驱动的做法是统计各档位的实际播放占比,砍掉占比 < 1% 的档位。
五、总结
视频转码平台的设计可以浓缩成四条主线:
- 分片是扩展的前提:按关键帧对齐切片,把长视频变成成千上万个可并行子任务,这是横向扩展的根基。
- DAG 编排管依赖:probe → split → transcode → merge → package → publish 的有向无环图,配合幂等调度与失败重试。
- 码率阶梯平衡成本与画质:固定阶梯简单但浪费,per-title 编码能省 20%~40% 带宽,代价是试编开销。
- 分发决定最终体验:清单短缓存、分片长缓存、热门预热、回源保护,让产物秒级可达全球用户。
延伸阅读:转码在短视频产品中的整体位置与成本模型见 短视频系统设计 ;DAG 依赖调度与失败重试的通用机制见 分布式任务调度系统设计 ;流媒体分发与播放器自适应见 视频流媒体平台 ;编解码器与媒体处理的底层实现见 WASM 媒体处理与编解码 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。