🏢 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