Rocky 's Blog

Socket.IO 재연결 옵션 검증: Thundering Herd 부하 테스트

  • Socket
  • thundering-herd
  • load-testing
2026. 03. 06.
게시글 썸네일

개요


배경 및 문제 제기

화상 강의 서비스의 특성에 맞춰 소켓 재연결 옵션을 최적화하는 작업에서 시작했다. 기존 설정은 재연결 대기 시간이 길어 사용자 경험을 저하시킨다고 판단했고, 이를 단축하는 방향으로 PR을 올렸다.

옵션기존 설정PR 제안값설명
reconnectionDelay1,000ms300ms최초 재연결 시도 지연 시간
reconnectionDelayMax5,000ms3,000ms재연결 시도 간격의 최대치
randomizationFactor0.50.3재연결 간격의 무작위 분산 비율
  • 관련 PR 링크
  • 팀 리뷰 중 "옵션을 더 느슨하게 잡자"는 의견을 듣게 되었다. 재연결 간격을 넓히면 서버 부하는 줄지만, 일반적인 네트워크 불안정 상황에서 복구 속도가 느려져 UX가 떨어진다는 우려가 있었다.

    관련 PR 리뷰
    관련 PR 리뷰

    어느 쪽이 맞는지 이론만으로 판단하기 어렵다고 생각했다. 이론적 추측만으로 판단하기보다, 실제 서버가 어느 정도의 부하까지 견디는지 데이터로 확인하기로 했다. PR의 설정값이 유효한지 검증하고 이를 바탕으로 최종 방향을 결정하고자 한다.

    Thundering Herd란 무엇인가

    Thundering Herd는 서버 재시작이나 네트워크 장애 이후, 모든 클라이언트가 짧은 시간 내에 동시에 재연결을 시도하여 서버 부하가 특정 순간에 폭증하는 현상이다. 단순히 동시 접속자가 많다는 문제가 아니라, 부하가 한 순간에 집중된다는 점이 핵심이다.

    Jitter를 통한 분산

    Socket.IO의 randomizationFactor는 재연결 타이밍에 무작위성을 부여해 요청을 시간 축으로 분산시킨다.

    분산 윈도우 = reconnectionDelay × 2 × randomizationFactor

    jitter 설정분산 윈도우1,000명 피크 속도3,856명 피크 속도
    0 (없음)0ms (순간 스파이크)
    0.3 (1차 변경)180ms5,556 req/s21,400 req/s
    0.5 (2차 변경)300ms3,333 req/s12,853 req/s

    분산 윈도우가 넓을수록 동일한 인원이 더 긴 시간에 걸쳐 재연결하므로 서버의 순간 피크 부하가 줄어든다.

    테스트 설계 및 목표

    테스트 스케일

    단일 강의실이 아닌 서버 전체의 활성 연결이 동시에 끊기는 최악의 시나리오를 가정했다.

    화상강의에서 강의실당 평균 100명으로 잡았고, 이 강의실이 100개 운영된다는 가정 하에 강의실 100개 × 강의실당 100명 = 10,000명 동시 재연결이 발생하는 상황을 측정하고자 한다.

  • 테스트 스케일: 100 / 500 / 1,000 / 5,000 / 10,000 VU (가상유저)
  • 테스트 방법 (3단계)
  • Phase 1 (한계 포착): 재연결을 단 1회만 허용한다. 최악의 케이스에서 서버가 어느 레이어(TCP 백로그, 이벤트 루프, 세션 TTL 등)부터 무너지는지 측정한다.
  • Phase 2 (옵션 조정): Phase 1 결과를 바탕으로 옵션값을 수정하고 동일 조건에서 재측정한다.
  • Phase 3 (복구 능력 검증): 실제 클라이언트 동작 방식인 무한 재시도를 적용한다. 일시적 포화 상태가 지수 백오프를 통해 성공으로 전환되는지 확인한다.
  • 결과 요약

    임계점 측정

    5,000 VU부터 초기 WebSocket 연결 거부(TCP Backlog 포화)가 발생하기 시작하며, 10,000 VU 시 약 10%의 실패율을 보였다.

    재시도 효과

    Phase 1/2에서 실패했던 건들은 지수 백오프 재시도(최대 2회)만으로 100% 복구됨을 확인했다.

    최종 판단

    단일 인스턴스의 한계는 초기 수용량에 있으나, 일단 연결된 세션의 재연결은 아래 설정값으로도 충분히 안정적이다. 따라서 UX 향상을 위해 제안된 공격적인 설정을 유지해도 무방하다.

    항목변경 전변경 후
    SOCKET_TIMEOUT5s30s
    randomizationFactor0.30.5
    reconnectionDelayMax3,000ms5,000ms
    reconnectionDelay300ms300ms

    적용한 해결책

    항목변경 전변경 후근거
    randomizationFactor0.30.5분산 윈도우 180ms → 300ms, 피크 부하 40% 감소
    SOCKET_TIMEOUT5s30s서버 포화 시 세션 유실 방지, 배포 다운타임 커버
    reconnectionDelayMax3,000ms5,000ms2차 이상 재시도 시 요청 밀집도 완화
    reconnectionDelay300ms300ms (유지)분산은 factor가 담당, UX 유지

    reconnectionDelay를 올리는 대신 randomizationFactor를 높인 이유는, 서버

    테스트 방법론


    인프라 구성

    테스트 환경은 부하 생성, 네트워크 제어, 서비스 엔진의 세 계층으로 설계했다.

    부하 생성 (Artillery)

    N개의 독립적인 Socket.IO 클라이언트(VU)를 생성하여 실제 사용자 접속 환경을 모사한다.

    네트워크 제어 (Toxiproxy)

    클라이언트와 서버 사이에서 TCP 프록시 역할을 수행한다. 테스트 시점에 reset_peer toxic을 주입하여 모든 연결을 강제로 끊는 장애 상황을 연출한다.

    서버 (NestJS + Redis + Prometheus)

    Socket.IO Gateway가 재연결을 처리하고, Redis에서 세션을 복원하며, 관련 지표를 Prometheus로 수집한다.

    Notion image

    모든 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_peerO높음스크립트 제어 가능, 실제 장애와 유사

    가상 사용자(VU) 시나리오 흐름

    테스트는 재연결 실패 시의 대응 방식에 따라 두 가지 모드로 진행했다.

    방식 A: 1차 재연결 (Phase 1 / 2)

    reconnection: false 설정 후 수동으로 1회만 재연결을 시도한다. 첫 시도 실패를 최종 실패로 간주하여, 서버에 가장 극심한 부하가 걸리는 최악의 임계 지점을 찾는 데 집중한다.

    Notion image
    방식 B: 자동 지수 백오프 (Phase 3)

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

    Notion image

    측정 지표 (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
    1005s15s20s
    50010s25s30s
    1,00020s35s40s
    5,00030s60s70s
    10,00060s90s100s

    Phase 1 - 현재 설정의 한계 파악


    테스트 설정 및 환경

    가장 집중된 부하가 걸리는 최악의 상황을 포착하기 위해 단 1회의 재연결만 허용하는 환경에서 테스트를 진행했다.

    구분옵션 이름설정값비고
    클라이언트reconnectionDelay300ms최초 재연결 시도 지연 시간
    클라이언트reconnectionDelayMax3,000ms재연결 시도 간격의 최대치
    클라이언트randomizationFactor0.3재연결 간격 랜덤화 비율
    서버SOCKET_TIMEOUT5sRedis 내 재연결 세션 유지 시간 (TTL)

    Phase 1 - 테스트 결과


    테스트 결과 분석

    재연결 성공률
    VU 수초기 연결 성공재연결 성공재연결 실패성공률비고
    5050500100%-
    1001001000100%-
    2002002000100%-
    5005005000100%-
    1000100010000100%-
    50004,1064,1060100%초기 연결 실패 894건은 재연결 시도 전 탈락
    10,0003,8563,47038690.0%SOCKET_TIMEOUT 만료

    5,000 VU에서 전체 VU 기준 성공률은 82%지만, 이는 초기 WebSocket 연결에 실패한 894건 때문이다. 이미 연결에 성공한 4,106명 기준으로는 재연결 100% 성공했다. 초기 연결 실패는 TCP backlog 포화로 인한 것이며 재연결 메커니즘과는 별개다.

    재연결 소요 시간
    VU 수minmeanp50p95p99max
    500.2s0.30s0.3s0.4s0.4s0.4s
    1000.2s0.30s0.3s0.4s0.4s0.4s
    2000.2s0.30s0.3s0.4s0.4s0.4s
    5000.2s0.30s0.3s0.4s0.4s0.4s
    1,0000.2s0.30s0.3s0.4s0.4s0.4s
    5,0000.2s0.30s0.3s0.4s0.5s0.5s
    10,0000.2s2.00s2.1s4.1s6.6s8.8s

    100~1,000 VU 구간에서는 VU 수와 무관하게 0.2~0.4s 고정이다.

    종합
  • 1,000 VU 이하: 재연결 성공률 100%, p99 소요 시간 0.4s 미만으로 매우 안정적이다.
  • 5,000 VU: 초기 연결 성공자(4,106명) 기준으로는 100% 성공했다. (초기 실패는 TCP 포화 이슈)
  • 10,000 VU: 재연결 성공률 90.0%로 감소하며, p99 소요 시간이 6.6s까지 치솟았다.
  • 왜 1,000 VU까지는 완벽하게 동작하는가?

    Jitter를 통한 부하 분산

  • 180ms의 윈도우에 1,000건이 분산되면서 초당 요청 수(req/s)가 Node.js와 Redis가 감당 가능한 범위 내에 머물렀다.
  • 이벤트 루프의 여유

  • Prometheus 측정 결과 이벤트 루프 지연이 최대 14.4ms에 불과하여 I/O 처리에 병목이 없었다.
  • 다중 워커의 부하 분산

  • Artillery 워커들이 여러 방으로 부하를 쪼개어 전달함으로써, 단일 방에 인원이 몰릴 때 발생하는 직렬화/브로드캐스트 부하를 방지했다.
  • 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건 실패)

    Notion image

    지수 백오프 시뮬레이션 및 문제점

    서버 포화 상태에서 재연결 시도가 반복될 때, 현재의 SOCKET_TIMEOUT(5s)이 유효한지 다시 계산해보았다.

    시도base delay범위 (±0.3 jitter)누적 최소누적 최대
    1차300ms210 ~ 390ms210ms390ms
    2차600ms420 ~ 780ms630ms1170ms
    3차1200ms840 ~ 1560ms1470ms2730ms
    4차2400ms1680 ~ 3120ms3150ms5850ms
    5차3000ms2100 ~ 3000ms

    서버가 일시적인 과부하 상태일 때, 클라이언트는 지수 백오프를 통해 재시도하지만 서버의 세션 유지 시간(TTL)이 너무 짧아 복구 기회를 놓치게 된다.

    결론

    현재의 공격적인 재연결 설정(reconnectionDelay: 300ms)은 1,000 VU 수준의 중소규모 부하에서는 탁월한 UX를 제공할 수 있다. 그러나 10,000 명 단위의 극심한 Thundering Herd 상황에서는 서버 처리 지연과 세션 TTL 사이의 잘못된 설정 차이로 인해 재연결 실패가 발생한다.

    따라서 다음 단계에서는 서버의 세션 유지 시간(SOCKET_TIMEOUT)을 상향 조정하고, 실제 프로덕션 환경처럼 지수 백오프가 이 간극을 메울 수 있는지 검증할 필요가 있다.

    Phase 2 - 소켓 옵션 최적화 및 검증


    주요 설정 변경 및 근거

    Phase 1에서 발견된 서버 처리 지연과 세션 TTL 사이의 미스매치를 해결하고, 피크 부하를 분산하기 위해 세 가지 옵션을 조정했다.

    항목변경 전변경 후조정 근거
    SOCKET_TIMEOUT5s30s서버 포화 시 처리 지연 및 배포/장애 다운타임 대응
    randomizationFactor0.30.5분산 윈도우 확대를 통한 피크 부하 약 40% 절감
    reconnectionDelayMax3,000ms5,000ms2차 이상 재시도 시 요청 밀집도 완화
    reconnectionDelay300ms300ms유지: UX를 저해하지 않으면서 Factor 조절로 분산 효과 확보
    UX와 부하의 타협점

    reconnectionDelay를 늘리는 대신 randomizationFactor를 0.5로 높였다. 이를 통해 분산 윈도우가 180ms에서 300ms로 넓어져, 피크 요청 속도를 21,400 req/s에서 12,853 req/s로 낮추면서도 사용자의 체감 복구 속도는 유지했다.

    factor1차 재연결 범위분산 윈도우3,856명 피크 속도
    0.3210 ~ 390ms180ms21,400 req/s
    0.5150 ~ 450ms300ms12,853 req/s
    0.860 ~ 540ms480ms8,033 req/s
    세션 유지 시간(TTL) 조정

    SOCKET_TIMEOUT을 30s로 상향하여 PM2 재시작이나 도커 컨테이너 복구 시 발생하는 다운타임(5~15s)은 물론, 서버 포화 시 발생하는 극심한 처리 지연(최대 9s) 상황에서도 세션이 증발하지 않도록 안전장치를 마련했다.

    변경 후 지수 백오프 시뮬레이션

    시도base delay범위 (±0.5 jitter)누적 최소누적 최대
    1차300ms150 ~ 450ms150ms450ms
    2차600ms300 ~ 900ms450ms1350ms
    3차1200ms600 ~ 1800ms1050ms3150ms
    4차2400ms1200 ~ 3600ms2250ms6750ms
    5차4800ms2400 ~ 7200ms4650ms13950ms
    6차+5000ms2500 ~ 5000ms--

    SOCKET_TIMEOUT=30s는 최대 누적 대기 시간을 충분히 커버한다.

    Phase 2 - 테스트 결과


    1,000 VU 결과

    항목변경 전변경 후차이
    재연결 성공률100%100%-
    min0.20s0.20s-
    p500.30s0.30s-
    p950.40s0.40s-
    p990.40s0.50s+0.10s
    max0.40s0.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.1s2.4s+0.3s
    p95 지연 시간4.1s4.9s+0.8s
    p99 지연 시간6.6s8.2s+1.6s (수치상 증가)
    max8.8s8.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.2s11.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명 규모의 동시 재연결을 수용하기에 충분한 구조적 안정성을 확보함을 확인했다.

    종합 비교 및 결론


    설정 변경 요약

    테스트 결과에 기반하여 사용자 경험과 서버 가용성의 균형을 맞춘 최종 설정안이다.

    항목변경 전변경 후근거
    reconnectionDelay300ms300ms (유지)최상의 UX 유지. 부하 분산은 Jitter 가 담당
    reconnectionDelayMax3,000ms5,000ms재시도 반복 시 요청 밀집도 완화
    randomizationFactor0.30.5피크 부하 약 40% 감소 효과 확인
    SOCKET_TIMEOUT5s30s서버 포화 및 배포 다운타임 시 세션 보호

    재연결 성능 및 메커니즘 분석

    재연결 소요 시간 (Latency)

    1,000 VU

  • p99 기준 0.1s 미세 증가했으나, 이는 분산 윈도우 확대에 따른 의도된 변화이며 실사용 체감은 불가능한 수준이다.
  • 10,000 VU

  • Max 지연 시간(8.8s)이 동일하다는 점은 서버의 처리 한계(이벤트 루프 큐 소진 속도)가 일정함을 의미한다. p99의 수치적 증가는 과거 실패하던 '느린 요청'들이 성공으로 전환되며 통계에 포함된 결과다.
  • SOCKET_TIMEOUT 동작 비교

    서버 포화 상태에서 세션 유지 능력을 시퀀스 다이어그램으로 비교했다.

    Notion image

    서버 규모별 안정성 판단

    단일 인스턴스 기준, 동시 연결 수에 따른 서버 상태를 정의했다.

    동시 연결 수상태대응 전략 및 설명
    ~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 테스트보다 더 혹독한 실제 배포 환경에서의 복구 흐름이다.

    1. t=0s: 서버 종료 및 RST 발생 → Redis 세션 TTL(30s) 시작
    2. t=0.3~5s: 클라이언트 재연결 시도 → 서버 기동 중으로 거부 (ECONNREFUSED) 발생
    3. t=10s: 서버 기동 완료 및 재연결 수용 시작
    4. t=10.3s: join_room 처리 → TTL이 30s로 넉넉하여 19.7s 남기고 성공

    네트워크 환경 영향

    로컬 환경(RTT 0.1ms) 대비 실제 클라우드 환경(RTT 5~150ms)에서는 재연결 소요 시간이 약간 증가할 수 있으나, 확보된 30s의 세션 유지 시간 내에서는 충분히 무시 가능한 수준이다.

    부하 패턴의 신뢰성

    Artillery의 다중 워커 구조는 실제 서비스에서 여러 강의실이 동시에 터지는 Thundering Herd 패턴을 정확히 모사한다. 따라서 본 테스트 결과는 실제 운영 환경에서도 높은 재현성을 가질 것으로 판단된다.

    마무리하며


    부하 테스트를 처음 진행하며 마주한 과정은 예상보다 훨씬 복합적이고 어려웠다. 하지만 실제 운영 환경을 가정하고 소켓 옵션 하나하나의 의미를 실측 데이터로 검증해 본 것은 의미 있는 경험이었다.

    설정값 뒤에 숨겨진 처리 과정과, 이를 뒷받침하기 위해 설정한 세션 유지 시간이 어떻게 유기적으로 맞물리는지 확인할 수 있었다. 이를 통해 단순한 기능 구현을 넘어, 예외 상황을 견디는 설계가 얼마나 중요한지 생각해보게 되었다.

    이번 테스트를 통해 서비스의 규모와 도메인 특성을 이해하는 것이 의사결정의 일부임을 느끼기도 했다. 화상 강의라는 도메인 특성상 찰나의 끊김도 사용자 경험에 치명적일 수 있다. 또한, 실제 목표 사용자 수가 명확하지 않아, 어디까지 커버해야하는지 고민도 많았다. 첫 기획 단계에서 이를 설정하고 나갔어야 했는데 그러지 못해 아쉬웠다.

    지금의 결과가 완벽하다고 생각하지 않는다. 이번에 발견한 단일 인스턴스의 한계를 넘어서기 위해 수평 확장이나 다른 개선점을 계속해서 찾아 나가며, 더욱 안정적이고 신뢰할 수 있는 서비스를 구축하는 데 집중할 것이다.

    관련 글