원클릭으로 여러 프로그램 스케줄링 — AWS Lambda의 15분 제한 극복
FAST 채널 운영자는 한 달치 프로그램을 스케줄링하고 Deploy 버튼을 한 번 누릅니다. 이 한 번의 클릭 뒤에는 수백 개의 프로그램이 수천 개의 MediaLive 스케줄 액션으로 변환되어 AWS에 순서대로 적용되어야 하며, 이 모든 작업은 Lambda의 15분 실행 제한 시간 내에 이루어져야 합니다. 중간에 실패하면 라이브 채널에 공백이 생기게 됩니다. 여기서는 대규모 재생 목록의 원클릭 배포를 안정적으로 만든 방법과 다음 단계가 더 큰 Lambda가 아닌 다른 런타임인 이유를 설명합니다.
간략한 개요
| 측면 | 세부 정보 |
|---|---|
| 런타임 | AWS Lambda (단일 호출), 615초 백엔드 타임아웃 |
| 출력 대상 | MediaLive BatchUpdateScheduleCommand |
| 배치 처리 | 요청당 최대 200개의 MediaLive 액션 — 실제로는 약 25개 프로그램 |
| 프로그램당 액션 | ~7–8개 (입력 전환 + 렌디션당 4개의 워터마크 + 2개의 SCTE-35), 광고 삽입 시 더 많음 |
| 폴백 | 배치 거부 시 프로그램당 재시도 |
| 관찰 결과 | 약 44초 만에 195개 프로그램 배포 (로컬 측정) |
| 문서화된 목표 | 360개 프로그램 ≤90초 |
| 확장 경로 | >1개월 재생 목록을 위한 Step Functions 청킹 — 계획 중, 아직 출시되지 않음 |
당면 과제
재생 목록 배포는 단순한 쓰기 작업이 아니라 오케스트레이션입니다. 운영자가 스케줄링한 모든 프로그램에 대해 MediaLive는 언제 입력을 전환하고, 각 렌디션에 대한 워터마크를 언제 켜고, SCTE-35 큐 포인트를 언제 삽입하고, 광고를 언제 삽입해야 하는지 정확히 알아야 합니다. 구조적 압력은 다음과 같습니다:
- 액션 수는 가산적이지 않고 곱셈적입니다. 각 프로그램은 광고 삽입 전에 약 7–8개의 액션을 발생시킵니다 — 입력 전환 하나, 워터마크 활성화 4개(StaticImageOutputActivate가 출력 픽셀 좌표를 사용하므로 렌디션당 하나), 그리고 SCTE-35 마커 두 개. 광고 삽입은 각각 세 개의 액션을 더 추가합니다. 360개 프로그램으로 구성된 한 달은 대략 2,800개의 액션이 전송됩니다.
- MediaLive는 채널당 순서 지정을 강제합니다. 스케줄 액션은 시간에 고정되어 있으며 서로를 참조합니다. API가 충돌로 거부하지 않도록 동일한 채널에 대한 쓰기 작업을 병렬화할 수 없습니다.
- AWS Lambda에는 15분이라는 엄격한 상한선이 있습니다. 이는 유연한 제한이나 구성 설정이 아닙니다. 오케스트레이터는 이 시간 내에 작업을 완료해야 하며, 그렇지 않으면 채널이 절반만 배포된 상태가 됩니다.
- 부분적 실패는 운영상 허용할 수 없습니다. 360개 프로그램 중 174번째 프로그램이 실패하여 실행이 중단되면, 운영자는 무엇이 적용되었고 무엇이 적용되지 않았는지 차이점을 파악할 수 없습니다. 채널은 공백이 있는 상태로 라이브되며, 시청자는 콘텐츠가 있을 것으로 예상했던 곳에서 슬레이트를 보게 됩니다.
- 첫 번째 버전은 프로그램당 하나의 BatchUpdateSchedule을 보냈고, 프로그램 사이에 200ms의 대기 시간을 두었습니다. 이는 프로그램당 약 2.5초의 실제 시간입니다. 360개 프로그램의 경우 MediaLive가 실제 작업을 시작하기도 전에 이미 Lambda 제한을 초과하게 됩니다.
따라서 해야 할 일은 더 빨리 쓰는 것이 아닙니다. 그것은 MediaLive가 요구하는 순서 보장을 잃지 않으면서 더 적게 쓰고, 부분적 실패에 견디며, 단일 호출 내에 머무는 것입니다.
기존 접근 방식이 실패하는 이유
명백한 모든 해결책은 튜닝 문제가 아닌 구조적인 이유로 실패합니다.
- "그냥 Lambda 타임아웃을 늘리세요." 그럴 수 없습니다. 15분은 AWS가 Lambda 실행에 대해 부과하는 엄격한 상한선이며, 콘솔에서 조절할 수 있는 설정이 아닙니다. 설령 그렇더라도, 프로그램당 비용은 카탈로그 규모에 비례하여 증가하며, 더 많은 실행 시간을 확보하는 것은 다음 한계를 늦출 뿐입니다.
- "ECS Fargate나 EC2로 옮기세요." 우리는 이 방안을 고려했지만 기각했습니다. 장기 실행 컨테이너는 런타임에 대한 소유권을 의미합니다: 상태 확인, 오토스케일링, 콜드 스타트 vs 웜 풀 트레이드오프, IAM 범위 지정, 그리고 버스트 방식으로 실행되는 서비스에 대한 온콜 로테이션 등. Lambda는 본질적으로 버스트성 워크로드에 대해 호출당 격리 및 유휴 비용 제로를 제공합니다. 단 하나의 병목 현상을 해결하기 위해 이 모든 것을 포기할 준비가 되어 있지 않았습니다.
- "MediaLive 쓰기를 병렬화하세요." MediaLive는 채널별 스케줄 업데이트를 직렬화합니다. 동일한 채널에 대한 동시 BatchUpdateSchedule 호출은 액션 타임라인에서 경쟁하며 거부됩니다. 유일하게 합법적인 병렬 처리는 배치 내부에 있지, 배치 간에는 아닙니다.
- "그냥 충돌하게 두고 운영자가 재시도하게 하세요." 이것은 최악의 옵션입니다. 프로그램 N에서 배포가 중단되면, 채널은 UI에서 아무도 설명할 수 없는 상태가 됩니다. 운영자는 차이점을 볼 수 없고, 블랙박스와 구멍이 뚫린 라이브 채널을 받게 됩니다. 시스템은 모든 것을 적용하거나, 운영자가 조치할 수 있는 프로그램별 상태를 포함한 부분적인 결과를 적용해야 합니다.
우리에게 남은 수단은 작업 자체의 형태였습니다: 배치 거부 시에만 행 수준 세분성으로 저하되는 폴백을 사용하여, 직렬로 실행되는 더 적고 더 큰 쓰기 작업.
우리의 해결책
루프가 시작되기 전에 Lambda 외부로 비용을 이동하고, 루프 내에서 MediaLive 쓰기 작업을 통합하며, 배치 실패 시에만 프로그램별 제출로 전환합니다. 이러한 세 가지 아이디어를 순서대로 적용했으며 — 오늘날 실제로 배포하는 카탈로그 크기에는 15분 제한이 적합하다는 의도적인 결정을 내렸습니다. 재생 목록이 단일 호출을 초과할 정도로 커지면, 더 큰 Lambda가 아닌 다른 런타임이 해결책이 됩니다.

