跳转至

EP17:从零部署:15 分钟跑起你自己的 CDN

上集回顾 EP16《如何应对突发性高并发场景》,Module 3 结束
下集预告 EP18《录像与回放:给直播加上 VOD》

0. 本集目标

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

  1. 把仓库里的 docker compose 当作当前部署模板来审查和运行,并说清每个组件在拓扑里的位置、端口与前置修复。
  2. 看懂这份本地编排里,哪些配置项刚好对应前面几集讲过的机制(节点池 / 容量 / 按需回源 / ABR / 节点鉴权)。
  3. 分清"环境起来了"和"端到端验证过了"——冒烟测试验证的是控制面健康,不是一条真实的推流到播放链路。

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

前面十六集讲了通用的媒体系统机制,这一集用 PPCDN 仓库里的 compose 作为部署案例。先明确状态:它是当前部署模板,不是开箱即用的发布包;原样运行前还缺数据库配置、mmx 镜像和 TLS 文件一致性修复。下面给出的命令会始终使用同一组 compose 文件,但只有完成这些前置项后才是可运行流程。“容器起来”和“媒体端到端跑通”仍是两回事。


2. 先看清要起哪几个组件

这套系统不是"一个进程",本地编排要同时拉起四样东西,正好是前面几集反复出现的那张拓扑图:

组件 容器名 端口 角色
nginx ppcdn-nginx 80 / 443 前门,统一入口:/origin/、/edge/、/v1/、/ws/ 的反向代理
ppcenter ppcdn-ppcenter 8090 控制面:节点注册、调度、鉴权、告警、管理后台
mmx-origin ppcdn-mmx-origin 1935(RTMP 入)、8889(WebRTC)、8189/udp 媒体面:唯一推流入口
mmx-edge ppcdn-mmx-edge 8890(WebRTC)、8190/udp 媒体面:从 Origin 回源、向观众分发

外加两个依赖:MySQL(ppcenter 的持久化,必需)和 Redis(默认关闭;控制面在无 Redis 时以只读模式运行)。

三个必须提前知道的依赖,别被"15 分钟"误导:

  1. mmx 镜像不来自本仓库。ppmmx 是独立仓库(本 monorepo 里只有它的配置文件);compose 里用的是 ppcdn/mmx:latest 镜像,你需要先从 ppmmx 仓库构建或获取这个镜像。
  2. ppcenter 硬依赖 MySQL。deploy/config/ppcenter-local.json 当前数据库字段为空;必须至少填 host=mysql、用户、密码和 dbName=philcdn,并保证与 MySQL override 一致。
  3. nginx 的 TLS 挂载当前不完整。deploy/nginx.conf 引用 /etc/nginx/tls.crt 和 /etc/nginx/tls.key,但 compose 没有挂载它们。运行前必须修复模板:开发环境可移除 443 SSL listener;需要 WebRTC/公网访问时应生成证书并把证书、私钥挂载到这两个路径。

因此「15 分钟」只适用于镜像已准备、数据库配置已填、TLS 模板已修复的环境。未满足前置条件时,本文不承诺原样命令可运行。

这不是唯一的自建路径

本集讲的是把整套控制面(ppcenter + mmx-origin + mmx-edge + nginx)自己跑起来。如果你的目标只是往别人已经在运营的 PPCDN 控制面上加一个自己花钱买的节点(比如只想贡献一台 VPS、按授权付费,不想自己运维调度/鉴权/计费),有一条更短的路:购买 ppmmx 授权(license code),在独立的 ppmmxDocker 仓库里跑一条 deploy.sh --license-code <code>,节点会以 NODE_ROLE_STANDALONE 角色(单实例同时接收推流并直接服务 WHEP 观众,不与其它节点组网)自动探测角色、注册、按授权计费,不需要本集的任何一步。这条路径已经上线且有真实自建客户在用(见 docs/deployment/ppcdn生产部署总结.md 2026-10-06 及以后多条记录)。它在 2026-10-08 暴露并修复过一个组合缺陷:licenseCode 注册时 ppcenter 从没把内部解析出的 nodeSecret 回传给节点,节点拿不到凭据,白名单永久同步失败、任何 appId 都推不进流;修复后改由节点自行上报密钥,console 也不再展示 nodeSecret。两条路径解决的是不同问题,不要混着看。


3. 15 分钟的步骤

第 1 步:拿到代码

git clone <ppcdn 仓库>
cd ppcdn

第 2 步:起依赖(MySQL / Redis)

默认 compose 不含数据库,用 override 叠上:

先定义本节所有命令使用的同一组文件:

COMPOSE="docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.mysql.yml"
$COMPOSE up -d mysql redis

它会起一个 MySQL 8.0(库名 philcdn)和一个 Redis 7,端口分别是 3306、6379。

