Backend/WebSocket
확장 가능한 실시간 WebSocket 서버 아키텍처 설계
수평 확장 WebSocket 서버에서 세션 라우팅과 메시지 복구를 설계하는 실무 기준을 정리합니다.
확장 가능한 실시간 WebSocket 서버 아키텍처 설계
문제 정의
WebSocket 서버를 여러 대로 늘리면 "어떤 유저가 어느 서버에 붙어 있는지"가 분산됩니다. 이 상태를 중앙에서 일관되게 관리하지 않으면 메시지 유실과 오배송이 발생합니다.
권장 구성
- Load Balancer: 연결 분산.
- WS 서버 N대: 실제 연결 유지.
- Redis Hash:
userId -> serverId세션 위치 저장. - Redis Pub/Sub: 서버 간 실시간 이벤트 전달.
- Redis Streams: 재연결 시 누락 메시지 복구.
데이터 모델
HSET ws:sessions {userId} {serverId}
PUBLISH ws:server:{serverId} {event-json}
XADD ws:events * userId {userId} payload {json}
핵심 흐름
- 연결 성공 시
ws:sessions에 세션 등록. - A 유저가 B 유저에게 메시지 전송.
- B의
serverId를 Hash에서 조회. - 해당 서버 채널로 Pub/Sub 발행.
- 수신 서버가 로컬 소켓으로 전달.
- 이벤트를 Streams에 기록해 재전송 가능 상태 유지.
서버 종료/장애 대응
- Heartbeat + TTL: 좀비 세션 자동 정리.
- Graceful Shutdown: 종료 전 해당 서버 세션 일괄 제거.
- Reconnect Resume: 클라이언트가 마지막 이벤트 ID를 보내면 Streams에서 누락분 재전송.
예시 코드(개념)
async function onConnect(userId: string, serverId: string) {
await redis.hset('ws:sessions', userId, serverId);
await redis.expire(`ws:alive:${userId}`, 30);
}
async function routeMessage(toUserId: string, payload: object) {
const targetServer = await redis.hget('ws:sessions', toUserId);
if (!targetServer) return;
await redis.publish(`ws:server:${targetServer}`, JSON.stringify({ toUserId, payload }));
await redis.xadd('ws:events', '*', 'userId', toUserId, 'payload', JSON.stringify(payload));
}
관측 지표
- 연결 수/활성 세션 수
- Pub/Sub 전달 지연(p95, p99)
- reconnect 후 누락 복구 성공률
- 서버별 fan-out 처리량
요약
확장형 WebSocket의 본질은 소켓 핸들링이 아니라 상태 일관성입니다. 세션 위치, 라우팅, 복구 경로를 분리해 설계해야 장애 시 복구가 가능합니다.