跳转至

EP15:横向扩容和低延迟的关系

在 YouTube 打开

上集回顾 EP14《节点池管理:把资源边界放进调度器》
下集预告 EP16《如何应对突发性高并发场景》

0. 本集目标

看完这一集,观众应该能做到四件事:

  1. 理解横向扩容的拓扑基础:把 Origin(收流)和 Edge(分发)拆成两种角色,才有"一次推流、N 份扇出"的空间;但代价是对称的——Origin 自己的处理时延会被这个 N 倍放大。
  2. 分清 Origin → Edge 的两种连法:主动推(push fanout)和按需回源(pull-on-demand)各自的成本模型,以及为什么这条链路最好走同 AZ/同 Region 的私网——这就是"region 组网"。
  3. 知道跨 region 怎么桥接:ppmmx 参与横向扩容拓扑的四种节点角色(Origin/Forward/Edge/Record),节点之间转发走的是 WHIP 会话,以及"一跳时延"目前是工程预算,不是逐跳实测出来的数字。
  4. 拿到一份可核实的容量换算:64 vCPU 的新加坡 Region 配额能开多少台 Edge,"峰值并发"在不同口径下分别是多少,以及为什么不能把单机压测的故障点直接乘以台数。

1. 横向扩容的起点:把 Origin 和 Edge 拆成两种角色

1.1 一次推流,N 份扇出

PPCDN 的媒体链路里,Origin 是媒体数据进入系统的唯一入口,Edge 从 Origin 回源、向观众分发,同一路流在同一台 Edge 上被多个观众共享一条回源链路(EP02 §1.1)。这个拆分带来的直接好处是:扩容观众容量,只需要加 Edge,不需要动推流端——ppobs 从始至终只认一个 Origin 地址,Edge 加到多少台、怎么加,对推流侧完全无感。

flowchart LR
    P["ppobs 推流"] --> O["Origin<br/>媒体数据进入系统的唯一入口"]
    O --> E1["Edge-1"]
    O --> E2["Edge-2"]
    O --> E3["Edge-3"]
    E1 --> V1["观众群 1"]
    E2 --> V2["观众群 2"]
    E3 --> V3["观众群 3"]

这正是 EP02 §2.3 讲过的"Edge 层可水平扩展":Edge 按节点池调度、优先导向剩余容量最大的节点,单台 Edge 过载不会拖垮全局(EP14 §1 的"剩余容量最大优先"排序)。横向扩容的第一前提,是先有这个 1 对 N 的拓扑结构,而不是先有更多机器。

1.2 代价对称:Origin 的处理时延会被 N 倍放大

好处和代价是一体两面的。所有 N 条下游分支,都要先经过同一个 Origin 才能分叉——Origin 自己的处理环节(接收、转发给 Record/Edge,不重新编码)如果多花 10ms,这 10ms 会被所有下游、所有观众一起继承,不像某一台 Edge 出问题那样只影响落在它上面的那批观众。

这是一条工程判断,不是这个项目哪份文档写死的 SLA 数字。目前没有人专门量过"Origin 这一跳自己的处理时延"——能拿到的是 EP12 记录的端到端基线:P2P 直连约 70ms,Edge 分发下 WHIP 入库约 108ms、SRT 入库约 386ms,这些数字已经包含了采集、编码、网络传播、解码、渲染的全部开销,Origin 自己的转发环节只占其中很小的一段。把 Origin 自身的处理开销压在几十毫秒的量级,是为后面每一层扇出预留的安全余量,是一种设计取向,不是已经被独立测量验证过的结论。


2. Origin → Edge 的两种连法:主动推 vs 按需回源

2.1 push fanout:主动推

生产配置里,Origin 对外转发走的是 forwardMmxTargets——每个下游目标各开一条独立的 WHIP 会话、独立的 Forwarder,互不影响(docs/deployment/ppcdn-production-deployment-plan.md §二)。deploy/config/mmx-origin.yml 的 paths.all 写的是 source: publisher + sourceOnDemand: false:只要配置里有这个目标,Origin 就一直往那推,不管这台 Edge 当前有没有观众。

