개요
부하 테스트의 목적은 목표 부하에서 SLO(서비스 수준 목표)를 지키며 얼마나 안정적·효율적으로 운영할 수 있는지를 수치로 확인하고, 그 근거로 용량·설정·코드를 결정하는 데 있다.
사용 툴
- K6: 부하 테스트 진행 (v1.2.3)
- InfluxDB: 부하 테스트 결과 데이터 저장할 시계열 DB (v1.11)
- Grafana: 부하테스트 결과를 대시보드 형태로 시각화 (v11.6.1)
- Jaeger: 트레이스 시각화를 통해 병목 구간 판단
아키텍처 개요
- 개발자가 작성한 시나리오로 K6가 HTTP 트래픽을 생성하여 부하 발생
- 각 요청에 대한 메트릭 데이터를 InfluxDB에 저장
- Grafana가 InfluxDB에 쿼리를 날려 실시간 대시보드 표시(p95, 에러율, RPS 등 주요 지표 시각화)
- Jaeger는 Spirng Boot에서 발생한 스팬을 수집하여 Jaeger UI에 시각화
부하 테스트 준비
1. 툴 설치 및 연결
2. 테스트 시나리오 설정
부하 테스트는 정의된 시나리오가 있어야 결과가 재현·해석·의사결정이 가능하다.
간단하게 테스트 시나리오 종류와 예시 스크립트를 확인해보자
기본 부하 테스트
- 목적: 정상 응답 및 안정된 응답 시간 유지
export const options = {
scenarios: {
/* =========================
* 기본 부하 테스트 (Steady-state)
* - 일정 TPS로 5분 유지, 안정 구간 성능(SLI) 확인
* - preAllocatedVUs ≈ rate × p95(sec) × 3 (대략치)
* ========================= */
basic_load: {
executor: 'constant-arrival-rate',
rate: 60, // 초당 60TPS
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 40,
maxVUs: 120,
},
},
};
점진적 부하 증가 테스트
- 목적: 사용자 증가 시 시스템 대응 능력 확인
export let options = {
scenarios: {
// 점진적 부하 증가 테스트
ramping_load: {
executor: 'ramping-arrival-rate',
timeUnit: '1s', // 요청이 만들어지는 기본 단위
startRate: '0', // 시작 TPS
preAllocatedVUs: 20,
maxVUs: 200,
stages: [
// 워밍업 (20TPS): 낮은 TPS에서 시작해서 천천히 올림 - 예열 단계
{duration: '30s', target: 20},
// 램프업 (100TPS): 목표 TPS까지 점진적으로 증가시키면서 시스템 확장성 확인
{duration: '2m', target: 100},
// 소크 (100TPS): 일정 TPS를 유지하면서 시스템 안정성 확인
{duration: '1m30s',target: 100},
// 램프다운 (0TPS): 부하를 점진적으로 감소시켜 리소스 정리 과정 확인
{duration: '1m', target: 0},
]
},
},
};
스트레스 테스트
- 목적: 시스템 최대 처리 한계 및 장애 점검
export const options = {
scenarios: {
/* =========================
* 스트레스 테스트 (Stress)
* - 목표를 서서히 초과하며 한계점/붕괴지점 탐색 → 회복 구간 확인
* - 총 6분
* ========================= */
stress_test: {
executor: 'ramping-arrival-rate',
timeUnit: '1s',
startRate: 0,
preAllocatedVUs: 80,
maxVUs: 400,
stages: [
{ duration: '1m', target: 50 }, // 워밍업
{ duration: '1m', target: 150 }, // 정상 상한 근처
{ duration: '1m', target: 250 }, // 스트레스 시작
{ duration: '1m', target: 350 }, // 한계 넘김(붕괴 관찰)
{ duration: '1m', target: 200 }, // 완화
{ duration: '1m', target: 100 }, // 회복 관찰
],
},
},
};
스파이크 테스트
- 목적: 갑작스러운 트래픽 증가 시 시스템 안정성 확인
export const options = {
scenarios: {
/* =========================
* 스파이크 테스트 (Spike)
* - 짧은 시간 급증/급락에 대한 탄력성 확인
* - keep-alive/TLS/풀/큐 즉시 대응 확인
* - 총 ~2.5분
* ========================= */
spike_test: {
executor: 'ramping-arrival-rate',
timeUnit: '1s',
startRate: 10, // 베이스라인
preAllocatedVUs: 60,
maxVUs: 400,
stages: [
{ duration: '30s', target: 10 }, // 안정 구간
{ duration: '10s', target: 250 }, // 급상승(스파이크)
{ duration: '20s', target: 10 }, // 급락
{ duration: '10s', target: 250 }, // 2차 스파이크
{ duration: '50s', target: 50 }, // 상향 안정화
{ duration: '30s', target: 0 }, // 램프다운
],
},
},
};
본 테스트에서는 점진적 부하 시나리오로 진행했고, 진행 순서는 다음과 같다.
- 워밍업 단계 (30초, 0 → 20TPS)
- 테스트 시작 시 요청 속도를 0에서 출발해 30초 동안 20TPS까지 올림.
- 목적: 갑작스러운 과부하를 주지 않고, 시스템/캐시/DB 커넥션 등을 예열(warm-up)하기 위함.
- 램프업 단계 (2분, 20TPS → 100TPS)
- 2분 동안 요청 속도를 점진적으로 100TPS까지 끌어올림.
- 목적: 서비스가 점차 늘어나는 트래픽에 맞춰 확장성(Scalability)을 잘 보장하는지 확인.
- 소크(Soak) 단계 (1분 30초, 100TPS 유지)
- 100TPS를 일정하게 유지하면서 시스템이 안정적으로 처리할 수 있는지 검증.
- 목적: 지속 부하 상황에서 지속 성능(throughput, latency, error율) 확인.
- 램프다운 단계 (1분, 100TPS → 0TPS)
- 1분 동안 점진적으로 요청 속도를 줄여 0TPS까지 내림.
- 목적: 트래픽 감소 시 리소스(스레드, 커넥션, 메모리)가 정상적으로 해제되는지, 리소스 정리(tear-down) 과정을 확인.
3. SLO 설정
SLO란 서비스의 성능과 안정성을 위한 내부적인 지표로 테스트 결과를 감으로 보지 않고, 정량 기준으로 성공/실패 여부를 판단하기 위해 필요하다. READ/WRITE API를 분리해서 설정하는 것이 좋다.
권장 SLO
- READ: p95 ≤ 300ms , p99 ≤ 600ms, 1s 초과 비율 < 0.1%, 실패율 < 0.0.1%
- WRITE: p95 ≤ 500ms, p99 ≤ 900ms, 1.5s 초과 비율 < 0.2%, 실패율 < 0.0.1%
thresholds: {
// 레이턴시
'http_req_duration': [
'p(95)<500', // p95 ≤ 500ms
'p(99)<900', // p99 ≤ 900ms
'rate<=0.002@1.5s', // 1.5s 초과 비율 < 0.2%
],
// 실패율 (0.1% 미만)
'http_req_failed': ['rate<0.001'],
},
- 평가 기준: k6 지표(p95/p99, 에러율), InfluxDB/Grafana 대시보드, Jaeger 트레이스(병목 구간) 종합 판단
4. 대용량 데이터 적재
- 인덱스, 캐시, 버퍼 등 성능 변화 및 병목 구간 확인을 위해 10만~100만 건 데이터 적재
- 주요 테이블: transaction, member, card 등
- 데이터 생성: 프로시저 또는 Mockaroo 활용
- 예시: 사용자 10만 명, 카드 20만 개, 거래내역 50만 건
5. K6 스크립트 작성
자세한 내용은 K6 공식 문서 가 잘 작성되어 있기 때문에 읽어보자.
본 테스트에서는 점진적 부하 증가 시나리오로 테스트 진행했다.
import http from 'k6/http';
import exec from 'k6/execution';
import {group, check} from 'k6';
import {randomString} from '../js/k6-utils.js';
export let options = {
scenarios: {
// 점진적 부하 증가 테스트
ramping_load: {
executor: 'ramping-arrival-rate',
timeUnit: '1s', // 요청이 만들어지는 기본 단위
startRate: '0', // 시작 TPS
preAllocatedVUs: 20,
maxVUs: 200,
stages: [
// 워밍업 (20TPS): 낮은 TPS에서 시작해서 천천히 올림 - 예열 단계
{duration: '30s', target: 20},
// 램프업 (100TPS): 목표 TPS까지 점진적으로 증가시키면서 시스템 확장성 확인
{duration: '2m', target: 100},
// 소크 (100TPS): 일정 TPS를 유지하면서 시스템 안정성 확인
{duration: '1m30s',target: 100},
// 램프다운 (0TPS): 부하를 점진적으로 감소시켜 리소스 정리 과정 확인
{duration: '1m', target: 0},
]
},
},
thresholds: {
// 읽기 요청 SLO
'http_req_duration{endopoint:read}': [
'p(95)<300', // 95% 요청 처리 시간 ≤ 300ms
'p(99)<600', // 99% 요청 처리 시간 ≤ 600ms
'rate<=0.002@1s', // 1s 초과 비율 < 0.2%
],
// 쓰기 요청 SLO
'http_req_duration{endpoint:write}': [
'p(95)<500', // 95% 요청 처리 시간 ≤ 500ms
'p(99)<900', // 99% 요청 처리 시간 ≤ 900ms
'rate<=0.002@1.5s', // 1.5s 초과 비율 < 0.2%
],
http_req_failed: ['rate<0.001'] // 실패율 < 0.1%
},
tags: {
testId: '${serverName}-${testId}',
endpoint: 'write' // 읽기 API 테스트: 'read', 쓰기 API 테스트: 'write'
}
}
/**
* 부하 테스트가 실행되기 전에 한 번 실행되는 function.
* 테스트에 필요한 데이터를 생성하거나 외부에서 불러와 전달 가능
*/
const BASE_URL = 'http://localhost:8080';
export function setup() {
// 부하테스트에 사용할 JWT 토큰 발급
const url = BASE_URL + '/oauth/token';
const basicAuth = 'Basic {yourAuthValue}';
const params = {
headers: {
'Authorization': basicAuth,
}
}
const response = http.post(url, null, params);
const token = response.json('data.accessToken');
return token; // setup function의 리턴 값은 default 함수에서 인자로 받음
}
/**
* 테스트 시나리오에 맞춰 반복적으로 실행되는 function
*/
export default function (token) {
// 카드 발급 API 부하테스트
const url = BASE_URL + '/your/api';
const payload = JSON.stringify({
cardNo: randomString(16, '0123456789'),
trafficCardNo: randomString(16, '0123456789'),
validDt: '3512',
});
const response = http.post(url, payload, params);
check(response, {'status was 200': (r) => r.status === 200}); // 응답 status가 200인지 체크
}
/**
* 부하 테스트가 실행된 후에 한 번 실행되는 function.
*/
export function teardown(data) {}
부하 테스트 진행
1. K6 스크립트 실행
$ ./k6 run --out influxdb=http://localhost:15086/k6 scripts/script.js
2. 실시간 Dashboard 확인

