Socket.IO 재연결 옵션 검증: Thundering Herd 부하 테스트
- Socket
- thundering-herd
- load-testing
개요
배경 및 문제 제기
화상 강의 서비스의 특성에 맞춰 소켓 재연결 옵션을 최적화하는 작업에서 시작했다. 기존 설정은 재연결 대기 시간이 길어 사용자 경험을 저하시킨다고 판단했고, 이를 단축하는 방향으로 PR을 올렸다.
| 옵션 | 기존 설정 | PR 제안값 | 설명 |
|---|---|---|---|
| reconnectionDelay | 1,000ms | 300ms | 최초 재연결 시도 지연 시간 |
| reconnectionDelayMax | 5,000ms | 3,000ms | 재연결 시도 간격의 최대치 |
| randomizationFactor | 0.5 | 0.3 | 재연결 간격의 무작위 분산 비율 |
팀 리뷰 중 "옵션을 더 느슨하게 잡자"는 의견을 듣게 되었다. 재연결 간격을 넓히면 서버 부하는 줄지만, 일반적인 네트워크 불안정 상황에서 복구 속도가 느려져 UX가 떨어진다는 우려가 있었다.

어느 쪽이 맞는지 이론만으로 판단하기 어렵다고 생각했다. 이론적 추측만으로 판단하기보다, 실제 서버가 어느 정도의 부하까지 견디는지 데이터로 확인하기로 했다. PR의 설정값이 유효한지 검증하고 이를 바탕으로 최종 방향을 결정하고자 한다.
Thundering Herd란 무엇인가
Thundering Herd는 서버 재시작이나 네트워크 장애 이후, 모든 클라이언트가 짧은 시간 내에 동시에 재연결을 시도하여 서버 부하가 특정 순간에 폭증하는 현상이다. 단순히 동시 접속자가 많다는 문제가 아니라, 부하가 한 순간에 집중된다는 점이 핵심이다.
Jitter를 통한 분산
Socket.IO의 randomizationFactor는 재연결 타이밍에 무작위성을 부여해 요청을 시간 축으로 분산시킨다.
분산 윈도우 = reconnectionDelay × 2 × randomizationFactor
| jitter 설정 | 분산 윈도우 | 1,000명 피크 속도 | 3,856명 피크 속도 |
|---|---|---|---|
| 0 (없음) | 0ms | ∞ (순간 스파이크) | ∞ |
| 0.3 (1차 변경) | 180ms | 5,556 req/s | 21,400 req/s |
| 0.5 (2차 변경) | 300ms | 3,333 req/s | 12,853 req/s |
분산 윈도우가 넓을수록 동일한 인원이 더 긴 시간에 걸쳐 재연결하므로 서버의 순간 피크 부하가 줄어든다.
테스트 설계 및 목표
테스트 스케일
단일 강의실이 아닌 서버 전체의 활성 연결이 동시에 끊기는 최악의 시나리오를 가정했다.
화상강의에서 강의실당 평균 100명으로 잡았고, 이 강의실이 100개 운영된다는 가정 하에 강의실 100개 × 강의실당 100명 = 10,000명 동시 재연결이 발생하는 상황을 측정하고자 한다.
테스트 방법 (3단계)
결과 요약
임계점 측정
약 5,000 VU부터 초기 WebSocket 연결 거부(TCP Backlog 포화)가 발생하기 시작하며, 10,000 VU 시 약 10%의 실패율을 보였다.
재시도 효과
Phase 1/2에서 실패했던 건들은 지수 백오프 재시도(최대 2회)만으로 100% 복구됨을 확인했다.
최종 판단
단일 인스턴스의 한계는 초기 수용량에 있으나, 일단 연결된 세션의 재연결은 아래 설정값으로도 충분히 안정적이다. 따라서 UX 향상을 위해 제안된 공격적인 설정을 유지해도 무방하다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| SOCKET_TIMEOUT | 5s | 30s |
| randomizationFactor | 0.3 | 0.5 |
| reconnectionDelayMax | 3,000ms | 5,000ms |
| reconnectionDelay | 300ms | 300ms |
적용한 해결책
| 항목 | 변경 전 | 변경 후 | 근거 |
|---|---|---|---|
randomizationFactor | 0.3 | 0.5 | 분산 윈도우 180ms → 300ms, 피크 부하 40% 감소 |
SOCKET_TIMEOUT | 5s | 30s | 서버 포화 시 세션 유실 방지, 배포 다운타임 커버 |
reconnectionDelayMax | 3,000ms | 5,000ms | 2차 이상 재시도 시 요청 밀집도 완화 |
reconnectionDelay | 300ms | 300ms (유지) | 분산은 factor가 담당, UX 유지 |
reconnectionDelay를 올리는 대신 randomizationFactor를 높인 이유는, 서버
테스트 방법론
인프라 구성
테스트 환경은 부하 생성, 네트워크 제어, 서비스 엔진의 세 계층으로 설계했다.
부하 생성 (Artillery)
N개의 독립적인 Socket.IO 클라이언트(VU)를 생성하여 실제 사용자 접속 환경을 모사한다.
네트워크 제어 (Toxiproxy)
클라이언트와 서버 사이에서 TCP 프록시 역할을 수행한다. 테스트 시점에 reset_peer toxic을 주입하여 모든 연결을 강제로 끊는 장애 상황을 연출한다.
서버 (NestJS + Redis + Prometheus)
Socket.IO Gateway가 재연결을 처리하고, Redis에서 세션을 복원하며, 관련 지표를 Prometheus로 수집한다.

