按「数据往哪走」分四类,比按文件名分有用得多。 六个环节里只有一个会成倍放大数据,就是转发。
9.1.1 讲的是骨架怎么搭,这一节讲里面的模块怎么分工。 分对类,扩容时才知道该加哪一类机器。
一、四类模块
① 接入类:RTSP 服务、RTMP 服务。 输入是推流端,输出是「还没解复用的流」。 ② 处理类:解复用、转发、转码。 输入是裸流,输出是「可以分发的单元」。 ③ 输出类:HLS 切片、HTTP-FLV、RTMP 再分发。 输入是可分发单元,输出是播放端。 ④ 管理类:REST 接口、配置、日志。 它不在数据链路上,只在旁边看着。
这样分的好处是「扩容只扩其中一类」: 看的人多就加输出,推流多就加接入。 处理类夹在中间,但它通常最先成为瓶颈——因为所有流都要过它,而且它干的活最重。
二、转发与转码不是一回事
这两个词经常被混着用,成本差一个数量级。
① 转发:不碰编码,只把已经压好的数据换个容器或者原样送出去。 CPU 几乎不花,只花内存与网卡。 一路变四路,就是四份数据往外走。 ② 转码:重新编码。 每一路都要独立算一遍, 四路就要四份 CPU,没有「算一次分四份」的好事。
所以「支持多少种输出协议」这个指标,成本在转发上,不在编码上。 而「支持多少种分辨率」这个指标,成本才在编码上。 看参数表的时候要分清这两件事。
三、一份流要过六道手
把链路摊开,一份流从进门到出门要经过六个环节:
① 网络收包:从 TCP 或 UDP 缓冲区拿到字节。 数据不变。 ② 解复用:把容器拆开,分成音频轨与视频轨。 数据只会变少,因为容器头被丢掉了。 ③ 缓存:按时间排好序,等关键帧。 数据不变,只是换了个地方放。 ④ 转发:复制给每个下游。 这是唯一成倍放大的一步,倍数就是下游数。 ⑤ 重新封装:换成 HLS 的 TS 或 HTTP-FLV。 会加一点容器头,通常不到百分之一。 ⑥ 网络发包:送出去。 数据不变。
认准这一点,容量规划就有依据了: 要看「几路输出」的带宽,别看「几路输入」的 CPU。 输入决定接入压力,输出决定出口带宽,两个数要分开算。
四、动手 Demo:模块分类——四类模块,一份流走一遍
先把模块按数据流向分四类, 再真把同一份流转发给 1/2/4 个下游,看字节怎么涨。
import io, os, tempfile, time
# head 是 FLV 的 9 字节文件头 + 4 字节 PreviousTagSize
# tags 是拆出来的 tag(11 字节头 + 载荷 + 4 字节尾部长度)
def split_flv(raw):
tags, off = [], 13
while off + 11 <= len(raw):
size = int.from_bytes(raw[off + 1:off + 4], "big")
tags.append(raw[off:off + 11 + size + 4])
off += 11 + size + 4
return raw[:13], tags
head, tags = split_flv(open(FLV_PATH, "rb").read())
print("%d 个 tag,共 %d 字节" % (len(tags), sum(len(t) for t in tags)))
for k in (1, 2, 4):
outs = []
for i in range(k):
fh = io.open(os.path.join(TMP, "o%d.flv" % i), "wb")
fh.write(head)
outs.append(fh)
t0 = time.perf_counter()
for t in tags:
for fh in outs:
fh.write(t)
el = (time.perf_counter() - t0) * 1e3
for fh in outs:
fh.close()
total = sum(os.path.getsize(os.path.join(TMP, "o%d.flv" % i))
for i in range(k))
print("%d 路 -> 输出 %d 字节,耗时 %.1f 毫秒" % (k, total, el))
实跑输出:
--------------------------------------------------------------------
模块分类——四类模块,一份流走一遍
--------------------------------------------------------------------
**① 模块怎么分类**
**按「数据往哪走」分,比按文件名分有用得多。**
类别 典型模块 输入 输出
-----------------------------------------------------------------------
接入 RTSP 服务、RTMP 服务 推流端 未解复用的流
处理 解复用、转发、转码 未解复用的流 可分发单元
输出 HLS 切片、HTTP-FLV、RTMP 可分发单元 播放端
管理 REST 接口、配置、日志 运维请求 状态与文件
**这样分的好处是「扩容只扩其中一类」:**
**看的人多就多加输出模块,推流多就多加接入模块。**
**处理模块夹在中间,加机器通常先加它。**
**② 一份流分发到 1/2/4 个下游,开销怎么涨**
**这是处理模块的核心工作,也是最容易被低估的地方。**
**读一次、分多份,省的是读,不省的是写。**
下游数 转发 tag 输出总字节 耗时毫秒 字节倍数
----------------------------------------------------------------
1 28 10503 5.0 1.00 倍
2 28 21006 4.5 2.00 倍
4 28 42012 11.4 4.00 倍
**这里做的是「纯转发」:不重新编码,连容器都不换,**
**只是把每个 FLV tag 原样写给每个下游。**
**先看字节那一列:1 路一万出头,4 路正好四倍——完美线性。**
**再看耗时那一列,它在涨,但每次跑都不一样:**
**沙箱里文件系统有缓存,同一段代码跑两遍能差一倍。**
**所以这里只认字节数,不拿耗时下倍数结论。**
**真要是重新编码,倍数会完全不同:**
**编码是 CPU 密集的,4 路就要 4 份 CPU,没有「一次算完分四份」的好事。**
**这也是为什么生产上「转码」永远比「转发」贵一个数量级。**
**③ 一份流被多少模块碰过**
**把上面的链路摊开,一份流从进门到出门要过六道手:**
序 环节 改的是什么 会不会放大数据
---------------------------------------------------------------
1 网络收包 无 不会
2 解复用 拆成音频轨与视频轨 不会,反而变小
3 缓存 按时间排好序 不会
4 转发 复制给每个下游 会,按下游数翻倍
5 重新封装 换容器格式 会,加容器头
6 网络发包 无 不会
**只有第 4 步会成倍放大数据,其余各步要么不变要么只加一点头。**
**所以「支持几种输出协议」这个指标,真正的成本在转发那一步。**
**四种输出就是四份数据要出去,跟编码一次没关系。**
字节是严格线性的,1 路一万出头,4 路正好四万二。 耗时却在原地打转,因为沙箱里文件系统有缓存,跑两遍能差一倍。 所以这里只认字节数,不拿耗时下倍数结论。 转发就是「同一个字节写给更多人」,CPU 一点都不多花。
30 秒实验
- 把转发换成「每路重新编码」,想想耗时怎么变——从线性变成接近四倍,因为编码没有共享。
- 把 tag 排序打乱再发,看播放器还能不能播——不能,时间戳乱了,FFmpeg 会报时间戳回退。
- 只转发视频 tag、丢掉脚本 tag,看文件还能不能播——能播,但播放器不知道分辨率与编码参数。
常见误区
- 「转发与转码差不多」——差一个数量级,一个不碰编码一个重编。
- 「输出协议越多 CPU 越吃不消」——先吃不消的是出口带宽。
- 「解复用会放大数据」——相反,它丢掉容器头,数据变小。
自测三问(点开对答案)
1. 四类模块是按什么分的? —— **按数据往哪走**:接入、处理、输出、管理。 2. 六个环节里哪个会成倍放大数据? —— **只有转发**,倍数就是下游数。 3. 转码为什么比转发贵一个数量级? —— **每一路都要独立重算一遍编码**,没有共享。- ☐ (本节完,下一节 9.2 EasyDarwin 的安装部署)