세션 분산 처리를 위한 설계 고민
배경
Push 프로젝트 를 진행하던 도중,
"10만 개 이상의 동시 세션을 감당하려면 어떻게 해야 할까?" 라는 물음에서 시작되었다.
여기서 말하는 세션이란, 서버와 클라이언트(gocli) 사이의 gRPC stream
연결을 의미한다.
즉, 클라이언트는 서버에 장기 연결(stream)을 유지하고, 서버는 해당 세션을 통해 메시지를 푸시하는 구조다.
이런 구조에서 session-manager
서비스를 스케일아웃해서 여러 pod로 운영하게 되면, 다음과 같은 문제가 발생한다:
"과연 어떤 세션이 어느 pod에 붙어 있는지 어떻게 알지?"
"특정 유저에게 푸시하려면 어떤 pod로 메시지를 보내야 하지?"
이 문제를 해결하기 위해 몇 가지 방식들을 고민해 보았다.
고민했던 옵션들
샤딩 기반 분산 방식
- 세션 id 또는 user id를 해싱하여 특정 pod에 매핑하는 형식
- 자동으로 균등 분산되고, 어느 유저가 어느 pod로 붙을지 알 수 있기 때문에 로직이 단순해짐.
예를 들면 pod의 수가 4개라고 하였을때
user id % 4
를 하여pod-1
,pod-2
,pod-3
,pod-4
에 배치하는 전략이다.
하지만 운영하는 pod의 수가 변동될 경우 세션 상태를 동기화 해야 한다는 문제가 존재하였다결론적으로, 이 방식은 정적이고 stateless한 구조에는 적합하지만,
gRPC stream 기반의 stateful 세션 관리에는 불안정 요소가 많다고 판단됐다.key-value Store 형식
Redis
나 별도의 저장소에SessionId - Pod
형태로 매핑하는 형식- pod 간 상태 공유 불필요 -> 서비스 간 결합도 감소
textuserId: 12345 → [ { sessionId: sess1, podId: pod-1 }, { sessionId: sess2, podId: pod-3 } ]
위와 같은 형태로 세션당 pod정보를 저장하여 해당 pod에 push 요청을 보내는 방식이다.
최종적으로 key-value Store
방식을 채택하였는데
주된 이유는 장애 대응이다. 만약 한 pod가 일시적 장애로 인해 접속할 수 없는 상태가 될 경우
해당 세션은 살아있는 pod로 접속시켜야 하고 이때 해당 세션이 어느 pod에 접속되어 있는지 맵핑 테이블이 존재해야 한다고 생각이 되었다.
결국 그 방법이 세션-pod
정보를 중앙에서 관리하는 것이 적절하다고 판단되었다.
세션의 실제 상태
를 가장 정확히 반영할 수 있는 방식이 무엇인가가 키 포인트 였다.
서비스간 변동점
초기 구조(As-Is)는 단일 인스턴스에서 동작하던 구조였기 때문에, 굳이 세션 분산 처리나 pod 간 라우팅을 고민할 필요가 없었다.
AS-IS
[Sender] ──> [SessionManager] ──> 유저 세션 목록 조회 + 메시지 전송
Sender는 단순히 SessionManager로 gRPC 호출
SessionManager는 다음을 모두 담당:
- 해당 유저의 세션 목록 조회
- 각 세션에게 메시지 전송
단일 프로세스 기준으로는 무리가 없지만, pod가 늘어날 경우 구조가 깨지기 시작한다
TO-BE (분산처리 구조)
[Sender]
│
└─> Redis (세션 정보 조회)
│
└─> [SessionManager(pod-X)] ──> 세션 스트림에 메시지 전송
Sender
는 Redis(key-store)를 통해 유저의 세션 정보를 직접 조회- 세션마다 어느 pod에 붙어 있는지 알 수 있으므로,
해당 pod에 존재하는 SessionManager에게만 직접 메시지 전송 요청 - 각 SessionManager는 오직 자신의 pod에 존재하는 세션만 관리하면 되므로 훨씬 단순해진다
구조는 조금 더 복잡해졌지만, 서비스 간 책임이 더 명확해지고, 확장에 유리한 구조가 되었다.
이러한 흐름을 통해 SessionManager
는 세션 스트림 관리에만 집중할 수 있고,
Sender는 라우팅 로직을 갖고 있으므로 메시지 흐름의 컨트롤러 역할에 집중하게 되었다.
결과적으로 각 서비스는 자기 역할에만 집중할 수 있는 구조가 되었다는 점에서 설계 방향이 옳았다고 판단한다.
추후 개선 사항
현재 구조에서는 하나의 세션에 대해 하나의 RPC 요청을 보내고 있다.
즉, 같은 유저가 여러 세션(기기)에서 접속 중이라면, 각각의 세션을 대상으로 별도의 메시지 전송 요청이 발생한다.
예를 들어, 다음과 같은 요청 흐름이 된다:
user123 → session-1 (pod-1) → RPC 요청 1건
→ session-2 (pod-1) → RPC 요청 1건
→ session-3 (pod-2) → RPC 요청 1건
이처럼 동일한 pod에 여러 세션이 연결되어 있음에도 불구하고,
pod-1에는 2건의 RPC 요청을 따로 보내게 된다
개선 방향: pod 단위로 세션을 묶어 요청하기
- Sender는 Redis에서 조회한 세션 정보를 pod 기준으로 그룹핑
- 동일한 pod에 붙어 있는 세션들을 배열로 묶어 한 번에 전송
예상 흐름은 다음과 같다:
user123 → pod-1: [session-1, session-2] → RPC 요청 1건
→ pod-2: [session-3] → RPC 요청 1건
결론적으로 네트워크IO가 감소하고 좀더 효율적으로 메세지를 전송할 수 있지 않을까 싶다.