第 3 步:填本地配置

编辑 deploy/config/ppcenter-local.json——它会被挂载成容器里的 /app/conf/ppcenter.json。重点三项:

  • mysql.host/user/password/dbName:指向刚起的 MySQL(主机名 mysql)。当前 override 只设置 root 密码,因此模板至少应填 user=root、password=ppcdn_local、dbName=philcdn;生产环境应改为专用用户和密钥。
  • nodeAuth.bearerToken:平台级节点接入 token,本地默认值是 local-dev-token-change-in-prod,生产环境必须换掉(EP06 讲过的边界)。
  • redis.enabled:想启用 Redis 就设为 true 并填地址;不启用时控制面以只读模式跑。

第 4 步:准备镜像并启动

先从独立 ppmmx 仓库准备 ppcdn/mmx:latest,并完成上面的 TLS 修复,然后在本仓库执行:

docker build -t ppcdn/ppcenter:latest -f deploy/ppcenter.Dockerfile ppcenter
$COMPOSE up -d --build

这里不使用 make docker-up,因为它只加载基础 compose,会漏掉本节所需的 MySQL/Redis override。

第 5 步:等健康检查通过

ppcenter 容器定义了 healthcheck:每 10s 打一次 http://localhost:8090/health。等它 healthy:

$COMPOSE ps
curl -f http://localhost:8090/health

第 6 步:访问

  • ppcenter 管理后台:http://localhost:8090(超管 SPA,构建时已打进去)。
  • 统一入口:http://localhost/(nginx),/origin/ 和 /edge/ 分别反代到两台 mmx。
  • Swagger:仓库里的 ppcenter/bin/docs/swagger.yaml。

第 7 步:收拾

$COMPOSE down -v

4. 当前冒烟模板与可运行替代命令

仓库有 make docker-smoke,但当前版本不能作为上述模板的可运行一键入口。deploy/smoke-test.sh 的 COMPOSE_FILE 从 deploy/ 目录又拼接了 deploy/docker-compose.yml,路径不一致;脚本也没有加载 MySQL override,且 nginx TLS 挂载仍未解决。修复这些问题前,请使用上一节同一组 $COMPOSE 命令,并手工执行以下控制面检查:

$COMPOSE up -d --build
curl -f http://localhost:8090/health
curl -i http://localhost:8090/admin/nodes
curl -i -X POST http://localhost:8090/v1/play/requests -H 'Content-Type: application/json' -d '{"streamName":"test","clientId":"smoke","requestRegion":"local","capabilities":["whep"]}'
$COMPOSE down -v

这些命令验证 ppcenter 能启动并访问控制面端点。它们不推流、不播放、不验证 P2P,也不证明 mmx 媒体链路可用。HTTP 状态码还会受鉴权和节点注册状态影响,检查时应结合响应体判断,而不是把多个状态码一概视为成功。


5. 三个配置项,对着前面几集读

deploy/config/mmx-origin.yml 和 mmx-edge.yml 里有几个键,正好把前面讲过的机制落到具体数值上:

① 节点身份与容量(对应 EP14 节点池 / EP15 容量 / EP16 告警)

mmxControl:
  nodeRole: origin          # 或 edge
  nodeRegion: local
  heartbeatIntervalSeconds: 10
  nodeCapacity: 30          # origin 配 30,edge 配 50

节点靠这套配置向 ppcenter 注册自己的角色、区域和容量(EP14 讲的 poolId 同理,也属于这种"部署时静态声明"的属性)。nodeCapacity 就是 EP15 里那个"软准入阈值"的分母,也是 EP16 里 capacity_high 告警的触发基准。

② ABR 自适应码率(对应 EP11)

# mmx-edge.yml
webrtcABREnable: true
webrtcABRWSPath: /ws/control
webrtcABRSwitchCooldown: 3000

Edge 开着 ABR,通过 /ws/control 这条 WebSocket 做分层切换——EP11 讲的"无缝切换"就落在这个开关和冷却时间上。

③ 回源方式:Publisher vs 按需回源(对应 EP15)

# mmx-origin.yml          # mmx-edge.yml
paths:                     paths:
  all:                       all:
    source: publisher          source: redirect
    sourceOnDemand: false      sourceOnDemand: true

Origin 是 publisher(等推流端推上来),Edge 是 redirect + sourceOnDemand: true——正是 EP15 讲的"没有观众的 Edge 不占 Origin 带宽"在配置里的样子。


6. 从本地到生产:同一套组件,三种形态

