跳转至

EP16:如何应对突发性高并发场景

上集回顾 EP15《横向扩容和低延迟的关系》
下集预告 EP17《从零部署:15 分钟跑起你自己的 CDN》

0. 本集目标

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

  1. 分清"负载大"和"负载来得急":突发高并发的难点不是总量,而是峰值来得比你的反应快——来不及手动扩容。
  2. 讲清楚三种常见手段的边界:P2P 只卸载每个 publisher 的固定少量观看,按需回源减少空闲上游开销,发布端降级保护发布链路;观众突发容量仍要靠预置 Edge、准入和扩容。
  3. 知道弹性机制兜不住时会发生什么:软阈值告警 + 人工扩容,以及它在突发场景下的真实局限(无自动扩容器、告警通道的 30 秒冷却会吞告警)。
  4. 正确理解云厂商额度:月度出站额度通常是计费阈值,超出后转为按量收费,并不天然等于断流;只有产品另有硬限额或欠费策略时才会影响服务。

1. 开场钩子(逐字稿要点)

上一集我们讲了横向扩容买到的是容量。但现实里的流量往往不是"缓慢上涨、让你从容加机器",而是——某场活动到点了,几秒钟之内,几百上千人同时点进来。这就是突发性高并发。这一集要回答的问题是:当负载不是"大",而是"来得急",这套系统靠什么顶住?诚实地说,它没有一台"自动扩容器"帮你瞬间加机器。它靠的是几层弹性机制,加上一套人工流程。这一集把这几层机制讲清楚,也把它们兜不住的地方老实讲出来。


2. 突发高并发难在哪:不是"总量大",是"来得快"

先区分两种负载:

  • 均匀高负载:用户数缓慢爬升,你看到容量告警,有时间按部就班加机器。这是 EP15 讨论的场景。
  • 突发高并发:流量在很短时间内冲到峰值,峰值远高于均值。典型来源是活动的开场瞬间、热门内容被分享出去、开播那一刻全体观众涌入。

后者的真正难点不在"总量",而在时间尺度:

  • 预判不了:你不知道这次到底会涌进多少人。
  • 来不及手动扩容:扩容目前是一套人工流程(EP15 讲过那 7 步),分钟级起步;而突发是秒级的。等你把机器加好,洪峰可能已经过去了——或者已经把某一层压垮了。
  • 放大了所有单点:平时能扛住的单台 Origin / 单台 Edge,在峰值下会先于其它环节饱和。突发本质上是把系统里最薄的那一环照出来。

所以应对突发,靠的不是"准备得足够大",而是有没有能在载荷变化的瞬间自动伸缩的东西。这套系统里有三样。


3. 辅助卸载:P2P 提供固定上限的分流

P2P 直连(EP13)可以减少一部分 Edge 出站,但必须先看供给端是谁。在这个案例里,媒体由 publisher 直接服务少量 viewer,并不是 viewer 再转发给 viewer:

  • publisher 把上行富余带宽用于直连 viewer;观众本身不成为新的分发节点。
  • 每个 publisher 同时最多服务 maxP2PSessions 路 P2P——平台默认值 3,超管可在后台 /admin/play-settings 调整(上限 50),/v1/publish/requests 把这个数字一起返回给 ppobs,ppobs 不再像早期版本那样把 3 写死在客户端里。因此单路直播无论从 10 个还是 10 万个观众增长,P2P 卸载的都是这固定的 N 路 Edge 观看,不会随观众数扩大。
  • 对大量 publisher 同时开播的业务,总卸载量可以随 publisher 数量增加;但这仍不是单个热门流的突发容量防线。

当然它不是无限的,两个必须讲清楚的边界:

  • 每个推流端同一时间最多服务 maxP2PSessions 路 P2P 直连(默认 3,EP13 的原子租约在分配时原子检查"已占用路数 ≥ 上限"),超过这个数的观众必须走 Edge。这个上限是平台级可调参数,不是写死在二进制里的常量,但不会因为单场直播的观众暴涨而自动变大,所以 P2P 仍只是一个有上限的成本优化,不是观众突发时的自动扩容。
  • P2P 的命中依赖真实网络行为。理想的 NAT 工程实践应该分别判断 mapping/filtering 行为;但 PPCDN 当前的服务端预筛选还停留在标签层——命中 symmetric/cgnat 标签就直接拒绝、连 ICE 都不会尝试,这是一个诚实的差距,不是已经做到的精细判断(见 EP13)。
  • 回退机制是服务端按编码格式提前分支后的顺序模型,不是"P2P 与 Edge 并行竞速":H264 客户端先单独尝试 P2P,失败或超时才改用随决策一起下发的 Edge 地址;HEVC 客户端直接走 Edge,不尝试 P2P。具体的超时/宽限时长应由真实网络样本校准,而不是固定写死。