모든 WebSocket 연결은 Toxiproxy(Port 4001)를 경유하도록 설정하여 RST 타이밍을 정밀하게 제어한다. 반면, 방 생성 등 일반 HTTP API 호출은 서버(Port 3000)에 직접 요청하여 네트워크 장애의 영향을 받지 않도록 분리했다.
Toxiproxy reset_peer를 선택한 이유
Thundering Herd 현상을 재현하려면 모든 클라이언트가 동일한 찰나에 연결이 끊겨야 한다. 일반적인 disconnect()는 FIN/CLOSE 프레임을 주고받는 정상 종료다. 반면, 실제 서버 장애(kill -9, 전원 차단)는 예고 없이 TCP RST 패킷이 발생한다.
reset_peer는 서버 재시작을 기다릴 필요 없이 스크립트로 즉시 장애 상황을 재현할 수 있다. 또한, 서버가 클라이언트를 순차적으로 끊는 방식보다 훨씬 높은 수준의 동시성을 보장한다.
| 방법 | 동시성 | 장애 재현도 | 특징 |
|---|---|---|---|
socket.disconnect() | X | 낮음 | 정상적인 종료 절차를 밟음 |
server.disconnectSockets() | X | 보통 | 서버가 루프를 돌며 순차 처리 |
Toxiproxy reset_peer | O | 높음 | 스크립트 제어 가능, 실제 장애와 유사 |
가상 사용자(VU) 시나리오 흐름
테스트는 재연결 실패 시의 대응 방식에 따라 두 가지 모드로 진행했다.
방식 A: 1차 재연결 (Phase 1 / 2)
reconnection: false 설정 후 수동으로 1회만 재연결을 시도한다. 첫 시도 실패를 최종 실패로 간주하여, 서버에 가장 극심한 부하가 걸리는 최악의 임계 지점을 찾는 데 집중한다.

방식 B: 자동 지수 백오프 (Phase 3)
reconnection: true 설정을 통해 실제 프로덕션 환경의 클라이언트 동작을 그대로 재현한다. 첫 시도 실패 시 대기 시간을 늘려가며 재시도하며, 이를 통해 서버의 포화 상태가 일시적 마비인지 구조적 결함인지 판별한다.

