一键调度多个节目——突破 AWS Lambda 的 15 分钟上限
FAST channel 运营商调度一个月的节目,然后点击一次部署。在这一键操作背后,数百个节目会变成数千个 MediaLive 调度动作,这些动作必须按顺序在 AWS 中落地,并且全部要在 Lambda 的 15 分钟执行上限内完成——中间的任何失败都会导致直播频道出现漏洞。本文将介绍我们如何使大型播放列表的一键部署变得可靠,以及为什么下一步是采用不同的运行时,而不是一个更大的 Lambda。
快速概览
| 方面 | 详情 |
|---|---|
| 运行时 | AWS Lambda(单次调用),615s 后端超时 |
| 输出目标 | 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 次重复支付。当前的后端为每个独有视频和广告标记各执行一次批量获取,然后并行对独有 URL 运行 ffprobe,并将结果存储在解析缓存中。内部循环变为缓存命中。ffprobe 的延迟成本仅为每个独有 URL 支付一次,而不是在循环中串行支付。
2. 将 MediaLive 写入合并为约 25 个节目的批次。在编排器内部,动作会累积到单个BatchUpdateScheduleCommand中,直到排队达到 200 个动作或到达最后一个节目。由于每个节目产生约 7-8 个动作,因此每个批次自然包含约 25 个节目。选择 200 个动作的上限是为了保守地保持远低于 MediaLive 的每个请求负载限制,并减少批次因大小而被拒绝的可能性——足够大以分摊网络成本,足够小以便在需要运行时使逐节目回退(下一个决策)成本低廉。一次网络往返取代了二十五次。以前在每个节目之间存在的 200ms 节流现在位于每个批次之间。
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