아키텍처
- NestJS 백엔드 (schedule.service.ts) — 배포를 사전 예열합니다: 모든 고유 동영상에 대해 한 번의 Mongo 왕복, 모든 광고 마커에 대해 한 번의 Mongo 왕복, 그 다음 고유 동영상 URL에 대해 병렬 ffprobe를 실행하고 결과를 캐시하여 보강 루프에 사용합니다.
- Lambda 오케스트레이터 (fastChannel-lambda-fun/index.js) — 배치 루프, 배치당 스로틀, 프로그램당 폴백, 고아(orphan) 정리 작업을 담당합니다.
- MediaLive BatchUpdateScheduleCommand — 유일한 쓰기 인터페이스입니다. 입력 전환, 워터마크, SCTE-35, 광고 삽입 등 모든 액션이 이를 통해 전달됩니다.
- MongoDB — 프로그램, 동영상, 광고 마커에 대한 진실의 원천입니다; 내부 루프 내에서는 쿼리되지 않습니다.
- scheduleResults map — 백엔드로 반환되는 프로그램별 scheduled / error 상태로, 운영자가 스택 트레이스가 아닌 차이점을 확인할 수 있도록 합니다.
주요 엔지니어링 결정
1. 루프 실행 전에 느린 작업을 미리 예열합니다. 원래 코드는 보강 루프 내에서 스케줄당 Mongo 조회와 스케줄당 ffprobe 호출을 수행했습니다 — 고전적인 N+1 이중 지불 방식입니다. 현재 백엔드는 모든 고유 동영상과 광고 마커를 각각 한 번의 왕복으로 일괄 가져온 다음, 고유 URL에 대해 ffprobe를 병렬로 실행하고 결과를 해상도 캐시에 저장합니다. 내부 루프는 캐시 히트가 됩니다. ffprobe 지연 시간은 루프에서 직렬로 처리되는 것이 아니라 고유 URL당 한 번만 지불됩니다.
2. MediaLive 쓰기 작업을 약 25개 프로그램 단위의 배치로 통합합니다. 오케스트레이터 내부에서, 200개의 액션이 대기열에 쌓이거나 마지막 프로그램에 도달할 때까지 액션들이 단일 BatchUpdateScheduleCommand로 누적됩니다. 각 프로그램이 약 7~8개의 액션을 생성하므로, 배치는 자연스럽게 각 25개 프로그램 정도가 됩니다. 200개 액션 제한은 MediaLive의 요청당 페이로드 제한보다 훨씬 낮게 유지하고 크기 때문에 배치가 거부될 가능성을 줄이기 위해 보수적으로 선택되었습니다 — 네트워크 비용을 상각할 만큼 충분히 크고, 프로그램당 폴백(다음 결정)이 실행되어야 할 때 비용이 적게 들 만큼 충분히 작습니다. 25번의 네트워크 왕복이 한 번으로 대체됩니다. 모든 프로그램 사이에 있던 200ms 스로틀은 이제 모든 배치 사이에 적용됩니다.
3. 속도를 위한 배치 처리, 정확성을 위한 폴백. 통합은 단일 불량 프로그램이 배치 내의 다른 24개 프로그램을 오염시키지 않을 때에만 안전합니다. submitProgramBatch가 예외를 발생시키면, catch 블록은 retryBatchAsIndividuals를 호출하여 실패한 배치 내의 각 프로그램을 자체 BatchUpdateScheduleCommand로 재제출하고, 프로그램당 status: 'scheduled' 또는 status: 'error'를 기록하며, 시도 사이에 200ms를 대기하고, 각 부분적인 성공 후에 액션 타임라인을 다시 고정합니다. 빠른 경로는 배치 처리됩니다. 복구 경로는 세분화됩니다. 운영자는 어떤 경우든 프로그램별 차이점을 얻습니다.
4. 배포 마지막에 고아 작업을 정리하고, 비행 중에 방지하지 않습니다. sweepIncompleteProgramGroups는 배포 마지막에 한 번 실행되어 깨끗한 최종 상태에 도달하지 못한 모든 액션 그룹을 제거합니다. 우리는 루프 동안 채널의 내부 일관성을 유지하려고 의도적으로 시도하지 않습니다 — 이는 15분 예산 내에 들어가야 하는 롤백 경로를 의미할 것이기 때문입니다. 정리는 단일 스윕이며 트랜잭션이 아닙니다.
5. Lambda를 확장하는 대신, 카탈로그가 확장될 때 교체합니다. 오늘날 우리가 배포하는 모든 것에 대해 단일 호출 경로는 예산 내에서 충분히 완료됩니다. 솔직한 스케일 아웃은 Step Functions 청킹입니다 — 재생 목록을 분할하고, 청크를 병렬 상태 머신으로 실행한 다음, 재조립합니다. 이는 1개월 이상 및 6개월 재생 목록을 위한 경로입니다. 이는 설계되었지만, 배포되지는 않았습니다. 이것을 다음 단계라고 부르는 것이 이미 실행 중이라고 가장하는 것보다 더 유용합니다.
결과
- 내부 테스트에서, 195개 프로그램 배포가 약 44초 만에 완료되었습니다 — 이는 수 분이 걸리던 단일 프로그램 처리 방식의 기준선에서 크게 단축된 것입니다.
- 360개 프로그램(약 1개월) 재생 목록에 대한 문서화된 목표는 615초 백엔드 타임아웃과 15분 Lambda 제한 내에서 90초 이하이며, 이는 충분히 여유 있는 시간입니다.
- 배치 내 하나의 불량 프로그램이 더 이상 다른 24개의 프로그램을 중단시키지 않습니다. 운영자는 모든 배포에서 프로그램별 상태 맵을 반환받습니다.
- 요청의 느린 부분들 — Mongo 조회 및 ffprobe —은 스케줄 항목당 한 번이 아니라 고유 리소스당 한 번만 처리됩니다.
- 15분 제한은 우리가 실제로 출시하는 카탈로그 크기에 대한 제한 요소가 더 이상 아닙니다. 다시 제한 요소가 될 경우, 더 큰 Lambda가 아닌 Step Functions 청킹이 해결책이 될 것입니다.
기술 스택: AWS Lambda · AWS MediaLive · NestJS · MongoDB · Node 18 · ffprobe · TypeScript · AWS SDK v3

