进入架构进阶部分。这一篇讲两件事,它们分别回答「怎么更便宜、更快」和「怎么把资源边界管住」:
-
**P2P 直连**:让媒体绕过边缘节点,从推流端直接到观众------理论上延迟最低、且不占边缘出口带宽的「最后一公里捷径」。
-
**节点池调度**:在「角色 + 区域」之外再加一维资源隔离边界,决定一个请求**能看见哪些节点**。
第一部分:P2P 直连
1. 预测与实测:标签只是预筛选,ICE 检查才决定能不能连上
「Full cone / restricted / symmetric NAT」这套经典分类,在现代 ICE 实践里被认为是**过度简化**。更准确的描述是分别观察:
-
地址/端口的 **mapping behavior**(映射行为);
-
入站包的 **filtering behavior**(过滤行为);
再结合是否支持 hairpinning、映射寿命、IPv6、UDP 是否可用、网络是否切换综合判断。CGNAT 只说明地址转换发生在运营商网络,**不等于必然无法打洞**。
但现实中的实现往往还在用「经典类型标签做硬性预筛选」这个简化版本,比如:只要任意一方报了 symmetric 或 CGNAT,就直接拒绝,连 ICE 都不尝试。
这是不是一个错误?**不是。** 对称型 NAT 和 CGNAT 组合的打洞成功率本来就低,提前拒绝能省掉一次几乎注定失败的信令和 ICE 开销,是合理的工程取舍。但不应该把它包装成「系统已经在用 mapping/filtering 行为做精细判断」------**准确的说法是:理想的 ICE/NAT 工程实践基于行为观测分级处理,而简单实现仍在用标签做硬性预筛选。**
关键要分清两个概念:
```
预筛选通过("值得尝试") ≠ 已经证明这两端能连上
真正决定直连成不成的,永远是客户端那一次 ICE candidate-pair 连通性检查
```
预筛选只负责把明显没希望的组合提前挡在门外省开销,**不负责预测「这一次具体会不会成功」**。
2. 诚实的边界:网络世界没有数学意义上的 100%
必须说清楚:**NAT 预筛选不可能在互联网环境下提供数学意义上的 100% 成功保证。** 即使通过了全部预筛选规则,真正尝试连接时仍可能因为防火墙策略、网络切换、地址映射恰好过期而失败。
所以「ICE 可能失败」必须配一条兜底路径。这里有一个值得强调的设计演进:**从「并行竞速」改成「顺序回退」。**
-
早期一些实现让直连和边缘**并行起跑**,谁先出帧用谁,用一个窗口限制等待时间。这会让首帧时间不可预测、建连逻辑复杂。
-
更清晰的模型是**顺序回退**:客户端先尝试直连;失败、超时或名额已满,就直接改用**随决策一起下发、早已拿在手里的边缘地址**。不是两条链路抢首帧,是一条主路径 + 一个随时能用的备用地址。
3. TURN 与 Edge:这是产品取舍,不是技术优劣
TURN 是 ICE 的标准 relay(中继)来源,它的价值是提高受限网络、防火墙或 UDP 不可用环境下的连接成功率,代价是中继带宽、部署运维以及可能增加的路径时延。
如果产品**已经有成熟的边缘媒体路径**,完全可以选择「直连失败后回退边缘」,而不部署 TURN。这是成本、协议一致性、覆盖率和运维复杂度之间的取舍。但要诚实:这个选择的代价是**对称 NAT / CGNAT 用户完全没有「走中继也能直连」的退路,只能走边缘**。如果将来要覆盖这批用户,TURN 部署会是绕不开的一步。
4. 名额分配:为什么要用「原子租约」
P2P 直连会占用推流端自己的上行带宽,所以不能无限制地让所有观众都跟同一个推流端直连。一个常见做法是限制「每个推流端同一时间最多服务 N 路直连」,第 N+1 个观众必须走边缘。
这个上限保护两件事:推流端上行不会被「一拖多」拖垮,以及已建立的几路直连不会因新连接加入而质量下降。
但这里藏着一个并发系统里的经典问题:如果多个观众几乎同时请求「判断能不能跟这个推流端连」,光靠「查询当前有几路,小于 N 就同意」会出现竞态------两个请求都查到「当前是 N−1 路」,都判断「还有名额」,结果同时获批,实际建立了 N+1 路。
**解决办法是把「检查名额是否够 + 占用一个名额」合并成一个不可分割的原子操作(atomic lease)**:请求进来时直接尝试原子性地扣减一个名额,扣减成功才继续,失败就直接判定不满足资格、返回边缘。这样不管多少请求同时涌进来,最终拿到名额的最多只有 N 个,不会超发。
> 这类「资源租约」的设计模式不是 P2P 专属。任何「有限资源、高并发申请」的场景都会遇到同样的问题,用的是同一类解法。
5. 信令走控制面,媒体走直连
直连建立前,Offer、Answer、ICE candidate 这些信令消息都要经过控制面转发。但控制面在这里做的只是**转发和身份校验**,不参与媒体数据本身(呼应架构篇的「控制面不碰媒体」)。校验的核心是确认信令双方的**身份和会话确实匹配**,防止有人把信令注入到别的直播流或别的用户的会话里。
信令通过之后,媒体数据走推流端和播放端之间的直连通道,完全不经过控制面。
6. 诚实的现状
一次真实的 P2P 直连跑通约 70ms,是特定实验条件下的端到端观测,不是理论值,也不是所有网络的承诺。连接可靠性近期有改善,但**「在测过的这些网络组合下能连上」不等于完成了覆盖各类 NAT 组合、CGNAT、多运营商、IPv4/IPv6、UDP 受限和网络切换的系统性验收**。设计完成、单次连通验证、大规模系统性验收,是三个不同阶段。
第二部分:节点池调度
7. 问题:为什么「角色 + 区域」两维不够用
很多系统的节点筛选只有两个维度:**角色**(Origin 还是 Edge)和**区域**(region/city)。同一角色的所有在线节点被当成一个大资源池,不区分这批机器归属谁、给谁用。
下面这些场景就不够用了:
-
**大客户/高优先级应用需要独占一批节点**,或同一租户的多个应用需要共享一批保留资源,避免被其它租户挤占;
-
**不同硬件规格/编码能力的节点要分开调度**,比如高规格节点只服务特定套餐;
-
**新版本或刚上线的节点需要先放进「灰度池」验证**,不能立刻进入生产候选;
-
**运维希望按团队或合同边界拆分节点的管理权限和可见范围**。
共同点是:需要在「角色 + 区域」之上再加一维**节点池**,作为资源的隔离边界。这一维不是给人分三六九等的「标签」,而是一条**「谁能被看见」的硬约束**。
8. 机制:池是第一层过滤,池内只按空闲度择优
一次调度请求的处理顺序:
```
调度请求(role, appId)
│
▼
解析请求所属的池
(app 显式绑定优先;否则按租户/用户默认池;都没有 → 共享池)
│
▼
按池过滤:只保留属于该池的可用节点
(不属于该池的节点,从这里开始就不在候选里)
│
▼
池内排序:剩余容量最大优先
│
▼
返回最优节点;主池无可用候选 → 进入回退判定
```
关键点是:**池过滤发生在最前面**。池内的排序再怎么变,都改不了「别的池的节点压根不在候选里」这个事实------这就是隔离的落点。
节点池的归属通常是**部署时静态确定**的属性,和区域一样,不是每次调度都变的动态数据。
9. 池归属由谁决定:从「节点自报」到「控制面权威分配」
这里有一段值得讲的演进。早期设计是**节点自报**:节点在部署配置里写一个池 ID,注册时上报给控制面。
但当系统允许自建节点用较低信任度的凭证注册之后,这套做法必须收紧:**如果允许节点自己上报「我属于哪个池」,一个自建节点理论上可以谎称自己属于别人的池,绕开隔离边界。** 所以池归属收回到控制面权威记录------节点创建时由控制面写入归属,节点对此没有发言权,也改不了。
这是一个很好的安全设计例子:**隔离承诺要成立,就必须把「声明归属」的能力从不可信的一方手里拿走。**
10. 回退语义:fail-closed,不静默突破隔离
这是整套机制里最需要讲清楚的一条。假设某个应用绑定了池,但池内**没有可用候选**,怎么办?(注意「没有可用候选」包括无节点、节点离线或不健康、处于排空状态、没有节点满足准入条件等,不能只用「节点数量为零」判断。)
两种做法语义完全不同:
-
**静默回退到共享池**:调度「永远不失败」,但隔离承诺被悄悄破坏了------业务方以为自己在用独占资源,实际已经跑到公共池上,成本核算和隔离预期全部错位。
-
**显式配置 + 显式告警**:回退是一个需要**主动打开**的开关,并且**默认关闭**。
推荐后者,默认 fail-closed:
```
主池内没有可用候选
│
▼
允许回退 ?
┌────┴─────┐
false true
│ │
▼ ▼
调度失败 在共享池重新计算候选
- 池耗尽告警 ├─ 有候选:回退成功 + 记录一条可区分的回退事件
└─ 无候选:调度失败
```
设计哲学很简单:**隔离是一条承诺,静默回退就等于承诺失效。** 所以它必须由业务方显式打开,而且无论哪种结果都要留下痕迹------要么失败被告警,要么回退被记录,绝不能「看起来一切正常」。
11. 两个「看不见」的用法:移出调度与灰度池
池机制里还藏着两个很实用的边界用法,靠的是同一个原理------**只要一个池没有任何应用解析到它,池里的节点就不会被任何调度选中**:
-
**`unassigned`(移出调度)**:一个专属的哨兵池。任何应用都不会解析到它,所以被置为该池的节点等于被干净地「拔出调度」,但进程还活着、连接还在,可以随时再搬回来。这就是后台「移除节点」按钮背后的动作。
-
**灰度池**:把新节点注册到一个暂时没有任何应用绑定的池里,它就自然地「隐身」在生产调度之外------不参与候选,但心跳、容量上报、健康检查都照常。等验证通过,再搬进真正的生产池,不需要为灰度单独发明一套流程。
12. 诚实的边界
-
**池约束的是「控制面把请求分给谁」**,不是天然的物理隔离。共享池内部仍可能由多个应用共用节点,网络和进程边界不会自动隔离。
-
**它通常只覆盖 Origin/Edge 两个角色。** 录像、跨区域桥接等角色如果走独立调度路径,就不在这套隔离机制的管辖范围内。
-
**「设计能力」和「产品暴露的能力」是两回事。** 设计里可以支持任意命名池、池优先级、节点多对多等灵活形态,但产品往往把常见诉求收敛成「一人一池 + 可选的 app 显式绑定」这样更简单、更保守的形态。
小结
-
**P2P 直连**:标签预筛选只负责「值得尝试」,ICE 检查才决定能不能连上;不承诺数学意义的 100%;用顺序回退而不是并行竞速;有成熟边缘路径时可不部署 TURN,但要接受 CGNAT 用户没有中继退路。
-
**名额分配**:用原子租约防止并发超发。
-
**信令走控制面、媒体走直连**:控制面只转发和校验身份。
-
**节点池**:在角色/区域之上加一维隔离边界,先按池过滤、池内按空闲度择优。
-
**归属权威化**:池归属由控制面决定,节点不能自报,防止绕开隔离。
-
**回退 fail-closed**:默认不回退,显式打开且留下痕迹,绝不静默突破隔离。
下一篇,我们把镜头拉远:**横向扩容和低延迟到底是什么关系**,以及当流量不是缓慢上涨、而是突然爆发时该怎么办。
**系列导航**
- 上一篇:06 · 可观测性(06-可观测性-端到端延迟稳定性与画质.md)