Skip to content

设计一个视频直播系统

问题

推流/拉流架构怎么搭?CDN 分发怎么做?转码为什么要走异步流水线?聊天互动怎么保证实时性?

这些是直播系统面试的核心问题,也是实际工程中决定用户体验的关键。


推流与拉流:从主播到观众

视频直播的本质是实时捕获 → 编码 → 传输 → 解码 → 播放。拆成两个环节:

  • 推流(Ingest):主播端采集音视频数据,编码后推送至源站。
  • 拉流(Playback):观众端从 CDN 边缘节点拉取已转码的流。

常见推流协议

协议延迟适用场景推流端支持
RTMP3-5s经典推流协议,Adobe 主导,OBS/FFmpeg 原生支持OBS、FFmpeg、手机端 SDK
WHIP1-2sWebRTC 推流新标准,浏览器直接推流浏览器、WHIP 客户端
SRT1-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 通过回源拉取。

转码参数详解

参数典型值说明踩坑
GOP1s(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

特性KafkaRocketMQ选型理由
吞吐量百万级 msg/s十万级 msg/s弹幕量大用 Kafka,转码任务量小用 RocketMQ
延迟10ms 级1ms 级转码任务对延迟不敏感,两者都满足
消息轨迹需额外组件原生支持排查转码失败时 RocketMQ 更方便
顺序消费分区内有序队列内有序同一个直播间的转码任务需要有序(按时间戳顺序)

实战中,转码任务队列推荐 RocketMQ,原因:转码任务量不大(一场直播产生几百个切片任务),但对可追溯性要求高(查转码失败原因)。B站直播转码系统用的就是 RocketMQ。


CDN 分发:动态调度与边缘节点

直播的流量集中、延迟敏感,对 CDN 的要求比点播高得多。

多 CDN 调度

国内大型直播平台不会只用一家 CDN,而是同时接入多家(阿里云 CDN、腾讯云 CDN、网宿等),根据用户的地理位置、运营商、节点负载动态调度。

DNS 解析 → 调度中心 → 返回最优 CDN 节点 IP

               回落 / 切换策略

调度策略细节

  1. 客户端发起拉流请求 → DNS 解析到调度中心 IP
  2. 调度中心根据客户端 IP 解析地理位置、运营商
  3. 查询实时可用 CDN 节点列表(含延迟、负载、健康状态)
  4. 返回最优节点,同时返回 2-3 个备用节点 IP
  5. 客户端首节点连接失败 → 自动切换备用节点

实践数据:多 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 实现长轮询:

java
@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 异步 Servletrequest.startAsync())。B站早期弹幕系统就是吃了这个亏,后来全面切到 Netty。

敏感词过滤: 用 AC 自动机(Aho-Corasick)做敏感词匹配,1000 个敏感词匹配 50 字的弹幕耗时 < 1ms。Java 实现可以用 org.ahocorasick:ahocorasick 库。

java
// 初始化 AC 自动机(启动时加载一次)
Trie trie = Trie.builder()
    .addKeywords(sensitiveWords)
    .build();
// 每条弹幕过滤
Collection<Emit> emits = trie.parseText(message);
if (!emits.isEmpty()) {
    // 替换为 *** 或直接拒绝
    return "消息包含敏感词";
}

频率控制: 每个用户每秒最多发 3 条弹幕,用 Redis 计数器

java
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

方案延迟适用场景成本
HLS10-30s直播回放、非互动直播最低
LL-HLS (Low-Latency HLS)2-5s互动直播中等
WebRTC<1s连麦、在线教育最高

LL-HLS 在标准 HLS 基础上做了两个关键改进:

  1. 切片更小:1-2 秒切片(传统 HLS 6 秒)
  2. 部分切片可用:不等整个切片完成就传输第一部分

三种方案的延迟分解

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 长轮询天然支持无序处理

问:弱网环境下怎么保证直播流畅?

三个层次:

  1. 编码端:动态调整编码参数(降低分辨率、码率、帧率)。抖音的做法是:检测到带宽下降 → 先从 1080p 降到 720p(码率降 60%),再降帧率(从 60fps 降到 30fps),最后才降码率。
  2. 传输层:SRT/UDP 的 FEC 纠错(丢包 5% 内 FEC 可恢复,超过 5% 需要 ARQ 重传)。
  3. 播放器端:自适应码率切换(ABR),根据实时带宽自动降级。HLS 的 ABR 算法切换延迟约 2 个切片(12s),LL-HLS 的 ABR 切换延迟约 1 个切片(2s)。

问:直播的延迟和成本怎么权衡?

延迟目标方案单路成本(元/小时)适用场景
< 30sHLS 标准切片0.1-0.3大型活动、秀场直播
< 5sLL-HLS + 边缘节点0.3-0.6电商直播、互动直播
< 1sWebRTC + SFU0.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)

总结

设计一个视频直播系统,核心思路是:

  1. 推流与拉流分离:RTMP/WHIP 推流到源站,HLS/LL-HLS 拉流
  2. 转码异步化:消息队列 + Worker 池,多码率输出,注意 GOP 对齐和时间戳漂移
  3. CDN 多级分发:多 CDN 动态调度 + 边缘节点计算 + 首帧缓存,秒开是关键
  4. 弹幕做取舍:HTTP 长轮询 + Redis List,允许丢消息;礼物走消息队列,保证不丢
  5. 延迟 vs 成本:LL-HLS 是当前性价比最优解,WebRTC 留给高互动场景
  6. AI 赋能:2024-2025 的趋势是用 AI 做内容审核、转码参数优化、内容推荐,这是 Java 后端转 Agent 工程师可以切入的方向

面试时不要只背架构图,要讲清楚为什么这么做——为什么弹幕不用 WebSocket,为什么 GOP 设 1 秒,为什么延迟和成本要 trade-off,Java 后端在直播系统里做什么、不做什么。最好能讲出你踩过的坑,那才是面试官想听的。


参考:抖音/快手直播技术架构、WebRTC 直播实践、CDN 边缘计算、HLS 规范 RFC 8216、SRS 开源流媒体服务器文档、B站直播技术分享

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。