WebSocket을 쓰는 글은 많다. 그런데 "동시에 몇 개나 버틸 수 있는가"
같은 기본적인 질문에 대한 한국어 자료는 의외로 적었다. 있어도 대부분
"수만 개 가능하다더라" 수준이고, 직접 측정한 데이터는 거의 없었다.
그래서 직접 해보기로 했다. 첫 질문은 단순하다.
Node.js + ws 라이브러리로 만든 WebSocket 서버 한 대는,
동시에 몇 개의 연결을 버틸 수 있을까?
이 시리즈에서는 이 질문에서 시작해 WebSocket을 한 단계씩 더 깊게
파볼 예정이다. 단일 서버 한계 → 수평 확장 → 프로토콜 직접 구현 →
라이브러리 소스코드 분석까지.
이번 글은 그 첫 단계인데, 실험을 시작하기 전에 측정 환경부터
만들었다. 그리고 그 과정에서 내린 설계 결정들을 정리해두려고 한다.
처음엔 부하 테스트 도구를 그냥 가져다 쓸까 했다. Artillery나 k6 같은.
근데 두 가지 이유로 직접 만들기로 했다.
첫째, 기존 도구들은 WebSocket을 "지원은 하지만 1급 시민으로 다루지
않는다". 대부분 HTTP 부하 테스트에 최적화돼 있다.
둘째, 그리고 더 중요한 이유. 부하 생성기를 직접 만들면 그 자체가
WebSocket을 깊게 이해하는 과정이 된다. 어차피 시리즈의 목적이
깊이 파는 거라면, 도구도 직접 만드는 게 맞다.
그래서 3개의 프로그램을 만들기로 했다.
```
[부하 생성기 머신][서버 머신]
load-generator.js ───────► server.js
metrics-collector.js
(CSV로 저장)
```
각자 책임이 하나씩이다. 이렇게 분리한 이유는 아래에서 하나씩
설명한다.
측정 대상 서버는 일부러 단순하게 만들었다. 클라이언트가 메시지를
보내면 그대로 돌려보내는 echo만 한다.
복잡한 비즈니스 로직을 넣으면 한계를 측정할 때 문제가 생긴다.
1만 개 연결에서 응답이 느려졌을 때, 그게 WebSocket의 한계인지
내 비즈니스 로직 때문인지 구분이 안 된다.
echo는 WebSocket이 할 수 있는 최소 단위 동작이다. 여기서
나오는 수치가 "이 환경에서의 이론적 상한"에 가장 가깝다.
서버는 /metrics 엔드포인트로 자기 상태를 JSON으로 노출한다.
| 메트릭 | 의미 |
|---|---|
| activeConnections | 현재 살아있는 연결 수 |
| totalConnections | 누적 연결 수 |
| messagesReceived/Sent | 메시지 처리량 |
| errors | 누적 에러 수 |
| heapUsed | Node.js 힙 사용량 |
| eventLoopLag | 이벤트 루프 지연(ms) |
이 중에서 eventLoopLag를 강조하고 싶다. Node.js 서버가 헐떡이기
시작하는 순간을 가장 빨리 보여주는 메트릭이다. CPU가 100% 안 찍혀도
이게 올라가기 시작하면 이미 응답이 밀리고 있다는 뜻이다.
측정 방법은 단순하다. setInterval로 100ms마다 함수를 실행하면서,
"기대했던 시간"과 "실제 호출된 시간"의 차이를 본다. 이벤트 루프가
바쁘면 이 차이가 벌어진다.
WebSocket은 8080, 메트릭 HTTP는 8081로 분리했다. 같은 포트에서
처리하지 않은 이유는, 부하가 심한 순간에 메트릭 조회 자체가
실패하면 가장 중요한 순간의 데이터를 잃기 때문이다.
서버가 깨지기 직전이 가장 보고 싶은 순간인데, 그 순간에 메트릭이
같이 깨지면 분석할 게 없다.
처음엔 server.js 안에서 직접 CSV에 쓰는 것도 고민했다. 코드 한 개로
끝나니까. 근데 두 가지 이유로 분리했다.
첫째, OS 메트릭은 서버 안에서 정확히 못 잡는다. CPU 사용률,
시스템 메모리, 열린 파일 디스크립터 수, TCP 소켓 상태 같은 건
/proc 파일시스템을 직접 읽어야 정확하다. Node.js 안에서
os.cpus() 같은 걸로 잡으면 부정확하거나 비싸다.
둘째, 더 중요한 이유. 서버가 죽어도 collector는 계속 돌아야 한다.
가장 중요한 데이터는 "서버가 죽기 직전 5초"인데, server.js 안에서
수집하면 그 순간 데이터가 같이 사라진다.
그래서 collector는 별도 프로세스로 돌면서 매 1초마다:
/metrics를 호출해서 앱 레벨 메트릭을 받는다/proc/stat, /proc/meminfo 등을 직접 읽어 OS 메트릭을 잡는다CSV로 떨군 이유는 단순해서다. Prometheus + Grafana 같은 스택을
쓸 수도 있지만, 첫 실험에 그게 필요하진 않다. CSV면 Python으로든
JS로든 후처리하기 쉽다.
부하 생성기에서 가장 고민한 건 시나리오 분리였다. 측정하고 싶은
시나리오가 세 개였다.
처음엔 시나리오마다 별도 스크립트로 만들까 했다. 근데 그러면
공통 로직(연결, ramp-up, 통계 수집)이 세 번 복붙된다. 그래서
CLI 인자로 시나리오를 주입하는 방식으로 갔다.
```bash
node load-generator.js \
--target ws://:8080 \
--total 10000 \
--ramp-up-rate 100 \
--message-interval 10000 \
--message-size 100
```
--message-interval 0--message-interval 10000--message-interval 100하나의 코드로 세 시나리오를 다 돌릴 수 있다.
부하 생성 패턴에서 가장 신경 쓴 건 점진적 증가다. 10000개를
한 번에 연결하면 두 가지 문제가 생긴다.
그래서 1초에 N개씩(--ramp-up-rate) 점진적으로 늘려간다.
이러면 "활성 연결 수가 X일 때 서버 상태가 Y였다"는 시계열을 그릴
수 있다. 이 곡선의 기울기가 바뀌는 순간이 한계점이다.
여기까지가 설계다. 오늘 작업한 건 그중에서:
100개는 아무 데서나 잘 돈다. 다음 단계는 EC2 두 대 띄워서 실제
네트워크를 사이에 두고 측정을 시작하는 것이다.
ulimit 벽(2편에서 계속)