一句话:P2P 可以降低少量 Edge 出站,但每个 publisher 默认最多 3 路(超管可调,不会随直播观众数自动变化);突发容量规划不能把它算作会自动扩张的资源。


4. 第二道防线:按需回源 + 预置空闲 Edge

第二层弹性的思路是:让空闲机器的成本接近于零,于是你可以提前铺很多台待命。

  • 按需回源(EP15 讲过):Edge 在观众到来之前,跟 Origin 之间没有管线;没有观众的 Edge,对 Origin 的额外负载接近 0。
  • 因此预置空闲 Edge 是划算的:你可以提前在多个区域起一批边缘机器,它们在没人看的时候几乎不消耗 Origin 带宽,只占自己的运行成本。
  • 突发来临、观众落上来时,这些 Edge 各自按需回源、开始分发。容量是"按观众的实际落点动态启用"的,而不是从一开始就全量占用上游。

代价仍然要诚实:按需回源有冷启动建链和首帧等待。可以通过并行尝试其它可用路径与合理超时降低影响;同一 Edge 上同路流的观众共享回源链路,则由第一个观众触发回源,后续观众复用。


5. 发布链路降级:保护 publisher,不替代观众容量

发布端自适应降级的目标,是在 publisher 上行或入口链路拥塞时保护主发布链路。它可能降低后续分发的单路码率,但触发信号和控制对象仍在发布侧,不能把它当作观众并发突发的容量控制器。

这套系统的降级是分层的、有顺序的(EP07 / EP11 讲过机制,这里从"突发"的角度重述):

  • 触发信号来自协议原生统计和产品派生指标:SRT 与 RTP/WebRTC 的原始计数语义不同,需要结合统计窗口、恢复结果和播放 deadline 再做决策,不能只把某个 counter 改名为统一 ULR。
  • 先压码率,再砍分辨率:第一步只降编码器码率(obs_encoder_update 实时生效、不中断推流),100% → 80% → 60%;码率压到底仍不达标,才进入分辨率阶段,砍掉最高的同播层(约 1s 中断)。
  • 恢复要慢、要稳:质量指标回落到恢复区间并持续一个观察窗口后才逐级恢复,分辨率阶段每加回一层还要再次确认不反弹——避免"加了又砍、砍了又加"的震荡。阈值与观察时间都是特定版本的产品参数,必须按协议和真实网络样本校准。

码率下降后,Edge 的每路出站字节数可能随之下降,这是附带收益;但如果 Edge 的瓶颈是连接数、CPU、内存或调度槽,而不是带宽,发布降级并不会增加 viewer 容量。观众洪峰仍要靠预置容量、负载均衡、准入/排队、限流和自动或人工扩容处理。

边界要说清楚:发布降级是针对 publisher 主链路质量的保护,不是观众突发容量防线。它治不了 Edge 连接数或机器数量不足,也不应等到 viewer 洪峰才触发。


6. 弹性兜不住时:软阈值告警 + 人工扩容

如果峰值突破了三层弹性能扛住的上限,剩下的就是监控 + 人。

现有的信号有三类:

信号 触发 含义
capacity_high 单节点已用/容量 ≥ 80%(warning)、≥ 95%(critical) "这台快满了,该准备加机器"
pool_exhausted app 解析到的主池无可用候选且禁止回退 调度直接失败
pool_fallback 主池无可用候选、允许回退且共享池有候选 落到共享池(默认只记日志,不告警)