측정 지표 (reconnect_duration_seconds) 는 TCP RST 수신 시점(disconnectAt)부터 서버로부터 join_room ACK를 수신한 시점(reconnectAt)까지의 구간을 측정한다. 여기에는 Jitter 대기, TCP 핸드셰이크, WS 업그레이드 시간이 모두 포함된다.
스케일별 파라미터 최적화
VU 규모가 커질수록 초기 연결 부하와 재연결 경쟁이 심화되므로, 테스트의 신뢰성을 위해 환경 파라미터를 유연하게 조정했다.
| 파라미터 | 역할 | VU 증가 시 조정 이유 |
|---|---|---|
| Ramp-up | 목표 VU 도달 시간 | 초기 연결 단계에서의 서버 과부하 방지 |
| 안정화 대기 | RST 발사 전 대기 시간 | 모든 VU가 join_room을 완료한 정적인 상태 확보 |
| Hold | 재연결 완료 대기 시간 | 재연결 경쟁으로 인해 길어지는 처리 시간 반영 |
스케일별 설정값
| VU 수 | Ramp-up | 안정화 대기 | Hold |
|---|---|---|---|
| 100 | 5s | 15s | 20s |
| 500 | 10s | 25s | 30s |
| 1,000 | 20s | 35s | 40s |
| 5,000 | 30s | 60s | 70s |
| 10,000 | 60s | 90s | 100s |
Phase 1 - 현재 설정의 한계 파악
테스트 설정 및 환경
가장 집중된 부하가 걸리는 최악의 상황을 포착하기 위해 단 1회의 재연결만 허용하는 환경에서 테스트를 진행했다.
| 구분 | 옵션 이름 | 설정값 | 비고 |
|---|---|---|---|
| 클라이언트 | reconnectionDelay | 300ms | 최초 재연결 시도 지연 시간 |
| 클라이언트 | reconnectionDelayMax | 3,000ms | 재연결 시도 간격의 최대치 |
| 클라이언트 | randomizationFactor | 0.3 | 재연결 간격 랜덤화 비율 |
| 서버 | SOCKET_TIMEOUT | 5s | Redis 내 재연결 세션 유지 시간 (TTL) |
Phase 1 - 테스트 결과
테스트 결과 분석
재연결 성공률
| VU 수 | 초기 연결 성공 | 재연결 성공 | 재연결 실패 | 성공률 | 비고 |
|---|---|---|---|---|---|
| 50 | 50 | 50 | 0 | 100% | - |
| 100 | 100 | 100 | 0 | 100% | - |
| 200 | 200 | 200 | 0 | 100% | - |
| 500 | 500 | 500 | 0 | 100% | - |
| 1000 | 1000 | 1000 | 0 | 100% | - |
| 5000 | 4,106 | 4,106 | 0 | 100% | 초기 연결 실패 894건은 재연결 시도 전 탈락 |
| 10,000 | 3,856 | 3,470 | 386 | 90.0% | SOCKET_TIMEOUT 만료 |
5,000 VU에서 전체 VU 기준 성공률은 82%지만, 이는 초기 WebSocket 연결에 실패한 894건 때문이다. 이미 연결에 성공한 4,106명 기준으로는 재연결 100% 성공했다. 초기 연결 실패는 TCP backlog 포화로 인한 것이며 재연결 메커니즘과는 별개다.
재연결 소요 시간
| VU 수 | min | mean | p50 | p95 | p99 | max |
|---|---|---|---|---|---|---|
| 50 | 0.2s | 0.30s | 0.3s | 0.4s | 0.4s | 0.4s |
| 100 | 0.2s | 0.30s | 0.3s | 0.4s | 0.4s | 0.4s |
| 200 | 0.2s | 0.30s | 0.3s | 0.4s | 0.4s | 0.4s |
| 500 | 0.2s | 0.30s | 0.3s | 0.4s | 0.4s | 0.4s |
| 1,000 | 0.2s | 0.30s | 0.3s | 0.4s | 0.4s | 0.4s |
| 5,000 | 0.2s | 0.30s | 0.3s | 0.4s | 0.5s | 0.5s |
| 10,000 | 0.2s | 2.00s | 2.1s | 4.1s | 6.6s | 8.8s |
100~1,000 VU 구간에서는 VU 수와 무관하게 0.2~0.4s 고정이다.
종합
왜 1,000 VU까지는 완벽하게 동작하는가?
Jitter를 통한 부하 분산
이벤트 루프의 여유
다중 워커의 부하 분산
10,000 VU에서 나타난 3단계 장애 레이어
10,000 규모의 동시 재연결 시, 서버는 세 가지 계층에서 순차적으로 무너지는 모습을 보였다.
Layer 1: TCP Backlog 포화 (연결 거부)
TCP accept 큐(기본값 511)의 한계를 초과하여 약 6,144건의 요청이 connect_error로 즉시 탈락했다. 이는 재연결 로직 이전에 OS 계층에서 발생하는 병목이다.
Layer 2: 이벤트 루프 포화 (처리 지연)
수용된 약 3,856건의 join_room 요청이 한꺼번에 몰리며 이벤트 루프가 마비되었다. 대규모 인원의 JSON 직렬화와 Redis I/O가 겹치며 p99 지연 시간이 6.6s까지 늘어났다.
Layer 3: SOCKET_TIMEOUT 미스매치 (세션 만료)
가장 핵심적인 실패 원인이다. 클라이언트는 0.3s 만에 요청을 보냈으나, 서버의 처리 지연이 Redis TTL(5s)을 초과했다. 서버가 뒤늦게 요청을 처리하려 했을 때는 이미 세션 데이터가 삭제된 상태였다. (386건 실패)

