设计一个视频直播系统
问题
推流/拉流架构怎么搭?CDN 分发怎么做?转码为什么要走异步流水线?聊天互动怎么保证实时性?
这些是直播系统面试的核心问题,也是实际工程中决定用户体验的关键。
推流与拉流:从主播到观众
视频直播的本质是实时捕获 → 编码 → 传输 → 解码 → 播放。拆成两个环节:
- 推流(Ingest):主播端采集音视频数据,编码后推送至源站。
- 拉流(Playback):观众端从 CDN 边缘节点拉取已转码的流。
常见推流协议
| 协议 | 延迟 | 适用场景 | 推流端支持 |
|---|---|---|---|
| RTMP | 3-5s | 经典推流协议,Adobe 主导,OBS/FFmpeg 原生支持 | OBS、FFmpeg、手机端 SDK |
| WHIP | 1-2s | WebRTC 推流新标准,浏览器直接推流 | 浏览器、WHIP 客户端 |
| SRT | 1-3s | 基于 UDP 的可靠传输,弱网环境表现好 | OBS(插件)、FFmpeg(5.0+) |
国内主流直播平台(抖音、B站)推流端仍以 RTMP 为主,逐步向 WHIP 过渡。B站 2023 年开放了 WHIP 推流能力,实测延迟比 RTMP 低 2-3 秒。
各协议原理与踩坑
RTMP(Real-Time Messaging Protocol)
底层基于 TCP,默认端口 1935。原理是维持一个长连接,通过 Chunk 流传输音视频数据。核心问题是握手开销大(3 次 TCP 握手 + RTMP 握手),弱网环境下频繁重连。
踩坑:RTMP 的握手包含 C0/C1/C2 三个往返,加上 TCP 的 3 次握手,一条推流连接建连至少 4 个 RTT。如果主播在弱网(如 4G 信号差),光建连就要 1-2 秒。我们曾遇到主播使用 4G 推流卡顿,排查发现是 RTMP 握手的 RTT 累积导致的,最终在前端加了「预连接」——在编码尚未启动时提前建立 RTMP 连接,减少真正推流时的等待时间。
Java 后端推流接入: 如果源站基于 Java,可以使用 Netty 实现 RTMP 协议解析。Netty 的 ChunkedWriteHandler 天然适合 RTMP 的 Chunk 流传输模式。参考实现:github.com/apache/mina 的 RTMP 实现,或者直接用 github.com/streamroot/rtmp-rtmp 库。但实战中建议源站用 C++(SRS、nginx-rtmp-module)或 Go(Livego),Java 做 RTMP 解析在内存和 GC 上不占优势——高峰期每路推流 10Mbps 码率,500 路同时推流,Java 堆内存 GC 停顿会导致明显的推流抖动。
WHIP(WebRTC-HTTP Ingestion Protocol)
基于 WebRTC 的新标准,用 Offer/Answer 模型通过 HTTP 信令建立连接,底层走 UDP + DTLS + SRTP。特点是浏览器可以直接推流,不需要 Flash 或 OBS 等客户端。
踩坑:WHIP 的 UDP 穿透依赖 STUN/TURN,如果主播的 NAT 类型是 Symmetric NAT,必须部署 TURN 中继转发,这会导致额外带宽成本。我们之前在东南亚某地区部署,当地运营商 NAT 类型复杂,TURN 服务器的带宽成本比预期高了 30%。
SRT(Secure Reliable Transport)
基于 UDP 的可靠传输协议,支持动态调整 FEC(前向纠错)和 ARQ(自动重传)的比例。弱网下丢包率 20% 仍能保持相对流畅。
实践数据:实测 4G 环境下丢包率 5% 时,RTMP 的卡顿率达到 15%,而 SRT 的卡顿率仅 2%。SRT 的代价是延迟比 RTMP 高约 1 秒,适合对延迟不敏感但对流畅度要求高的场景(如户外直播)。
协议栈示意
┌─────────────┐ RTMP/WHIP ┌──────────────┐
│ 主播端 OBS │ ──────────────────→ │ 源站 / Ingest │
└─────────────┘ └──────────────┘
│
转码流水线(异步)
│
┌───────┴───────┐
│ 对象存储 │
│ (HLS 切片) │
└───────┬───────┘
│
CDN 边缘节点分发
│
┌────────────┼────────────┐
↓ ↓ ↓
观众 HLS 观众 LL-HLS 观众 WebRTC时序:推流到播放的完整链路
主播端 源站 转码 Worker OSS CDN 观众端
│ │ │ │ │ │
│── RTMP 推流 ──→│ │ │ │ │
│ │── 写原始切片 ──→│ │ │ │
│ │ │ │ │ │
│ │←── ack 返回 ────│ │ │ │
│ │ │── 转码 H.264 → │ │ │
│ │ │── 多码率输出 → │ │ │
│ │ │── 生成 m3u8 → │ │ │
│ │ │ │── CDN 预热 → │ │
│ │ │ │ │── 请求 ─→│
│ │ │ │←── 回源 ───│── 响应 ─→│
│ │ │ │ │ │转码:为什么要异步?
直播源站收到的原始流通常是 RTMP(H.264/AAC),观众端需要多种码率的选择。转码是计算密集型任务,不能阻塞推流主流程。
架构设计
推流接入 → 消息队列 → 转码 Worker Pool → 输出 HLS 切片 → 对象存储
↓
CDN 预热- 消息队列解耦:推流成功即返回,转码任务异步执行。
- 多码率输出:原画(1080p 8Mbps)、高清(720p 3Mbps)、标清(480p 1Mbps)。
- 转码结果写入对象存储(OSS/S3),CDN 通过回源拉取。
转码参数详解
| 参数 | 典型值 | 说明 | 踩坑 |
|---|---|---|---|
| GOP | 1s(60帧) | 关键帧间隔 | 设 2s 首屏慢 1 倍,设 0.5s 码率暴增 30% |
| 切片时长 | 6s(HLS)/ 2s(LL-HLS) | 分段长度 | 6s 切片在弱网下失败重试要等 6s,用户直接关页面 |
| 编码器 | x264 medium / hardware NVENC | 软件硬解选择 | 硬件转码快但同码率下画质差 10-15% |
| 音频编码 | AAC 128kbps / Opus 64kbps | 音质 vs 带宽 | Opus 在 48kbps 就能达到 AAC 128kbps 的听感 |
| 分辨率链 | 1080p→720p→540p→360p | 自适应码率 | 级数越多首屏越慢,常见 3 级就够了 |
亲自踩过的坑
坑 1:GOP 对齐问题
如果多个码率流的 GOP 不对齐,观众在切换码率时会出现画面跳跃。解决方案:所有码率流强制使用同一关键帧时间戳(-force_key_frames "expr:gte(t,n_forced*1)")。
坑 2:转码集群的冷启动
转码 Worker 启动时加载 FFmpeg 库需要 2-3 秒,如果直播间突然涌入大量观众需要即时转码,冷 Worker 来不及响应。实战中我们预先保持 20% 的 Worker 池在热备状态,同时用 GPU 转码做冷启动加速(从 3 秒降到 300ms)。
坑 3:HLS 切片的时间戳漂移
长直播(超过 2 小时)后,FFmpeg 的 PTS 会累积漂移,导致切片时间戳不连续,播放器报错。解决方案:每隔 30 分钟强制重置 PTS(-force_key_frames "expr:gte(t,n_forced*1)" + -avoid_negative_ts make_zero)。
转码的消息队列选型:Kafka vs RocketMQ
| 特性 | Kafka | RocketMQ | 选型理由 |
|---|---|---|---|
| 吞吐量 | 百万级 msg/s | 十万级 msg/s | 弹幕量大用 Kafka,转码任务量小用 RocketMQ |
| 延迟 | 10ms 级 | 1ms 级 | 转码任务对延迟不敏感,两者都满足 |
| 消息轨迹 | 需额外组件 | 原生支持 | 排查转码失败时 RocketMQ 更方便 |
| 顺序消费 | 分区内有序 | 队列内有序 | 同一个直播间的转码任务需要有序(按时间戳顺序) |
实战中,转码任务队列推荐 RocketMQ,原因:转码任务量不大(一场直播产生几百个切片任务),但对可追溯性要求高(查转码失败原因)。B站直播转码系统用的就是 RocketMQ。
CDN 分发:动态调度与边缘节点
直播的流量集中、延迟敏感,对 CDN 的要求比点播高得多。
多 CDN 调度
国内大型直播平台不会只用一家 CDN,而是同时接入多家(阿里云 CDN、腾讯云 CDN、网宿等),根据用户的地理位置、运营商、节点负载动态调度。
DNS 解析 → 调度中心 → 返回最优 CDN 节点 IP
↓
回落 / 切换策略调度策略细节:
- 客户端发起拉流请求 → DNS 解析到调度中心 IP
- 调度中心根据客户端 IP 解析地理位置、运营商
- 查询实时可用 CDN 节点列表(含延迟、负载、健康状态)
- 返回最优节点,同时返回 2-3 个备用节点 IP
- 客户端首节点连接失败 → 自动切换备用节点
实践数据:多 CDN 调度后,全国平均首帧延迟从 1.2s 降到 0.6s,卡顿率从 5% 降到 1.8%。
边缘节点计算
边缘节点不只是缓存切片,还承担:
- HLS 切片拼接:把多个切片动态拼接成连续流
- 协议转换:接收 HLS 切片,转成 LL-HLS 或 WebRTC 输出
- 首帧缓存:缓存最近的 GOP,新用户拉流直接返回,实现秒开
秒开优化
直播首屏加载慢的核心原因是 GOP 太大。播放器拿到 HLS 索引文件后,需要找到一个完整的 GOP 才能开始解码。
优化前:GOP = 2s → 首屏等待 2+ 秒
优化后:GOP = 1s + 边缘缓存最近关键帧 → 首屏 < 1 秒秒开方案的三个维度:
| 优化手段 | 效果 | 成本 |
|---|---|---|
| 缩小 GOP 到 1s | 首屏快 50% | 码率增加 8-10% |
| 边缘缓存最近 1 个 GOP | 首屏快 0.5-1s | 边缘内存 + 128KB/流 |
| 播放器快速解码 | 首屏快 0.2-0.3s | 无额外成本 |
| DNS 预解析 | 首屏快 0.1-0.2s | 无额外成本 |
踩坑:首帧缓存不能缓存太久,否则直播实时性变差。我们设置缓存过期时间为 2 秒,用户看到的是 2 秒前的画面,肉眼基本无感。
聊天互动:弹幕系统设计
弹幕是直播中最具特色的交互,也是工程技术上最考验系统设计的地方。
为什么不用 WebSocket?
百万级观众同时在线,每个 WebSocket 长连接需要维护心跳、内存状态。大规模直播场景下,WebSocket 的连接数成为瓶颈,而且弹幕允许丢消息(少量丢失不影响体验),不需要 TCP 级的可靠传输。
弹幕 vs 礼物:不同的可靠性设计
| 特性 | 弹幕 | 礼物/点赞 |
|---|---|---|
| 可靠性要求 | 低(允许丢) | 高(不能丢) |
| 延迟要求 | 低(2-5s 可接受) | 高(<1s) |
| 传输协议 | HTTP 长轮询 | TCP + 消息队列 |
| 存储 | Redis List,不持久化 | 消息队列 + DB |
| 成本 | 极低 | 高 |
实际方案:HTTP 长轮询 + 本地缓冲
弹幕发送 → HTTP API → Redis List LPUSH → LTRIM 500
↓
观众端 ← 每 2 秒 HTTP 拉取 ← 弹幕池 (LRANGE 0 499)- 写入:
LPUSH danmu:room_{room_id} {message}+LTRIM danmu:room_{room_id} 0 499 - 读取:
LRANGE danmu:room_{room_id} 0 499 - Redis 过期时间:5 分钟(直播结束自动清理)
性能数据:单台 Redis 实例(8GB,4 核)可以支撑 10 万观众同时在线,弹幕延迟 500ms-2s。如果上 WebSocket,单台服务器最多支撑 5 万连接(受限于内存和 OS 的 fd 限制)。
弹幕系统的 Java 后端实现要点
从 8 年 Java 后端转过来,弹幕系统的实现有几个关键点:
Spring WebFlux 实现长轮询:
@RestController
@RequestMapping("/danmu")
public class DanmuController {
private final StringRedisTemplate redis;
@GetMapping("/poll/{roomId}")
public Flux<DanmuMessage> poll(@PathVariable String roomId) {
// 用 WebFlux 的 Flux 实现长轮询,避免阻塞 Tomcat 线程
return Flux.interval(Duration.ofSeconds(2))
.flatMap(tick -> {
List<String> messages = redis.opsForList()
.range("danmu:room:" + roomId, 0, 499);
return Flux.fromIterable(
messages.stream()
.map(DanmuMessage::fromJson)
.collect(Collectors.toList())
);
})
.timeout(Duration.ofSeconds(30)) // 30 秒无消息断开
.takeUntil(msg -> msg.isStopSignal()); // 直播结束信号
}
}注意: 这里不能用 @Async + CompletableFuture 做长轮询,因为 Tomcat 线程池会很快耗尽。实测 1000 个长轮询连接能把 Tomcat 默认 200 线程池打满,后续请求全部排队。必须用 WebFlux(Netty 事件驱动)或 Servlet 3.1 异步 Servlet(request.startAsync())。B站早期弹幕系统就是吃了这个亏,后来全面切到 Netty。
敏感词过滤: 用 AC 自动机(Aho-Corasick)做敏感词匹配,1000 个敏感词匹配 50 字的弹幕耗时 < 1ms。Java 实现可以用 org.ahocorasick:ahocorasick 库。
// 初始化 AC 自动机(启动时加载一次)
Trie trie = Trie.builder()
.addKeywords(sensitiveWords)
.build();
// 每条弹幕过滤
Collection<Emit> emits = trie.parseText(message);
if (!emits.isEmpty()) {
// 替换为 *** 或直接拒绝
return "消息包含敏感词";
}频率控制: 每个用户每秒最多发 3 条弹幕,用 Redis 计数器
String freqKey = "danmu:freq:" + userId;
Long count = redis.opsForValue().increment(freqKey);
if (count == 1) {
redis.expire(freqKey, Duration.ofSeconds(1)); // 1 秒过期
}
if (count > 3) {
return "发送太频繁";
}礼物特效与弹幕的区别
礼物特效不能丢(用户花钱了),需要保证到达:
礼物 → 消息队列 (Kafka/RocketMQ) → 持久化 → 推送给主播 + 观众- 礼物走 Kafka 消息队列,保证至少一次投递
- 每条礼物记录落库,方便后续结算
- 礼物和弹幕走不同通道,弹幕负载高时不影响礼物送达
AI 驱动的弹幕内容审核
2024-2025 年,主流直播平台已经用 AI 做弹幕实时审核,替代了传统的规则匹配:
弹幕发送 → HTTP API → AI 内容审核 API → 通过 → 展示
└→ 拒绝 → 记录 + 通知运营- 用 LLM API(如 GPT-4o-mini 或国内的小模型)做弹幕内容分类:广告、色情、辱骂、正常
- 每条弹幕调用成本约 0.0001 元(小模型,50 字以内)
- 比人工审核效率高 100 倍,准确率 95%+(误杀率 1% 以下)
- 在 Agent 架构中,弹幕审核可以设计为一个独立的 Agent:接收弹幕流 → 调用 NLU 模型 → 输出审核结果 → 触发对应动作
弹幕系统的其他工程细节
热门弹幕聚合:同一句话被大量用户刷屏时,聚合显示「XXX 等 1000 人说了这句话」,减少渲染压力。实现方式:Redis 对弹幕内容做 hash,统计相同内容的出现次数,超过阈值后聚合。
录制与回放
直播同时需要录制,用于回放和审核。
- 推流到源站时,同时写入 HLS 切片到对象存储
- 录制文件按时间戳分段,断流后自动拼接
- 特别注意:时间戳对齐,避免回放时音画不同步
- 切片完整性校验:每个切片写入时计算 MD5,回放时校验
踩坑:如果直播中断后恢复,新录制的切片时间戳从 0 重新开始,播放器会认为是不同流。解决方案:在 m3u8 索引中记录连续的时间戳,断流后从最后时间戳继续,而不是重置。
录制系统的 Java 架构
从 Java 后端的角度看,录制服务可以设计为:
推流接入 → 录制 Worker (Spring Boot) → OSS 客户端 → 对象存储
↑
Redis Stream(记录切片元数据)
↓
MongoDB(录制索引,用于回放查询)- 录制 Worker 用 Spring Boot 的
@KafkaListener消费转码队列中的切片完成事件 - 切片元数据(时间戳、MD5、大小、路径)写入 Redis Stream
- 直播结束后,从 Redis Stream 读取完整切片列表,写入 MongoDB 做持久化索引
- 回放时根据 MongoDB 索引拼接完整 m3u8
延迟优化:从 HLS 到 LL-HLS 到 WebRTC
| 方案 | 延迟 | 适用场景 | 成本 |
|---|---|---|---|
| HLS | 10-30s | 直播回放、非互动直播 | 最低 |
| LL-HLS (Low-Latency HLS) | 2-5s | 互动直播 | 中等 |
| WebRTC | <1s | 连麦、在线教育 | 最高 |
LL-HLS 在标准 HLS 基础上做了两个关键改进:
- 切片更小:1-2 秒切片(传统 HLS 6 秒)
- 部分切片可用:不等整个切片完成就传输第一部分
三种方案的延迟分解
HLS 延迟 10-30s 的来源:
- 切片时长 6s → 等 6s 完成 1 个切片 → 6s
- m3u8 索引更新间隔 2s → 2s
- 播放器缓冲 2 个切片 → 12s
- 合计:约 20s
LL-HLS 延迟 2-5s 的来源:
- 切片时长 2s → 2s
- 部分切片可用 → 0.5s
- 播放器缓冲 1 个切片 → 2s
- 合计:约 4.5s
WebRTC 延迟 <1s 的来源:
- UDP 传输,无缓冲
- 不需要切片和索引
- 延迟 = 编码延迟 + 网络传输延迟
混合方案是实战最优解
实际产品中常见的是混合方案:
- 观众端优先走 LL-HLS
- 连麦/互动场景降级为 WebRTC
- 弱网环境降级为 HLS(容忍高延迟,保证流畅)
观众端 → 检测网络质量 → 延迟 > 200ms → 走 LL-HLS
→ 延迟 < 50ms 且需要互动 → 走 WebRTC
→ 丢包率 > 10% → 走 HLS实践数据:某直播平台从纯 HLS 切换到混合方案后,付费用户留存率提升 12%,主要原因是互动场景延迟从 15s 降到 2s。
AI/Agent 在直播系统中的应用(2024-2025 趋势)
对于转 Agent 工程的 Java 后端,直播系统有几个 AI 落地点值得关注:
1. AI 智能转码参数调优
传统转码参数是固定的(固定码率、固定分辨率链),但 AI 可以根据画面内容动态调整:
- 静态画面(如主播独白、PPT 分享):降码率 30-50%,画面质量不变
- 动态画面(如游戏直播、户外运动):升码率,保证画面不模糊
- 实现:用轻量级 CNN 模型分析每帧画面复杂度,输出码率调整建议
2. AI 内容审核 Agent
直播流 → 抽帧(每 5 秒 1 帧)→ 视觉模型鉴黄/鉴暴 → 告警
弹幕流 → 实时 NLP 审核 → 违规处罚(禁言/封号)- 可以用 GPT-4o-mini 或豆包等国内模型做弹幕审核
- 视觉审核用开源模型(如 DeepSeek-VL),部署在 GPU 节点上
- 审核 Agent 的架构设计:接收事件 → 调用模型 → 决策 → 执行(封禁/降权/警告)
3. AI 推荐直播内容
- 根据用户观看历史,AI 推荐直播间
- 用 Embedding 做直播间内容相似度匹配
- 和传统推荐系统的区别:直播是实时流,推荐结果需要考虑「直播是否还在进行」和「主播当前状态」
面试时的高频追问
问:为什么弹幕不用 WebSocket?
实际并不是不能用,而是成本和收益不匹配。弹幕的核心特征:
- 允许丢消息(丢几条不影响体验)
- 延迟容忍度宽(2-5s 能接受)
- 观众在百万级,WebSocket 长连接维护成本高
WebSocket 用于弹幕的典型问题:
- 单机 5 万连接后,内存占用约 2GB,CPU 用于心跳和 epoll 事件处理
- 如果主播突然断网重连,WebSocket 需要重新握手,比 HTTP 长轮询复杂
- 弹幕不要求顺序性,HTTP 长轮询天然支持无序处理
问:弱网环境下怎么保证直播流畅?
三个层次:
- 编码端:动态调整编码参数(降低分辨率、码率、帧率)。抖音的做法是:检测到带宽下降 → 先从 1080p 降到 720p(码率降 60%),再降帧率(从 60fps 降到 30fps),最后才降码率。
- 传输层:SRT/UDP 的 FEC 纠错(丢包 5% 内 FEC 可恢复,超过 5% 需要 ARQ 重传)。
- 播放器端:自适应码率切换(ABR),根据实时带宽自动降级。HLS 的 ABR 算法切换延迟约 2 个切片(12s),LL-HLS 的 ABR 切换延迟约 1 个切片(2s)。
问:直播的延迟和成本怎么权衡?
| 延迟目标 | 方案 | 单路成本(元/小时) | 适用场景 |
|---|---|---|---|
| < 30s | HLS 标准切片 | 0.1-0.3 | 大型活动、秀场直播 |
| < 5s | LL-HLS + 边缘节点 | 0.3-0.6 | 电商直播、互动直播 |
| < 1s | WebRTC + SFU | 0.6-2.0 | 连麦、在线教育、远程医疗 |
单路成本指:1 个主播 + 1 万观众的平均带宽成本。WebRTC 的 SFU 需要转发每一路流,成本是纯 CDN 分发的 3-5 倍。面试时如果能算出这个数字,面试官会认为你有真实项目经验。
问:Java 后端做直播系统,哪些组件自己做,哪些用现成的?
| 组件 | 自研还是用现成 | 推荐方案 |
|---|---|---|
| 推流接入 | 现成(C++/Go) | SRS(推荐)、Livego、nginx-rtmp |
| 转码 | 现成 | FFmpeg、阿里云/腾讯云转码服务 |
| 消息队列 | 现成 | RocketMQ(推荐)、Kafka |
| 弹幕服务 | 自研 | Spring WebFlux + Redis |
| 礼物服务 | 自研 | Spring Boot + Kafka + MySQL |
| 录制回放 | 现成 + 自研 | OSS + 自研索引服务 |
| 内容审核 | API 调用 | 阿里云/腾讯云内容审核 API,或用 AI 自建 |
| CDN 调度 | 现成 | 阿里云多 CDN 调度、自研调度器 |
| 播放器 | 现成 | 阿里云播放器、VLC、ExoPlayer(Android) |
总结
设计一个视频直播系统,核心思路是:
- 推流与拉流分离:RTMP/WHIP 推流到源站,HLS/LL-HLS 拉流
- 转码异步化:消息队列 + Worker 池,多码率输出,注意 GOP 对齐和时间戳漂移
- CDN 多级分发:多 CDN 动态调度 + 边缘节点计算 + 首帧缓存,秒开是关键
- 弹幕做取舍:HTTP 长轮询 + Redis List,允许丢消息;礼物走消息队列,保证不丢
- 延迟 vs 成本:LL-HLS 是当前性价比最优解,WebRTC 留给高互动场景
- AI 赋能:2024-2025 的趋势是用 AI 做内容审核、转码参数优化、内容推荐,这是 Java 后端转 Agent 工程师可以切入的方向
面试时不要只背架构图,要讲清楚为什么这么做——为什么弹幕不用 WebSocket,为什么 GOP 设 1 秒,为什么延迟和成本要 trade-off,Java 后端在直播系统里做什么、不做什么。最好能讲出你踩过的坑,那才是面试官想听的。
参考:抖音/快手直播技术架构、WebRTC 直播实践、CDN 边缘计算、HLS 规范 RFC 8216、SRS 开源流媒体服务器文档、B站直播技术分享