这套信号的设计意图就是"扩容触发器":容量到 80% 就该行动。但放在突发场景里,它有几个必须讲清楚的局限:

  • 没有自动扩容执行器。告警只是通知人,人再走那套人工 7 步流程。对"秒级突发"来说,这个反应链太慢了——扩容是追着洪峰跑,而且是事后追。
  • 每条告警通道都有一个独立的全局 30 秒冷却:通用 webhook 和 Telegram(生产目前接的是 Telegram 运营群)各自维护自己的冷却锁,但对同一条通道而言,只要 30 秒内已经发过一次,后面所有告警(不管哪个节点、哪种类型)都会被静默丢弃。突发时往往多个节点同时到阈值,结果只有最先到达的一条通知发得出去。这是一个真实的缺口——最需要密集告警的时刻,恰恰是告警被吞得最厉害的时刻。生产已经因为这条告警管线暴露过一次真实 bug(2026-09-15 修复):CheckTrafficQuota/CheckCapacity 等四个检查点只要指标低于 resolve 阈值就无条件重发一次"resolved"通知,导致 Telegram 群从 2026-09-14 17:32 UTC 起每小时收到一条重复的 traffic_quota ... 0% of quota,直到改成只在真正解除了一条 active 告警时才通知才停止——这也说明这整套告警管线是真的在生产跑、真的会被突发或异常状态放大,不是纸面设计。
  • pool_exhausted / pool_fallback 不落库,未配置 webhook 时只在服务端日志里可见,后台看不到。

还有一个在突发场景里绕不开的取舍:隔离(fail-closed)和可用性(fallback)是冲突的。 app 解析到的主池如果没有可用候选且配置为不回退,调度会直接失败;池可以是 app 独占池,也可以是租户/用户共享池。要不要临时借用默认共享池,是业务优先级问题,而且共享池本身也必须有可用候选。


7. 第四个信号:云厂商出站流量配额——已有自动告警,但仍是近似值

还有一项指标早期版本完全看不到、现在已经补上了一部分:云厂商的月度出站流量额度。

  • 套餐内出站流量通常是一个计费阈值(例如 AWS Lightsail 的月度流量额度)。突发会快速消耗额度;超出后通常产生额外费用,而不是自动断流。是否限速或停服必须以具体云产品、区域和账户策略为准。
  • ppcenter 的并发容量监控(capacity/pipelines)本身确实不是字节流量,这一点没变。但系统新增了 TrafficQuotaChecker(internal/manager/traffic_quota_checker.go,2026-09-13 提交、2026-09-15 确认在生产生效):它每小时汇总每个节点本月的下行字节数(TrafficUsageNodeDaily,由 Edge 节点既有的用量上报管线产出,和计费用的是同一份数据),跟 alarm.trafficQuotaBytes(默认 3TB,对应三节点统一的 AWS Lightsail $12/mo 档位)比较,到 70%(alarm.trafficQuotaWarning)自动抬出 AlarmTrafficQuota,回落到 60%(alarm.trafficQuotaResolve)自动清除——走的是和 capacity_high 完全一样的告警管线,一样可配 webhook/Telegram,一样受上一节那条 30 秒冷却约束。生产 Telegram 运营群从 2026-09-14 起已经在真实收到 traffic_quota origin-node-1 ... monthly traffic N% of quota 这类通知。
  • 但这仍不是"对接了云厂商账单 API":TrafficQuotaChecker 比较的是节点自己上报的字节数 vs. 写在 ppcenter 配置里的一个固定数字,不是 AWS 账单系统的真实用量。云厂商套餐、区域或计费策略一旦变化(换机型、换流量阶梯),trafficQuotaBytes 不会跟着自动更新,需要运维手工同步这个常量;而且它按节点算的是字节近似值,不是账户级别的美元成本。真正精确的月度账单仍然只能登录云控制台核对。
  • 结果是:以前"并发告警完全看不到流量花了多少"这个问题,现在已经用一份自报字节数的近似值解决了一半;但"这个配额数字是否还贴合当前云账单"这件事,仍然要靠人工盯着云厂商的套餐和价格变化去维护。

换句话说:突发可能在节点饱和前先把成本曲线推过套餐内额度——现在至少有一层基于自报字节数的自动预警(70% 发警、60% 清除),比早期"纯靠人工登录控制台"好了一截,但配额数字本身仍是需要人工维护的近似值,不应被当成和云厂商账单一样精确。这首先是预算问题,不应被描述为必然的可用性中断;若供应商确有硬限额,则另设服务风险告警。