本地这份 compose 只是最小形态。同一套四组件,仓库里还给了另外两种形态:

  • K8s 清单:deploy/k8s-*.yaml(ppcenter、mmx-origin、mmx-edge、nginx、infra),用 Service DNS 代替硬编码 IP,配置通过 ConfigMap 注入,探针用 /health。适合要上集群的场景。
  • 生产部署:一台台机器上用 systemd 拉起 mmx,前门是 nginx + 正式 TLS 证书,MySQL/Redis 拆到独立实例;配套的 deploy/remote-deploy-*.sh 脚本负责停服务、备份旧二进制、换新版、重启、看状态、清旧备份。扩容仍是 EP15 讲的人工 7 步流程——加机器后,还要去 Origin 的 forwardMmxTargets 追加新 Edge 的私有 IP 并 reload。

local compose → K8s → systemd 生产可以复用组件和协议,但差别绝不只在编排和密钥。网络暴露、负载均衡、持久卷、升级策略、故障域、观测和证书自动化都会改变生产行为。

Kubernetes 尤其要补三类前置设计:

  • 公网 UDP:WebRTC 媒体端口不能只靠集群内 Service DNS;需要 LoadBalancer、NodePort、host networking 或支持 UDP 的入口,并开放云安全组和节点防火墙。
  • ICE 可达性:候选地址必须是客户端可达的公网 IP/端口,处理 Service NAT、external traffic policy 与多网卡地址通告;复杂 NAT 场景还要评估 STUN/TURN,而不是假定 Pod IP 可达(这类问题不是 K8s 专属——第 7 节有一个已确认的、同类型的 docker bridge 网络真实案例)。
  • TLS 与域名:浏览器 WebRTC/鉴权入口需要可信证书、正确 SNI 和 WebSocket/HTTP 升级配置;应使用 Ingress/Gateway 加 cert-manager 或等价方案,并明确 UDP 媒体不等同于 HTTPS Ingress。

6.1 PPCDN 自己的生产形态

上面是通用要求;PPCDN 实际的生产机队(完整方案见 docs/deployment/ppcdn生产部署方案.md,实际发生的事见同目录 ppcdn生产部署总结.md)把它落成了几个具体取舍,正好是这一集"从本地到生产"的落地版:

  • 媒体面同 Region 同可用区:Origin / Record / Edge 统一部署在媒体 Region(如 AWS 新加坡)的同一个可用区,Origin→Record/Edge 走私有 IP 免媒体流量费;一旦跨 AZ 就要按约 $0.01/GB 双向计费。这一步必须在创建实例时显式指定并逐台核实,不是默认保证。
  • 控制面与数据面可以分离、甚至跨 Region:ppcenter 与 ppcenter-db(MySQL8 + Redis)可放在与媒体面不同的 Region——因为节点都是向外拨号,ppcenter 从不主动回连媒体节点,两者之间不需要媒体数据通道,只要求控制面到数据层可私网/安全网络可达(跨 Region 要单独核算流量与延迟)。把数据层拆成独立实例,是用 +$12/mo 换内存隔离、独立扩容与独立快照。
  • 容量是两条互不相干的线:一台 $12/mo(2vCPU/2GB)Lightsail Edge 有两个上限——瞬时并发(mmxNodeCapacity,生产取 12)和 Lightsail 月度出站配额(3TB ≈ 平均 5.5 并发)。必须分开设置、分开监控:只看并发告警、不看月度流量,会在月底收到远超实例本身的超额账单。这也是 EP15/EP16 反复强调"按实测校准分母"的同一个坑。
  • 按 Region 的 vCPU 配额推算节点数:媒体 Region 申请到多少 vCPU 直接决定 Edge 台数——目前口径是 64 vCPU,减去 Origin/Record 各 2 vCPU 后可放 30 台 $12 Edge,对应约 360 峰值并发 / 约 165 平均并发;若要 500 峰值并发,按每台 12 并发反推约需 88 vCPU,实际申请 96~100 留余量。这些是用量上限的估算,不是 SLA,真实可用容量还受 CPU/内存、公网出口、P2P 命中率、TLS/DNS 与 Origin 转发目标规模影响。
  • 扩容仍是纯人工 7 步:容量告警能触发 webhook,但扩容执行器尚未实现;每加一台 Edge,都要同步往 Origin 的 forwardMmxTargets 追加它的私有 IP 并 reload。

