Design Twitch 트위치 - 100만 동시 시청자를 버티는 실시간 스트리밍 시스템 디자인
100만 동시 시청자 시대의 라이브 스트리밍 아키텍처 - CDN, WebSocket, Pub/Sub, 결제 이벤트를 분리하는 법
Twitch(트위치)는 단순한 웹 동영상 재생 서비스가 아니라, 초저지연 라이브 미디어 전송 인프라와 대규모 실시간 Pub/Sub 메시징 시스템이 결합된 분산 플랫폼에 가깝습니다.
e스포츠 결승전이나 글로벌 탑 스트리머의 방송에 단일 채널 기준 수십만 명에서 100만 명 수준의 동시 시청자가 몰릴 수 있습니다. 이때 시스템은 1080p 60FPS 고해상도 미디어 스트림을 전 세계 엣지 인프라로 짧은 지연 시간 안에 배포해야 합니다.
동시에 채팅, 이모티콘, 구독, 후원, 시청자 수 같은 실시간 이벤트도 처리해야 합니다. 특히 단일 채널에서 초당 수만 건의 채팅 메시지가 발생하면, 이를 일반적인 데이터베이스 쓰기 경로로 처리해서는 안 됩니다. 미디어 전송 경로, 채팅 경로, 결제 및 이벤트 경로를 서로 분리해야 안정적인 처리가 가능합니다.
이 글에서는 Twitch 같은 라이브 스트리밍 서비스를 다음 질문을 중심으로 설계합니다.
“100만 명 수준의 시청자에게 고화질 라이브 비디오를 전송할 때, 원본 서버와 API 서버에 미디어 트래픽이 직접 집중되지 않도록 트랜스코딩과 CDN 인프라를 어떻게 나눌 것인가?”
“단일 채널에서 초당 수만 개의 채팅이 발생할 때, 데이터베이스 쓰기 병목과 브라우저 DOM 렌더링 부담을 어떻게 줄일 것인가?”
“상태가 무겁고 비용이 큰 비디오 처리 파이프라인과, 가볍고 휘발성이 강한 실시간 채팅 파이프라인을 어떻게 분리할 것인가?”
“시청자의 네트워크 상태가 계속 바뀔 때, 비디오 플레이어는 어떤 기준으로 1080p, 720p, 480p 화질을 전환해야 하는가?”
“실시간 시청자 수, 구독, 후원 같은 이벤트를 전 세계 시청자에게 지연을 줄이면서 전달하려면 어떤 fan-out 구조가 필요한가?”
Twitch 같은 라이브 스트리밍 서비스 설계는 대규모 미디어 전송(Video Delivery)과 휘발성 실시간 트래픽 제어(Graceful Degradation)를 함께 다루기 좋은 시스템 디자인 문제입니다.
Functional Requirements - 기능적 요구사항
스트리머는 OBS 같은 방송 소프트웨어를 통해 RTMP ingest endpoint로 비디오/오디오 스트림을 송출할 수 있어야 합니다.
시청자는 본인의 네트워크 상태에 따라 1080p, 720p, 480p 등 서로 다른 해상도와 비트레이트의 미디어를 자동으로 수신할 수 있어야 합니다.
사용자는 채널별 WebSocket 세션에 연결되어 실시간 채팅 메시지를 송수신할 수 있어야 하며, 커스텀 이모티콘은 메시지 본문이 아니라 이모티콘 ID와 CDN URL 메타데이터 기반으로 처리되어야 합니다.
전 세계 분산 노드에서 시청 중인 단일 채널의 동시 시청자 수와 라이브 상태는 수 초 단위의 지연을 허용하되, 사용자 화면에는 일관된 방향으로 갱신되어야 합니다.
후원(Bits), 구독(Subscription) 같은 트랜잭션이 성공하면, 해당 채널을 시청 중인 사용자들에게 오버레이 이벤트가 실시간으로 전파되어야 합니다.
Non-Functional Requirements - 비기능적 요구사항
Ultra-Low Latency - 초저지연 미디어 파이프라인
스트리머의 캡처 장비에서 인코딩된 영상이 시청자 화면에 렌더링되기까지의 Glass-to-Glass Latency를 수 초 단위로 낮추는 것을 목표로 합니다.
일반 HLS보다 낮은 지연을 위해 LL-HLS 또는 유사한 저지연 스트리밍 방식을 사용할 수 있지만, 실제 지연 시간은 네트워크 상태, CDN 설정, 플레이어 버퍼 전략에 따라 달라집니다.
Graceful Degradation - 우아한 성능 저하
대형 이벤트로 인해 단일 채널에 초당 수만~수십만 건의 채팅 버스트가 발생할 수 있습니다.
이 경우 시스템은 모든 메시지를 동일한 품질로 저장하고 전달하려고 하기보다, 채팅 전송 경로를 메인 스토리지에서 분리하고, 필요하면 batching, throttling, sampling, dropping을 적용해야 합니다.
목표는 모든 메시지를 동일한 경로에서 보존하는 것이 아니라, 비디오 재생과 기본 채팅 경험을 안정적으로 유지하는 것입니다.
High Availability & Flash Crowd 확장성
대형 e스포츠 경기, 유명 스트리머의 방송 시작처럼 특정 채널로 트래픽이 짧은 시간 안에 집중될 수 있습니다.
이때 미디어 전송은 CDN과 엣지 캐시 계층에서 흡수하고, 채팅과 이벤트는 channel sharding, WebSocket gateway fan-out, partitioned pub/sub 구조로 분산 처리해야 합니다.
Scope 정리
In Scope
RTMP ingest layer부터 transcoder farm, media origin, CDN 배포로 이어지는 라이브 미디어 write/read path 설계
HLS/LL-HLS 기반 미디어 segment와 manifest 갱신 구조
클라이언트 비디오 플레이어의 ABR(Adaptive Bitrate) 전략
단일 채널 초대형 CCU 상황을 처리하기 위한 WebSocket gateway, channel sharding, partitioned pub/sub 기반 실시간 메시징 구조
초고빈도 채팅 수신과 클라이언트 DOM 렌더링 부담을 줄이기 위한 batching, throttling, dropping 전략
Out of Scope
방송 종료 후 VOD 영구 저장소로 이관되는 스토리지 아키텍처와 VOD 전용 트랜스코딩 파이프라인
스트리머 정산 대시보드와 광고 수익 분배를 위한 데이터 웨어하우스 내부 설계
실시간 저작권 음원 필터링과 유해 영상 실시간 ML 추론 파이프라인
가정하는 규모 - Scale Estimation
DAU: 3,500만 명
Peak Global CCU: 500만 명
Maximum CCU in a Single Channel: 최대 100만 명 수준의 extreme case
Peak Outbound Network Bandwidth: 단일 대형 채널 기준 수 Tbps, 글로벌 피크 기준 수십 Tbps 이상
Peak Chat Ingestion QPS: 글로벌 기준 수십만 QPS 이상
Peak Chat Fan-out Events: 수천만 fan-out event/sec 수준까지 확장 가능하도록 설계
Target Latency: 미디어 지연은 수 초 단위, 채팅 및 이벤트 라우팅은 수십~수백 ms 단위를 목표로 설계
Core Entities
트위치 시스템의 데이터 모델링에서 중요한 점은 모든 데이터를 하나의 관계형 데이터베이스에 넣지 않는 것입니다.
유저, 채널, 구독 관계처럼 오래 유지되어야 하는 데이터는 영구 저장소에 둡니다.
반면 라이브 채팅, 실시간 시청자 수, 영상 청크 전송 상태처럼 초당 매우 자주 변하는 데이터는 메인 RDBMS의 동기 쓰기 경로에서 분리합니다.
즉, Twitch의 데이터 모델은 크게 세 가지 경로로 나뉩니다.
영구 저장 데이터
→ accounts, channels, subscriptions
미디어 인프라 메타데이터
→ live_streams, ingest node, manifest path, available renditions
휘발성 실시간 데이터
→ chat messages, viewer count, presence state
이 구분을 하지 않으면 채팅이나 시청자 수처럼 초고빈도로 변하는 데이터가 계정, 채널, 결제 같은 중요한 영구 데이터 경로에 영향을 줄 수 있습니다.
1. accounts, 사용자 및 스트리머 계정
서비스를 이용하는 일반 시청자와 방송을 송출하는 스트리머의 계정 정보입니다.
이 테이블은 실시간 채팅 메시지나 시청 기록을 저장하는 곳이 아닙니다. 웹소켓 세션 인증, 채널 소유권 확인, 스트리머 권한 검증에 필요한 최소 계정 정보를 저장합니다.
id: UUID
username: VARCHAR(50), UNIQUE
email: VARCHAR(255)
role: VIEWER | STREAMER | ADMIN
created_at: TIMESTAMP
updated_at: TIMESTAMPusername은 로그인과 채팅 디스플레이에 사용되므로 unique index가 필요합니다. role은 스트리머 권한, 관리자 권한, 일반 시청자 권한을 구분하는 데 사용합니다.
2. channels, 스트리머 채널 메타데이터
스트리머의 방송 채널을 나타내는 엔티티입니다. 채널 페이지, 검색, 추천, 팔로우 목록에서 자주 조회되므로 read-heavy한 데이터입니다. 다만, 실시간 시청자 수나 초고빈도 채팅 상태를 이 테이블에 직접 업데이트하면 안 됩니다.
id: UUID
streamer_id: UUID, UNIQUE
title: VARCHAR(255)
game_category_id: INT
stream_key_hash: VARCHAR(255)
is_live: BOOLEAN
current_live_stream_id: UUID, NULLABLE
version: INT
created_at: TIMESTAMP
updated_at: TIMESTAMPstream_key는 평문으로 저장하지 않는 것이 좋습니다. 대신, stream_key_hash 또는 별도 secret store에 저장하고, ingest 인증 시 검증합니다. is_live는 현재 라이브 상태를 나타내지만, 초 단위로 계속 업데이트되는 viewer count와는 분리해야 합니다.
version은 스트리머가 방송 제목이나 카테고리를 수정할 때 낙관적 락을 적용하기 위한 필드입니다.
3. live_streams, 실시간 라이브 세션 로그
스트리머가 방송을 시작할 때 생성되는 라이브 세션 정보입니다. 하나의 채널은 여러 번 방송할 수 있으므로, channels와 live_streams는 1:N 관계입니다. 이 테이블은 영상 파일 자체를 저장하지 않습니다. 인제스트 노드, 트랜스코딩 상태, 매니페스트 경로 같은 미디어 인프라 메타데이터를 추적합니다.
id: UUID
channel_id: UUID, INDEX
ingest_region: VARCHAR(50)
ingest_node_id: VARCHAR(100)
origin_manifest_url: VARCHAR(512)
available_renditions: JSONB
status: STARTING | LIVE | ENDING | ENDED
last_viewer_count_snapshot: INT
started_at: TIMESTAMP
ended_at: TIMESTAMP, NULLABLEavailable_renditions 예시는 다음과 같습니다.
{
"renditions": [
{"resolution": "1080p60", "bitrate_bps": 8000000, "manifest_name": "1080p60/index.m3u8"},
{"resolution": "720p60", "bitrate_bps": 5000000, "manifest_name": "720p60/index.m3u8"},
{"resolution": "480p30", "bitrate_bps": 2000000, "manifest_name": "480p30/index.m3u8"}
]
}last_viewer_count_snapshot은 실시간 카운터의 최종 값이 아니라, 장애 대응이나 UI fallback에 사용할 수 있는 주기적 스냅샷입니다. 실시간 시청자 수 자체는 Redis, Flink, Kafka Streams, HyperLogLog 같은 별도 경로에서 집계하는 편이 적합합니다.
4. stream_emotes, 채널 전용 커스텀 이모티콘
Twitch 채팅에서는 커스텀 이모티콘이 매우 자주 사용됩니다. 하지만 채팅 메시지 안에 이미지 바이너리를 직접 넣으면 안 됩니다. 채팅 payload에는 이모티콘 코드나 ID만 포함하고, 클라이언트는 미리 받은 메타데이터를 바탕으로 CDN에서 이미지를 렌더링합니다.
id: UUID
channel_id: UUID, NULLABLE
emote_code: VARCHAR(50)
cdn_url: VARCHAR(512)
asset_version: INT
is_global: BOOLEAN
created_at: TIMESTAMPchannel_id가 NULL이면 글로벌 공용 이모티콘으로 볼 수 있습니다.
asset_version은 이모티콘 이미지가 갱신되었을 때 클라이언트 캐시를 무효화하는 데 사용할 수 있습니다. 채팅 메시지는 이렇게 가볍게 유지합니다.
{
"sender": "viewer_ko",
"message": "ㅋㅋㅋ",
"emotes": ["pog_1", "kappa_2"]
}5. channel_subscriptions, 사용자 구독 및 후원 관계 관계 테이블
사용자가 특정 스트리머를 유료 구독하는 관계를 나타냅니다. 구독은 결제와 연결되므로 일반 채팅처럼 휘발성 이벤트로 처리하면 안 됩니다. ACID 트랜잭션이 필요한 영구 데이터입니다.
id: UUID
subscriber_id: UUID
channel_id: UUID
tier: TIER_1 | TIER_2 | TIER_3
is_active: BOOLEAN
started_at: TIMESTAMP
expires_at: TIMESTAMP
created_at: TIMESTAMP
updated_at: TIMESTAMPsubscriber_id + channel_id에는 index가 필요합니다.
사용자가 해당 채널에서 구독자 전용 이모티콘을 사용할 수 있는지, 구독자 전용 채팅에 참여할 수 있는지 판단할 때 자주 조회되기 때문입니다.
6. donation_events 또는 bits_transactions
후원 및 Bits 트랜잭션
기존 초안에서는 후원과 구독을 channel_subscriptions 안에서 함께 설명했는데, 실제 모델링에서는 분리하는 편이 좋습니다. 구독은 지속적인 관계이고, 후원/Bits는 개별 금융 이벤트입니다.
id: UUID
viewer_id: UUID
channel_id: UUID
amount: INT
currency_or_bits_type: VARCHAR(20)
payment_status: PENDING | CONFIRMED | FAILED
event_id: UUID
created_at: TIMESTAMP결제 성공 후에는 DONATION_CONFIRMED 또는 BITS_USED 이벤트를 발행하고, 이 이벤트를 WebSocket overlay fan-out 경로로 전달합니다. 이때 결제 기록은 영구 저장소에 남기고, 오버레이 전파는 비동기로 분리합니다.
RDBMS 동기 쓰기 경로에서 분리해야 하는 데이터
1. 실시간 채팅 메시지
초대형 채널에서는 초당 수만 건의 채팅 메시지가 발생할 수 있습니다. 이를 매번 RDBMS에 동기 INSERT하면 디스크 쓰기, index update, lock 경합, replication lag 비용이 커집니다. 따라서, 라이브 화면에 전달되는 채팅 경로는 RDBMS 동기 쓰기에서 분리합니다. 대신 다음과 같은 구조를 사용합니다.
WebSocket Gateway
→ Partitioned Pub/Sub
→ Gateway Fan-out
→ Client Rendering Buffer다만 모든 채팅을 “영구 저장하지 않는다”고 단정하면 안 됩니다. 모더레이션, 신고, 스팸 탐지, VOD chat replay, 분석 목적의 일부 메시지는 별도 비동기 로그 파이프라인에 저장할 수 있습니다.
즉, 정리하면 이렇습니다.
라이브 채팅 전달 경로
→ RDBMS 동기 쓰기 제외
모더레이션/분석/VOD replay 경로
→ 비동기 로그 저장 가능2. 실시간 동시 시청자 수
동시 시청자 수를 다음처럼 업데이트하면 안 됩니다.
UPDATE live_streams
SET viewer_count = viewer_count + 1
WHERE id = 'stream_123';전 세계 수백만 명의 시청자가 들어오고 나갈 때마다 같은 row를 업데이트하면 row lock 경합과 replication 지연이 커집니다. 대신, presence event를 별도 스트림으로 처리합니다.
viewer_joined
viewer_left
heartbeat이 이벤트를 Redis, Kafka Streams, Flink, HyperLogLog, Count-Min Sketch 같은 구조로 집계합니다. 정확한 1명 단위 숫자보다 수 초 단위의 근사치가 더 적합합니다. 전달 방식도 여러 선택지가 있습니다.
별도 lightweight metadata endpoint
WebSocket metadata channel
manifest sidecar metadata
m3u8 metadata tag다만 .m3u8에 직접 viewer count를 넣는 방식은 CDN 캐시 정책과 최신성 제어를 같이 설계해야 하므로 단정하지 않는 편이 좋습니다.
최종 정리
Twitch의 Core Entity 설계에서 중요한 원칙은 이것입니다.
영구성이 필요한 데이터는 RDBMS에 둡니다.
accounts
channels
live_streams metadata
subscriptions
donation transactions
emote metadata초고빈도 실시간 데이터는 RDBMS 동기 쓰기 경로에서 분리합니다.
chat messages
viewer presence
viewer count
live overlay fan-out채팅은 WebSocket Gateway와 partitioned Pub/Sub 경로로 처리합니다. 시청자 수는 row update가 아니라 분산 카운터와 주기적 스냅샷으로 처리합니다. 후원과 구독은 금융 데이터이므로 영구 저장하고, 화면 오버레이 전파는 이벤트 기반으로 분리합니다.
API Signatures
Twitch 같은 실시간 라이브 스트리밍 플랫폼은 사용자가 채널을 클릭하면 영상이 나오고, 타이핑을 하면 채팅이 뜨고, 후원이나 구독이 발생하면 화면 위에 오버레이 알림이 표시되는 서비스입니다.
하지만 시스템 내부에서는 세 가지 성격이 완전히 다른 데이터 경로가 동시에 동작합니다.
첫 번째는 라이브 비디오 전송 경로입니다.
스트리머가 OBS 같은 인코더에서 RTMP로 원본 영상을 송출하면, 시스템은 이를 ingest server에서 받아 transcoder farm으로 전달합니다. 이후 1080p60, 720p60, 480p30 같은 여러 화질로 변환하고, HLS/LL-HLS segment와 manifest를 생성한 뒤 CDN edge로 배포합니다.
두 번째는 실시간 채팅 경로입니다.
채팅은 영상과 다르게 크기는 작지만 빈도가 매우 높습니다. 단일 대형 채널에서 초당 수만 개의 메시지가 발생할 수 있으므로, 이를 일반적인 RDBMS 동기 쓰기 경로에 태우면 디스크 쓰기, 인덱스 갱신, replication 비용이 급격히 증가합니다. 따라서 채팅 전송 경로는 WebSocket Gateway와 partitioned Pub/Sub 구조로 분리합니다.
세 번째는 실시간 이벤트 경로입니다.
구독, Bits, 후원, 라이브 상태 변경, 시청자 수 갱신 같은 이벤트는 채널을 보고 있는 사용자 화면에 빠르게 전파되어야 합니다. 다만 결제 자체는 ACID 트랜잭션으로 안전하게 저장하고, 화면 오버레이 전파는 비동기 이벤트 파이프라인으로 분리합니다.
이 섹션에서는 Twitch의 주요 API를 아래 네 가지 경로로 나누어 설명합니다.
Live Video Stream Ingestion & Playlist Read API
High-Frequency Chat Ingestion & Connection Management API
Live Metadata & Event Overlay Streaming API
Weak Network Support & Local Mutation Sync API
1. Live Video Stream Ingestion & Playlist Read API, 라이브 미디어 스트림 인제스트 및 재생 경로
스트리머가 방송 시작 버튼을 누르는 순간부터 시청자의 비디오 플레이어가 영상을 재생하기까지의 경로입니다.
스트리머의 인코더는 RTMP ingest endpoint로 원본 스트림을 송출합니다. 시청자의 플레이어는 직접 API 서버에서 비디오 바이너리를 받지 않고, CDN edge에 있는 manifest와 media segment를 요청합니다.
POST /v1/streams/ingest/authorize
{
"channel_id": "chan_faker123",
"stream_key": "live_sk_882319fka90123",
"ingest_region": "ap-northeast-2",
"encoder_spec": {
"codec": "H.264",
"bitrate": 8000000,
"fps": 60,
"resolution": "1920x1080"
}
}이 API는 스트리머가 실제 미디어를 보내기 전에 stream key가 유효한지 확인하는 경량 인증 경로입니다.
Twitch 기준으로 방송 소프트웨어는 RTMP URL과 stream key를 사용해 ingest subsystem으로 스트림을 송출합니다. stream key는 방송 권한을 식별하는 민감한 값이므로 평문 저장을 피하고, hash 또는 secret store 기반 검증을 사용하는 편이 안전합니다.
Playback Manifest API
GET /v1/streams/chan_faker123/playback
Headers:
Authorization: Bearer <JWT_TOKEN>
X-Client-Network-Bandwidth: 15400000응답 예시는 다음과 같습니다.
{
"channel_id": "chan_faker123",
"live_stream_id": "stream_20260521_001",
"master_manifest_url": "https://edge-cdn.example.com/live/chan_faker123/master.m3u8",
"supported_renditions": [
"1080p60",
"720p60",
"480p30",
"audio_only"
],
"low_latency_enabled": true
}API 서버는 비디오 청크 바이너리를 직접 반환하지 않습니다.
대신 CDN edge에 있는 master.m3u8 URL만 반환합니다. 이후 플레이어는 CDN edge에서 manifest와 media segment를 직접 가져옵니다.
비디오 전송의 단계별 데이터 흐름, Data Flow
1.미디어 수신 및 인증
스트리머의 OBS 인코더가 지정된 RTMP 수신 서버(Ingest Server)로 영상을 쏩니다. 수신 서버는 매번 DB를 조회하는 대신, 캐시된 크리덴셜 정보를 활용해 대기시간(Latency) 없이 빠르게 Stream Key를 검증합니다.
2.실시간 트랜스코딩
수신된 원본 영상 스트림은 트랜스코더 서버군(Transcoder Farm)으로 넘겨집니다. 여기서 원본 고화질 영상을 시청자의 다양한 네트워크 환경에 맞춰 여러 화질(1080p, 720p, 480p, Audio-only)로 실시간 인코딩합니다.
3.청크(Chunk) 및 매니페스트 생성