8. 诚实的现状与本集立场

把这一集的结论收一下:

  • 今天应对观众突发的主要能力 = 预置 Edge + 按需回源 + 调度/准入 + 人工扩容。它没有自动弹性伸缩。
  • P2P 每个 publisher 默认最多只卸载 3 路(超管可调,但不会随观众数自动变大);发布降级保护 publisher 链路。两者都不能替代热门流的 Edge 容量。
  • 所以对突发的现实策略是:
  • 提前把空闲 Edge 铺出去(它们闲时几乎不吃上游带宽),让突发有地方落;
  • 把 P2P 默认的 3 路上限当作固定卸载收益,不要计入随 viewer 增长的容量;
  • 把扩容流程脚本化,把人工 7 步压到分钟以内;
  • 把容量实测校准(EP15 讲过:origin/record 的 mmxNodeCapacity 目前仍偏乐观,edge 已相对接近经济性推导值),别让软阈值保护一个虚高的分母;
  • 让 TrafficQuotaChecker 的配额常量跟着云账单走——字节级监控已经自动化了,但 trafficQuotaBytes 是写在配置里的固定数字,云厂商套餐一变就得有人去改它,否则这层告警会悄悄失真。
  • 最后一条立场:突发会放大所有单点。单区域部署、单台 Origin、单台 Edge,在均匀负载下也许够用,但在突发下就是最先断的地方。冗余(N+1)在低负载阶段的价值,往往比"再多一点容量"更高——这正是 EP15 结尾留下的那句话。

9. 框图

9.1 突发高并发的三层弹性防线

负载瞬间冲高
      │
      ▼
┌───────────────────────────────────────────────┐
│ 辅助卸载:P2P 直连                               │
│  每个 publisher 默认最多 3 路(超管可调)           │
│  不随 viewer 增长;失败顺序回退 Edge,非并行竞速     │
└───────────────────────────────────────────────┘
      │ 仍有流量落到 Edge
      ▼
┌───────────────────────────────────────────────┐
│ 第二层:按需回源 + 预置空闲 Edge                 │
│  空闲 Edge 对 Origin 负载 ≈ 0 → 可提前铺很多台    │
│  观众落上来才回源,同路观众共享一条回源链路        │
└───────────────────────────────────────────────┘
      │ 节点/链路开始拥塞
      ▼
┌───────────────────────────────────────────────┐
│ 发布链路保护:自适应降级                          │
│  ULR 超标 → 先压码率(不中断)→ 再砍最高层        │
│  保护 publisher;不解决 Edge 连接数/机器不足       │
└───────────────────────────────────────────────┘
      │ 三层都兜不住
      ▼
┌───────────────────────────────────────────────┐
│ 兜底:capacity_high 告警 → 人工扩容(7 步流程)   │
│  ⚠ 无自动扩容执行器;每条通道 30s 冷却会吞告警     │
│  ⚠ 流量配额已有 TrafficQuotaChecker 自动告警,    │
│    但配额数字仍需人工跟云账单同步                  │
└───────────────────────────────────────────────┘

9.2 隔离承诺与突发可用性的冲突

app 的主池没有可用候选
        │
        ▼
allowFallbackToDefault ?
        │
   ┌────┴──────┐
  false        true
   │            │
   ▼            ▼
调度失败      回退到 default 共享池
(守住隔离     (保住可用性,
  承诺,         但隔离承诺临时失效
  牺牲可用性)   且默认不告警)

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

突发高并发最终要靠可用的 Edge 容量、调度准入和足够快的扩容。按需回源让预置 Edge 的上游空载成本较低;P2P 每个 publisher 默认只卸载最多 3 路(超管可调,但不会随观众数自动变大);发布降级保护主发布链路,而不是观众容量。云厂商月度额度主要改变超额成本而不是自动断流——现在系统里有 TrafficQuotaChecker 按节点自报字节数自动告警,但那只是一个近似值,配额数字本身仍要人工跟云账单同步。

到这里,Module 3「架构进阶」告一段落。从下一集开始进入 Module 4「实战与验证」——第一件事,就是把整套系统从零部署起来,亲手跑一遍前面讲的这些机制。