지수 백오프 시뮬레이션 및 문제점
서버 포화 상태에서 재연결 시도가 반복될 때, 현재의 SOCKET_TIMEOUT(5s)이 유효한지 다시 계산해보았다.
| 시도 | base delay | 범위 (±0.3 jitter) | 누적 최소 | 누적 최대 |
|---|---|---|---|---|
| 1차 | 300ms | 210 ~ 390ms | 210ms | 390ms |
| 2차 | 600ms | 420 ~ 780ms | 630ms | 1170ms |
| 3차 | 1200ms | 840 ~ 1560ms | 1470ms | 2730ms |
| 4차 | 2400ms | 1680 ~ 3120ms | 3150ms | 5850ms |
| 5차 | 3000ms | 2100 ~ 3000ms | — | — |
서버가 일시적인 과부하 상태일 때, 클라이언트는 지수 백오프를 통해 재시도하지만 서버의 세션 유지 시간(TTL)이 너무 짧아 복구 기회를 놓치게 된다.
결론
현재의 공격적인 재연결 설정(reconnectionDelay: 300ms)은 1,000 VU 수준의 중소규모 부하에서는 탁월한 UX를 제공할 수 있다. 그러나 10,000 명 단위의 극심한 Thundering Herd 상황에서는 서버 처리 지연과 세션 TTL 사이의 잘못된 설정 차이로 인해 재연결 실패가 발생한다.
따라서 다음 단계에서는 서버의 세션 유지 시간(SOCKET_TIMEOUT)을 상향 조정하고, 실제 프로덕션 환경처럼 지수 백오프가 이 간극을 메울 수 있는지 검증할 필요가 있다.
Phase 2 - 소켓 옵션 최적화 및 검증
주요 설정 변경 및 근거
Phase 1에서 발견된 서버 처리 지연과 세션 TTL 사이의 미스매치를 해결하고, 피크 부하를 분산하기 위해 세 가지 옵션을 조정했다.
| 항목 | 변경 전 | 변경 후 | 조정 근거 |
|---|---|---|---|
| SOCKET_TIMEOUT | 5s | 30s | 서버 포화 시 처리 지연 및 배포/장애 다운타임 대응 |
| randomizationFactor | 0.3 | 0.5 | 분산 윈도우 확대를 통한 피크 부하 약 40% 절감 |
| reconnectionDelayMax | 3,000ms | 5,000ms | 2차 이상 재시도 시 요청 밀집도 완화 |
| reconnectionDelay | 300ms | 300ms | 유지: UX를 저해하지 않으면서 Factor 조절로 분산 효과 확보 |
UX와 부하의 타협점
reconnectionDelay를 늘리는 대신 randomizationFactor를 0.5로 높였다. 이를 통해 분산 윈도우가 180ms에서 300ms로 넓어져, 피크 요청 속도를 21,400 req/s에서 12,853 req/s로 낮추면서도 사용자의 체감 복구 속도는 유지했다.
| factor | 1차 재연결 범위 | 분산 윈도우 | 3,856명 피크 속도 |
|---|---|---|---|
| 0.3 | 210 ~ 390ms | 180ms | 21,400 req/s |
| 0.5 | 150 ~ 450ms | 300ms | 12,853 req/s |
| 0.8 | 60 ~ 540ms | 480ms | 8,033 req/s |
세션 유지 시간(TTL) 조정
SOCKET_TIMEOUT을 30s로 상향하여 PM2 재시작이나 도커 컨테이너 복구 시 발생하는 다운타임(5~15s)은 물론, 서버 포화 시 발생하는 극심한 처리 지연(최대 9s) 상황에서도 세션이 증발하지 않도록 안전장치를 마련했다.
변경 후 지수 백오프 시뮬레이션
| 시도 | base delay | 범위 (±0.5 jitter) | 누적 최소 | 누적 최대 |
|---|---|---|---|---|
| 1차 | 300ms | 150 ~ 450ms | 150ms | 450ms |
| 2차 | 600ms | 300 ~ 900ms | 450ms | 1350ms |
| 3차 | 1200ms | 600 ~ 1800ms | 1050ms | 3150ms |
| 4차 | 2400ms | 1200 ~ 3600ms | 2250ms | 6750ms |
| 5차 | 4800ms | 2400 ~ 7200ms | 4650ms | 13950ms |
| 6차+ | 5000ms | 2500 ~ 5000ms | - | - |
SOCKET_TIMEOUT=30s는 최대 누적 대기 시간을 충분히 커버한다.
Phase 2 - 테스트 결과
1,000 VU 결과
| 항목 | 변경 전 | 변경 후 | 차이 |
|---|---|---|---|
| 재연결 성공률 | 100% | 100% | - |
| min | 0.20s | 0.20s | - |
| p50 | 0.30s | 0.30s | - |
| p95 | 0.40s | 0.40s | - |
| p99 | 0.40s | 0.50s | +0.10s |
| max | 0.40s | 0.50s | +0.10s |
성공률 100%를 유지했으며, p99 지연 시간이 0.1s 소폭 증가했다. 이는 분산 윈도우 확대에 따른 자연스러운 결과이며, 사용자 입장에서는 인지하기 어려운 수준의 변화다.
10,000 VU 결과
| 항목 | 변경 전 (Jitter 0.3, TTL 5s) | 변경 후 (Jitter 0.5, TTL 30s) | 변화 |
|---|---|---|---|
| 재연결 성공률 | 90.0% | 90.6% | +0.6%p 상승 |
| 재연결 실패 건수 | 386건 | 356건 | 30건 감소 |
| p50 지연 시간 | 2.1s | 2.4s | +0.3s |
| p95 지연 시간 | 4.1s | 4.9s | +0.8s |
| p99 지연 시간 | 6.6s | 8.2s | +1.6s (수치상 증가) |
| max | 8.8s | 8.8s | 동일 |
p99 지연 시간 증가
수치상으로는 지연 시간이 늘어난 것처럼 보이나, 이는 통계적인 착시일 뿐이다.
기존(TTL 5s)에는 5초 이상 걸리는 요청들이 모두 실패 처리되어 통계에서 제외되었으나, 변경 후(TTL 30s)에는 5~9초 사이의 요청들이 성공으로 전환되어 통계에 포함되었기 때문이다.
잔여 실패 건수(356건) 분석
SOCKET_TIMEOUT을 30s로 늘렸음에도 남은 356건의 실패는 세션 만료(Layer 3) 문제가 아니다. 재연결을 시도하는 과정에서 새로운 WebSocket 연결 자체가 거부되는 Layer 1(TCP Backlog 포화) 문제다.
이는 애플리케이션 설정 변경만으로는 해결할 수 없는 OS 레벨의 병목이다. 그러나 실제 환경에서는 클라이언트가 단 한 번만 시도하고 포기하지 않으므로, 다음 단계인 지수 백오프 재시도를 통해 구제 가능한 영역인지 검증이 필요하다.
결론
옵션 조정을 통해 서버 포화 상태에서의 세션 유실(Layer 3) 문제를 해결했다. 이제 남은 문제는 초기 연결 거부(Layer 1)로 인해 발생한 실패 건들이 실제 서비스 환경(무한 재시도)에서 지수 백오프를 통해 정상적으로 복구되는지 확인하는 것이다.
Phase 3 - 자동 지수 백오프 테스트
테스트 의도 및 환경
Phase 1/2가 서버 부하의 최악 시나리오를 확인했다면, Phase 3는 실제 프로덕션 환경의 클라이언트 동작(reconnection: true)을 모사하여 최종적인 복구 능력을 검증하고자 했다.
Layer 1(TCP Backlog 포화)으로 인한 연결 거부 건들이 재시도를 통해 얼마나 복구되는가? 가 핵심 검증 사항이다.
Toxiproxy와 reconnection: true 호환성 이슈Socket.IO reconnection: true 활성화 시 Toxiproxy TCP RST가 disconnect 이벤트를 트리거하지 않는 문제가 발생했다.
Socket.IO의 reconnection: true 모드에서는 TCP RST를 수신하면 Engine.IO transport 레이어가 즉시 내부 재연결 타이머를 시작한다. Artillery 프로세서의 disconnect 핸들러가 실행되기 전에 라이브러리 상태가 이미 reconnecting으로 전환되므로, disconnectAt 시점 측정과 이후 시나리오 흐름 제어가 불가능해진다.
이를 해결하기 위해 reconnection: false를 유지하면서, Artillery 프로세서 내부에 수동 지수 백오프 루프를 구현했다.
이 우회 방법이 실제 클라이언트와 동일한 이유는, 테스트에서 중요한 것이 클라이언트 내부 타이머 관리 방식이 아니라 서버가 받는 재연결 요청의 타이밍 분포이기 때문이다. 수동 루프는 Socket.IO 내부 구현과 동일한 공식을 적용한다.
delay(n) = min(300ms × 2^(n-1), 5,000ms) × (1 ± 0.5 × rand)서버 관점에서 재연결 요청이 도착하는 시간 분포가 실제 클라이언트와 동일하므로, 서버 부하 프로파일은 완전히 동등하다.
Phase 3 - 테스트 결과
10,000 VU
| 메트릭 | Phase 2 (1차 재연결) | Phase 3 (지수 백오프 적용) | 변화 및 의미 |
|---|---|---|---|
| 재연결 성공률 | 90.6% | 100% | 실패 건수 완전 제거 |
| 재연결 실패 | 356건 | 0건 | 모든 클라이언트 복구 성공 |
| p99 지연 시간 | 8.2s | 11.4s | 실패가 ‘느린 성공’으로 전환됨 |
| 재시도 횟수 (Max) | 1회 | 2회 | 최대 2회 이내 전원 복구 |
p95/p99 지연 시간이 늘어난 것은 성능 악화가 아니다. Phase 2에서 실패로 누락되었던 356건이 1~2회의 재시도 끝에 성공하면서 통계에 포함되었기 때문이다. 빠른 실패보다 느린 성공이 사용자 경험 측면에서 훨씬 우월하다.
테스트 결과, Thundering Herd로 인한 백로그 포화는 매우 일시적(~300ms 내외)임이 밝혀졌다. 1차 시도에서 거부당한 클라이언트가 지수 백오프 대기 후 2차 시도를 했을 때는 이미 백로그에 여유가 생겨 전원 성공했다.
시도 횟수 분포 및 분석
10,000 VU 규모에서도 최대 재시도 횟수는 2회에 그쳤다. 이는 현재 설정된 jitter와 딜레이 값이 서버의 일시적 마비를 극복하기에 충분히 효율적임을 나타낸다.
| 시도 횟수 | 해당 비중 | 분석 및 해석 |
|---|---|---|
| 1회 (p50) | 약 90% | 최초 시도 시 백로그 여유가 있어 즉시 성공 |
| 2회 (p95~p99) | 약 10% | 300~600ms 대기 후 재시도 시 백로그 여유 확보 |
| 3회 이상 | 0% | 미발생 (일시적 포화 구간이 매우 짧음 확인) |
결론
지수 백오프를 통해 Phase 1/2에서 관측된 Layer 1(TCP Backlog 포화) 재연결 실패를 완전히 제거할 수 있음을 확인했다.
일시적 포화와 자동 복구의 조화
Phase 2에서 발생한 356건의 실패는 서버의 구조적 결함이 아니라, Thundering Herd가 TCP Backlog(511)를 순간적으로 점유하며 발생한 병목이었다. 지수 백오프는 이 일시적 포화 구간이 지나갈 때까지 대기한 후 재시도함으로써 모든 실패 케이스를 성공으로 전환시켰다.
재시도 횟수의 효율성
10,000 VU의 극한 상황에서도 최대 재시도 횟수는 2회를 넘지 않았다. 이는 1차 재시도 딜레이(300ms)와 jitter(±150ms)의 조합만으로도 충분한 부하 분산 효과가 발생하여, 백로그가 비워지는 속도와 클라이언트가 다시 진입하는 속도가 적절한 균형을 이루고 있음을 증명한다.
최종 판단: 구조적 안정성 확보
결과적으로 300ms의 공격적인 reconnectionDelay 설정은 단독으로는 위험할 수 있으나, 적절한 지터(0.5)와 클라이언트의 지수 백오프 메커니즘이 결합될 때 10,000명 규모의 동시 재연결을 수용하기에 충분한 구조적 안정성을 확보함을 확인했다.
종합 비교 및 결론
설정 변경 요약
테스트 결과에 기반하여 사용자 경험과 서버 가용성의 균형을 맞춘 최종 설정안이다.
| 항목 | 변경 전 | 변경 후 | 근거 |
|---|---|---|---|
| reconnectionDelay | 300ms | 300ms (유지) | 최상의 UX 유지. 부하 분산은 Jitter 가 담당 |
| reconnectionDelayMax | 3,000ms | 5,000ms | 재시도 반복 시 요청 밀집도 완화 |
| randomizationFactor | 0.3 | 0.5 | 피크 부하 약 40% 감소 효과 확인 |
| SOCKET_TIMEOUT | 5s | 30s | 서버 포화 및 배포 다운타임 시 세션 보호 |
재연결 성능 및 메커니즘 분석
재연결 소요 시간 (Latency)
1,000 VU
10,000 VU
SOCKET_TIMEOUT 동작 비교
서버 포화 상태에서 세션 유지 능력을 시퀀스 다이어그램으로 비교했다.