7. 诚实的边界

  • 冒烟 ≠ 端到端。修复后的 make docker-smoke 或本文手工命令都只验证控制面。真正验证一条流,需要 ppobs 推流 + ppplayer 播放,并且要处理真实的 NAT/网络条件。
  • 本仓库的集成测试只覆盖 record 角色的录像面(integrationTest/record,编译真实 mmx 二进制、真 ffmpeg 推流验证分片落盘)。origin/edge 的媒体链路目前没有自动化端到端测试。
  • 本地默认值全是要换的:nodeAuth.bearerToken 是 dev 值,TLS 是自签,MySQL 密码是本地口令。生产上线前这些一个都不能原样带过去。
  • 把这份 compose 原样搬到公网云主机上,WHEP 播放大概率连不上(已确认、尚未修复的真实 bug)。本地配置里的 webrtcAdditionalHosts: [localhost, 127.0.0.1, mmx-origin/mmx-edge] 只解决「同机通过 localhost 访问」这一种场景。一旦把同一份模板部署到公网云主机(仍是 docker bridge 网络 + 端口映射),默认的 webrtcIPsFromInterfaces: true 会采集到 docker 内部网桥 IP(例如 172.18.0.2)而不是公网 IP,WebRTC ICE 候选地址指向一个外部永远到不了的地址——推流(RTMP/SRT)不受影响,但播放端会卡在 deadline exceeded while waiting connection。这是 2026-10-08 给自建节点做压测时在生产环境确认的真实缺陷(详见 docs/deployment/ppcdn生产部署总结.md 对应条目),截至目前仍未修复。公网部署前必须手动把公网 IP 加进 webrtcAdditionalHosts,或把网络模式换成 host。
  • mmx 镜像不在本仓库构建。本地 compose 用的是预构建的 ppcdn/mmx:latest,要自己从独立的 ppmmx 仓库产出。
  • compose/smoke 是当前模板,不是已验证的一键发行物。先修数据库配置、smoke 路径/override 和 TLS 挂载,再谈耗时;「15 分钟」是准备完备环境下的目标,不是保证。
  • 这台机器大概能撑多少路,已有一个实测基准可参考:2026-10-08 对一台 1 vCPU / 2GB 的自建节点(ppmmx-standalone-2,走的是独立 ppmmxDocker 仓库的 standalone 部署,不是本文这份 origin+edge 两容器的 compose)做过阶梯加压测试——纯 H.264 视频、1280×720@30、约 2.5 Mbps、SRT 推流,加压端与被测机近同机房(RTT ~4ms)。结果:18 路 WHEP 观众以内全程稳定(CPU 峰值 53%),第 19 路在约 15 秒内把 CPU 从 ~40% 冲到 88~103% 并开始丢帧,18→19 路之间几乎没有过渡带——单核机器接近饱和时是悬崖式拥塞,不是线性退化。这不是对本文 compose 拓扑的直接测试(这里 origin/edge 分在两个容器里分担负载,standalone 是单进程全扛),只能当作「同等级小机器、同一套 mmx 媒体内核」的容量参考起点,自己的硬件和码率仍需单独实测校准。完整方法与数据见 docs/test/ppmmx-standalone-1vcpu-stress-test.zh-CN.md。

8. 框图

8.1 本地 compose 的组件与端口

                       ┌───────────────────────────┐
   浏览器 / 客户端 ───▶│        nginx   :80/:443     │
                       │  /origin/  /edge/  /v1/  /ws/│
                       └───────┬───────────┬────────┘
                               │           │
              ┌────────────────┘           └────────────────┐
              ▼                                             ▼
      ┌───────────────┐                             ┌───────────────┐
      │  mmx-origin   │◀── 回源(仅观众到来后)─────│   mmx-edge    │
      │ :1935 RTMP入  │                             │  :8890 WebRTC │
      │ :8889 WebRTC  │                             │  :8190/udp    │
      │ role=origin   │                             │  role=edge    │
      │ cap=30        │                             │  cap=50, ABR  │
      └───────┬───────┘                             └───────┬───────┘
              │       节点注册 / 心跳 / 指令               │
              └───────────────┬─────────────────────────────┘
                              ▼
                     ┌─────────────────┐        ┌──────────────┐
                     │   ppcenter      │───────▶│   MySQL(必需) │
                     │   :8090 控制面  │        └──────────────┘
                     │  /health /admin │        ┌──────────────┐
                     └─────────────────┘───────▶│ Redis(可选)   │
                                                └──────────────┘

8.2 一条命令的旅程

当前手工 smoke(同一组 compose 文件)
      │
      ▼
$COMPOSE up -d --build
      │
      ├─ /health                        控制面健康
      ├─ 检查 /admin/nodes、/v1/play/requests
      └─ $COMPOSE down -v              清场

验证的是"编排 + 控制面"通;不是"推流 → 播放"通

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

这一集把当前 compose 当作部署模板审查:先补齐 mmx 镜像、数据库配置和 TLS 挂载,再用同一组 compose 文件启动、检查和清理;现有 make docker-smoke 在路径与依赖上仍需修复。控制面冒烟不等于媒体端到端。到了 Kubernetes,还必须单独解决公网 UDP、ICE 可达地址和可信 TLS,不能把生产差异缩减成“只是换了编排”。

下一集,我们给这套跑起来的直播系统加上一块新能力:录像与回放——把直播流落成可按场次回放的 VOD。