好处是路径始终"热着",没有建链等待;代价是每加一台走这条路的 Edge,都要同步去 Origin 的 forwardMmxTargets 里追加一条私有 IP 并 reload,而且从追加的那一刻起,不管有没有观众,Origin 出站就立刻多一条转发会话。

2.2 pull-on-demand:按需回源

deploy/config/mmx-edge.yml 的 paths.all 写的是 source: redirect + sourceOnDemand: true:Edge 在观众到来之前,跟 Origin 之间没有任何管线。流程是观众发起 WHEP 播放请求 → 这台 Edge 才去 Origin 拉这一路流 → 同一台 Edge 上同一路流的多个观众共享这一条回源链路。EP14 §2.3 讲的控制台自助建节点,走的正是这条路——Edge 每次回源前向 ppcenter 动态解析 Origin 地址(POST /internal/mmx/v1/origin/resolve),不需要 Origin 预先认识它。

好处是一台没有观众的 Edge,对 Origin 的额外负载接近 0,加 Edge 不需要碰 Origin 配置;代价是冷启动会增加建链和首帧等待。

2.3 两种连法同时存在,分工不同

生产里不是"二选一打穿全局",而是按角色分工:预先规划、长期在线的核心 Edge 走 push fanout,接进 forwardMmxTargets 固定清单,换一条始终热着、延迟最可预期的路径;用户自助添加、规模和上线时间都不确定的 Edge,走 pull-on-demand,不用每加一台就去改 Origin。

连法 配置 对 Origin 的即时负载 代价 典型场景
push fanout(主动推) source: publisher
sourceOnDemand: false
配置即生效,立即占用出站 每加一台要改 Origin 配置并 reload 预先规划的核心 Edge
pull-on-demand(按需回源) source: redirect
sourceOnDemand: true
接近 0,直到有观众落上来 冷启动建链 + 首帧等待 自助添加、动态扩出来的 Edge

2.4 为什么这条链路最好走内网:region 组网

docs/deployment/ppcdn-production-deployment-plan.md 把 Origin/Record/Edge 刻意摆在同一可用区:同 AZ 内网免费;同 Region 跨 AZ 要按约 $0.01/GB 双向计费;跨 Region 费用更高。拓扑图上标得很清楚——forwardMmx(WHIP 推流)私有 IP · 同 AZ · 免费。把 Origin 和 Edge 摆进同一个 AZ/Region,省的不只是账单,更是把这条承载实时媒体的链路挪出公网,避开公网拥堵带来的抖动——这正是第 1 节说的"把 Origin 这一跳的开销压低"在部署层面的落地。

要说准确:这目前是一条部署层的约定,不是调度器强制的硬边界。ppcenter/internal/manager/node_manager.go 的排序逻辑明确写着"Region is deliberately not consulted: pool members compete purely on free capacity"——池内候选只按剩余容量排序,不看 region。region//admin/region-routes 提供的是一层城市→候选节点的软性优先级(regionRank,只影响候选排序,不参与过滤),真正的隔离边界是 EP14 讲过的 PoolID。把节点摆进哪个 AZ、哪个 Region,今天仍然是运维在建节点时的人工决定。


3. 跨 region:引入 Forward 节点

单个 Region 的 Lightsail vCPU 配额是有上限的(第 5 节会算这笔账)。要把流量桥接到另一个 Region 的 Origin-Edge 集群,ppmmx 新增了一种节点角色:NODE_ROLE_FORWARD(ppcenter/pb3/mmx.proto)。

