扛住顶级体育赛事直播:在 OpenResty Edge 中构建 HLS 视频直播分发层
直播 HLS 分发的核心难题不是带宽,而是把回源总量严格收敛到“每个对象、全网一次”。OpenResty Edge 通过三层机制实现这一点:为播放列表与分片分别配置页面规则(相反的 TTL)、“Use stale proxy cache” 动作(阻止过期刷新惊群并在源站故障时兜底)、以及网关分区(Partition)构建的父层缓存。本文介绍的这套配置方案来自一套真实的生产环境:在一场全球关注的大型体育赛事直播中,多个 OpenResty Edge 网关节点组成私有 CDN,在赛事高峰扛住海量并发观众,而背后算力昂贵的 GPU 转码源站自始至终只需应对“每个对象一次”的回源、稳如磐石。与按流量计费、回源行为如同厂商黑盒的托管 CDN 不同,这套分发层完全运行在你自己的基础设施上,源站保护策略始终掌握在自己手中。
HLS(HTTP Live Streaming)的缓存看起来非常简单:无非是对 .m3u8 播放列表和 .ts 分片的 HTTP 请求。但直播 HLS 有一个会击穿“朴素缓存方案”的特性——播放列表每隔几秒就会过期,而每一次过期都可能引发一场压垮源站的请求风暴(缓存击穿/惊群);当多个 CDN 节点回源到同一个源站时,压力还会随节点数成倍放大。下面我们从原理讲到配置,一步步搭出一个能真正保护源站的直播分发层。
为什么直播 HLS 会击穿朴素的缓存方案
一路 HLS 直播流由两种性质截然不同的对象组成:
| m3u8 播放列表 | ts 分片 | |
|---|---|---|
| URL | 固定不变,内容每几秒更新 | 每个分片都是全新 URL |
| 可缓存性 | 极短 TTL(秒级) | 长 TTL(写入后不可变) |
| 故障模式 | 过期时刻的回源风暴 | 冷启动拉取时的回源风暴 |
其中播放列表的过期刷新,是最容易被忽视的风险点。nginx 式的缓存锁只对“新建”缓存条目去重,并不会串行化“已过期”条目的刷新。而直播播放列表按设计每几秒就过期一次——在数千观众的真实并发下,每次过期都可能有上百个请求同时打到本应只承受几个请求的源站。
这种风险在多节点架构下会被急剧放大。生产环境中,通常是多个 CDN/边缘节点回源到同一个源站:假设有 N 个边缘节点,每个节点在播放列表过期时刻泄漏一波回源请求,源站承受的就是 N 倍的风暴。而源站链路一旦被打满,就会进入自我放大的恶性循环——随机丢包、回源变慢、超时又释放出更多请求,最终让所有节点、所有观众的直播一起崩掉。
换句话说:决定系统存亡的不是任何单个节点的带宽配比,而是“回源总量”是否被严格控制在源站的承受能力之内。缓存去重每失效一分,源站压力就按节点数和观众数成倍增长。
因此,配置目标是:
- 每次播放列表刷新,无论有多少观众,每个节点只允许一次回源。
- 每个新分片,每个节点只允许一次回源。
- 源站变慢或宕机时,提供过期(stale)内容兜底,而不是集体回源或直接报错。
- 节点数量众多时,增加父层缓存(shield),把“每节点一次”进一步收敛为“全网一次”。
下面在 OpenResty Edge 中逐项落地。OpenResty Edge 通过 Admin 管理控制台管理一切——应用(Application)、上游(Upstream)、页面规则(Page Rule)——并且配置变更下发到网关节点时无需 reload 或重启,这对在线调优直播系统非常方便。
第 1 步:创建指向 HLS 源站的上游
在应用下定义一个指向 HLS 源站(打包器或转码器)的上游。如果有备用源站,一并加入。
这里给出一条与具体 UI 无关、长期有效的容量建议:把源站的承载能力当作全网共享的硬预算来对待。估算方法很简单:源站链路带宽 ÷ 单个分片拉取速率 ≈ 源站能承受的并发回源上限。注意这个上限是所有边缘节点共享的——10 个节点各自“只多回源几次”,在源站汇聚后就是几十倍的压力。务必让全网预期的并发回源总数远低于这个上限;按照下文的缓存策略,稳态下的回源流量应该只是“每个对象、每个节点一份拷贝”。
第 2 步:为播放列表和分片分别创建页面规则
由于播放列表和分片需要完全相反的缓存策略,应各建一条页面规则:
规则 1——播放列表。 条件(Enable when):Variable 选 URI,Operator 选 Suffix matches,Value 填播放列表的扩展名(本文截图环境使用自定义扩展名 .m38u;标准 HLS 命名为 .m3u8,请以你的实际流命名为准)。
规则 2——分片。 条件:URI 后缀匹配 .ts(如果分发 fMP4/CMAF,可用 “Add more value” 追加 .m4s、.mp4)。这条规则里还需要加一个动作:Set response header Access-Control-Allow-Origin: "*"——网页端播放器(hls.js 等)跨域拉流必需,在 OpenResty Edge 里加一条响应头动作即可,无需改源站。
两条规则都指向同一个上游,Balancing policy 为 Round Robin,并且都勾选底部的 “Skip any subsequent page rules when this rule matches”——命中即终止,避免后续规则意外覆盖缓存策略。
第 3 步:代理与重试设置(Proxy)
进入规则编辑页,Proxy 区块打开后选择上游。生产配置中的关键项:
- Proxy to upstream:选择第 1 步创建的上游;可通过 “Add a new backup upstream” 配置备用上游(仅当所有主上游节点全部失败时才会启用)。
- Retry times:勾选 “Same as the number of upstream nodes”,重试次数自动等于上游节点数。
- Retry condition:
error, timeout, invalid header, http 500, http 502, http 503, http 504——单节点拉取失败立即换节点重试,而不是把错误直接抛给播放器。 - Connect / Send / Read timeout:各 6 秒。直播场景下这三个值不宜过长——超时越短,失败切换越快;6 秒是一个稳妥的起点,如果源站响应稳定,连接超时可以进一步压到 2~3 秒。
第 4 步:缓存设置(Cache)
在规则编辑页打开 Cache 开关,逐项设置:
Cache key components:URI + Query string。这是默认组合,是否保留 Query string 取决于你的 URL 形态:如果播放器会附加因观众而异的查询参数(鉴权 token、会话 ID、时间戳),必须把 Query string 组件删掉(点组件右侧的 ×),否则每个观众一个缓存键,命中率悄无声息地归零——一千个观众对同一个分片就是一千次回源,鉴权改由单独的访问规则完成。配错的症状在日志里一目了然:同一文件反复 MISS,彼此只差查询字符串。本例的流 URL 不带易变参数,因此保留默认即可。
Caching by Default:ENABLED。源站不发送 Cache-Control/Expires 时,网关默认不缓存;打开此项后这类响应也会被缓存。
Always Cache:ENABLED——忽略源站的缓存控制头,强制按此处的过期时间缓存。这是直播场景推荐的做法,TTL 完全由网关掌控:
- 播放列表规则:Expiration Time 2 秒,Status Code 只填 200(约为分片时长的一半;本例分片约 3~4 秒)。
- ts 分片规则:Expiration Time 1 小时,Status Code 只填 200。分片一旦写入即不可变,理论上可以缓存更久,1 小时对直播绰绰有余。
注意 Status Code 一栏只填 200——绝不要把 5xx 加进去,否则一次源站故障会被缓存下来,源站恢复后仍持续向所有人提供错误页。
下面两张图分别是播放列表规则(2 秒)与 ts 分片规则(1 小时)的实际过期时间配置:
Cache scope:Domain(按域名隔离缓存)。
Browser Cache:ENABLED,过期时间与网关缓存对齐——播放列表 2 秒、分片 1 小时。让观众的浏览器也短暂缓存,进一步削减边缘节点的重复请求。
Convert request method HEAD to GET:ENABLED,避免 HEAD 探测请求绕过缓存。
第 5 步:添加 “Use stale proxy cache” 动作——最关键的一步
这是直播 HLS 最重要、也最容易被漏掉的一项设置。在 OpenResty Edge 中,它就是页面规则里的一个动作:点击 “Add a new action”,选择 “Use stale proxy cache”,然后在 “Use in the following cases” 中勾选:
- ✅ updating——覆盖播放列表每 2~3 秒一次的过期刷新:一个请求去源站刷新时,其他所有请求立即拿到略微过期的旧副本;
- ✅ error、timeout——源站连接失败或超时时,用旧内容兜底;
- ✅ http 502、http 503——源站返回错误时,同样用上一次成功的响应兜底,而不是把错误透传给播放器。
为什么 updating 如此关键?前文说过,缓存锁只对新建缓存条目去重,并不会串行化已过期条目的刷新。而直播播放列表按设计每几秒就过期一次——缺少这一项时,每次过期都可能触发回源风暴;且缓存锁的作用域仅限单个节点,多节点的泄漏在源站上直接叠加。勾上 updating 之后,每个刷新周期、每个节点只回源一次。
启用后,源站故障也将优雅降级:观众持续收到最后一份播放列表,播放器停在直播边缘继续轮询,源站恢复后无缝续播——而不是所有播放器集体报错退出。
两条规则(播放列表与分片)都应添加这个动作。分片虽然是全新 URL、多数情况下没有旧副本可用,但对被重复请求的分片以及源站瞬时抖动仍有兜底价值;真正为分片托底的是第 3 步的重试机制(retry condition + 备用上游)。
这里顺带厘清两件常被混淆的事:
- 缓存 5xx(错误做法):Always Cache 的 Status Code 里包含错误码,会把错误页存进缓存并持续提供——第 4 步已强调只填 200。
- 5xx 时提供过期缓存(正确做法):“Use stale proxy cache” 勾选 http 502/503,源站出错时提供上一次成功的响应。
一个隐含前提:过期缓存兜底只有在过期对象仍在磁盘上时才有效。缓存过期不等于删除——过期文件会按缓存区容量策略淘汰,而不是 TTL 一到就消失,这正是 stale 机制能工作的原因。
第 6 步:多节点架构——用网关分区(Partition)构建父层缓存
以上所有机制的作用域都是单个网关节点:缓存锁和过期缓存兜底把每个节点的回源收敛到“每对象一次”。但当边缘节点数量增长后,源站看到的仍然是“节点数 × 每对象一次”——对播放列表这种每几秒刷新一次的对象,几十个节点的合计轮询压力依然可观,而且任何一个节点的配置回退(比如某节点漏配了过期缓存复用)都会直接暴露给源站。
OpenResty Edge 的网关分区(Gateway Partition)机制天然支持解决这个问题。在 OpenResty Edge 中,分区是网关集群的分组,每个分区可以独立发布不同的应用配置。利用这一点可以直接搭出两级缓存(shield / 父层)架构——这也是构建私有 CDN 网络时的通用做法:
观众 ──> 边缘分区(edge partition,N 个集群/节点)
──> 父层分区(shield partition,靠近源站的 1 个小集群)
──> GPU 转码源站
具体做法:
- 创建父层分区:在 Gateway Clusters 页面新建一个网关集群,部署在源站同机房或邻近位置,并将其归入一个专用分区(如
shield-partition)。 - 向父层分区发布“回源应用”:该应用的上游指向真实源站,页面规则按本文前五步完全相同的方式配置(分规则、缓存键、TTL、过期缓存兜底)。
- 修改边缘分区的上游:面向观众的边缘应用,上游不再指向源站,而是指向父层分区的节点。
- 两套应用配置都在同一个 OpenResty Edge 的 Admin 控制台中管理、发布,无需任何节点 reload。
这样无论边缘节点有多少,源站对每个对象只会收到全网一次拉取。父层同时还是天然的“配置防波堤”:即使某个边缘节点发生惊群,风暴也会被父层的缓存与去重吸收,不会穿透到脆弱的转码源站。此外,规则编辑页 Proxy 区块中还有 “Use Multi-tier Network policy” 开关(企业版功能),用于按多层网络策略组织回源路径,可与分区架构配合使用。
另外,Cache 区块中可以看到 “Gateway Cluster Level Cache Sharing” 开关:开启后,同一集群内通过一致性哈希让同一资源始终落到同一个节点,由该节点统一持有缓存,集群内其他节点向它取数据而不是各自回源(使用前需先在 Global Config General 页面开启 Cluster Hash)。这在单个集群内部又进一步收敛了回源量,与分区级的父层架构是互补的两层收敛。
验证配置
下发变更(OpenResty Edge 推送配置到网关节点无需 reload),然后从客户端测试:
# 播放列表:首次请求 MISS,随后约 2 秒内 HIT,每次过期刷新时出现
# STALE/UPDATING——绝不应出现成片的 MISS
curl -sI https://your-edge-domain/live/stream.m3u8 | grep -i cache
# 分片:每个分片恰好 MISS 一次,之后 1 小时内全部 HIT
curl -sI https://your-edge-domain/live/seg-1001.ts | grep -i cache
正式上线前,建议再用 srs-bench 模拟真实 HLS 播放器行为(轮询播放列表 + 下载分片)做一轮压测:
# 模拟 2000 个并发 HLS 播放器
docker run --rm -it --network=host ossrs/srs:sb \
./objs/sb_hls_load -c 2000 \
-r https://your-edge-domain/live/test-channel.m3u8
压测中观察播放列表过期时刻的源站请求数,按模式定位问题:
- 健康:无论观众多少,源站每个播放列表刷新周期约 1 个请求,每个新分片约 1 个请求。
- 过期惊群:每个播放列表周期都出现回源请求爆发 → “Use stale proxy cache” 的
updating未生效。 - 缓存键碎片化:同一对象带着不同查询字符串被反复拉取 → 修正缓存键。
- 超时级联:源站变慢期间回源请求不降反升 → 检查错误/超时场景的过期缓存兜底,并缩短连接超时。
最后,故意在直播中把源站关停 30 秒。播放器应停在直播边缘然后恢复——而不是报错退出。
总结
在 OpenResty Edge 上跑 HLS 直播的思维模型:
- 播放列表与分片分开建页面规则——两者需要完全相反的 TTL(本例:播放列表 2 秒、分片 1 小时),并勾选“命中即跳过后续规则”。
- 守住缓存键,防范易变查询字符串——键碎片化会悄无声息地摧毁去重;URL 无易变参数时保留默认 URI + Query string 即可。
- “Use stale proxy cache” 动作是核心机制,而不是缓存锁——务必勾选
updating(过期刷新去重)与error/timeout/http_502/http_503(故障兜底);锁只覆盖冷启动拉取,只有 stale 能阻止播放列表的过期惊群。 - Always Cache 的状态码只填 200——源站出错时靠 stale 提供旧内容,但绝不缓存错误本身;分片层再配上重试条件与备用上游。
- 把源站承载能力当作全网共享的硬预算——在压测中验证每个节点对每个对象只有一次回源;源站压力等于单节点泄漏量乘以节点数,去重路径失效不只是性能下降,而是会触发自我放大的雪崩。
- 节点多了就用分区(Partition)搭父层缓存——把“每节点一次”收敛为“全网一次”;集群级缓存共享(Gateway Cluster Level Cache Sharing)可在集群内部再收敛一层。
把这几点做对,一个算力昂贵的转码源站就能从容支撑大型赛事级别的观众规模——观众的增长只发生在边缘,源站永远只看到“每对象一份拷贝”。这正是用 OpenResty Edge 构建私有 CDN 做直播分发的价值所在。
常见问题(FAQ)
HLS 直播的播放列表应该缓存多久?
一个稳妥的经验值是分片时长的一半左右:分片 3~4 秒时,播放列表 TTL 设 2 秒。TTL 过长会让观众看到滞后的直播边缘,过短则加大回源频率;配合 “Use stale proxy cache” 的 updating 选项,每个刷新周期每个节点只回源一次,TTL 的选择就只影响直播延迟,不再影响源站压力。
为什么缓存锁挡不住直播场景的惊群?
缓存锁(proxy cache lock)只串行化新建缓存条目的并发回源,对已过期条目的刷新不生效。点播场景对象一次写入长期有效,锁就够用;直播播放列表每几秒过期一次,每次过期都是一次“锁管不着”的并发刷新,必须靠 stale-while-updating 机制去重。
分发 fMP4/CMAF 流(.m4s 分片)时配置有区别吗?
分片规则的 URI 后缀匹配追加 .m4s、.mp4 即可,缓存策略与 .ts 完全相同(不可变、长 TTL)。播放列表(.m3u8)的策略也不变。
直播分发应该用托管 CDN 还是自建?
小规模观众或一次性活动,托管 CDN 是最省事的选择。但随着观众规模增长,账本会反转:托管 CDN 的流量费随观众数线性增长,而自建分发层的容量成本基本固定;同时源站保护行为(stale 策略、父层缓存、缓存键)完全由自己掌控,而不是厂商黑盒。需要说明的是,自建不等于从零拼装:OpenResty Edge 是商业化的自托管软件——网关节点跑在你自己的机器上,所有配置在同一个控制台中管理。
边缘节点多少时值得加父层(shield)分区?
没有硬性阈值,判断依据是源站预算:源站看到的稳态回源量约等于“节点数 × 每对象一次”。当播放列表的合计轮询频率(节点数 ÷ TTL)开始逼近源站的从容承受范围,或者你希望获得“配置防波堤”的容错价值时,就值得加父层——在 OpenResty Edge 中只是多建一个分区和一个回源应用,成本很低。
关于 OpenResty Edge
OpenResty Edge 是一款专为微服务和分布式流量架构设计的全能型网关软件,由我们自主研发。它集流量管理、私有 CDN 构建、API 网关、安全防护等功能于一体,帮助您轻松构建、管理和保护现代应用程序。OpenResty Edge 拥有业界领先的性能和可扩展性,能够满足高并发、高负载场景下的苛刻需求。它支持调度 K8s 等容器应用流量,并可管理海量域名,轻松满足大型网站和复杂应用的需求。
关于作者
章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。
章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算”、“动态追踪 ”和 “机器编程 ”的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJIT、GDB、SystemTap、LLVM、Perl 等,并编写过 60 多个开源软件库。
关注我们
如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:
翻译
我们提供了英文版原文和中译版(本文) 。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!


























