Node.js 부하 테스트 및 메트릭 수집 아키텍처 정리
일단 전체적인 구성 코드가 필요하면 댓글남겨주길 바랍니다.
배경
Node.js(Express) 기반 백엔드 애플리케이션의 성능 특성과 시스템 안정성을 검증하기 위해
부하 테스트(k6)와 런타임 메트릭 수집(Prometheus)을 분리하여 구성한다.
- k6: 트래픽 시뮬레이션 및 부하 결과 수집
- Prometheus: 서버 내부 메트릭 수집
- Grafana: 시각화 단일 창구
- InfluxDB: k6 결과 저장소
부하 결과와 서버 메트릭을 분리 저장하여 원인 분석 가능성을 높이는 것이 핵심 목적이다.
핵심 질문
- 부하 테스트 결과(k6)와 서버 메트릭(Prometheus)을 어떻게 분리·연결할 것인가?
- Node.js 애플리케이션에서 어떤 메트릭을 노출해야 하는가?
- Grafana에서 두 종류의 메트릭을 어떻게 통합 시각화할 것인가?
논의 요약
전체 아키텍처 흐름
- k6는 백엔드에 HTTP 부하를 발생시키고 결과를 InfluxDB에 저장
- 백엔드는
prom-client를 통해 런타임 메트릭을/metrics로 노출 - Prometheus는 주기적으로 백엔드 메트릭을 스크래핑
- Grafana는 Prometheus + InfluxDB를 데이터소스로 사용하여 시각화
역할 분리 원칙
| 구성 요소 | 역할 |
|---|---|
| k6 | 외부 관점의 부하·지연·에러율 측정 |
| prom-client | 내부 관점의 CPU, 메모리, 이벤트루프 상태 노출 |
| Prometheus | 시계열 메트릭 수집 및 집계 |
| InfluxDB | k6 부하 테스트 결과 저장 |
| Grafana | 통합 대시보드 제공 |
결정 사항
1. Express 메트릭 노출
- 파일:
src/middleware/metrics.js - 사용 라이브러리:
prom-client - 노출 메트릭:
- HTTP 요청 수
- 응답 시간(latency)
- 상태 코드별 요청 수
- Node.js 기본 메트릭
- CPU 사용률
- 메모리 사용량
- 이벤트 루프 지연
- 엔드포인트:
GET /metrics
2. Prometheus 설정
- 파일:
monitoring/prometheus/prometheus.yml - 주요 설정:
- backend 서비스 스크래핑
- 수집 주기: 15초
- 목적:
- 서버 내부 상태를 시간 흐름에 따라 안정적으로 수집
3. Grafana 설정
- 데이터소스 설정:
- 파일:
monitoring/grafana/provisioning/datasources/prometheus.yml
- 파일:
- 대시보드 설정:
- 경로:
monitoring/grafana/provisioning/dashboards/
- 경로:
- 구성 내용:
- Prometheus 데이터소스 자동 등록
- Node.js 메트릭 기반 대시보드 프리셋 사용
4. k6 부하 테스트 스크립트
- 파일:
k6/scripts/load-test.js - 테스트 특징:
- VU(Virtual User) 기반 부하 모델
- 피보나치 계산 엔드포인트 테스트
- 단계별 부하 증가 시나리오
- 결과 처리:
- 실시간으로 InfluxDB 전송
5. InfluxDB 설정
- 서비스 포트:
8086 - 역할:
- k6 결과 메트릭 저장
- 연동:
- Grafana에서 InfluxDB 데이터소스로 사용
- 저장 대상:
- 요청 지연
- 성공/실패 비율
- VU 수 변화
6. Docker Compose 확장
- 파일:
docker-compose.monitoring.yml - 포함 서비스:
prometheus(9090)grafana(3000)influxdb(8086)k6(선택적 실행)
- 목적:
- 로컬/테스트 환경에서 모니터링 스택 일괄 기동
일단이렇게 준비를 했으면 실제로 올려보겠다
docker compose -f docker-compose.dev.yml -f docker-compose.monitoring.yml down
-v && \
docker compose -f docker-compose.dev.yml -f docker-compose.monitoring.yml build
--no-cache && \
docker compose -f docker-compose.dev.yml -f docker-compose.monitoring.yml up -d
다음 처럼 들어가면된다
- Grafana: http://localhost:3000 (로그인: admin / admin)
- Prometheus: http://localhost:9090
- 대시보드 찾기 (Grafana 10.x):
- 좌측 메뉴 ☰ (햄버거) 클릭
- Dashboards 클릭
- Node.js Backend 또는 k6 Load Test 선택
싱글모드로 킨다음에
# 전체 기동
docker compose \
-f docker-compose.dev.yml \
-f docker-compose.monitoring.yml \
up -d
# backend만 중지
docker compose \
-f docker-compose.dev.yml \
-f docker-compose.monitoring.yml \
stop backend
# backend 단독 실행 (단일 프로세스)
docker compose \
-f docker-compose.dev.yml \
-f docker-compose.monitoring.yml \
run --service-ports -d \
-e CLUSTER_MODE=false \
backend
# 테스트 하기위해서 실행시킨다.
docker compose -f docker-compose.dev.yml -f docker-compose.monitoring.yml run --rm k6
# 나는 nginx를 우회시키기 위해 다음 처럼 함
docker compose -f docker-compose.dev.yml -f docker-compose.monitoring.yml run --rm -e BASE_URL=http://host.docker.internal:8080 k6
# 다음 테스트를 위해서 influx DB에 들어가 있는 데이터삭제
docker exec video-creator-influxdb influx -execute 'DROP DATABASE k6; CREATE DATABASE k6'
# Prometheus 데이터 초기화
docker exec video-creator-prometheus sh -c 'rm -rf /prometheus/*' && docker restart video-creator-prometheus
# InfluxDB k6 데이터 초기화
docker exec video-creator-influxdb influx -execute 'DROP DATABASE k6; CREATE
DATABASE k6'
테스트 할 대상
여기서 파보나치 40 에 대해서 1초 2초 가량 예상되는 계산에 대해 진행해보겠다
따라서 목표는 1초에서 2초내외로 성능을 유지할 수 있는지를 확인하는것인데
┌─────┬──────────────────┬────────────┐
│ n │ 호출 횟수 (대략) │ 예상 시간 │
├─────┼──────────────────┼────────────┤
│ 30 │ 약 100만 │ ~10ms │
├─────┼──────────────────┼────────────┤
│ 40 │ 약 10억 │ ~1~2초 │
├─────┼──────────────────┼────────────┤
│ 45 │ 약 300억 │ ~30초 │
├─────┼──────────────────┼────────────┤
│ 50 │ 약 1조 │ ~10분 이상 │
└─────┴──────────────────┴────────────┘
싱글 코어 테스트
✔ Container video-creator-influxdb Running 0.0s
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: /scripts/load-test.js
output: InfluxDBv1 (http://influxdb:8086)
scenarios: (100.00%) 1 scenario, 12 max VUs, 4m0s max duration (incl. graceful stop):
* default: Up to 12 looping VUs for 3m30s over 5 stages (gracefulRampDown: 30s, gracefulStop: 30s)
INFO[0210]
========== Test Summary ========== source=console
INFO[0210] Total Requests: 384 source=console
INFO[0210] Failed Requests: 0 source=console
INFO[0210] Avg Response Time: 3506.35ms source=console
INFO[0210] p95 Response Time: 7908.03ms source=console
INFO[0210] Max Response Time: 16542.98ms source=console
INFO[0210] Requests/sec: 1.83 source=console
INFO[0210] ================================== source=console
running (3m30.4s), 00/12 VUs, 384 complete and 0 interrupted iterations
default ✓ [======================================] 00/12 VUs 3m30s

멀티 코어 테스트
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: /scripts/load-test.js
output: InfluxDBv1 (http://influxdb:8086)
scenarios: (100.00%) 1 scenario, 12 max VUs, 4m0s max duration (incl. graceful stop):
* default: Up to 12 looping VUs for 3m30s over 5 stages (gracefulRampDown: 30s, gracefulStop: 30s)
INFO[0210]
========== Test Summary ========== source=console
INFO[0210] Total Requests: 1261 source=console
INFO[0210] Failed Requests: 0 source=console
INFO[0210] Avg Response Time: 707.73ms source=console
INFO[0210] p95 Response Time: 1881.10ms source=console
INFO[0210] Max Response Time: 2256.76ms source=console
INFO[0210] Requests/sec: 6.00 source=console
INFO[0210] ================================== source=console
running (3m30.0s), 00/12 VUs, 1261 complete and 0 interrupted iterations
default ✓ [======================================] 00/12 VUs 3m30s

훨씬더 높은 부하상태에서도 평균타임을 잘 지켜낸걸 볼 수 있다.
이제 실제로 분당 1000건 정도를 발생시켜서 테스트를 해볼건데
이게 실제 서버사양은아니고 노트북사양이니까 오차가 발생할 수는 있다.
맥북프로m4 깡통으로 진행했으며, 실제 서비스 API 가 아닌 일부로 병목을 발생시키기 위해 만든
소스를 쓰기때문에 실제에서는 훨씬 빠르게 응답 해야 한다.
그리고 멀티코어라고해도 현재 테스트된 수치로 실제 서비스를 가면 박살날 수 있기때문에
늘 모니터링과 알림을 잘하고 관리하는 것도 중요하다.
이제 분당 요청 1000건을 던져서 어떻게 보장하는지 확인해보자 메모리는 도커에 할당된 메모리라 작게 잡아놔서 높게잡힌다
싱글코어에서는 다음처럼 나왔다
========================================== source=console
INFO[0090] PERFORMANCE TEST RESULTS source=console
INFO[0090] ========================================== source=console
INFO[0090] Duration: 1min | Target: 1000 req | fib(40) source=console
INFO[0090] ------------------------------------------ source=console
INFO[0090] Total Time: 90.00s source=console
INFO[0090] Total Requests: 207 source=console
INFO[0090] Avg Response: 28923ms source=console
INFO[0090] Min Response: 536ms source=console
INFO[0090] Max Response: 60006ms source=console
INFO[0090] P95 Response: 60005ms source=console
INFO[0090] Throughput: 2.30 req/s source=console
INFO[0090] ========================================== source=console
running (1m30.0s), 000/100 VUs, 207 complete and 4 interrupted iterations
constant_load ✓ [======================================] 004/100 VUs 1m0s 16.67 iters/s

자원사용량은 다음과 같다.

(극단적이긴한데 실제로는 45% 정도 잡힌다.)
멀티코어에서는 다음처럼 나왔다
/ \ | |\ \ | (‾) |
/ __ \ || \\ _____/ .io
execution: local
script: /scripts/load-test.js
output: InfluxDBv1 (http://influxdb:8086)
scenarios: (100.00%) 1 scenario, 100 max VUs, 1m30s max duration (incl. graceful stop):
* constant_load: 16.67 iterations/s for 1m0s (maxVUs: 50-100, gracefulStop: 30s)
WARN[0052] Insufficient VUs, reached 100 active VUs and cannot initialize more executor=constant-arrival-rate scenario=constant_load
INFO[0072]
========================================== source=console
INFO[0072] PERFORMANCE TEST RESULTS source=console
INFO[0072] ========================================== source=console
INFO[0072] Duration: 1min | Target: 1000 req | fib(40) source=console
INFO[0072] ------------------------------------------ source=console
INFO[0072] Total Time: 72.01s source=console
INFO[0072] Total Requests: 927 source=console
INFO[0072] Avg Response: 4460ms source=console
INFO[0072] Min Response: 550ms source=console
INFO[0072] Max Response: 24559ms source=console
INFO[0072] P95 Response: 10222ms source=console
INFO[0072] Throughput: 12.87 req/s source=console
INFO[0072] ========================================== source=console
running (1m12.0s), 000/100 VUs, 927 complete and 0 interrupted iterations
constant_load ✓ [======================================] 000/100 VUs 1m0s 16.67 iters/s


분당 1,000건 부하 테스트 총정리
배경
테스트 목적
- 분당 1,000건 요청을 실제로 발생시킨 상황에서
시스템이 얼마나 많은 요청을 성공적으로 처리했는지 검증
테스트 환경
- 노트북 로컬 환경
- 실제 서버 사양 아님
- 절대 성능 수치보다 상대 비교 목적
- CPU-bound 연산:
fib(40) - 동일한 k6 시나리오:
constant-arrival-rate
핵심 전제
- 요청은 분당 1,000건 실제로 투입
- 결과 지표(
Total Requests,Throughput)는
성공적으로 처리 완료된 요청 수를 의미
핵심 질문
- 분당 1,000건 요청 중 실제 성공 처리량은 얼마인가?
- 싱글코어와 멀티코어 간 처리 성공률 차이는 어느 정도인가?
논의 요약
1. 테스트 결과 요약 (요청 대비 성공 기준)
| 항목 | 싱글코어 | 멀티코어 |
|---|---|---|
| 투입 요청 | 1,000 req/min | 1,000 req/min |
| 성공 처리 수 | 207 | 927 |
| 성공 처리 비율 | 20.7% | 92.7% |
| 처리량 (req/s) | 2.30 | 12.87 |
| 평균 응답 시간 | 28,923ms | 4,460ms |
| P95 응답 시간 | 60,005ms | 10,222ms |
2. 싱글코어 해석
- 요청의 약 80% 이상이 처리되지 못함
- 원인
fib(40)으로 인한 CPU 100% 점유- 이벤트 루프 블로킹 → 요청 큐 적체
- 의미
- 싱글코어 구조에서는
분당 1,000건 부하 자체를 감당 불가 - 응답 시간 또한 사실상 타임아웃 수준
- 싱글코어 구조에서는
3. 멀티코어 해석
- 요청의 약 93% 처리 성공
- CPU 연산이 워커 단위로 분산되며:
- 요청 큐 적체 감소
- 평균 및 P95 응답 시간 대폭 개선
Insufficient VUs경고 의미- 서버 성능 한계가 아니라
- 부하 발생기(k6)가 요청을 더 이상 투입하지 못한 상태
- 의미
- 실제 서버 환경에서는
100% 처리에 더 근접할 가능성 높음
- 실제 서버 환경에서는
4. 성능 차이의 본질
- 본 테스트가 보여주는 핵심은:
- “서버가 얼마나 빠른가”가 아니라
- “던진 요청 중 몇 건을 실제로 살아서 처리했는가”
- 멀티코어 적용 효과
- 성공 처리량: 약 4.5배 증가
- 처리율(TPS): 약 5~6배 증가
- 지연 시간: 약 6배 개선
결정 사항
- 처리량 지표는 반드시 성공 처리 기준으로 해석
- CPU-bound 작업 환경에서:
- 싱글코어 구조는 실사용 불가
- 멀티코어 구조는 분당 1,000건 부하를 현실적으로 소화 가능
- 본 테스트는:
- 멀티프로세스/멀티코어 아키텍처의 필요성을 정량적으로 입증
진짜 문제
즉, 멀티코어를 활용하게 함으로써 자원을 최대한 쓸 수 있게 만드는게 목적인건데
방금처럼 잘못된 방법으로 쓰게 되면 다음 처럼 문제가 발생한다.
보는것처럼 단일에서는 실제로 자원은 많은데 처리에 해당 자원을 안쓰고
가장 문제는 인프라 관리자가 걸어둔 적정 수준의 사용률까지 올라오지 않아
AWS의 ECS 같은 자동확장에 걸리지 않고, 서비스는 응답을 못치고 있는데
인프라는 확장되지 않는 말그대로 박살난 상태가 되버리기 때문에 문제가 되는 것이다.
내가 담당했던 프로젝트중에 나는 이미 예상치를 오버해서 인프라스팩을 잡아놨고
문제가 있으면 안되는 수준인데, 서버 자원을 40%도 쓰지 못하는데 응답이 죽기 시작해
장애가 난 경험이 있다.
이처럼 멀티코어 같은 자원을 극한까지 사용하는 최적화 기법은 백엔드 개발자든
뭔 개발자든 반드시 필요한 영역이라는걸 또한번 느낄 수 있다.
'JS' 카테고리의 다른 글
| Node.js 에서 멀티코어를 활용하자 - 3 - 기본 Cluster (0) | 2026.02.02 |
|---|---|
| Node.js 에서 멀티코어를 활용하자 - 실무에서 쓰자 개념 (0) | 2026.02.02 |
| Node.js 에서 멀티코어를 활용하자 - 개념 (0) | 2026.02.02 |
| TDZ (Temporal Dead Zone) (0) | 2026.01.19 |
| npm 호이스팅과 js 호이스팅 (1) | 2026.01.19 |