서버 규모별 안정성 판단
단일 인스턴스 기준, 동시 연결 수에 따른 서버 상태를 정의했다.
| 동시 연결 수 | 상태 | 대응 전략 및 설명 |
|---|---|---|
| ~1,000명 | 안정 | 재연결 성공률 100%, p99 < 0.5s. 현재 설정으로 완벽 대응 가능 |
| ~5,000명 | 주의 | 초기 연결(Layer 1) 일부 누락 가능하나, 지수 백오프 시 100% 복구 |
| 10,000명~ | 한계 | 단일 인스턴스 처리 불가. Redis Adapter 기반 수평 확장 필요 |
결론
현재 설정(300ms)은 충분한가?
그렇다. 지수 백오프 덕분에 10,000 VU에서도 최종 실패율 0%를 달성했다.
가장 임팩트가 컸던 설정은?
SOCKET_TIMEOUT 상향(5s → 30s)이다. 서버 코드 한 줄 변경만으로 Layer 3 실패를 완전히 제거했다.
Jitter(0.5)를 올린 실질적 효과는?
피크 부하를 40%나 낮췄음에도 속도 차이는 사용자가 인지하기 어려운 수준의 변화다.
지수 백오프가 필수적인가?
필수다. 10,000 VU 상황에서 발생한 356건의 일시적 연결 거부 를 백오프 재시도가 모두 해결했다.
실제 운영 환경 시뮬레이션
서버 재시작 시나리오
Toxiproxy 테스트보다 더 혹독한 실제 배포 환경에서의 복구 흐름이다.
- t=0s: 서버 종료 및 RST 발생 → Redis 세션 TTL(30s) 시작
- t=0.3~5s: 클라이언트 재연결 시도 → 서버 기동 중으로 거부 (ECONNREFUSED) 발생
- t=10s: 서버 기동 완료 및 재연결 수용 시작
- t=10.3s:
join_room처리 → TTL이 30s로 넉넉하여 19.7s 남기고 성공
네트워크 환경 영향
로컬 환경(RTT 0.1ms) 대비 실제 클라우드 환경(RTT 5~150ms)에서는 재연결 소요 시간이 약간 증가할 수 있으나, 확보된 30s의 세션 유지 시간 내에서는 충분히 무시 가능한 수준이다.
부하 패턴의 신뢰성
Artillery의 다중 워커 구조는 실제 서비스에서 여러 강의실이 동시에 터지는 Thundering Herd 패턴을 정확히 모사한다. 따라서 본 테스트 결과는 실제 운영 환경에서도 높은 재현성을 가질 것으로 판단된다.
마무리하며
부하 테스트를 처음 진행하며 마주한 과정은 예상보다 훨씬 복합적이고 어려웠다. 하지만 실제 운영 환경을 가정하고 소켓 옵션 하나하나의 의미를 실측 데이터로 검증해 본 것은 의미 있는 경험이었다.
설정값 뒤에 숨겨진 처리 과정과, 이를 뒷받침하기 위해 설정한 세션 유지 시간이 어떻게 유기적으로 맞물리는지 확인할 수 있었다. 이를 통해 단순한 기능 구현을 넘어, 예외 상황을 견디는 설계가 얼마나 중요한지 생각해보게 되었다.
이번 테스트를 통해 서비스의 규모와 도메인 특성을 이해하는 것이 의사결정의 일부임을 느끼기도 했다. 화상 강의라는 도메인 특성상 찰나의 끊김도 사용자 경험에 치명적일 수 있다. 또한, 실제 목표 사용자 수가 명확하지 않아, 어디까지 커버해야하는지 고민도 많았다. 첫 기획 단계에서 이를 설정하고 나갔어야 했는데 그러지 못해 아쉬웠다.
지금의 결과가 완벽하다고 생각하지 않는다. 이번에 발견한 단일 인스턴스의 한계를 넘어서기 위해 수평 확장이나 다른 개선점을 계속해서 찾아 나가며, 더욱 안정적이고 신뢰할 수 있는 서비스를 구축하는 데 집중할 것이다.
