라이브 스트리밍 채팅 수집 시스템을 KEDA와 Karpenter로 확장한 방법
CEO & Fullstack Engineer
1. 이 글의 범위
SOOP·치지직·씨미의 방송을 찾고 채팅을 정규화해 PostgreSQL·Redis·ClickHouse로 나누는 과정은 멜로밍 랭킹 - SOOP, 치지직, 씨미 채팅 수집 및 처리 아키텍처에서 다뤘습니다.
이 글은 그 파이프라인의 데이터 모델이나 랭킹 집계를 반복하지 않습니다. 수집해야 할 채널 수가 바뀔 때 다음 네 가지 질문에 어떻게 답했는지에 집중합니다.
- CPU가 아니라 어떤 수요를 확장 신호로 사용할 것인가?
- 필요한 Worker Pod 수를 얼마나 빠르게 늘리고 줄일 것인가?
- Pod을 배치할 노드를 어떻게 제때 공급할 것인가?
- Scale-down과 Spot 중단 중 WebSocket 연결을 어떻게 안전하게 넘길 것인가?
이를 위해 KEDA, Karpenter, Spot Instance를 하나의 확장 제어 루프로 연결했습니다.
2. 배경: 왜 이 조합이 필요했는가
라이브 스트리밍 채팅 수집 서비스의 트래픽 패턴은 매우 불규칙합니다. 인기 방송의 시작과 종료에 따라 많은 WebSocket 연결이 짧은 시간에 생성되거나 사라지고, 이에 따른 메시지 처리량도 급격히 변합니다.
기존의 HPA(Horizontal Pod Autoscaler)만으로는 한계가 있었습니다:
- CPU/메모리 기반 스케일링의 한계: 채팅 수집 서비스의 부하는 CPU보다 연결 수와 채널 수에 비례합니다. CPU 사용률이 낮더라도 수천 개의 채널을 관리하면 이미 과부하 상태일 수 있습니다.
- 노드 프로비저닝 지연: Pod이 스케일아웃되어도 노드가 없으면 Pending 상태로 대기합니다. Cluster Autoscaler는 ASG 기반이라 노드 추가까지 수 분이 걸립니다.
- 비용 효율성: 피크 트래픽에 맞춘 고정 인프라는 유휴 시간의 비용 낭비가 큽니다.
이 세 가지 문제를 KEDA + Karpenter + Spot Instance 조합으로 해결했습니다.
3. 확장 제어 루프
데이터 파이프라인 전체가 아니라 확장에 관여하는 구성 요소만 남기면 다음과 같습니다.
flowchart LR
LIVE["수집 대상 채널 증가"]
COORD["Coordinator\nassigned·pending 계산"]
PROM["Prometheus\n수요 메트릭"]
KEDA["KEDA\nWorker replica 조정"]
PENDING["Pending Pod"]
KARP["Karpenter\n노드 프로비저닝"]
WORKER["Worker Pool\nWebSocket 연결"]
LIVE --> COORD --> PROM --> KEDA --> WORKER
KEDA --> PENDING --> KARP --> WORKER
WORKER -->|"가용 용량"| COORD
Coordinator는 할당된 채널과 아직 Worker를 찾지 못한 pending 채널을 계산합니다. Prometheus가 이 값을 노출하고 KEDA가 필요한 Worker replica 수로 변환합니다. 기존 노드에 자리가 없으면 Pod은 Pending이 되고, Karpenter가 스케줄링 요구사항에 맞는 노드를 공급합니다.
이 글에서 다루는 핵심은 이 폐루프의 속도와 안전장치입니다. 데이터가 어느 저장소로 흘러가는지는 확장 판단과 분리합니다.
4. KEDA 기반 Pod 오토스케일링
왜 KEDA인가
HPA v2도 custom.metrics.k8s.io를 통해 커스텀 메트릭 스케일링을 지원하지만, Prometheus 어댑터 설정이 복잡하고 스케일링 정책의 세밀한 제어가 어렵습니다.
KEDA는:
- Prometheus 쿼리를 직접 트리거로 사용 가능
- 스케일업/다운 속도를 독립적으로 제어
- 0→N 스케일링 (idle 상태에서 즉시 스케일아웃) 지원
- fallback 값 설정으로 메트릭 소스 장애 시에도 안정적 운영
ScaledObject 구성
KEDA ScaledObject의 핵심 설계를 살펴보겠습니다.
트리거: 비즈니스 메트릭 기반 쿼리
스케일링 기준으로 사용하는 Prometheus 쿼리의 구조는 다음과 같습니다:
(할당된 채널 수 × 리더 필터) + 대기 중인 채널 수
- 할당된 채널 수: Coordinator가 Worker들에게 이미 분배한 채널의 총합
- 리더 필터: Coordinator가 2대 이상일 때 리더 인스턴스의 메트릭만 사용 (이중 집계 방지)
- 대기 채널 수: 아직 어떤 Worker에도 할당되지 않은 채널
이 합이 threshold를 넘으면 Worker Pod을 추가합니다. Worker 1대의 최대 용량 대비 약 70% 수준에서 선제적으로 스케일아웃하는 셈입니다.
fallback 설정
fallback:
failureThreshold: 3 # 3회 연속 메트릭 수집 실패 시
replicas: 4 # 안전한 replica 수로 고정
Prometheus가 일시적으로 불가용해도 서비스가 최소한의 용량을 유지합니다.
비대칭 스케일링 정책
스케일업과 스케일다운의 속도를 의도적으로 비대칭으로 설정했습니다:
| 방향 | 속도 | stabilization | 이유 |
|---|---|---|---|
| 스케일업 | 빠르게 (수십 초 단위) | 짧은 안정화 | 방송 시작 시 빠르게 대응 |
| 스케일다운 | 천천히 (수 분 단위) | 긴 안정화 | 핸드오프 중 메시지 유실 방지 |
스케일업은 빠르게, 스케일다운은 천천히. 채팅 수집 서비스에서 이 비대칭이 중요한 이유:
- 스케일업: 방송 시작 시 수백 개 채널이 동시에 활성화됩니다. 짧은 주기로 Pod을 추가하면 빠르게 필요한 처리 용량에 도달할 수 있습니다.
- 스케일다운: Worker가 제거되면 해당 Worker의 채널들이 다른 Worker로 핸드오프됩니다. 이 과정에서 잠시 메시지 유실이 발생할 수 있으므로, 최대한 천천히 줄여야 합니다.
5. Spot Instance 운영 전략
Spot Instance는 On-Demand 대비 60~90% 저렴하지만, 클라우드 프로바이더가 언제든 짧은 사전 통보로 회수할 수 있습니다. 실시간 채팅 수집 서비스에서 이를 안전하게 사용하기 위한 전략이 필요합니다.
Karpenter를 활용한 노드 프로비저닝
KEDA가 Pod 수를 늘려도 노드가 없으면 소용없습니다. Karpenter는 Pending Pod의 스케줄링 요구사항을 직접 관찰하여 필요한 노드를 즉시 프로비저닝합니다.
워크로드 특성에 따라 여러 NodePool을 운영하고 있습니다. 비용 최적화를 위해 일반 워크로드에는 Spot Instance 우선 + On-Demand 폴백 전략을, 중단이 허용되지 않는 핵심 서비스에는 On-Demand 전용 풀을 사용합니다. 인스턴스 아키텍처는 ARM64(Graviton)를 기본으로 사용하여 추가적인 비용 절감 효과를 얻고 있습니다.
인터럽션 감지 파이프라인
graph LR
EB[AWS EventBridge] --> SQS[SQS Queue] --> KC[Karpenter Controller]
EB -.->|Health Events| SQS
EB -.->|Spot Interruption| SQS
EB -.->|Rebalance Recommendation| SQS
EB -.->|Instance State Change| SQS
KC -->|1| Cordon[노드 Cordon]
KC -->|2| Drain[Pod Drain 시작]
KC -->|3| New[새 노드 프로비저닝]
KC -->|4| Resched[Pod 재스케줄링]
AWS 인프라 이벤트를 EventBridge Rule로 캡처하여 SQS 큐로 전달합니다:
- Spot Interruption Warning: 종료 예정 통보. 즉시 drain 시작.
- Rebalance Recommendation: “더 안정적인 인스턴스로 옮기는 게 좋겠다”는 소프트 시그널. 선제적 마이그레이션.
- Health Events: AWS 인프라 유지보수 알림.
- State Change: 인스턴스 라이프사이클 이벤트.
Karpenter 컨트롤러가 SQS를 폴링하여 이벤트를 처리하고, 영향받는 노드의 Pod을 안전하게 다른 노드로 이동시킵니다.
다층 방어 전략
Spot 인터럽션으로부터 채팅 서비스를 보호하는 4개 레이어:
Layer 1: 인스턴스 다양성
인스턴스 카테고리(컴퓨팅/메모리/범용)와 세대를 넓게 지정하여 Karpenter가 가용한 Spot 용량을 최대한 활용합니다. 특정 인스턴스 타입에 의존하지 않으므로, 한 타입의 Spot 용량이 소진되어도 다른 타입으로 대체됩니다.
Layer 2: Pod Disruption Budget
maxUnavailable: 1로 설정하여, Karpenter가 인터럽션 경고를 받아 graceful drain을 수행할 때 Pod이 1개씩만 순차적으로 evict되도록 합니다. 단, PDB는 자발적 disruption(voluntary eviction)에만 적용된다는 점에 주의해야 합니다. 클라우드 프로바이더가 경고 없이 인스턴스를 강제 종료하면 PDB와 무관하게 Pod이 즉시 kill됩니다. 따라서 이 방어 레이어는 Karpenter가 인터럽션 경고를 수신하고 사전에 drain을 수행하는 것에 의존합니다.
Layer 3: Graceful Drain
Worker Pod은 SIGTERM을 받으면 점진적으로 채널을 다른 Worker로 핸드오프합니다. Spot 사전 경고 시간 + terminationGracePeriod로 안전한 마이그레이션이 가능합니다.
Layer 4: KEDA fallback
Prometheus가 일시적으로 불가용해도 사전 설정된 안전 replica 수를 유지합니다.
6. 세 시스템의 연동
KEDA, Karpenter, Spot Instance가 어떻게 협력하는지 시나리오로 살펴보겠습니다.
시나리오: 인기 스트리머 방송 시작
T+0s: Discover가 새로운 활성 채널 다수 감지
→ Coordinator가 대기 큐에 추가
→ 처리 대상 채널 수 급증
T+30s: KEDA가 채널 수 메트릭 확인 → threshold 초과 → Worker 스케일아웃
→ 새 Pod 생성 → 기존 노드에 배치 가능한 만큼 배치, 나머지는 Pending
T+35s: Karpenter가 Pending Pod 감지
→ ARM64 Spot 인스턴스 프로비저닝 시작
T+90s: 새 노드 Ready → Pending Pod 스케줄링
→ 증가한 Worker들이 채널 분산 처리
T+120s: KEDA 재확인 → 채널이 계속 증가 중이면 추가 스케일아웃
시나리오: Spot 인터럽션 발생
T+0s: Spot 인터럽션 경고 발송 → SQS에 도착
→ Karpenter가 해당 노드를 Cordon (새 Pod 스케줄 방지)
T+5s: Karpenter가 대체 노드 프로비저닝 시작
→ PDB 확인: 해당 노드의 Pod을 1개씩 순차 drain
T+10s: Worker Pod이 SIGTERM 수신
→ readiness probe 실패 → 트래픽 수신 중단
→ Coordinator에 drain 시그널 전송
→ 채널 핸드오프 시작
T+60s: 대체 노드 Ready → 새 Worker Pod 스케줄링
T+90s: 핸드오프 완료, 기존 Pod 종료
T+120s: 원래 인스턴스 종료
→ 이미 모든 Pod이 안전하게 이동 완료
7. Scale-down을 안전하게 만드는 Drain 프로토콜
KEDA가 replica를 줄이거나 Spot 노드가 회수될 때 Worker는 수집 중인 WebSocket을 다른 Worker로 넘겨야 합니다. 이 글에서는 데이터 저장 단계의 중복 제거를 다시 설명하지 않고, 스케일링 관점의 종료 순서에 집중합니다. 중복과 멱등성 설계는 랭킹 수집·처리 아키텍처 글에서 다룹니다.
graph TD
SIG["SIGTERM 수신"] --> D1["draining 전환\n새 채널 할당 거부"]
D1 --> D2["readiness 실패\n새 트래픽 차단"]
D2 --> D3["Coordinator에 drain 통지\n채널 재배정"]
D3 --> D4["신규 Worker 연결 확인"]
D4 --> D5["기존 WebSocket 종료"]
D5 --> D6["Pod 종료"]
Scale-down을 느리게 제한한 이유가 여기에 있습니다. 한 번에 여러 Worker를 줄이면 재배정, 플랫폼 재연결과 readiness 회복이 동시에 몰립니다. replica 감소 속도는 단순한 비용 설정이 아니라 애플리케이션 handoff가 감당할 수 있는 속도여야 합니다.
종료 유예 시간도 고정된 관례가 아니라 실제 handoff 상한보다 길어야 합니다. Coordinator의 헬스체크만 기다리지 않고 Worker가 SIGTERM 직후 명시적인 drain 신호를 보내도록 해, 종료 감지와 재배정 사이의 공백을 줄였습니다.
8. 배포 파이프라인에서의 KEDA 제어
Worker 배포 시 KEDA와의 충돌을 방지하기 위한 전용 파이프라인이 있습니다.
문제: KEDA vs Rolling Update
일반적인 Deployment 업데이트 중에 KEDA가 메트릭 변동을 감지하면:
- 새 이미지로 롤아웃하는 중에 Pod 수를 줄이려 할 수 있음
- 또는 롤아웃 중 일시적 부하 증가로 불필요한 스케일아웃 발생
해결: KEDA Pause/Resume 패턴
배포 파이프라인에서 다음 순서로 KEDA를 제어합니다:
graph TD
S1["1. 현재 상태 스냅샷<br/>(replica 수, 이미지 태그)"] --> S2["2. KEDA paused-replicas 설정<br/>스케일링 일시 중단"]
S2 --> S3["3. 이미지 업데이트<br/>롤아웃 대기"]
S3 -->|성공| S5["5. paused-replicas 제거<br/>KEDA 스케일링 재개"]
S3 -->|실패| S4["4. 이전 이미지로 롤백"]
S4 --> S5
핵심은 KEDA의 autoscaling.keda.sh/paused-replicas 어노테이션입니다. 이 어노테이션이 설정되면 KEDA가 해당 replica 수를 고정하고, 어노테이션을 제거하면 정상 스케일링으로 복귀합니다.
롤백 시에도 반드시 마지막 단계(KEDA resume)를 실행해야 합니다. KEDA가 pause 상태로 남으면 메트릭 변동에 대응하지 못해 더 큰 장애로 이어질 수 있습니다.
이 패턴은 KEDA를 사용하는 모든 서비스의 배포에 적용할 수 있는 범용적인 방법입니다.
9. 운영하면서 겪은 문제들
문제 1: 메트릭 라벨 변경 후 스케일링 장애
증상: KEDA가 대기 채널 수를 0으로 읽어 스케일아웃이 안 됨
원인: 서비스 코드에서 Prometheus 메트릭의 라벨을 변경했는데, KEDA의 Prometheus 쿼리가 이전 라벨 기준으로 작성되어 있어 메트릭이 매칭되지 않았습니다.
교훈: Prometheus 메트릭의 라벨을 변경할 때는 반드시 KEDA ScaledObject, 대시보드, 알림 규칙의 PromQL을 모두 확인해야 합니다. 메트릭 라벨은 코드 변경이지만 영향 범위는 인프라 전체입니다.
문제 2: Spot 인터럽션 중 핸드오프 실패
증상: Spot 노드가 회수되면서 Worker가 종료되었는데, 해당 Worker의 채널들이 다른 Worker로 이동하지 않음
원인: Coordinator의 헬스체크 타임아웃보다 Spot 인터럽션 + drain이 더 빨리 진행되어, Coordinator가 Worker를 장애 상태로 인식하기 전에 이미 프로세스가 종료됨
해결: preStop hook에 짧은 sleep을 추가하고, Worker가 SIGTERM 즉시 Coordinator에 명시적 drain 시그널을 전송하도록 변경. 헬스체크 타임아웃에 의존하지 않고 능동적으로 핸드오프를 트리거합니다.
문제 3: 노드 통합과 PDB 충돌
증상: 저트래픽 시간대에 Karpenter가 노드를 통합하려 하지만, PDB 때문에 Pod을 evict하지 못해 빈 노드와 과밀 노드가 공존
해결: 노드 통합 정책에 충분한 대기 시간을 두어 일시적 부하 변동에 반응하지 않도록 설정. PDB는 서비스 안정성을 위해 양보하지 않되, Karpenter에게 충분한 시간을 주는 것이 핵심입니다.
10. 마무리
이 글에서 다룬 핵심 설계 원칙을 정리하면:
- 비즈니스 메트릭 스케일링: CPU가 아닌 채널 수로 스케일링하여 실제 부하를 반영
- 비대칭 스케일링 정책: 스케일업은 공격적으로, 스케일다운은 보수적으로
- Scale-down 안전장치: PDB + Graceful Drain + KEDA Fallback
- Spot 다층 방어: 인스턴스 다양성, PDB, Graceful Drain, fallback의 4중 방어
- 배포-스케일링 분리: 배포 중 KEDA를 일시 정지하여 간섭 방지
KEDA, Karpenter, Spot Instance는 각각 다른 레이어의 문제를 해결하지만, 함께 동작할 때 비로소 비용 효율적이면서도 안정적인 실시간 서비스 운영이 가능해집니다. 수집된 메시지의 정규화·저장·랭킹 처리는 이 확장 제어 루프와 분리해 랭킹 수집·처리 아키텍처 글에 정리했습니다.
다음 글에서는 이렇게 수집한 채팅 데이터를 멜로밍 랭킹 서비스에서 활용하기 위한 ClickHouse 운영기를 다루겠습니다.