라이브 스트리밍 시스템 설계
동시 시청자 100만 규모 라이브 방송을 비디오·채팅·결제 세 경로로 나눠 설계하는 원리를 인제스트부터 팬아웃까지 정리
Contents
100만 동시 시청자 규모에서는 비디오·채팅·결제라는 성격이 다른 세 트래픽을 한 경로에 섞지 않는 것이 설계의 출발점입니다.
세 갈래 트래픽 분리
동시 시청자 100만 명 규모의 라이브 방송은 한 종류의 부하 문제가 아닙니다. 비디오 전송, 채팅, 결제는 데이터 크기도 지연 허용치도 정합성 요구도 서로 다릅니다. 이 셋을 한 경로에 묶으면 한쪽의 폭주가 나머지까지 같이 무너뜨립니다.
규모를 먼저 가정해야 설계 판단의 기준이 섭니다. 이 글은 아래 규모를 전제로 두며, 수치는 설계 연습용 가정값입니다.
- 일일 활성 사용자(Daily Active Users) 3,500만, 글로벌 피크 동시 접속 500만
- 단일 채널 최대 동시 시청자 100만 (극단값)
- 피크 송출 대역폭: 대형 단일 채널 수 Tbps, 글로벌 피크 수십 Tbps 이상
- 피크 채팅 유입: 글로벌 초당 수십만 건 이상, 팬아웃 초당 수천만 이벤트
- 목표 지연: 미디어 수 초, 채팅·이벤트 수십~수백 밀리초
세 트래픽의 성격을 한 표로 정리하면 분리의 이유가 분명해집니다. 핵심은 각 경로가 절대 어기면 안 되는 제약이 다르다는 점입니다.
| 경로 | 트래픽 성격 | 핵심 제약 | 절대 금지 |
|---|---|---|---|
| 비디오 전송 | 무겁고 상태가 큼 (수 Tbps) | 촬영부터 재생까지 지연 수 초 | 원본·API 서버가 미디어 바이너리 직접 서빙 |
| 채팅 | 초당 수만 건, 휘발성 | 팬아웃 폭증, 클라이언트 렌더링 부하 | 관계형 데이터베이스에 동기 삽입 |
| 결제·이벤트 | 저빈도, 금융 | 원자성·정합성(ACID) | 채팅과 같은 휘발성 경로로 처리 |
경로를 나누면 채팅 버스트가 결제를 막지 않고, 미디어 트래픽이 API 서버를 죽이지 않습니다.
비디오 경로
이 경로는 가장 무겁고 지연을 수 초까지 허용합니다. 송출부터 재생까지 다섯 단계를 거치며, 각 단계가 부하를 단계적으로 흡수합니다.
수신과 트랜스코딩
스트리머의 송출 도구는 RTMP(Real-Time Messaging Protocol)로 원본을 보냅니다. RTMP는 TCP 위에서 핸드셰이크를 마친 뒤 스트림을 흘립니다. 손실 네트워크에 강한 SRT(Secure Reliable Transport)도 수신에 쓰입니다. WebRTC를 쓰는 WHIP(WebRTC-HTTP Ingestion Protocol)도 같은 용도의 후보입니다.
실제 대규모 서비스는 전 세계 거점(Point of Presence)마다 미디어 프록시를 둡니다. 어떤 프로토콜이든 거점에서 종단한 뒤, 백본 내부에서는 하나의 정규 프로토콜로 변환합니다. 인증은 매번 데이터베이스를 조회하지 않고, 캐시된 스트림 키 검증으로 지연을 줄입니다. 스트림 키는 평문 대신 해시나 비밀 저장소로 다룹니다.
수신한 원본 한 줄기는 여러 화질로 푸는 적응형 비트레이트(Adaptive Bitrate) 사다리로 바뀝니다. 트랜스코딩은 이 경로에서 가장 비싼 단계입니다.
1080p60 한 스트림을 트랜스코딩하는 데 대략 4~6 CPU 코어가 든다고 알려져 있습니다(추정). 동시 방송이 수만 채널이면 이 비용이 인프라의 큰 축이 됩니다. Twitch가 FFmpeg 대신 자체 트랜스코더를 만든 배경이 여기에 있습니다(Twitch 엔지니어링 블로그).
패키징과 지연
트랜스코딩 결과는 짧은 세그먼트와 매니페스트(.m3u8)로 잘려 콘텐츠 전송 네트워크(CDN)에 올라갑니다. 일반 HLS(HTTP Live Streaming)는 지연이 1030초라 라이브에 부족합니다. 저지연 HLS(LL-HLS)가 이를 25초까지 줄이며, 핵심 장치는 셋입니다(LL-HLS 공개 자료 기준, 추정 범위).
- 부분 세그먼트: 6초 세그먼트를 200~500밀리초 조각으로 더 잘라, 완성 전에 조각부터 전송
- 프리로드 힌트: 아직 없는 다음 조각의 URL을 미리 광고하고, 요청이 오면 데이터가 준비될 때까지 응답을 보류
- 델타 플레이리스트·블로킹 리로드: 변경분만 받고, 다음 버전이 준비될 때까지 요청을 막음
공통 컨테이너 포맷(CMAF)을 쓰면 HLS와 DASH(Dynamic Adaptive Streaming over HTTP)가 같은 세그먼트를 공유해 저장 공간과 캐시를 아낍니다. 초저지연이 필요한 경우 WebRTC도 후보지만, 규모가 커지면 선택이 갈립니다.
| 방식 | 지연 | 확장성 | 적합한 상황 |
|---|---|---|---|
| 저지연 HLS·DASH | 2~5초 | 기존 CDN 그대로 사용 | 일반 대규모 단방향 방송 |
| WebRTC | 0.5초 미만 | 선택적 전달 서버 필요, 비쌈 | 양방향·경매·통화 |
100만 명이 단방향으로 볼 때는 저지연 HLS가 현실적인 답입니다. WebRTC급 초저지연은 CDN 캐시를 활용하기 어려워, 이 규모에서는 비용이 빠르게 불어납니다.
CDN과 화질 선택
API 서버는 비디오 바이너리를 직접 반환하지 않습니다. 재생 요청에는 CDN의 매니페스트 URL만 돌려주고, 이후 플레이어가 엣지에서 직접 세그먼트를 가져옵니다. 트래픽의 무게는 엣지 계층이 받아냅니다.
- 엣지는 세그먼트를 30~60초 캐시해 여러 시청자가 재사용하게 합니다(방송 종료 시 낡은 데이터 방지와의 균형).
- 같은 세그먼트로 몰린 동시 요청은 요청 합치기로 원본에 한 번만 전달해 순간 폭주를 흡수합니다.
- 단일 대형 채널 수 Tbps, 글로벌 피크 수십 Tbps를 이 계층이 흡수합니다.
화질 전환은 서버가 아니라 클라이언트가 판단합니다. 적응형 비트레이트 알고리즘은 세 계열로 나뉩니다. 처리량 기반은 최근 다운로드 속도로 정해, 시작·탐색처럼 버퍼가 빌 때 강합니다. 버퍼 기반(BOLA)은 버퍼 수위만 보고 비트레이트와 리버퍼링을 저울질합니다.
둘을 섞은 혼합형이 dash.js의 기본값으로 실무 표준에 가깝습니다. 이 세 계열 비교는 ACM 논문 'Improving Bitrate Adaptation in the DASH Reference Player'에 정리돼 있습니다.
채팅 경로
메시지는 가볍지만 휘발성이 크고, 한 건이 수만 명에게 퍼지는 팬아웃 문제를 안습니다. 초당 수만 건을 관계형 데이터베이스 밖에서 처리하는 것이 이 경로 설계의 축입니다.
계층형 팬아웃
Twitch 채팅은 Go로 작성된 분산 시스템으로, 두 컴포넌트가 핵심입니다(Twitch 개발 문서). 엣지는 클라이언트와 붙는 종단으로, Internet Relay Chat(IRC) 프로토콜을 TCP와 웹소켓 양쪽에서 처리합니다. 펍섭은 엣지 노드들 사이에서 메시지를 내부 분배합니다.
한 번 발행하면 펍섭이 관련 엣지에만 전달하고, 각 엣지가 자기에게 붙은 시청자에게 로컬로 뿌립니다. 발행은 1회, 팬아웃은 종단에서 N회로 나뉘는 구조입니다. Discord는 같은 문제를 Elixir 액터 모델로 풀어, 채널과 세션을 각각 프로세스로 만들고 메시지를 전달합니다(Discord 엔지니어링 블로그). 언어만 다를 뿐 '1회 발행 → 관심 있는 곳에만 분배 → 종단에서 로컬 전달'이라는 골격은 같습니다.
웹소켓 게이트웨이와 샤딩
시청자와 붙는 웹소켓 게이트웨이는 연결 수가 곧 부하입니다. 노드 한 대가 이상적으로 튜닝되면 수십만 유휴 연결을 들지만, 실무에서는 워커당 5만~10만 연결을 잡는 편입니다(추정). 병목은 세 곳입니다.
- 파일 디스크립터: 연결마다 하나씩 소모
- 메모리: 연결당 수 KB 단위로 누적
- CPU: 메시지 처리 비용
앞단 로드 밸런서는 일관 해싱이나 스티키 세션으로 연결을 특정 노드에 고정합니다. 노드 사이 전파는 Redis·Kafka·NATS 같은 펍섭 백플레인이 맡습니다. 단일 거대 채널은 한 서버가 못 받으므로, 채널 샤딩으로 시청자를 여러 파티션에 나눠 각 파티션이 자기 구독자에게만 전달하게 합니다.
버스트 대응과 클라이언트
대형 이벤트에서 채팅이 초당 수만~수십만 건으로 터지면, 모든 메시지를 같은 품질로 보존하기는 어렵습니다. 이때 우선순위는 비디오와 기본 채팅 경험을 지키는 데 둡니다. 점진적 성능 저하(graceful degradation)로 부하를 흘려보냅니다.
- 배칭: 여러 메시지를 묶어 전송 빈도를 낮춤
- 스로틀링: 초당 N개로 전송률 제한 (예: GetStream은 초당 5개 초과 시 스로틀)
- 샘플링·드롭: 과부하 시 일부 메시지를 의도적으로 누락
대신 모더레이션·신고·다시보기·분석용 데이터는 별도 비동기 로그 파이프라인에 저장해, 라이브 전달 경로와 분리합니다(GetStream 라이브 스트림 권장 사례). 서버가 다 보내도 브라우저가 못 버티는 병목도 남습니다. 화면에 보이는 메시지만 문서 객체 모델(DOM)에 올리는 가상 목록, 프레임 단위로 리렌더를 묶는 배칭, 오래된 메시지 잘라내기로 DOM 노드 수를 묶습니다.
채팅 본문에는 이미지 바이너리를 넣지 않고 이모티콘 ID만 실어 보냅니다. 클라이언트가 미리 받은 메타데이터로 CDN에서 이미지를 렌더합니다.
{ "sender": "viewer_ko", "message": "ㅋㅋㅋ", "emotes": ["pog_1", "kappa_2"] }자산 버전(asset_version) 필드로 이미지가 갱신되면 클라이언트 캐시를 무효화합니다. 페이로드를 가볍게 유지하는 것이 팬아웃 비용을 줄이는 직접적인 방법입니다.
시청자 수 집계와 결제 경로
동시 시청자 수와 결제는 채팅과 또 다른 경로로 다룹니다. 하나는 정확성보다 가용성이 중요하고, 다른 하나는 그 반대입니다.
먼저 동시 시청자 수는 같은 행을 갱신하는 방식으로 세면 안 됩니다. 아래 쿼리는 수백만 명이 입퇴장할 때마다 같은 행에 락 경합과 복제 지연을 일으킵니다.
UPDATE live_streams SET viewer_count = viewer_count + 1 WHERE id = 'stream_123';대신 입퇴장을 별도 이벤트 스트림으로 흘립니다. 시청자는 그냥 창을 닫으므로 명시적 퇴장에 기대지 않고 하트비트로 판단합니다. 집계는 Redis나 스트림 처리 엔진으로 근사합니다. HyperLogLog 같은 카디널리티 추정기는 표준 오차 약 0.81%로 1천만 명을 카운터당 12KB 고정 공간에 셉니다(Redis 공식 문서 기준). 정확한 1명 단위보다 수 초 단위 근사치가 라이브 화면에 더 적합합니다.
결제는 정반대 원칙을 따릅니다. 구독·후원은 원자성을 갖춘 영구 저장으로 다루고, 결제 상태를 명시적으로 기록합니다. 결제가 확정되면 별도 이벤트를 발행하고, 화면 오버레이 알림은 비동기로 웹소켓 경로에 전달합니다. 돈은 정확하게 저장하고, 알림은 최선 노력으로 전파해 정합성과 속도를 분리합니다.
정리
라이브 스트리밍 설계의 뼈대는 비디오·채팅·결제를 한 경로에 섞지 않는 분리 원칙입니다. 비디오는 트랜스코딩과 CDN 엣지로 무게를 흡수하고, 채팅은 계층형 팬아웃과 점진적 성능 저하로 폭주를 흘려보냅니다. 동시 시청자 수는 근사로 빠르게 세고, 결제는 원자적으로 정확히 저장해 서로 다른 제약을 각자 만족시킵니다. 이렇게 나누면 한 경로의 버스트가 다른 경로를 끌어내리지 않고, 각 계층을 독립적으로 키울 수 있습니다.