直播的成本结构是「一进多出」:主播上传一份,服务端要为每个观众单独下发一份。所以真正贵的是分发,不是推流。
1.3.1 走完了一条链路的八个环节,这一节把镜头拉远,看整体是什么形状。第 8~13 章讲的 Live555、Nginx、SRS、ZLMediaKit,全都在实现这个形状。
一、基本模型:一进多出
不管技术栈怎么变,直播的系统形状永远是这三层:
① 源(主播):一路上行,带宽由主播家的宽带上限决定。 ② 中心(服务器/集群):接收这一路,转封装,再分发给观众。 ③ 边缘(观众):N 路下行,每路都要一份完整的数据。
关键的不对称在于:上行是 1,下行是 N。 这个不对称是整个直播行业的成本来源。
有人会问:能不能让主播直接连观众(P2P)? 技术上可以(WebRTC 就支持),但主播的上行扛不住——1 Mbps 的上行,连两个观众就满了。 所以直播几乎从来不是 P2P,只有一对一通话才是。
二、带宽是乘出来的
服务端的出口带宽 = 观众数 × 单路码率。 这个乘法让很多人第一次算的时候吓一跳。
4 Mbps 的 1080p,1000 人看就是 4 Gbps。 一台普通服务器的网卡是 10 Gbps,也就是说一千人出头就把一块网卡占满了。 一万人看要 40 Gbps,十万要 400 Gbps——到这个时候,单机彻底不可能,必须靠集群和 CDN。
而 CDN 的本质就是「把这个乘法拆成很多个小乘法」: 源站只往上发给几十个边缘节点,每个边缘节点再发给几千人。 源站的出口从「人数 × 码率」变成「节点数 × 码率」。
这就是为什么 HLS 这种「切片 + 普通 HTTP」的协议能赢: 它让 CDN 用现成的静态缓存就能干活,不需要为流媒体专门改造。
三、三种分发拓扑
① 单机转发:一台 Nginx-rtmp 或 SRS,几百到几千路就顶住了。 优点是简单、延迟低;缺点是没有冗余,挂了全挂。 ② 集群(源站 + 边缘):一台源站收流,多台边缘回源再分发。 SRS 的 cluster、ZLMediaKit 的边缘模式都是这个思路。 优点是能扩容;缺点是要处理「回源」带来的额外延迟。 ③ CDN:把边缘铺到各个城市、各个运营商。观众就近取流,延迟低、带宽成本被摊薄。 缺点是贵,而且你对链路几乎没有控制权。
实际系统经常是三者叠加: 自建源站 → 自建一层边缘 → 再往外接 CDN。 每一层都在把「一个观众一份」摊薄一点。
四、两个特殊形态:连麦与合流
基本模型是「一进多出」,但有两类场景会打破它:
① 连麦:多个主播同时推流,观众要同时看到多路。这时候服务端要做「合流」——把几路画面拼成一路再分发。 否则观众就要同时拉 N 路,带宽和同步都受不了。 ② 互动直播(PK):两个主播的画面并排,中间还要实时同步比分、弹幕。 这已经超出「流媒体」的范畴,属于「实时音视频 + 信令」。
这两类场景的延迟要求是「连麦级」的(400 毫秒内),所以它们几乎一定用 WebRTC 而不是 RTMP。第 13 章的 WebRTC 就是为这个准备的。
五、动手 Demo:真拉:1 路、4 路、16 路同时拉同一份数据
本地起一个 HTTP 服务,用 1/4/16 个线程同时拉同一份 3 MB 数据, 量「服务端总共吐了多少字节」——这是分发的真实成本。
import http.server, socketserver, threading, time, urllib.request
SIZE = 3 << 20
DATA = bytes(SIZE)
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
self.send_response(200)
self.send_header("Content-Length", str(len(DATA)))
self.end_headers()
try:
self.wfile.write(DATA)
except Exception:
pass
class S(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
srv = S(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
url = "http://127.0.0.1:%d/f" % srv.server_address[1]
def one():
with urllib.request.urlopen(url) as r:
while r.read(65536):
pass
for n in (1, 4, 16):
ts = [threading.Thread(target=one) for _ in range(n)]
t0 = time.perf_counter()
for t in ts:
t.start()
for t in ts:
t.join()
el = time.perf_counter() - t0
print("%2d 路:%.3f 秒,服务端共吐出 %d MB" % (n, el, n * SIZE // (1 << 20)))
srv.shutdown()
实跑输出:
--------------------------------------------------------------------
一路推流 N 路分发——带宽为什么是【乘】出来的
--------------------------------------------------------------------
① 真拉:同一份 3 MB 数据,1/4/16 个观众同时拉
观众数 总耗时 服务端吐出的字节 单观众平均
------------------------------------------------------------
1 路 0.004 秒 3 MB 682.4 MB/秒
4 路 0.018 秒 12 MB 653.7 MB/秒
16 路 0.539 秒 48 MB 89.1 MB/秒
**注意「服务端吐出的字节」是随观众数线性涨的**:1 路 3 MB,16 路就是 48 MB。
**这就是直播的成本结构。**
**主播只上传一份,服务端要为每个观众单独下发一份。**
② 换算成真实带宽
**一路 1080p 直播按 4 Mbps 算,服务端出口带宽 = 观众数 × 4 Mbps。**
观众数 出口带宽 意味着什么
------------------------------------------
100 400 Mbps 单机可扛
10000 40 Gbps 必须上 CDN
1000000 4 Tbps 多家 CDN 分层
**1 Mbps 上行的小主播,能给多少人看?只有自己那台服务器。**
**再多的人都靠服务端下发,跟他家的上行带宽没关系。**
③ 所以直播系统里真正贵的是「分发」
**① 单机转发**(Nginx-rtmp、SRS):简单,几百到几千路就顶住了。
**② 集群**(SRS 集群、ZLMediaKit 边缘):一层一层往下发。
**③ CDN**:把「一个观众一份」摊到很多台机器、很多个机房。
**这三种做法回答的是同一个问题:怎么把「乘出来的带宽」摊平。**
「服务端吐出的字节」是随观众数【线性】涨的:1 路 3 MB,16 路就是 48 MB。 注意单观众的速率在 16 路时明显掉下来——本地环回也有上限,16 个线程抢一个进程的 CPU。 真实网络里这个瓶颈出现在网卡、在交换机、在运营商链路上,量级更大但道理一样。
30 秒实验
- 把 SIZE 改成 30 MB 再跑 16 路,看总吞吐还能不能撑住——这里是本地环回,真实场景要先除以网络带宽。
- 算一笔账:4 Mbps 的流,1000 人看要多少出口带宽?4 Gbps,一块万兆网卡的一半。
- 把「16 路」改成「64 路」,看耗时是不是还是线性的——如果不是,说明瓶颈已经从网络换成了 CPU 调度。
常见误区
- 「主播上行够快就能直接连观众」——1 Mbps 上行连两个观众就满了。直播几乎不用 P2P,只有一对一通话才用。
- 「上 CDN 就不用管带宽了」——CDN 只是把乘法拆成小乘法,总流量没变,账单也没少。
- 「单机转发扛不住就换大机器」——出口带宽是网卡物理限制,换 CPU 没用,只能加机器或上 CDN。
自测三问(点开对答案)
1. 10 万人看 4 Mbps 的直播,源站出口要多大? —— **不做 CDN 是 400 Gbps,物理上不可能。**接了 CDN 后,源站只需要「节点数 × 4 Mbps」。 2. 为什么连麦要先合流再分发? —— **否则每个观众要同时拉 N 路,带宽和音画同步都受不了。** 3. HLS 为什么特别适合 CDN? —— **它就是一堆静态文件加一个文本播放列表,CDN 不用做任何流媒体改造就能缓存。**- ☐ (本节完,下一节 1.3.3 视频直播系统的模块)