Forward 是一个纯 WHIP 中继:不接收 OBS 直推,不对观众提供播放,数据流是"被动收 + 主动转发"——

  1. Region A 的 Origin 用既有的 forwardMmx 把流推给 Forward 节点(source: publisher,这是被动收的一端);
  2. Forward 节点再用自己的 forwardMmxTargets 把整条多层 simulcast 流原样推给 Region B 的 Edge 集群(这是主动转发的一端)。
flowchart LR
    subgraph RA["Region A"]
        OA["Origin"] -- "push(forwardMmx)" --> FA["Forward 节点<br/>被动收 + 主动转发"]
    end
    subgraph RB["Region B"]
        EB1["Edge"]
        EB2["Edge"]
    end
    FA -- "forwardMmxTargets(push)" --> EB1
    FA -- "forwardMmxTargets(push)" --> EB2

两个细节值得记住:第一,Forward 复用的是既有 mmx-to-mmx 转发实现,没有新写一条转发代码路径,本质上只是让 push fanout 多串一跳;第二,Forward 不对自建客户开放创建(EP14 §2.3 的控制台建节点四选一里没有它)——跨 Region 桥接到哪、怎么桥,目前是平台统一规划的内部角色,不交给用户自己配。

老实说现状:docs/ppcdn-development-progress.zh-CN.md §3.18 把这项工作标注为"已完成(代码与配置),真实跨区域联调待验证"。也就是说 ppmmx/ppcenter 两端的代码、配置、单元测试都已经就位,但目前还没有做过一次真实的跨 Region 联调——这是待验证项,不是已经在生产跑通的能力。


4. ppmmx 的四种横向扩容角色,以及节点间的转发协议

4.1 四种角色:Origin / Forward / Edge / Record

ppcenter/pb3/mmx.proto 的 NodeRole 枚举实际有 7 个值,但真正参与"跨节点转发、可以横向扩容"这套拓扑的是四种:

角色 职责 回源/下发方式
NODE_ROLE_ORIGIN 接收推流(WHIP/SRT/ERTMP),是媒体数据的唯一入口 向下游 push(forwardMmxTargets)
NODE_ROLE_FORWARD 跨区域桥接:被动收 + 主动转发 上游 push 收,下游 push 发
NODE_ROLE_EDGE 从 Origin 回源,向观众提供 WHEP push 或 pull-on-demand(第 2 节)
NODE_ROLE_RECORDER 按场次 split-rec 录像,上传对象存储 上游 push 收(source: publisher)

剩下 3 个枚举值不在本集讨论范围:NODE_ROLE_STANDALONE 是单实例自成一体、不与其它节点组网的角色(EP14 §2.1 已经讲过),不参与跨节点转发;NODE_ROLE_PUBLISHER/NODE_ROLE_PLAYER 是协议预留位,不对应真实部署的 ppmmx 服务器角色。

4.2 节点间协议:WHIP 会话,不是另起一套协议

不管是 Origin→Record、Origin→Edge 还是 Origin→Forward→Edge,节点之间转发走的都是独立的 WHIP 会话——"每个目标是一条独立的 WHIP 会话与独立的 Forwarder,互不影响"(ppcdn-production-deployment-plan.md §二)。Forward 复用既有 mmx-to-mmx 转发实现这件事也说明同一点:节点间没有为"转发"这件事另起一套协议,WHIP 会话里原样带着标准的 RTP/SRTP 媒体,整条多层 simulcast 原封不动地往下传,中间节点不解码、不转码。

4.3 一跳的时延预算:量级在几十毫秒,但这是工程预算,不是逐跳实测

节点间转发不重新走一遍编解码管线,理论上开销应该很小——主要是网络 RTT、WHIP 协商和很小的缓冲。

