第 9 章 / 共 57 章 · 约 8 分钟
案例 7:设计视频流平台(类 YouTube)
先花 10 分钟自己设计:需求、核心实体、API 和高层架构,各写几行。写完或者跳过之后,下面的讲解才会展开。
先写你的思路
只存在这台设备上;登录后会存进你的账号。
视频平台是面试里的高频题。它的难点不在 QPS,而在两件事:海量存储和出口带宽。答题时先把这两个数算出来,后面的设计(对象存储、转码、CDN)就都有了依据。
1. 需求澄清
功能需求(核心)
- 用户上传视频,单个文件可达几 GB
- 用户观看视频,播放器能根据网速自动切换清晰度
- 查看视频元数据:标题、描述、缩略图、播放量
通常声明为范围外:搜索、推荐、评论、直播、广告。面试官追问时再展开。
非功能需求
- 读多写少:播放次数比上传次数高约三个数量级(每天 10 亿次播放对 50 万个上传,约 2000 倍)
- 起播快(首帧最好在 2 秒内),播放过程中少卡顿
- 上传可靠:网络中断后能续传,已上传的视频不能丢
- 高可用;播放量这类元数据允许短暂不一致
- 用户分布在全球
2. 量级估算
| 项目 | 假设 | 结果 |
|---|---|---|
| 播放量 | DAU 2 亿,每人每天看 5 个 | 10 亿次/天,平均约 1.2 万 QPS,峰值 3~5 万 |
| 上传量 | 每天 50 万个,平均 300 MB | 原始文件约 150 TB/天 |
| 出口带宽 | 每次平均看 5 分钟,平均码率 2 Mbps | 每次约 75 MB,每天约 75 PB,平均约 7 Tbps |
结论很直接:元数据的 QPS 用常规的缓存加分库就能扛住;7 Tbps 的出口流量没有任何单个机房能承担,必须依靠 CDN。存储方面,原始文件加上多档转码结果,再乘以副本数,每天新增几百 TB,只能放对象存储,并且要考虑冷热分层。
3. API 设计
POST /videos -> {videoId, uploadId, partUrls[]} 创建视频,拿到分片上传地址
PUT <presigned partUrl> -> 客户端把分片直接传到对象存储
POST /videos/{id}/complete -> 通知上传完成,触发转码
GET /videos/{id} -> 元数据 + manifest 地址
GET <cdn>/{id}/master.m3u8 -> 各清晰度列表,之后按 segment 拉取
要点:视频字节流不经过应用服务器,API 只负责签发地址和记录状态。
4. 高层架构
查看图表源码
flowchart LR
accTitle: 视频平台:上传路径与观看路径
subgraph upload ["上传路径"]
c1["Client"] -->|"(1) 创建"| api1["API Service"] --> meta[("Metadata DB")]
c1 -->|"(2) 分片上传"| raw[("Object Store(原始)")]
raw -->|"(3) 事件"| mq["Message Queue"] --> tw["Transcode Workers"]
tw -->|"(4) 写入各清晰度"| proc[("Object Store(转码后)")]
tw -->|"(5) status=READY"| meta
proc --> cdn["CDN"]
end
subgraph watch ["观看路径"]
c2["Client"] -->|"元数据 + manifest 地址"| api2["API Service"]
c2 --> edgec["CDN Edge"] -->|"miss"| regional["Regional Cache"] -->|"miss"| proc2[("Object Store")]
end
上传路径写入量大、耗时长、可以异步;观看路径读多、对延迟敏感。把两条路径分开设计,是这道题的骨架。
5. 深入讨论
5.1 上传:预签名 URL + 分片上传
- 应用服务器签发有时效的预签名 URL,客户端直接把文件传到对象存储。几 GB 的流量不会占用应用服务器的带宽和连接。
- 文件切成 5~10 MB 的分片并发上传。某一片失败只重传这一片;服务端记录已完成的分片号,客户端断网重连后从断点继续。
- 视频状态用状态机管理:
UPLOADING -> UPLOADED -> PROCESSING -> READY,任一步失败进入FAILED,方便重试和排查。
5.2 转码流水线
为什么要转码:用户上传的容器格式和编码五花八门;播放端需要多种分辨率和码率;同时还要生成缩略图、做内容审核。
关键做法是先切片再并行:按关键帧(GOP)边界把视频切成若干秒的小段,每段独立转码。一个 1 小时的视频串行转码要很久,切成几百段分给几百个 worker,几分钟就能完成。
整个处理过程可以表示成一个 DAG:
查看图表源码
flowchart LR
accTitle: 转码 DAG
split["切片(split)"] --> video["视频:240p / 480p / 720p / 1080p(逐段)"]
split --> audio["音频编码"]
split --> thumb["缩略图 / 内容审核"]
video --> pkg["封装(HLS/DASH)"]
audio --> pkg
thumb --> pkg
pkg --> ready["READY"]
工程要点:
- 任务通过消息队列分发,worker 无状态,按队列堆积量自动扩缩容
- 任务幂等:每段的输出写到确定的 key,重做只是覆盖,不会产生脏数据
- 关键帧对齐:所有档位都用 closed GOP 和固定的关键帧间隔(比如每 2 秒一个),保证各档的 segment 边界落在同一时间点。否则 ABR 切换清晰度时,新档位的 segment 无法从边界处独立解码,会花屏或卡顿
- 调度器记录 DAG 中每个节点的状态,全部完成后才把视频标记为 READY
- 控制成本:先用 H.264 产出基础档位,播放量上来以后再补 VP9、AV1 这类压缩率高但编码昂贵的格式
5.3 自适应码率(ABR)
HLS 和 DASH 的思路相同:每个清晰度(rendition)切成 2~6 秒的 segment,再用一个 manifest 列出所有档位和对应的 segment 地址。
| 档位 | 分辨率 | 参考码率 |
|---|---|---|
| 低 | 426x240 | 0.4 Mbps |
| 中 | 854x480 | 1.2 Mbps |
| 高 | 1280x720 | 2.5 Mbps |
| 超高 | 1920x1080 | 5 Mbps |
选档由客户端决定:播放器根据最近几个 segment 的下载速度和缓冲区水位,决定下一个 segment 拉哪一档。缓冲快见底就降档,带宽富余就升档。服务端完全无状态,segment 只是普通的静态文件,这正是它适合放在 CDN 上的原因。
5.4 CDN 与缓存策略
- 视频热度呈长尾分布:少数热门视频占了大部分流量,它们在 CDN 边缘的命中率很高
- 冷门视频未命中时,先经过区域缓存再回源到对象存储;多级缓存能减轻源站压力
- 大型平台会把缓存服务器直接放进运营商机房,进一步减少骨干网流量
- segment 内容不可变,可以设置很长的 TTL;需要更新时换一个带版本号的 URL。代价是下架或版权删除时,边缘节点上的缓存不会自己消失:要么主动调用 CDN 的 purge 接口,要么给 segment 用带过期时间的签名 URL,过期后边缘直接拒绝请求
- 预热:头部创作者发布新视频时,提前把前几个 segment 推到边缘节点
5.5 元数据与播放量
- 元数据(标题、作者、状态、manifest 地址)存在关系库里,按 videoId 分片,前面加 Redis 缓存
- 播放量不能每次播放都直接
UPDATE数据库,热门视频会形成写热点。做法是:播放事件写入消息队列,流处理按时间窗口聚合后批量写回;或者先在 Redis 里计数,定期落盘 - 防刷:按用户、设备、IP 去重,异步过滤异常流量。展示的数字晚几秒、略有误差都可以接受
6. 取舍与常见追问
- 存储成本:几个月无人观看的视频转到低频存储层;极冷的视频可以只保留原始文件和少数档位,有人看时再补转码
- 点播与直播:直播用 RTMP、SRT 等协议推流,转码必须实时完成,segment 更短(1~2 秒)以降低延迟,manifest 的 TTL 也要很短
- 全球化:元数据在多个区域部署只读副本;上传就近接入最近区域的对象存储
- 为什么不把视频存数据库:数据库适合小而结构化的数据。大文件放对象存储,数据库里只存路径
7. 面试答题清单
- 先算出口带宽,用数字说明 CDN 的必要性
- 上传路径和观看路径分开讲
- 讲清楚 segment + manifest + 客户端选档这套机制
- 常见失分点:视频流量经过应用服务器;同步转码,阻塞上传请求;每次播放直接写数据库计数
8. 回答分级
容量与带宽
- Bad:只估算 QPS,不算出口带宽,CDN 只是一句“加个 CDN”。
- Good:算出每天约 75 PB、平均约 7 Tbps 的出口流量,据此说明 CDN 必不可少;原始文件和转码结果放对象存储。
- Great:用长尾分布解释边缘命中率,冷视频走边缘、区域缓存、源站三级回源;提到在运营商网络内部署缓存、大创作者发布时预热首批分片,以及冷视频降级存储、按需重新转码来控制成本。
上传与转码
- Bad:视频字节经过应用服务器;同步转码,上传请求一直挂到转码结束。
- Good:预签名 URL 加分片上传,支持断点续传;转码通过消息队列交给无状态 worker,状态走到 READY 才能播放。
- Great:按 GOP 边界切片并行转码,任务幂等、输出写到确定的 key;所有清晰度用固定的关键帧间隔,让分片边界对齐,ABR 切换才不会卡顿;更省带宽的编码格式只给热门视频做。
播放与计数
- Bad:由服务端为每个用户决定清晰度;每次播放执行
UPDATE views = views + 1。 - Good:HLS/DASH 分片加 manifest,客户端按测得的吞吐和缓冲水位选清晰度;播放事件进队列,批量聚合后写入。
- Great:说明分片不可变所以能长 TTL 缓存,同时给出下架和版权删除的办法(主动 purge,或带过期时间的签名 URL);异步按用户、设备、IP 去重,防止刷播放量。
9. 不同级别的期望
| 级别 | 期望 |
|---|---|
| Mid(L4) | 在提示下分开上传和播放两条路径,用上对象存储和 CDN,知道转码要异步 |
| Senior(L5) | 先算出口带宽再推导架构,讲清分片上传、并行转码和客户端 ABR,处理好播放计数的热点 |
| Staff(L6+) | 从成本出发:存储分层、编码格式的投入产出、运营商内缓存和预热;覆盖下架与 CDN 缓存的冲突、直播和点播的差异以及全球部署 |