🏢 Discord 기술 스택 분석
디스코드가 전 세계 1.5억 명에게 실시간 음성/비디오 통화와 채팅을 제공하는 기술 스택 심층 분석 - Elixir, Rust, WebRTC로 구축한 차세대 커뮤니케이션 플랫폼
🏢 Discord 기술 스택 분석
게이머를 위해 시작했지만 전 세계 커뮤니티의 홈이 된 Discord의 기술 스택을 심층 분석합니다.
"Imagine a place..." - 실시간 커뮤니케이션의 미래를 만드는 기술
📊 회사 개요
서비스 규모
- 월간 활성 사용자: 1.5억 명+
- 일일 메시지: 40억+ 건
- 음성 통화: 월 40억+ 분
- 서버(커뮤니티): 1,900만+ 활성 서버
- 동시 접속자: 피크 1,500만+
엔지니어링 조직
- 엔지니어: 300명+ (전체 직원 700+)
- 문화: "Build Belonging"
- 원격 우선: 전 세계 분산 팀
- 기술 다양성: Elixir, Rust, Python, Go
기술적 도전과제
- 초저지연: 전 세계 실시간 음성/비디오
- 대규모 동시성: 수백만 동시 연결
- 안정성: 게임 중 끊김 없는 통신
- 확장성: 급격한 성장 대응
🏗️ 아키텍처 Overview
시스템 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Clients │────►│ Gateway │────►│ Guilds │ │(Desktop/Web)│ │ (Elixir) │ │ (Elixir) │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────▼─────┐ ┌─────▼─────┐ │ │WebRTC SFU │ │ Message │ │ │ (Rust) │ │ Store │ │ └───────────┘ └───────────┘ │ │ │ └────────────────────┤ │ ┌─────▼─────┐ ┌─────▼─────┐ │ Media │ │ ScyllaDB │ │ Server │ │(Cassandra)│ └───────────┘ └───────────┘ 핵심 설계 원칙
- Erlang/OTP: 고가용성과 fault tolerance
- Actor Model: 수백만 개의 경량 프로세스
- Event Sourcing: 모든 상태 변경 추적
- Microservices: 서비스별 최적 언어 선택
- Edge Computing: 글로벌 엣지 네트워크
기술 진화
- 2015: Python 모놀리스로 시작
- 2016: Elixir/Erlang 마이그레이션
- 2017: Rust로 성능 최적화
- 2019: Go 서비스 추가
- 2021: 대규모 인프라 재설계
- 2023: AI 기능 통합
🧩 기술 스택 상세
Backend 기술
Elixir/Erlang 코어
# Discord의 Guild (서버) GenServer defmodule Discord.Guild do use GenServer defstruct [:id, :name, :owner_id, :channels, :members, :roles] # Guild 시작 def start_link(guild_id) do GenServer.start_link(__MODULE__, guild_id, name: {:via, Registry, {Discord.GuildRegistry, guild_id}}) end # 메시지 처리 def handle_cast({:message, %{channel_id: channel_id, content: content} = message}, state) do # 권한 확인 if authorized?(message.author_id, channel_id, state) do # 메시지 저장 MessageStore.save(message) # 온라인 멤버에게 브로드캐스트 state.members |> Enum.filter(&Member.online?/1) |> Enum.each(fn member -> Gateway.push(member.session_id, {:message, message}) end) # Presence 업데이트 Presence.track(self(), "guild:#{state.id}", message.author_id, %{ status: "online", typing: false }) end {:noreply, state} end # 음성 채널 참가 def handle_call({:join_voice, channel_id, user_id}, _from, state) do case VoiceServer.allocate(state.region) do {:ok, server} -> # WebRTC 연결 정보 생성 credentials = generate_webrtc_credentials(user_id) # 음성 상태 업데이트 new_state = update_voice_state(state, user_id, channel_id, server) {:reply, {:ok, %{server: server, credentials: credentials}}, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end # 대규모 Guild 최적화 defp maybe_lazy_load_members(state) do if length(state.members) > 1000 do # 대규모 서버는 멤버 지연 로딩 %{state | members: :lazy} else state end end end Rust 고성능 서비스
// Discord의 음성 서버 (Rust) use tokio::net::UdpSocket; use webrtc::peer_connection::RTCPeerConnection; use std::sync::Arc; use dashmap::DashMap; pub struct VoiceServer { connections: Arc<DashMap<u64, VoiceConnection>>, mixer: Arc<AudioMixer>, config: ServerConfig, } struct VoiceConnection { user_id: u64, peer_connection: RTCPeerConnection, audio_track: Arc<dyn MediaStreamTrack>, opus_encoder: OpusEncoder, jitter_buffer: JitterBuffer, } impl VoiceServer { pub async fn new(config: ServerConfig) -> Result<Self> { let socket = UdpSocket::bind(&config.bind_address).await?; Ok(Self { connections: Arc::new(DashMap::new()), mixer: Arc::new(AudioMixer::new(config.sample_rate)), config, }) } pub async fn handle_audio_packet(&self, packet: AudioPacket) -> Result<()> { // Opus 디코딩 let pcm_data = self.decode_opus(packet.data)?; // Jitter buffer에 추가 if let Some(mut conn) = self.connections.get_mut(&packet.user_id) { conn.jitter_buffer.push(pcm_data, packet.timestamp); // 믹싱을 위한 오디오 준비 if let Some(frame) = conn.jitter_buffer.pop() { self.mixer.add_source(packet.user_id, frame); } } // 다른 참가자에게 전송 self.broadcast_mixed_audio(packet.channel_id).await?; Ok(()) } async fn broadcast_mixed_audio(&self, channel_id: u64) -> Result<()> { let participants = self.get_channel_participants(channel_id); for user_id in participants { // 각 사용자별 맞춤 믹스 (자신 제외) let mixed = self.mixer.mix_excluding(user_id); // Opus 인코딩 let encoded = self.encode_opus(mixed)?; // WebRTC DataChannel로 전송 if let Some(conn) = self.connections.get(&user_id) { conn.peer_connection .send_audio(encoded) .await?; } } Ok(()) } } // 저지연 오디오 믹서 struct AudioMixer { sources: Arc<DashMap<u64, AudioSource>>, sample_rate: u32, } impl AudioMixer { fn mix_excluding(&self, exclude_user_id: u64) -> Vec<i16> { let mut output = vec![0i32; 960]; // 20ms @ 48kHz self.sources.iter() .filter(|entry| *entry.key() != exclude_user_id) .for_each(|entry| { let source = entry.value(); for (i, &sample) in source.samples.iter().enumerate() { output[i] = output[i].saturating_add(sample as i32); } }); // 클리핑 방지 output.iter() .map(|&s| (s.max(-32768).min(32767)) as i16) .collect() } } Backend 기술 스택:
- 언어: Elixir (50%), Rust (20%), Python (15%), Go (15%)
- 프레임워크: Phoenix (Elixir), Actix (Rust), FastAPI
- 실시간: WebSocket, WebRTC
- 메시징: NATS, RabbitMQ
- RPC: gRPC, Protobuf
Frontend 기술
React Desktop App
// Discord Desktop App (Electron + React) import { useEffect, useState } from "react"; import { Gateway } from "@discord/gateway"; import { VoiceConnection } from "@discord/voice"; interface ChannelViewProps { channelId: string; guildId: string; } export function ChannelView({ channelId, guildId }: ChannelViewProps) { const [messages, setMessages] = useState<Message[]>([]); const [voiceState, setVoiceState] = useState<VoiceState | null>(null); const gateway = useGateway(); useEffect(() => { // 실시간 메시지 구독 const unsubscribe = gateway.subscribe(`channel:${channelId}`, (event) => { switch (event.type) { case "MESSAGE_CREATE": setMessages((prev) => [...prev, event.message]); break; case "MESSAGE_UPDATE": setMessages((prev) => prev.map((msg) => msg.id === event.message.id ? event.message : msg ) ); break; case "TYPING_START": showTypingIndicator(event.userId); break; } }); return unsubscribe; }, [channelId]); // 음성 채널 연결 const joinVoiceChannel = async () => { const connection = new VoiceConnection({ guildId, channelId, selfDeaf: false, selfMute: false, }); // WebRTC 연결 설정 await connection.connect(); // 로컬 오디오 스트림 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); connection.setInputStream(stream); setVoiceState(connection); }; return ( <div className="channel-view"> <MessageList messages={messages} /> <MessageInput channelId={channelId} /> <VoiceControls connected={!!voiceState} onJoin={joinVoiceChannel} onLeave={() => voiceState?.disconnect()} /> </div> ); } // 최적화된 메시지 렌더링 const MessageList = React.memo(({ messages }: { messages: Message[] }) => { const virtualizer = useVirtualizer({ count: messages.length, getScrollElement: () => scrollRef.current, estimateSize: () => 50, overscan: 5, }); return ( <div ref={scrollRef} className="message-list"> {virtualizer.getVirtualItems().map((virtualItem) => ( <MessageItem key={messages[virtualItem.index].id} message={messages[virtualItem.index]} style={{ height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, }} /> ))} </div> ); }); Frontend 기술:
- Desktop: Electron + React
- Web: React + TypeScript
- Mobile: React Native
- 상태관리: MobX, Zustand
- UI: 자체 디자인 시스템
Data Infrastructure
메시지 저장소
# Discord의 메시지 저장 시스템 from cassandra.cluster import Cluster from cassandra.policies import DCAwareRoundRobinPolicy import msgpack import zstd class MessageStore: def __init__(self): self.cluster = Cluster( contact_points=['scylla-1', 'scylla-2', 'scylla-3'], load_balancing_policy=DCAwareRoundRobinPolicy(local_dc='us-west') ) self.session = self.cluster.connect('discord') async def save_message(self, message): # 메시지 압축 compressed = zstd.compress( msgpack.packb({ 'content': message.content, 'embeds': message.embeds, 'attachments': message.attachments, }), level=3 ) # ScyllaDB에 저장 query = """ INSERT INTO messages ( channel_id, message_id, author_id, content, timestamp, edited_timestamp ) VALUES (?, ?, ?, ?, ?, ?) USING TTL ? """ # 메시지 보관 정책에 따른 TTL ttl = self.get_message_ttl(message.channel_id) await self.session.execute_async( query, ( message.channel_id, message.id, message.author_id, compressed, message.timestamp, message.edited_timestamp, ttl ) ) # 검색 인덱스 업데이트 await self.update_search_index(message) async def get_messages(self, channel_id, before=None, limit=50): query = """ SELECT * FROM messages WHERE channel_id = ? AND message_id < ? ORDER BY message_id DESC LIMIT ? """ rows = await self.session.execute_async( query, (channel_id, before or MAX_ID, limit) ) messages = [] for row in rows: # 압축 해제 data = msgpack.unpackb( zstd.decompress(row.content) ) messages.append(Message.from_db(row, data)) return messages 데이터 인프라:
- 주 데이터베이스: ScyllaDB (Cassandra 호환)
- 캐시: Redis Cluster
- 검색: Elasticsearch
- 분석: Google BigQuery
- 메시지 큐: NATS, Kafka
Infrastructure & DevOps
글로벌 인프라
# Kubernetes 배포 설정 apiVersion: apps/v1 kind: StatefulSet metadata: name: discord-guild-service namespace: discord-prod spec: serviceName: guild-service replicas: 100 podManagementPolicy: Parallel selector: matchLabels: app: guild-service template: metadata: labels: app: guild-service spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - guild-service topologyKey: kubernetes.io/hostname containers: - name: guild image: discord/guild-service:v2.3.0 resources: requests: memory: "4Gi" cpu: "2" limits: memory: "8Gi" cpu: "4" env: - name: BEAM_SCHEDULER_COUNT value: "8" - name: ERL_MAX_PORTS value: "1048576" livenessProbe: httpGet: path: /health port: 4000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 4000 initialDelaySeconds: 5 periodSeconds: 5 인프라 기술:
- 클라우드: Google Cloud Platform + 자체 데이터센터
- CDN: Cloudflare (글로벌 엣지)
- 컨테이너: Kubernetes
- 모니터링: Prometheus, Grafana, Datadog
- CI/CD: GitLab CI, ArgoCD
💡 핵심 기술 인사이트
1. Elixir/Erlang의 힘
# Discord의 분산 시스템 설계 defmodule Discord.Presence do use Phoenix.Presence, otp_app: :discord, pubsub_server: Discord.PubSub # 수백만 사용자의 presence 추적 def track_user(guild_id, user_id, meta) do track(self(), "guild:#{guild_id}", user_id, meta) end # CRDT 기반 동기화 def handle_diff(diff, state) do # Conflict-free 상태 병합 for {user_id, %{metas: [meta | _]}} <- diff.joins do # 사용자 온라인 이벤트 broadcast_user_presence(user_id, :online, meta) end for {user_id, _} <- diff.leaves do # 사용자 오프라인 이벤트 broadcast_user_presence(user_id, :offline, %{}) end {:ok, state} end end 2. WebRTC 최적화
// 커스텀 WebRTC SFU (Selective Forwarding Unit) pub struct DiscordSFU { router: MediaRouter, bandwidth_estimator: BandwidthEstimator, quality_controller: QualityController, } impl DiscordSFU { pub async fn handle_rtp_packet(&self, packet: RtpPacket) -> Result<()> { // 1. 대역폭 추정 self.bandwidth_estimator.update(&packet); // 2. 품질 제어 (시뮬캐스트/SVC) let target_quality = self.quality_controller.decide_quality( packet.ssrc, self.bandwidth_estimator.get_estimate() ); // 3. 선택적 포워딩 let recipients = self.router.get_recipients(packet.ssrc); for recipient in recipients { // 수신자별 최적화 let adapted_packet = match recipient.connection_quality { Quality::High => packet.clone(), Quality::Medium => self.downscale_packet(&packet, 720), Quality::Low => self.downscale_packet(&packet, 480), }; // NACK, FEC 처리 recipient.send_with_redundancy(adapted_packet).await?; } Ok(()) } } 3. 대규모 실시간 동기화
// Go로 작성된 Presence 서비스 type PresenceService struct { shards map[uint64]*PresenceShard ring *consistent.Ring redisCli *redis.ClusterClient } type PresenceShard struct { mu sync.RWMutex users map[uint64]*UserPresence lastSync time.Time } func (ps *PresenceService) UpdatePresence(userID uint64, status Status) error { // Consistent hashing으로 샤드 결정 shardID := ps.ring.Get(fmt.Sprintf("user:%d", userID)) shard := ps.shards[shardID] shard.mu.Lock() defer shard.mu.Unlock() // 로컬 업데이트 presence := &UserPresence{ UserID: userID, Status: status, UpdatedAt: time.Now(), } shard.users[userID] = presence // Redis에 비동기 동기화 go ps.syncToRedis(userID, presence) // 구독자에게 브로드캐스트 ps.broadcastUpdate(userID, presence) return nil } // 효율적인 bulk 동기화 func (ps *PresenceService) BulkSync() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for range ticker.C { for _, shard := range ps.shards { shard.mu.RLock() updates := make([]*UserPresence, 0, len(shard.users)) for _, presence := range shard.users { if presence.UpdatedAt.After(shard.lastSync) { updates = append(updates, presence) } } shard.mu.RUnlock() if len(updates) > 0 { ps.bulkSyncToRedis(updates) shard.lastSync = time.Now() } } } } 📈 성과 & 지표
기술적 성과
- 메시지 지연: < 100ms (글로벌 평균)
- 음성 지연: < 60ms (동일 지역)
- 가용성: 99.95%+
- 동시 음성 채널: 수백만 개
- 메시지 처리량: 초당 수백만 건
확장성 성과
- 서버당 사용자: 최대 80만 명 (Midjourney)
- 최대 음성 참가자: 10,000명 (Stage Channels)
- 파일 업로드: 500MB (Nitro)
- 화면 공유: 4K 60fps 지원
🎓 Discord에서 배울 점
✅ 적용 가능한 패턴
- Actor Model: Erlang/OTP의 강력함
- 언어별 최적화: 적재적소에 최적 언어 사용
- WebRTC SFU: 대규모 실시간 통신
- Consistent Hashing: 분산 시스템 샤딩
- Event Sourcing: 상태 관리와 히스토리
❌ 주의사항
- Elixir 학습곡선: 함수형 + Actor 모델
- 다중 언어 복잡성: 운영 오버헤드
- 실시간 요구사항: 네트워크 품질 의존
- 스케일 도전: 대규모 서버 관리
📚 추천 리소스
- Discord Engineering Blog
- How Discord Stores Billions of Messages
- Discord's Architecture
- Elixir in Production
🔮 미래 전망
현재 집중 분야
- AI 통합: 대화 요약, 번역, 모더레이션
- Forum Channels: 구조화된 대화
- Activities: 앱 내 미니게임/활동
- 크리에이터 수익화: 서버 구독, 티켓팅
- 엔터프라이즈: Slack 대체 시장
기술 투자 영역
- Rust 확대: 더 많은 핵심 서비스
- AI/ML: 콘텐츠 모더레이션, 추천
- 엣지 컴퓨팅: 더 낮은 지연시간
- 블록체인: Web3 커뮤니티 지원
- AR/VR: 메타버스 통합
"우리는 단순한 채팅 앱이 아닌, 전 세계 커뮤니티가 모이는 디지털 공간을 만들고 있습니다."
- Discord Engineering
마지막 업데이트: 2025-01-28