要诚实说清楚:目前的时延测量没有单独覆盖过 Origin→Edge 或 Origin→Forward→Edge 这一段。docs/design/p2p-latency-measurement-plan.md §4 的表格写得很明确——SEI/OBU 时间戳方案覆盖的是采集到渲染的完整链路;Edge 侧 WHEP 的 currentRoundTripTime 只覆盖"最后一跳"(Edge↔观众),不覆盖 Origin→Edge 段,不能代表完整链路延迟。换句话说,"一跳大约 20ms"是一个倒推出来的工程预算,不是已经被逐跳测量验证过的数字:EP12 记录的端到端基线本身只有 70~108ms(SRT 入库为 386ms),如果每多转发一跳都加上两位数、三位数毫秒,几跳叠加下来就会把这条基线吃光。把单跳开销控制在几十毫秒的量级,是为了让"加一跳 Forward 桥接到另一个 Region"这件事,不会把整条链路的时延预算透支——这也是为什么 Forward 目前只用来做一次性的跨 Region 桥接,不会被设计成可以无限串联的中继链。


5. 一个真实的容量换算:64 vCPU 的新加坡 Region,能开多少台 Edge

5.1 documented 的那部分:64 vCPU → 30 台 Edge

docs/deployment/ppcdn-production-deployment-plan.md §4.6 按已确认的生产口径算过这笔账(Origin/Record 保留在同一 Region/AZ,不占用额外 vCPU 之外的资源):

项目 计算 数值
媒体 Region Lightsail vCPU 配额 已申请/规划 64 vCPU
Origin 占用 1 × 2 vCPU 2 vCPU
Record 占用 1 × 2 vCPU 2 vCPU
Edge 可用 vCPU 64 − 2 − 2 60 vCPU
最大 Edge 数量 60 ÷ 2 vCPU/台 30 台

文档原话强调:"这里的 30 台是 64 vCPU 配额下的计算上限,不是业务 SLA。"

5.2 30 台 Edge,"峰值并发"到底是哪个数字

30 台之后,"能撑多少观众"取决于每台 Edge 的并发上限按哪个口径算,这里有四个完全不同性质的数字:

口径 单台并发 30 台合计 性质
生产实际准入(mmxNodeCapacity) 100 约 3000 今天真正在跑、调度器用来做准入判断的配置容量(EP16 已从早期的 12 上调到 100,即满容量的一个留有余量的基准值)
Lightsail 月度配额折算均值 5.5 约 165 按 3TB 月配额折算的平均并发,不是峰值
EP14 压测"干净通过"上限 250 理论 7500 单机实验室压测的清洁通过点;EP16 已把这个数字采纳为 Edge 的满容量规划上限,但仍是线性折算,没有在 30 台集群上联调验证过
EP14 压测"主机失联"故障点 300 理论 9000 单机内存耗尽的失败点,依然不是容量,不采用
64 vCPU 新加坡 Region → 30 台 Edge:四种口径下的"峰值并发"

 生产准入 (100/台)      |████████████████                    3000  ← 今天真正在跑的数字(EP16 已从 12 上调)
 配额月均 (5.5/台)      |████                                165
 压测干净通过 (250/台)  |███████████████████████████████████ 7500  ← EP16 已采纳为满容量规划上限,仍未联调过
 压测故障点 (300/台)    |████████████████████████████████████████ 9000  ← 内存耗尽的失败点,依然不是容量
                        +-----------------------------------------▶

把 EP14 压测报告里最后一档"主机失联"(300 路,内存耗尽)直接乘以 30 台,算术上确实能凑出约 9000 这个数字——但这是把"单机压测的故障点"当成了"可以安全操作的容量",混淆了口径,不采用。EP14 §3.3 已经强调过:300 不是通过点,是故障点;真正干净通过、0 断连的是 250。EP16 把这个 250 采纳为 Edge 的满容量,日常配置容量则设在 100(满容量留出安全余量的基准值,经过 EP16 上调,此前是 12)——这是比早期准入门槛宽松得多的数字,但仍不是测出来的容量上限,也从没有在 30 台的真实集群规模上做过联调验证。

