이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 부하 테스트를 단순 RPS 측정이 아니라 실패 방식 학습으로 설계하려면 무엇이 필요한가?
- k6/wrk 결과와 서버 metric, trace, 로그를 어떻게 같은 실험으로 묶는가?
- 부하 테스트가 오히려 잘못된 결론을 내게 만드는 함정은 무엇인가?
개요
부하 테스트는 시스템을 망가뜨리기 위한 놀이가 아니라, 어디서부터 SLO가 깨지고 어떤 자원이 먼저 포화되는지 배우는 실험이다. 좋은 부하 테스트는 실제 traffic mix, auth, payload, cache 상태, think time, ramp-up을 반영하고 서버뿐 아니라 client generator의 한계도 확인한다.
- open-loop와 closed-loop 부하 모델을 구분한다.
- 단일 endpoint보다 실제 사용자 journey 비율을 반영한다.
- cache warm/cold, 로그인 상태, payload 크기, 지역 latency를 고려한다.
- 결과는 평균이 아니라 percentiles, error, saturation, cost로 해석한다.
원리
부하 테스트 계획은 다음 질문으로 시작한다.
목표: 어떤 SLO를 검증하는가?
범위: 어떤 endpoint와 dependency를 포함하는가?
패턴: ramp-up, spike, soak 중 무엇인가?
관측: 어떤 metric과 log를 저장하는가?
중단: 어떤 조건에서 테스트를 멈추는가?
복구: 테스트 데이터와 cache를 어떻게 정리하는가?테스트 전에 이 질문에 답하지 않으면 RPS 숫자는 나오지만 운영 판단은 남지 않는다.
테스트 종류
| 종류 | 목적 | 주의 |
|---|---|---|
| Smoke | 배포 후 기본 동작 확인 | 부하가 아니라 기능 확인 |
| Load | 예상 traffic에서 SLO 확인 | 실제 mix 반영 |
| Stress | 한계점 찾기 | 중단 조건 필수 |
| Spike | 급격한 증가 대응 | autoscaling, queue 관찰 |
| Soak | 장시간 안정성 | memory leak, connection leak |
| Failover | 일부 dependency 장애 | fallback/backpressure 확인 |
k6 예시
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
scenarios: {
browse_and_order: {
executor: "ramping-arrival-rate",
startRate: 20,
timeUnit: "1s",
preAllocatedVUs: 200,
stages: [
{ duration: "2m", target: 100 },
{ duration: "5m", target: 300 },
{ duration: "2m", target: 0 }
]
}
},
thresholds: {
http_req_failed: ["rate<0.01"],
"http_req_duration{type:read}": ["p(95)<300"],
"http_req_duration{type:write}": ["p(95)<800"]
}
};
export default function () {
const read = http.get("https://api.example.com/products", { tags: { type: "read" } });
check(read, { "read 200": r => r.status === 200 });
const write = http.post("https://api.example.com/orders", JSON.stringify({ productId: 1 }), {
headers: { "Content-Type": "application/json", "Idempotency-Key": `${__VU}-${__ITER}` },
tags: { type: "write" }
});
check(write, { "write accepted": r => [200, 201, 202, 429].includes(r.status) });
sleep(Math.random() * 3);
}관측 체크리스트
테스트와 같은 시간대에 다음을 저장한다.
- LB 5xx, target 5xx, target response time
- app p95/p99 latency, error rate, request count
- DB pool active/pending, slow query, lock wait
- HTTP client pool pending, external API latency/quota
- cache hit ratio, hot key, eviction
- CPU, memory, GC, file descriptor, socket state
- queue lag, consumer 처리량
- retry rate, 429, circuit breaker open
코드와 명령으로 확인하기
테스트 client가 병목인지 확인한다.
sar -n DEV 1
top
ulimit -n간단한 wrk 테스트는 빠른 smoke에 유용하다.
wrk -t4 -c200 -d60s --latency https://api.example.com/products결과는 서버 metric과 합쳐야 의미가 있다. wrk p99가 높아졌는데 서버 metric은 멀쩡하면 client나 network path를 의심한다.
인프라 협업 포인트
- 운영과 같은 LB/CDN/WAF를 통과할지, 내부에서 app만 칠지 목적별로 정한다.
- 테스트 트래픽이 실제 사용자와 섞이지 않도록 tenant, API key, IP, header를 구분한다.
- 인프라 팀과 테스트 시간, 최대 RPS, 중단 조건, rollback/차단 방법을 공유한다.
- cloud 비용, external API 비용, rate limit 위반, 데이터 오염 가능성을 사전에 확인한다.
실전 팁
- 처음부터 최대 부하를 걸지 말고 ramp-up으로 saturation point를 찾는다.
- cache warm 상태와 cold 상태를 둘 다 테스트한다.
- 부하 테스트는 성공 결과보다 실패 직전 metric snapshot이 더 값지다.
- 테스트 데이터는 idempotency key와 cleanup 절차를 둔다.
- 개인 프로젝트는 k6 smoke + wrk 단기 테스트 + 간단한 Grafana dashboard만 있어도 큰 학습 효과가 있다.
위험 신호!
- localhost에서 단일 endpoint만 테스트하고 운영 capacity를 추정한다.
- 테스트 client의 CPU/network 한계를 확인하지 않는다.
- error를 무시하고 latency만 본다.
- 테스트 후 DB/cache/queue에 남은 데이터를 정리하지 않는다.
확인 질문
확인 질문
- 부하 테스트에서 traffic mix가 중요한 이유는 무엇인가?
- 읽기/쓰기, payload, auth, cache hit, external API fan-out이 다르면 병목 위치가 완전히 달라지기 때문이다.
- ramp-up 테스트가 유용한 이유는 무엇인가?
- 어느 지점에서 latency, error, saturation이 급격히 나빠지는지 찾을 수 있기 때문이다.
- 테스트 client 병목을 확인해야 하는 이유는 무엇인가?
- 부하 생성기가 먼저 한계에 걸리면 서버의 실제 capacity보다 낮은 결과를 얻게 되기 때문이다.