이번에 채팅SDK 를 구현하면서 백엔드 서버를 어떻게 배포해야하나 고민하게 되었다. 현재 인프라에서는 백엔드 서버를 로드밸런싱을 하고 있다. RESTAPI 였다면 매 요청이 Statless 하기때문에 로드밸런싱을 하더라도 로그인 빼고는 크게 고민할게 없었다. 하지만 소켓의 경우에는 connection이 계속 유지되어야하기 때문에 로드밸런싱을 할 시에 연결유지를 위해 Sticky Session 방식을 사용해야할까? 만약 세션고정을 할거면 로드밸런싱 장점이 너무 퇴색되는거 아닌가..? 다들 어떻게 하고 있는거지? 라는 궁금증이 생겼다.
결론적으로는 웹소켓 서버 배포할때 세션고정대신 3가지 방법을 통해 로드밸런싱의 이점을 가져갈 수 있다. 해결방법을 알아보기전에 먼저 원리부터 살펴보자
WebSocket 통신 원리
웹소켓 통신은 크게 3가지 라이프 사이클이 있다.
1. 오프닝 핸드쉐이크(Opening Handshake): HTTP 통신으로 소켓 연결을 요청한다.
2. 데이터 전송 (Data transfer): 클라이언트와 서버간 양방향 데이터 교환
3. 클로징 핸드쉐이크(Closing Handshake): 연결 종료
해결방안 01. Redis 클러스터링을 이용한 상태동기화
Redis는 WebSocket 서버 간의 상태 동기화에 적합한 인메모리 데이터 저장소이다. 분산 환경에서 클라이언트의 세션 데이터를 Redis에 저장하고, 이를 기반으로 상태를 동기화하면 세션 고정을 피하면서도 안정성을 유지할 수 있다.
구현 방법:
- 클라이언트 상태 저장: WebSocket 연결 ID, 사용자 세션 정보, 구독 데이터 등을 Redis에 저장한다.
- 상태 동기화: 서버 간 Redis를 통해 클라이언트 상태를 공유하고, 연결이 끊어진 경우에도 데이터를 복구한다.
- Redis 클러스터 구성: 고가용성과 확장성을 위해 Redis 클러스터를 설정한다.
장점:
- 서버 간 상태 일관성을 보장한다.
- 클라이언트가 어느 서버로 연결되더라도 동일한 상태를 유지할 수 있다.
해결방안 02. Connection Draining
Connection Draining은 Pod나 서버가 종료될 때 기존 연결을 강제로 끊지 않고, 일정 시간 동안 연결을 유지하여 안전하게 종료하는 메커니즘이다. 이를 통해 WebSocket 연결의 중단을 최소화할 수 있다. 이때 일정시간 연결유지는 k8s의 Termination Grace Period 를 이용한다.
구현 방법:
- Kubernetes Termination Grace Period:
- Pod가 종료되기 전에 terminationGracePeriodSeconds를 설정하여 기존 연결을 안전하게 마무리한다.
- 로드밸런서의 Connection Draining 설정:
- AWS ALB에서는 deregistration_delay.timeout_seconds 속성을 통해 설정 가능하다.
장점:
- 연결 끊김을 최소화하여 사용자 경험을 향상시킨다.
- Pod 업데이트나 스케일링 시 안정성을 제공한다.
해결방안 03. RabbitMQ, Kafka 등의 메세지 브로커 이용
메시지 브로커는 분산된 WebSocket 서버 간 메시지 전달과 상태 동기화를 가능하게 한다. RabbitMQ나 Kafka를 사용하면 클라이언트가 특정 서버에 고정되지 않고도 메시지를 처리할 수 있다.
구현 방법:
- Pub/Sub 패턴 활용:
클라이언트는 메시지 브로커에 구독을 설정하고, 서버는 메시지를 게시하여 클라이언트로 전달한다. - 메시지 큐 활용
연결이 끊어진 동안에도 메시지를 큐에 저장하고, 클라이언트가 재연결하면 전달한다.
장점
서버 간 메시지 전달을 중앙에서 관리할 수 있다.
WebSocket 서버를 수평 확장(Scale-out)할 수 있다.
주의점
메시지 브로커는 MSA(Microservices Architecture) 환경에서 의미가 있다. 모놀리틱 아키텍처에서의 메세지 브로커 사용은 오버엔지니어링이라 생각한다.
번외: 코드로 재연결 로직 구현
public void reconnect() {
try {
stompClient.connect(WS_URL, new MyStompSessionHandler());
} catch (Exception e) {
// 재연결 실패 시 백오프(backoff) 전략 적용
Thread.sleep(1000);
reconnect();
}
}
클라이언트 단에서 재연결 로직을 구현하면, 서버의 상태와 상관없이 끊김을 최소화할 수 있다. 하지만 역시 코드로는 한계가 있는데 아무리 재연결 로직을 작성하더라도 인프라적으로 끊기고 다시 연결되는 사이의 텀을 매꿀 수가 없다.
WebSocket 서버를 로드밸런싱하면서 세션 고정을 피하는 방법을 정리해보자면 아래와 같다.
- Redis를 활용한 상태 동기화: 분산 서버 간 상태 일관성을 유지.
- Connection Draining: Pod나 서버의 종료 시 연결을 안전하게 종료.
- RabbitMQ/Kafka: MSA 환경에서 서버 간 메시지 전달과 동기화를 지원.
현재 내가 구현하는 채팅 SDK 프로젝트는 모놀리스이고 백엔드 인원 대비 인프라 인원이 많은 관계로 2번이 가장 적합하다고 판단된다.
'Infra' 카테고리의 다른 글
develop 브랜치와 feature 브랜치가 컨플릭트 났던 이슈 트러블 슈팅 (0) | 2025.02.05 |
---|---|
서버를 옮겼더니 젠킨스 접속 속도가 느려진 이슈 트러블 슈팅 (0) | 2025.01.24 |
Jenkins가 Flutter 경로를 찾지 못해 발생한 이슈 트러블 슈팅 (0) | 2025.01.23 |
오픈소스 클라우드 인프라 관리 플랫폼 OpenStack 알아보기 (0) | 2024.11.08 |
ESXi와 vCenter를 활용한 온프레미스 인프라 구축 (0) | 2024.11.08 |