结论:64 vCPU 的新加坡 Region,算力上限是 30 台 Edge;但上面四个口径有一个共同的盲区——全部都是"单台数字 × 30"的线性折算,没有一个考虑过 Origin 自己能不能把流转发给这 30 台 Edge。下一节把这个盲区补上。

5.3 后续更正:这四个口径都没算对,真正的瓶颈在 Origin 的网卡

docs/deployment/ppcdn-production-deployment-plan.md §4.8 后来补充的真实测算,推翻了"单台容量 × Edge 台数"这整条思路:Origin 用 forwardMmx 把每一路直播流全量广播给每一台 Edge,Origin 自己的出站带宽 = 同播路数 × Edge 台数 × 单路码率——这个数字和观众数量完全无关,却会在 Edge 台数远没涨到 30 台之前,就先把 Origin 自己的网卡挤爆。7500 和 9000 这两个数字,不是"偏乐观",而是从一开始就没有对的维度:它们把瓶颈设想在 Edge 一侧(CPU/内存),但 30 台 Edge 规模下,真正先顶到天花板的是 Origin。

按 7.5Mbps 的 simulcast 整梯(观众实际只拉其中一层,按 1.5Mbps/观众评估;这是本集的示意口径,与 EP11 的示意阶梯不同)重新测算,一台 5Gbps 网络档的 Origin:

Edge 台数 性质 峰值观众($12 档 Edge) 峰值观众($24 档 Edge)
15 5 Gbps Origin 安全值 ~2,700 ~3,300
22 5 Gbps Origin 理论上限 ~4,000 ~4,800
四种旧口径 vs 新口径(按 Origin 网卡约束重算)

 旧:压测干净通过线性折算 (250/台×30)  |███████████████████████████████████ 7500  ← 盲区:假设 Origin 转发能力无限
 新:22 台 Edge,5Gbps Origin 理论上限  |████████████████████                 4800  ← 考虑 Origin 自己的出站带宽
 新:15 台 Edge,5Gbps Origin 安全值    |█████████████                        3300  ← 留了余量的规划值

机型选择上还有一层:1.5Mbps 的观众负载下,$12 档(2vCPU/2GB)单台先撞到的是内存瓶颈;$24 档(2vCPU/4GB)同价多一倍内存、多 33% 流量配额,单台能多扛不少观众——同样预算,$24 档更划算,这也是上表两列差距的来源。想突破这个约 15~22 台 Edge 的规模,三条路:把 forwardMmx 从"全量广播"改成"按需转发"(只给确实有观众在看的 Edge 转发,最有效)、用多台 Origin 分层 fan-out、或者换更大网络档的 Origin 实例。

注意这个口径只算了一套 H264 梯子:如果推流端同时发布 H264 与 HEVC 两条轨(EP20),Origin 到 Edge 的转发量还要相应增加,所以 HEVC 带来的下行节省要与这部分内部转发增量一起核算,不能只看观众侧。

这次更正真正的教训不是"数字改小了",是"折算方法本身漏看了一整条链路":64 vCPU 配额算出的是能开多少台 Edge,和"这 30 台 Edge 实际能一起扛多少观众"是两个完全不同的问题——中间那一跳(Origin 能不能把流转发给所有 Edge)必须单独验算,不能假设它理所当然成立。


6. 框图

6.1 Origin/Edge 分离:1 对 N 扇出

flowchart LR
    P["ppobs 推流"] --> O["Origin<br/>唯一入口"]
    O --> E1["Edge-1"]
    O --> E2["Edge-2"]
    O --> E3["Edge-3"]

6.2 push fanout vs pull-on-demand

flowchart LR
    subgraph PUSH["push fanout:主动推(forwardMmxTargets)"]
        direction TB
        O1["Origin"] -- "持续推送,不管有没有观众" --> E1["预置核心 Edge"]
    end
    subgraph PULL["pull-on-demand:按需回源(sourceOnDemand)"]
        direction TB
        V["第一个观众到达"] -- "触发回源" --> E2["动态扩出来的 Edge"]
        E2 -. "之前无管线,零额外负载" .-> O2["Origin"]
    end