주요 지표에 대한 설명은 다음과 같다.
- http_req_duration: 전체 HTTP 요청 소요 시간 (ms)
- http_req_blocked: 네트워크 스택 대기 시간(CPU, OS 포함)
- http_req_connecting: TCP 연결 설정 시간
- http_req_tls_handshaking: TLS hand shake 시간
- http_req_sending: 요청 데이터 전송 시간
- http_req_wating: 서버 응답 대기 시간 (서버 처릭 시간)
- http_req_receiving: 서버 응답 데잉터 수신 시간
3. Jaeger를 통해 문제되는 요청에 대한 트레이스 확인

개선하기
문제 상황

- 점진적 부하 테스트를 수행하던 중 특정 구간에서 요청 처리 시간이 급격히 증가하는 것을 확인할 수 있다.
원인 분석

- Jaeger를 통해 문제되는 구간의 요청 Trace를 분석했다.
- 여러 요청에서 스팬과 스팬 사이의 공백 구간이 길게 관찰되는 것을 볼 수 있다.
- 이는 애플리케이션 내부 로직 지연보다는 리소스 확보 대기 시간이 길어지고 있음을 의미한다.
가설 설정
- 트래픽이 증가하는 과정에서 커넥션 풀 사이즈가 부하 테스트 트래픽을 감당하지 못해 대기 시간이 증가했다고 판단했다.
해결 시도
- HikariCP 공식 Wiki를 참고하여 maximumPoolSize 값을 조정
pool size = Tn x (Cm - 1) + 1
- Tn: 전체 Thread 갯수
- Cm: 하나의 Task에서 동시에 필요한 Connection 수
pool size = 200 x (2 - 1) + 1 = 201
spring:
datasource:
hikari:
maximum-pool-size: 201 # 10 -> 201 변경
결과


