EP14:节点池管理:把资源边界放进调度器¶
| 上集回顾 | EP13《P2P 直连:比 CDN 更快的最后一公里》 |
|---|---|
| 下集预告 | EP15《横向扩容和低延迟的关系》 |
0. 本集目标¶
看完这一集,观众应该能做到四件事:
- 说清楚"统一节点管理"具体指什么:ppobs 推流、pplayer 拉流,从来不自己挑节点,每一次都是 ppcenter 的一次实时调度决策。
- 分清 ppmmx 的几种节点类型,以及一台节点在控制台的全生命周期——怎么创建、怎么拿到部署凭证、怎么开始计费。
- 理解"用户添加的节点由用户独占"这句话的准确含义:这是调度层面的隔离,靠节点池实现,不是物理隔离。
- 拿到一份可核实的压测数据,知道一台 AWS Lightsail 节点大致能扛多少并发观众,瓶颈出在哪里,以及"2 vCPU / 4GB 支持 300 并发"这个说法里,哪部分是实测、哪部分是推算。
1. 统一节点管理:ppobs / pplayer 怎么拿到服务它们的节点¶
1.1 客户端从不自己挑节点¶
ppobs 要推流、pplayer 要看某条流时,它们只知道"我要推流"或"我要看这条流",从不知道、也不关心具体会连到哪一台物理机器。节点选择这件事,完整地收在 ppcenter 一处:所有 Origin / Edge 节点的在线状态、剩余容量、所属池,都实时汇总在控制面,每一次推流 / 拉流请求都是一次调度决策,不是客户端读一份写死地址的配置文件。这就是"统一节点管理"最直接的含义。
1.2 推流:ppobs 申请,ppcenter 分配 Origin¶
ppobs 发起推流时调用 POST /v1/publish/requests;ppcenter 在 SelectOriginForPublish(ppcenter/internal/manager/node_manager.go)里,先按该 app 能看见的候选池过滤出在线、健康、未排空的节点(§2.2 细讲池过滤规则),再按剩余容量最大排序(workerId 升序做平局),选出一个 Origin,把它的 WHIP 入口地址连同一次性推流 token 一起返回。此后 ppobs 的推流数据直接发给这台具体的 Origin,不再经过 ppcenter 转发。SRT 推流走的是同一套过滤与排序逻辑的另一入口 SelectOriginForSRTPublish,按流重新选边时是 SelectOriginForStream——触发时机和协议不同,规则是同一套。
1.3 拉流:pplayer 申请,ppcenter 分配 Edge¶
pplayer 发起播放时调用 POST /v1/play/requests;ppcenter 在 FindEdgesForStream(同文件)里对这条流做同样的"池过滤 + 容量择优",选出一个 Edge,把 WHEP 地址返回给 pplayer——如果这次请求同时满足 P2P 直连的准入条件,响应里还会带上 EP13 讲过的 p2p-connect 决策和签名好的 Edge 兜底地址,但节点选择本身走的还是这同一套规则。
1.4 五个调用点,一套规则¶
SelectOriginForPublish / SelectOriginForStream / SelectOriginForSRTPublish(选 Origin 的三条触发路径)加上 FindEdgesForStream / SelectEdgeForStream(选 Edge),这五个函数内部走的是同一套"池过滤 → 剩余容量排序"规则,没有各自为政的选点逻辑。这正是"统一管理"更严格的含义:不只是"有个地方记着所有节点",而是"调度决策只有一份实现,被所有入口复用"——新增一个触发场景,只是多传一个 appId 参数接进这套既有逻辑,不会再长出第二套选点算法。
flowchart TD
A1["ppobs:POST /v1/publish/requests"] --> C["ppcenter 统一调度器<br/>SelectOriginFor* / FindEdgesForStream"]
A2["pplayer:POST /v1/play/requests"] --> C
C --> D["按池过滤:只保留该 app 能看见的在线节点<br/>(§2.2)"]
D --> E["池内排序:剩余容量最大优先,workerId 升序平局"]
E --> F1["返回 Origin WHIP 地址<br/>(推流)"]
E --> F2["返回 Edge WHEP 地址<br/>(拉流,可能附带 P2P 决策)"]
2. ppmmx 节点类型区分,以及如何添加一个属于自己的节点¶
2.1 五种节点角色¶
ppcenter/pb3/mmx.proto 的 NodeRole 枚举定义了节点角色,真正对应"可以部署一台 ppmmx 服务器"的是其中五种:
| 角色 | 职责 | 是否参与 Origin/Edge 调度候选 |
|---|---|---|
NODE_ROLE_ORIGIN |
接收推流(WHIP/SRT/ERTMP),转发给 Record 与 Edge | 是,SelectOriginFor* |
NODE_ROLE_EDGE |
从 Origin 回源,向观众提供 WHEP 拉流 | 是,FindEdgesForStream/SelectEdgeForStream |
NODE_ROLE_RECORDER |
录像:按场次 split-rec,上传对象存储 | 否,走独立的录像触发路径,不进 Origin/Edge 候选集 |
NODE_ROLE_FORWARD |
跨区域桥接转发(2026-10-06 新增) | 否,设计上独立于 Origin/Edge 调度,不对自建客户开放创建 |
NODE_ROLE_STANDALONE |
独立组网:单实例同时接收推流并直接提供 WHEP 播放,不与其它节点组网、不转发、不回源 | 否,自成一体,不参与跨节点调度 |
(枚举里还有 NODE_ROLE_PUBLISHER/NODE_ROLE_PLAYER 两个值,是协议预留位,不对应实际部署的 ppmmx 服务器角色,这里不展开。)
2.2 节点池归属:谁来决定"这台节点是谁的"¶
节点创建时——不管是平台在控制台建节点,还是用户自建节点首次用 licenseCode 完成注册——都会在 mmx_nodes 表里生成一条凭证记录,PoolID 默认写入该节点所属用户的 pool-<userId>(store/mmx_node_store.go 的 NodePoolIDForUser(userID))。节点此后每次用 nodeSecret/licenseCode 重连注册,ppcenter 都会先查出这条凭证记录,再由服务端把 cred.PoolID 组装进内部调度用的结构,传给调度器——节点自己对此没有任何发言权,当前的 NodeRegister 注册消息里压根没有 poolId 这个字段。
这是一次刻意的安全收紧:一旦允许自建节点用可信程度较低的 licenseCode 注册,就不能再让节点自己上报"我属于哪个池"——否则一个自建节点理论上可以在注册时谎称自己属于别人的池,绕开"一个用户一个池"这条隔离边界。把池归属收回到控制面权威记录(节点连不上、改不了),隔离承诺才不会被节点端绕过。
2.3 控制台三步,添加一个属于自己的节点¶
这条自助添加节点的路径 2026-10-08 已上线生产、端到端验证通过:
- 登录 console,在
ppmmx菜单里"增加节点":填节点名,选节点类型(standalone/origin/record/edge四选一,不含forward——这是平台内部角色,不对自建客户开放)。提交后立即拿到一个license code;此时节点记录状态是pending,不产生任何费用,可以随便创建、放多久都不收钱。 - 在自己的服务器上跑一条部署命令:独立仓库
ppmmxDocker提供的一键脚本只需要一个参数——deploy.sh --license-code <code>(Windows 用deploy.ps1)。脚本自动装 Docker、探测公网 IP、调GET /v1/ppmmx/bootstrap?licenseCode=...查出节点类型、生成.env、拉起容器——不需要客户知道或填写nodeSecret,也不需要重新选一次节点类型(这两项在控制台建节点时就已经落库)。 - 容器启动后自动注册并开始计费:节点经
/ws/mmx注册,此后每 30 分钟加密上报一次硬盘序列号 + 网卡 MAC + license code(AES-256-GCM,密钥由HKDF(licenseCode)派生)。第一次上报成功,节点状态从pending翻转为active,计费从这一刻才开始算——按 $2.5 美元 / 实例 / 天计,不限部署时长、不限节点规模。
| license 状态 | 含义 | 是否计费 |
|---|---|---|
pending |
已创建,尚未部署或从未成功上报 | 否 |
active |
已部署,首次授权上报成功 | 是 |
soft_deleted |
账户欠费,已下发 STOP_WORK 指令下线 |
否;充值后自动清除标志、恢复 active |
stale |
连续超过 2 小时未收到上报(大概率机器已销毁/永久离线) | 否;自动停止计费,无需人工操作,下次成功上报自动恢复 active |
2.4 为什么"用户添加的节点由用户独占"¶
机制很直接:这台节点的池被固定写成 pool-<userId>;这个用户名下的 app,如果没有显式绑定到别的池,会按产品规则自动解析到自己这个 pool-<userId>;调度时先按池过滤——不属于这个池的节点,从候选集这一步就被排除,不管它剩余容量还有多少。
两点需要说准确,避免过度解读:
- "独占"是调度层面的隔离,不是物理隔离。网络、进程边界不会自动划清——如果这个用户名下有多个 app 都没有显式绑定别的池,它们会共享同一个
pool-<userId>,不是"一个 app 独占一台机器"。 - 用户自己的节点还没部署好时,不会被拦死。没有显式绑定记录的 app 自动解析到的是"自己所属用户的池",这条自动推导出来的绑定默认允许回退到共享的
default池(allowFallbackToDefault=true)——用户自己还没加节点时,先用平台共享资源兜底;一旦用户部署了自己的节点,新的调度请求立刻优先落到自己的池上,不用等旧连接断开。但凡事先声明回退,发生回退也要留痕迹(pool_fallback记录),不能悄悄发生——这条 fail-closed 设计哲学在节点池机制里是统一的。
flowchart TD
A["用户在 console 创建节点<br/>选类型,拿 license code(pending,不计费)"] --> B["ppmmxDocker:deploy.sh --license-code"]
B --> C["容器注册 + 首次授权上报成功<br/>license_status: pending → active,开始计费"]
C --> D["节点池归属固定为 pool-<userId><br/>(控制面权威写入,节点自己改不了)"]
D --> E["该用户的 app:<br/>无显式绑定 → 自动解析到 pool-<userId>"]
E --> F{"池内是否有可用候选?"}
F -->|是| G["调度落在用户自己的节点上<br/>——其他用户的 app 从候选集这步就被过滤掉"]
F -->|否,节点未就绪/离线| H["按 allowFallbackToDefault=true<br/>回退共享 default 池(留痕 pool_fallback)"]
3. 部署建议:AWS Lightsail,以及一台节点到底能扛多少观众¶
3.1 为什么推荐 Lightsail¶
生产部署方案里把 Origin/Record/Edge 放在 AWS Lightsail 而不是普通 EC2,主要是两笔账:
- 出口更便宜:按实测的 Edge 出站流量折算,Lightsail 等效出站单价约 $0.011/GB,约为同规格 EC2 出站单价(约 $0.09/GB)的 1/8。
- 同区域组网更简单:同 Region、同可用区的多台 Lightsail 实例,私有 IP 天然互通,不需要额外配置 VPC peering;入站流量只占用套餐配额、不单独计费,只有出站超出配额才收超额费。
3.2 压测数据:2 vCPU / 2 GB 节点的阶梯加压结果¶
以下数据来自 ppmmx standalone 压测报告(v4 正式稿,2026-10-07,被测软件 ppmmx v1.19.1rc064,加压工具 whep-loadgen,真实 pion/webrtc PeerConnection 发起连接、读 RTP 后丢弃)。被测节点是 AWS Lightsail(新加坡)2 vCPU / 2 GB,对一路 2.5 Mbps WHEP 流做阶梯式并发加压,每档稳定运行 2 分钟:
| 并发档位 | 判定 | 建连 | CPU 均值 / 峰值 | RSS 均值 / 峰值 | 可用内存最低值 |
|---|---|---|---|---|---|
| 12 / 25 / 50 / 75 / 100 | 通过 | 全部建连,0 断连 | — | — | — |
| 150 | 通过 | 150/150,0 断连 | 101% / 138% | 640 / 713 MB | 723 MB |
| 200 | 通过 | 200/200,0 断连 | 128% / 174% | 828 / 940 MB | 511 MB |
| 250 | 通过 | 250/250,0 断连 | 154% / 202% | 1014 / 1163 MB | 289 MB |
| 300 | 主机失联 | 客户端已连上 | 163% / 204% | 1224 / 1514 MB | 33 MB,内存耗尽 |
AWS Lightsail 2vCPU/2GB:并发 vs 可用内存(MB)
12~100 |████████████████████████████████████ (未记录具体值,全部通过)
150 |████████████████████████████ 723
200 |████████████████████ 511
250 |███████████ 289
300 |█ 33 ——内存耗尽,主机失联,被迫重启
+----------------------------------------▶
250 路干净通过,0 断连;300 路不是一个"通过的容量点",而是一次内存耗尽故障——客户端确实连上了(两台加压机各 150 路),但主机可用内存跌到约 33 MB、SSH 停止响应,节点被迫重启。报告原文结论:这台机器先撞到的是内存,其次是 CPU,不是网络(新加坡区实测公网出口约 2.4 Gbps,300 路加压全程几乎没用到)。
3.3 容量建议:"2 vCPU / 4GB 支持 300 并发",哪部分是实测、哪部分是推算¶
报告给出的实测结论是:2 vCPU / 2 GB 节点,可按约 250 路并发 2.5 Mbps WHEP 观众估算,每路预留约 4~5 MB RSS,并把内存当作约束资源——这是逐档验证(12/25/50/75/100 全通过、150/200/250 逐档验证)撑出来的实测结论,不是推算。
300 路这个数字,严格说是失败点,不是通过点。但失败的具体原因(内存耗尽,而不是 CPU 打满或网络触顶)给出了一条清晰的工程修复路径:把内存从 2 GB 翻倍到 4 GB,直接针对的就是这个具体故障模式。这也是为什么"2 vCPU / 4 GB 节点可以支持 300 并发"是一个有依据、但未被这份报告独立验证过的推荐配置——报告本身在"局限"一节写得很清楚:只测过 2 vCPU / 2 GB(Lightsail)与 2 vCPU / 4 GB(另一家出口带宽不足的提供商,瓶颈是网络而非本机资源),没有独立跑过"2 vCPU / 4 GB 在给足带宽的提供商上能扛多少路"这一组测试。
还有一个容易被忽略的细节:250 路时 CPU 峰值已经是 202%(两核基本打满)。多出来的内存解决的是内存墙,不会凭空变出更多 CPU——如果目标并发显著超过 300,CPU 大概率会先于内存成为下一个瓶颈,需要同时考虑升到 4 vCPU,而不是只加内存。
结论:把"2 vCPU / 2 GB → 实测 250 路,推荐 2 vCPU / 4 GB 争取 300 路余量"当作当前最可信的说法;如果业务目标稳定高于 300 路,在独立验证更大规格之前,应按"CPU 和内存都可能成为瓶颈"做保守预留,而不是线性外推这组数据。
4. 框图¶
4.1 统一调度:从请求到节点¶
flowchart TD
A1["ppobs:POST /v1/publish/requests"] --> C["ppcenter 统一调度器"]
A2["pplayer:POST /v1/play/requests"] --> C
C --> D["池过滤 → 容量排序"]
D --> E1["Origin(推流)"]
D --> E2["Edge(拉流)"]
4.2 节点类型与调度参与度¶
flowchart LR
subgraph In["参与 Origin/Edge 调度候选"]
O["ORIGIN<br/>接收推流"]
E["EDGE<br/>向观众分发"]
end
subgraph Out["不参与 Origin/Edge 调度候选"]
R["RECORDER<br/>按场次录像"]
F["FORWARD<br/>跨区域桥接(不对自建客户开放)"]
S["STANDALONE<br/>独立组网,自成一体"]
end
4.3 自建节点:从创建到独占调度¶
sequenceDiagram
participant U as 用户(console)
participant C as ppcenter
participant D as ppmmxDocker(用户自己的服务器)
U->>C: 创建节点(选类型)
C-->>U: license code(pending,不计费)
U->>D: deploy.sh --license-code <code>
D->>C: GET /v1/ppmmx/bootstrap(查节点类型)
D->>C: WS /ws/mmx 注册
D->>C: 每 30 分钟:加密上报 SN+MAC+license(COMMAND_TYPE_LICENSE_REPORT)
C-->>D: LICENSE_ACK(valid=true)
Note over C: 首次上报成功:pending → active,开始计费 $2.5/实例/天
Note over C: 节点池固定为 pool-<userId>;该用户的 app 默认解析到此池,调度优先落在这台节点上
5. 结尾与下集预告(逐字稿要点)¶
这一集讲了三件事:PPCDN 的节点管理是统一的——ppobs 推流、pplayer 拉流,从来不自己挑节点,每一次都是 ppcenter 按池过滤、按剩余容量排序做出的实时调度决策,五个调用入口共用同一套规则。ppmmx 有五种节点角色,Origin/Edge 参与调度候选,Recorder/Forward/Standalone 各走各的路径;用户可以在控制台自助添加节点,拿到 license code、一键部署、首次上报后开始计费,这台节点的池被固定写成属于这个用户,调度层面的"独占"就是这么来的,不是物理隔离。最后是一份可核实的压测数据:AWS Lightsail 2 vCPU / 2 GB 节点实测能干净扛住 250 路 WHEP 并发,300 路会因内存耗尽失败;"2 vCPU / 4GB 支持 300 并发"是一个有依据的推荐配置,不是这份报告独立验证过的数字,这个区别值得记住。
下一集,我们把镜头拉远一点:横向扩容和低延迟之间到底是什么关系——加机器这件事看起来简单,其实也有代价和取舍。