视频转码系统的架构设计:从任务调度到分布式转码的性能优化
视频转码系统的架构设计:从任务调度到分布式转码的性能优化
一、背景与问题定义
视频转码是内容平台的基础设施级能力。用户上传一个源文件(可能是 iPhone 拍摄的 HEVC 编码 4K 视频,也可能是 Android 设备的 H.264 1080P),平台需要将其转换为多档清晰度(4K/1080P/720P/480P)和多封装格式(HLS-DASH),才能覆盖不同网络环境和终端设备的播放需求。
转码系统的核心挑战有三个:第一,转码是计算密集型任务,单机 GPU 吞吐有上限,必须分布式;第二,转码任务有优先级差异——头部创作者的内容必须优先处理,而长尾内容可以排队;第三,转码进度的实时感知直接影响创作者体验——上传后 "转码中" 状态持续太久会挫伤发布意愿。
本文复盘一套日均处理 50 万转码任务的分布式系统架构设计,涵盖任务生命周期管理、调度策略、进度通知和不同清晰度的差异化处理。
二、转码任务的生命周期与整体架构
2.1 任务生命周期
一个转码任务从创建到完成经历五个阶段:
PENDING → QUEUED → RUNNING → COMPLETING → COMPLETED
↘ FAILED (可重试) → PENDING
- PENDING:任务刚创建,尚未入队。此时可编辑优先级和转码参数。
- QUEUED:任务已入优先级队列,等待调度器分配执行节点。
- RUNNING:转码节点已领取任务,FFmpeg 进程正在执行。
- COMPLETING:转码完成,进行后处理——分片上传、M3U8 生成、CDN 预热。
- COMPLETED / FAILED:终态。FAILED 状态支持自动重试(最多 3 次)。
2.2 整体架构图
三、分布式调度的核心设计
3.1 优先级队列
任务优先级由三因子加权决定:
priority = α × creator_level_score + β × video_hotness_score + γ × wait_time_bonus
- creator_level_score:头部创作者 100 分,腰部 60 分,长尾 20 分,保证核心内容供给者的体验。
- video_hotness_score:预处理阶段的 AI 模型预估视频热度(0~100),热度越高越优先。
- wait_time_bonus:防饥饿因子,每等待 5 分钟 +5 分,确保长尾任务不会被无限期推迟。
α、β、γ 的典型值为 0.4、0.3、0.3。
Redis Sorted Set 天然支持这个模型,score 即为 priority 值。
@Service
public class TranscodeScheduler {
private static final String QUEUE_KEY = "transcode:queue";
private final StringRedisTemplate redisTemplate;
public void enqueueTask(TranscodeTask task) {
double priority = calculatePriority(task);
redisTemplate.opsForZSet().add(QUEUE_KEY, task.getTaskId(), priority);
task.setState(TaskState.QUEUED);
}
public TranscodeTask dequeueTask() {
// ZPOPMAX 原子操作,取最高优先级任务
Set<ZSetOperations.TypedTuple<String>> result =
redisTemplate.opsForZSet().popMax(QUEUE_KEY, 1);
if (result == null || result.isEmpty()) return null;
String taskId = result.iterator().next().getValue();
return taskRepository.findById(taskId)
.map(task -> {
task.setState(TaskState.RUNNING);
return taskRepository.save(task);
})
.orElse(null);
}
}
3.2 资源匹配与 Worker 管理
Worker 节点启动时向调度器注册自身能力:GPU 型号(T4/A10/A100)、可用显存、最大并发任务数。调度器维护 Worker 的能力视图,将不同清晰度的任务分配到合适的 Worker:
- 4K 转码(高计算量)优先分配到 A100 节点
- 1080P/720P 分配到 T4 节点即可
- H.265 编码(计算量比 H.264 高 4 倍)需要更多 GPU 资源
Worker 以心跳(5 秒间隔)上报当前负载,调度器据此决策。心跳超时 30 秒则将 Worker 标记为离线,其正在执行的任务由调度器重新分配到其他 Worker。
3.3 失败重试与幂等性
转码失败的原因多样:源文件损坏、GPU OOM、网络抖动导致输出上传失败。重试策略:
@Transactional
public void handleTaskFailure(String taskId, String errorMessage) {
TranscodeTask task = taskRepository.findByIdWithLock(taskId);
task.incrementRetryCount();
task.setLastError(errorMessage);
if (task.getRetryCount() <= 3) {
// 指数退避 + 随机抖动,避免重试风暴
long delaySeconds = (long) Math.pow(2, task.getRetryCount())
+ ThreadLocalRandom.current().nextInt(10);
task.setState(TaskState.PENDING);
task.setNextRetryAt(Instant.now().plusSeconds(delaySeconds));
taskRepository.save(task);
} else {
task.setState(TaskState.FAILED);
taskRepository.save(task);
notifyService.pushTranscodeFailed(task.getUserId(), task.getVideoId());
}
}
幂等性的保证在 Worker 侧:Worker 在领取任务后先检查输出存储中是否已存在对应清晰度的完整文件(通过文件大小校验),若存在则跳过转码直接进入 COMPLETING 阶段。
四、不同清晰度的转码策略与进度通知
4.1 差异化转码策略
| 清晰度 | 编码格式 | 码率范围 | GOP大小 | 优先级策略 |
|---|---|---|---|---|
| 4K | H.265 | 15-25 Mbps | 2s | 仅头部创作者 |
| 1080P | H.264/H.265 | 4-8 Mbps | 2s | 全部视频 |
| 720P | H.264 | 2-4 Mbps | 3s | 全部视频 |
| 480P | H.264 | 1-2 Mbps | 4s | 全部视频 |
值得注意的设计:4K 转码仅对头部创作者开放——因为 4K 的转码成本是 1080P 的 4~6 倍,而长尾内容的 4K 播放量极低,ROI 不合理。对于长尾内容,平台只生成 1080P 及以下清晰度。
4.2 FFmpeg 参数模板
# 1080P H.264 模板
ffmpeg -i input.mp4 \
-c:v libx264 -preset faster -crf 23 \
-vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
-c:a aac -b:a 128k -ar 44100 \
-g 48 -keyint_min 48 -sc_threshold 0 \
-f hls -hls_time 4 -hls_list_size 0 -hls_segment_filename \
"output_1080p_%05d.ts" output_1080p.m3u8
关键参数解释:-g 48 设置 GOP 为 2 秒(24fps × 2),在拖动体验和编码效率之间取平衡。-sc_threshold 0 禁用自适应场景检测,确保固定的 GOP 间隔(对 HLS 分片一致性很重要)。-preset faster 在编码速度和质量之间取折中。
4.3 进度实时通知
转码进度通过 FFmpeg 输出的 time= 字段解析,实时推送到前端:
public class TranscodeProgressTracker {
public void monitorFfmpegOutput(Process ffmpegProcess, String taskId,
long totalDurationSeconds) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(ffmpegProcess.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
Optional<Double> currentTime = parseTimeFromFfmpegLine(line);
currentTime.ifPresent(time -> {
int progress = (int) (time / totalDurationSeconds * 100);
// 限流:每 2 秒最多推送一次进度
if (shouldPush(taskId, progress)) {
TranscodeProgressEvent event = new TranscodeProgressEvent(
taskId, Math.min(progress, 99),
TranscodePhase.TRANSCODING);
websocketService.push(taskId, event);
}
});
}
} catch (IOException e) {
log.error("Failed to monitor ffmpeg output", e);
}
}
}
后处理阶段(上传 + CDN 预热)也按 20% 的权重计入进度条,最终在全部完成时推送 100%。
五、总结
转码系统的设计本质上是资源调度问题——如何在有限的 GPU 算力约束下,最小化用户等待时间。优先级队列用 Redis Sorted Set 实现,天然支持高并发入队和原子出队;Worker 能力注册 + 心跳让调度器始终掌握全局负载视图;分清晰度的差异化策略在成本和体验之间取得平衡。
未来的方向:探索 Serverless GPU(如 AWS Lambda + GPU)实现弹性扩缩,将长尾任务迁移到成本更低的竞价实例;引入 AV1 编码进一步降低带宽成本(尽管编码时间更长);以及构建跨数据中心的转码调度能力,利用不同区域的 GPU 资源池做全局负载均衡。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)