오디오 조립을 VPC 밖 Lambda 함수로 둔다
맥락
직전 설계는 api·realtime·worker 셋을 한 저장소의 boot 모듈로 가르는 것이었고, worker 를
따로 둔 이유는 하나였다 — 인코딩이 api 의 CPU 를 나눠 쓰면 안 된다.
ADR-0022로 포맷이 바뀌면서 그 CPU 가 없어졌다.
| 5시간 회의 | 직전 설계 (PCM) | 지금 (Opus) |
|---|---|---|
| 내려받을 양 | 864 MB | 72 MB |
| ffmpeg 가 하는 일 | Opus 인코딩 | -c copy — 컨테이너만 다시 쓴다 |
| 실제 병목 | CPU | S3 GET 수백 번 |
그리고 이 일은 뾰족하다. 회의 종료가 정각에 몰리고 나머지 시간엔 아무 일도 없다. 상시 태스크를 띄우면 노는 값을 낸다.
Lambda 를 한 번 검토했다가 물린 적이 있는데, 그때 든 근거가 틀렸다 — "임시 디스크가 부족하다"는
/tmp 가 10 GB 까지 되므로 사실이 아니었다. 다만 그때는 진짜 제약을 안 봤다. 이 함수가 RDS 를
부르면 VPC 에 들어가야 하고, 들어가면 pyannote 를 부르려고 NAT 가 필요해지고, ENI·커넥션 풀·서브넷이
전부 딸려 온다. 그러면 「관리 대상을 줄인다」는 이득이 사라진다.
결정
-
조립을 ECS 서비스가 아니라 Lambda 함수로 둔다. ffmpeg 를 담은 컨테이너 이미지로 배포한다.
workerboot 모듈을 만들지 않는다 — Spring 쪽은api·realtime둘로 갈린다. -
함수는 DB 도 벤더도 인터넷도 모른다. 이것이 이 결정이 기대는 성질이고, 깨지면 결정도 깨진다.
입력 { meetingId, [세그먼트 키 순서대로], [무음 삽입 위치] }출력 { mergedKey, durationMs } ← 「조립 완료 → 제출」 큐로- RDS 를 안 부르므로 VPC 밖에 둔다. VPC 없는 Lambda 도 S3 는 AWS 내부망으로 가므로 NAT 요금이 없다.
- pyannote 를 안 부르므로 벤더 키를 안 갖는다. 제출과 webhook 수신을 api 한쪽에 모은다 — 직전 설계의 비대칭(worker 가 보내고 api 가 받는다)이 없어진다.
-
되돌아오는 길에 큐를 하나 더 둔다. Lambda 의 반환값은 아무 데도 안 가므로 「조립 완료 → 제출」 큐가 필요하다. 홉 하나를 내주고 격리를 산다. 큐가 둘에서 셋이 된다.
-
출력 키를 결정적으로 둔다(
{meetingId}/merged.ogg). 큐의 「적어도 한 번」 전달이 같은 바이트를 덮을 뿐이라 멱등 처리가 따로 필요 없다. -
기계 매핑(화자 구간 ↔ 전사 세그먼트)은 큐로 빼지 않는다. 구간 겹침 조인 하나이고 회의당 한 번 돌며 데이터가 이미
heymoa안에 있다. ADR-0006이 세운 기준 — 같은 프로세스 안에서 끝나는 일에는 큐를 안 끼운다 — 을 그대로 적용한다.
대안
대안 1 — ECS 서비스 worker. 직전 설계. 배포 아티팩트가 늘지 않고 Spring 코드로 짤 수 있다.
기각한 이유: 상시 태스크가 대부분의 시간에 논다. 그리고 CPU 를 나눠 쓰는 문제 자체가 없어져서
분리의 원래 목적이 사라졌다.
대안 2 — ECS RunTask 로 일회성 컨테이너. 15분 제한이 없고 패키징이 평범하다.
기각한 이유: 시작에 30~60초가 드는데 일 자체가 수십 초다. 시작이 본체보다 오래 걸린다.
대안 3 — Lambda 를 VPC 안에 두고 DB 도 보게 한다. 잡 상태를 함수가 직접 쓰면 큐 하나가 준다. 기각한 이유: ENI·NAT·커넥션 풀이 딸려 오고 Lambda 를 고른 이유가 그 자리에서 상쇄된다. 큐 하나가 그보다 싸다.
결과
- ECS 서비스가 셋으로 유지된다(api·realtime·ai). IaC 가 없는 형상에서 콘솔로 만들 것이 하나 안 는다.
- 함수가 VPC 밖이라는 것이 격리를 강제한다. "조립은 DB 를 안 본다"가 문장이 아니라
rds-sg에 넣을 소스 자체가 없다는 사실로 강제된다. - api 에 S3
GetObject권한이 새로 생긴다. presigned URL 을 서명해야 하기 때문이다. 바이트는 여전히 api 를 안 지난다. - 예약 동시성이 DLQ 를 오염시킬 수 있다. 막힌 메시지가 실행도 못 해 보고
receiveCount를 소진한다 — 함수가 한 번도 안 돌았는데 DLQ 에 쌓인다. DLQ 유입을 「실패」와 「대기열이 밀렸다」로 갈라 볼 수 있어야 한다. - 관측이 CloudWatch 에 묶인다. 실행 환경이 응답 직후 얼어 배치 익스포터가 전송 전에 멈춘다.
그리고 판단에 쓰는 지표(
ApproximateAgeOfOldestMessage·DLQ 깊이·Throttles)가 CloudWatch 에만 있다 — ADR-0017의 "앱이 직접 OTLP 로 민다"가 이 함수에는 그대로 적용되지 않는다. - 15분 제한은 지금 형상에서 여유가 크지만 실측 전에는 모른다. 넘기면 대안 2 로 돌아간다.