我们如何通过克服 AWS Lambda 的 15 分钟限制实现一键式节目排程
一个 FAST 频道运营商安排一个月的节目,并点击一次 Deploy。在这一键的背后,数百个节目变成了数千个 MediaLive 调度操作,它们必须按顺序落地到 AWS 中,所有这些都必须在 Lambda 的 15 分钟执行上限内完成——中间的任何失败都会导致直播频道出现漏洞。以下是我们如何使大型播放列表的一键部署变得可靠,以及为什么下一步是采用不同的运行时,而不是更大的 Lambda。
快速概览
| 方面 | 详情 |
|---|---|
| 运行时 | AWS Lambda (单次调用), 后端超时 615 秒 |
| 输出目标 | MediaLive BatchUpdateScheduleCommand |
| 批处理 | 每次请求最多 200 个 MediaLive 操作 — 实际中约 25 个节目 |
| 每个节目的操作 | 约 7-8 个 (输入切换 + 每个渲染的 4 个水印 + 2 个 SCTE-35),带广告插播时更多 |
| 回退 | 任何批次被拒绝时,按节目进行重试 |
| 实际观测 | 195 个节目在大约 44 秒内部署完成 (本地测量) |
| 文档目标 | 360 个节目在 ≤90 秒内完成 |
| 扩展路径 | Step Functions 分块处理 >1 个月的播放列表 — 已规划,未发布 |
挑战
部署播放列表不是简单的写入——它是一种编排。对于操作员调度的每个节目,MediaLive 需要准确知道何时切换输入、何时为每个渲染打开水印、何时插入 SCTE-35 提示点以及何时插入广告。结构性压力包括:
- 操作数量是乘法的,而非加法的。 在广告插播前,每个节目会发出约 7-8 个操作——一次输入切换,四次水印激活(每个渲染一次,因为 StaticImageOutputActivate 使用输出像素坐标),以及两个 SCTE-35 标记。广告插播每次会额外增加三个操作。一个包含 360 个节目的月份,大约会产生 2,800 个网络操作。
- MediaLive 强制按频道进行排序。 调度操作是时间锚定的并且相互引用;如果不在 API 拒绝冲突的情况下,您无法并行写入同一频道。
- AWS Lambda 有一个硬性 15 分钟上限。 这不是一个软限制,也不是一个配置项。编排器必须在此限制内完成,否则频道将处于半部署状态。
- 部分故障在操作上是不可接受的。 如果 360 个节目中的第 174 个失败并中止运行,操作员将无法查看哪些已部署,哪些未部署。频道上线时会出现空白;观众会看到空白画面而不是预期内容。
- 第一个版本为每个节目发送一个 BatchUpdateSchedule,节目之间有 200 毫秒的暂停。 这意味着每个节目大约需要 2.5 秒的实际时间。在 360 个节目时,MediaLive 还没开始实际工作,您就已经超过了 Lambda 的上限。
因此,任务不是 写得更快。而是减少写入次数,在部分故障中幸存,并保持在一次调用内——同时不失去 MediaLive 所要求的排序保证。
现有方法为何失败
所有显而易见的规避方法都因结构性原因而失败,而非调优问题。
- “只需增加 Lambda 超时时间。” 你不能。15 分钟是 AWS 对 Lambda 执行的硬性上限;它不是控制台中的一个配置项。即使可以,每个节目的成本也会随着目录的增长而增加——购买更多时间只会延迟下一个瓶颈。
- “迁移到 ECS Fargate 或 EC2。” 我们考虑过但拒绝了。长时间运行的容器意味着我们要自己管理运行时:健康检查、自动扩缩、冷启动与热池的权衡、IAM 范围限定,以及针对突发性工作负载的服务轮值。Lambda 为我们提供了每次调用的隔离性以及零闲置成本,适用于这种固有的突发性工作负载。我们还没准备好放弃这些来解决一个瓶颈。
- “并行化 MediaLive 写入。” 调度更新由 MediaLive 为每个频道序列化。针对同一频道的并发 BatchUpdateSchedule 调用会在操作时间线上产生竞争并被拒绝。唯一合法的并行化是在批次内部,而不是跨批次。
- “就让它崩溃,让操作员重试。” 这是最糟糕的选择。当部署在第 N 个节目处中止时,频道处于一种无法从 UI 描述的状态。操作员无法获得差异视图;他们得到的是一个黑盒和一个有漏洞的直播频道。系统必须要么全部部署成功,要么在部署部分结果时提供操作员可以据此采取行动的每个节目状态。
我们剩下的杠杆是工作本身的形态:更少、更大的写入,串行执行,并在批次被拒绝时才降级到行级粒度的回退机制。
我们的解决方案
在循环开始之前将成本移出 Lambda,在循环内部合并 MediaLive 写入,并且只在批处理失败时降级为按节目提交。这三个思想按此顺序进行——以及一个经过深思熟虑的决定,即对于我们今天实际部署的目录大小,15 分钟的上限是没问题的。当播放列表超出单次调用时,答案不是一个更大的 Lambda;而是一个不同的运行时。
架构
- NestJS 后端 (schedule.service.ts) — 预热部署:所有唯一视频进行一次 Mongo 往返,所有广告标记进行一次往返,然后对唯一视频 URL 进行并行 ffprobe,并将结果存储在解析缓存中。
- Lambda 编排器 (fastChannel-lambda-fun/index.js) — 负责批处理循环、每批次的限流、每节目的回退以及孤儿清理。
- MediaLive BatchUpdateScheduleCommand — 唯一的写入接口。所有操作——输入切换、水印、SCTE-35、广告插入——都通过它。
- MongoDB — 节目、视频和广告标记的单一数据源;在内部循环中从不查询。
- scheduleResults map — 每个节目的 scheduled / error 状态返回给后端,以便操作员获得差异视图,而不是堆栈跟踪。
关键工程决策
1. 在循环运行前预热慢速部分。 原始代码在富化循环内部进行了按调度的 Mongo 查找和按调度的 ffprobe 调用——经典的 N+1 次重复支付。当前的后端通过一次往返批量获取所有唯一视频和广告标记,然后并行运行 ffprobe 处理唯一 URL,并将结果存储在解析缓存中。内部循环变为缓存命中。ffprobe 延迟对每个唯一 URL 只支付一次,而不是在循环中串行支付。
2. 将 MediaLive 写入合并为大约 25 个节目的批次。 在编排器内部,操作会累积到一个单一的 BatchUpdateScheduleCommand 中,直到排队达到 200 个操作或到达最后一个节目。由于每个节目产生约 7-8 个操作,批次自然会包含大约 25 个节目。选择 200 个操作的上限是为了保守地保持在 MediaLive 每请求负载限制之下,并减少批次因大小而被拒绝的可能性——它足够大以分摊网络成本,又足够小以使当必须运行时,按节目回退(下一个决策)成本较低。一次网络往返取代了二十五次。以前在每个节目之间存在的 200 毫秒节流现在存在于每个批次之间。
3. 批处理以提高速度,回退以确保正确性。 只有当单个有问题的节目不会“毒害”批次中其他二十四个节目时,合并才是安全的。当 submitProgramBatch 抛出异常时,捕获会调用 retryBatchAsIndividuals,它会将失败批次中的每个节目作为单独的 BatchUpdateScheduleCommand 重新提交,记录每个节目的 status: 'scheduled' 或 status: 'error' 状态,在每次尝试之间暂停 200 毫秒,并在每次部分成功后重新锚定操作时间线。快速路径是批处理的。恢复路径是细粒度的。无论哪种方式,操作员都会获得每个节目的差异视图。
4. 在部署结束时清理孤儿,而不是在飞行中阻止它们。 sweepIncompleteProgramGroups 在部署结束时运行一次,并移除任何未达到干净终止状态的操作组。我们刻意不在循环期间尝试保持通道内部一致性——那将意味着一个回滚路径,而该路径本身必须符合 15 分钟的预算。清理是一个单一的清除操作,而不是一个事务。
5. 不要扩大 Lambda;当目录增大时替换它。 对于我们今天部署的所有内容,单次调用路径都能在预算内很好地完成。真正的横向扩展是 Step Functions 分块——将播放列表分区,将分块作为并行状态机运行,然后重新组合。这是用于超过 1 个月和 6 个月播放列表的路径。它已设计,但尚未部署。称之为下一步比假装它已经在运行更有用。
结果
- 在内部测试中,一个包含 195 个节目的部署在大约 44 秒内完成——相比于一次一个节目的数分钟基线有所下降。
- 一个 360 个节目(约 1 个月)播放列表的文档目标是 ≤90 秒,这舒适地处于 615 秒的后端超时和 15 分钟的 Lambda 上限之内。
- 批次中的一个坏节目不再会中止其他 24 个。操作员从每次部署中都会收到一个按节目划分的状态图。
- 请求的慢速部分——Mongo 查找和 ffprobe——对每个唯一资源只支付一次,而不是每个调度条目支付一次。
- 15 分钟的上限对于我们实际发货的目录大小不再是限制因素。当它再次成为限制时,Step Functions 分块是解决方案,而不是更大的 Lambda。
技术栈: AWS Lambda · AWS MediaLive · NestJS · MongoDB · Node 18 · ffprobe · TypeScript · AWS SDK v3