6.3 跨 region:Forward 桥接

flowchart LR
    subgraph RA["Region A"]
        OA["Origin"] -- "push" --> FA["Forward<br/>被动收 + 主动转发"]
    end
    subgraph RB["Region B"]
        EB1["Edge"]
        EB2["Edge"]
    end
    FA --> EB1
    FA --> EB2

6.4 64 vCPU → 30 Edge:四种容量口径,和被纠正的那一个

flowchart TB
    Q["64 vCPU 新加坡 Region"] --> A["Origin 2vCPU + Record 2vCPU"]
    Q --> B["Edge 可用 60 vCPU ÷ 2 = 30 台"]
    B --> C1["生产准入 100/台 → 约 3000<br/>今天真正在跑的数字(EP16 已从 12 上调)"]
    B --> C2["配额月均 5.5/台 → 约 165"]
    B --> C3["旧:压测干净通过 250/台 → 理论 7500<br/>✗ 已被 §5.3 推翻:忽略了 Origin 自己的转发带宽"]
    B --> C4["压测故障点 300/台 → 理论 9000<br/>内存耗尽,依然不是容量"]
    C3 -.->|"§4.8 真实测算"| D["新:按 Origin 网卡口径<br/>15 台 Edge ~3,300 / 22 台 ~4,800<br/>($24 档 Edge,5 Gbps Origin)"]

7. 结尾与下集预告(逐字稿要点)

这一集说清楚了横向扩容的拓扑基础:Origin 和 Edge 拆成两种角色,才有"一次推流、N 份扇出"的空间,但代价对称——Origin 自己的处理时延会被这个 N 倍放大,所以要尽量压在几十毫秒的工程预算内,这是设计取向,不是已经逐跳测过的数字。Origin 到 Edge 有两种连法:预先规划的核心路径走主动推(forwardMmxTargets),动态扩出来的边缘走按需回源(sourceOnDemand),两者分工不同、同时存在;这条链路最好走同 AZ/同 Region 的私网,省真金白银的跨区费用,也避开公网抖动,但这目前是部署层的人工约定,不是调度器强制的硬边界。跨 Region 要桥接,就引入 Forward 节点——被动收、主动转发,复用既有转发实现,代码和配置都已就位,但真实跨区域联调还没做过。节点间转发走的是 WHIP 会话,原样转发不解码不转码,"一跳约几十毫秒"是倒推出来的工程预算,不是独立测量过的逐跳实测值。最后算了一笔真账:64 vCPU 的新加坡 Region 配额,刨掉 Origin/Record,能开 30 台 Edge——但"30 台 Edge 能一起扛多少观众"不能拿单台容量直接乘以台数,Origin 自己用 forwardMmx 全量广播给所有 Edge,它自己的出站网卡才是真正的瓶颈:按 5Gbps 网络档的 Origin 测算,15 台 Edge($24 档)安全值约 3,300 观众、22 台理论上限约 4,800——比"250×30=7500"这个早先的线性折算小得多,因为后者完全没考虑 Origin 转得过来转不过来。

加 Edge 这件事,手动做的话是一套七步流程:新建机器 → 部署 mmx → 配置节点 ID 和容量 → 开防火墙、配 TLS → 在 Origin 的 forwardMmxTargets 追加新节点的私有 IP 并 reload → 若有 region 映射再更新一次 → 在 ppcenter 确认节点 online 并拉一路 WHEP 验证。这套流程分钟级起步,负载缓慢爬升时够用。

下一集,我们看一种这套流程完全来不及应付的场景——当流量不是缓慢上涨,而是突然爆发,Edge 怎么用配置容量留出弹性余量、扩容来不及时怎么靠"业务降级许可"联动自动把画质降到 720P 顶住窗口期。