- 동일한 조건에서 다시 부하 테스트 진행했다.
- 지표 상 요청 처리 시간이 안정화되고, Trace 상 스팬 간 공백 구간이 크게 줄어든 것을 확인할 수 있다.
참조
https://grafana.com/docs/grafana/latest/getting-started/
Get started with Grafana Open Source | Grafana documentation
Getting started with managing your metrics, logs, and traces using Grafana In this webinar, we’ll demo how to get started using the LGTM Stack: Loki for logs, Grafana for visualization, Tempo for traces, and Mimir for metrics.
grafana.com
https://docs.influxdata.com/influxdb/v2/get-started/
Get started with InfluxDB | InfluxDB OSS v2 Documentation
Thank you for your feedback! Let us know what we can do better:
docs.influxdata.com
'etc' 카테고리의 다른 글
| 소프트웨어 버전 관리 (0) | 2025.06.29 |
|---|---|
| 프로그래머스 - SQL 고득점 Kit 완료 (0) | 2025.03.02 |
| ajax - 기초 문법 (2) | 2024.11.15 |
| 티스토리 - hELLO 스킨 '카테고리의 다른 글' 2중 노출 되는 문제 해결 방법 (0) | 2024.07.18 |
| QA - Test Case를 통한 QA 테스트 (1) | 2024.07.18 |