부하 테스트 · 테스트 설계와 워밍업 · 실습
부하 사다리와 포화 지점
목표
동시성을 사다리처럼 올려 가며 처리량과 지연의 관계를 측정하고, 포화 지점과 SLA 를 지키는 최대 지점을 숫자로 찾습니다. 그리고 리틀의 법칙으로 그 측정 자체가 유효했는지 검증합니다. 산출물은 /root/lt2/ 아래에 둡니다.
왜 중요한가
부하 테스트에서 가장 조용한 실패는 "부하 생성기가 목표 부하를 내지 못한 것"입니다. 응답 하나가 2초를 잡아먹으면 동기식 생성기는 그 2초 동안 보냈어야 할 요청을 보내지 않습니다. 시스템이 가장 느렸던 구간의 샘플이 통째로 사라지고, 생성기는 자신이 만들어 낸 백프레셔에 협조해 버립니다. 그 결과 부하를 올려도 p99 가 움직이지 않고, 프로덕션에서만 꼬리가 나쁩니다. 그래서 모든 실행에 한 줄짜리 수용 검사를 붙입니다. 보고된 실제 처리량이 설정한 목표 처리량과 일치하는가. 일치하지 않으면 그 실행의 지연 분포는 신뢰할 수 없습니다. 이 실습의 6·7단계가 정확히 그 검사입니다.
준비 — 부하 대상 띄우기
이 실습은 우분투 24.04 VM 에서 돌고 도커도 함께 깔려 있습니다. 그런데도 대상을 컨테이너가 아니라 파이썬 프로세스로 띄웁니다. 못 해서가 아니라 그쪽이 맞기 때문입니다.
부하 대상은 아무거나 띄우면 안 됩니다. 요청 하나에 드는 비용이 뚜렷한 대상이어야 사다리가 사다리답게 보이고, 6단계의 리틀의 법칙 검산(rps × 평균 지연 ≈ 동시성)도 성립합니다. nginx 로 정적 파일만 돌려주면 한 건이 1ms 도 걸리지 않아서, 재는 것이 서버가 아니라 부하 생성기 자신이 됩니다 — 사다리를 올려도 무릎이 나타나지 않고, 나타나더라도 그것은 대상의 한계가 아니라 hey 의 한계입니다.
아래 대상은 요청 하나에 50ms 를 쓰도록 만들어 두었습니다. 그 50ms 가 있어야 동시성을 올릴 때 처리량이 오르다 멎는 지점이 눈에 보입니다.
mkdir -p /root/lt2cat > /root/lt2/target.py <<'PY'import timefrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServerBODY = b"labhub load-test target"class H(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" disable_nagle_algorithm = True # 헤더와 본문이 따로 나가면 지연 ACK 로 40ms 가 얹힌다 def do_GET(self): time.sleep(0.05) # 요청 하나의 처리 비용 self.send_response(200); self.send_header("Content-Length", str(len(BODY))) self.end_headers(); self.wfile.write(BODY) def log_message(self, *a): passThreadingHTTPServer(("127.0.0.1", 8085), H).serve_forever()PYsetsid nohup python3 /root/lt2/target.py >/dev/null 2>&1 &curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8085/ # 200굳이 컨테이너로 옮기고 싶다면 docker run -d --name lt-web -p 127.0.0.1:8085:8080 -v /root/lt2:/app python:3.12-alpine python /app/target.py 처럼 같은 파이썬 대상을 담아야 합니다. 위 문단의 이유로, nginx 로 바꾸면 이후 단계의 숫자가 전부 의미를 잃습니다.
단계
1. /root/lt2/plan.md 에 사다리 계획을 씁니다. 반드시 포함할 것: 동시성 단계 1, 2, 5, 10, 20 다섯 개, 각 단계의 지속 시간, 워밍업 계획, 그리고 성공 기준(예: p95 임계값).
2. /root/lt2/ramp.sh 를 만들고 chmod +x 합니다. 출력 디렉터리를 첫 인자($1)로 받아 동시성 1, 2, 5, 10, 20 을 차례로 실행하고, 결과를 $1/c<동시성>.txt(예: c10.txt) 로 저장해야 합니다.
3. 스크립트를 /root/lt2/out 에 대해 실행합니다. /root/lt2/out/c1.txt, c2.txt, c5.txt, c10.txt, c20.txt 다섯 개가 만들어져야 하고, 각 파일의 총 응답이 100건 이상이며 Requests/sec 와 95% in 줄이 남아 있어야 합니다.
4. /root/lt2/ramp.csv 를 만듭니다. 형식은 concurrency,rps,p95,errors 이고 동시성 1/2/5/10/20 다섯 행을 씁니다. rps 와 p95 는 해당 cN.txt 에서 읽은 값, errors 는 상태 코드 분포에서 2xx 가 아닌 응답 수(정수, 정확히 일치해야 함)입니다.
5. /root/lt2/knee.txt 에 knee_concurrency= 한 줄을 씁니다. 판정 규칙은 다음과 같습니다. 동시성을 1 → 2 → 5 → 10 → 20 순서로 보면서, 직전 단계 대비 RPS 증가율이 10% 미만이 되는 첫 지점을 찾고 그 직전 동시성을 답으로 씁니다. 끝까지 10% 이상 늘었다면 답은 20 입니다.
6. /root/lt2/little.txt 에 네 줄을 씁니다: concurrency=10, rps=(out/c10.txt 의 Requests/sec), latency_avg_s=(같은 파일의 Average), product=(rps × 평균 지연, 소수 둘째 자리). 이 곱이 동시성 10 과 크게 다르면 그 실행은 목표 부하를 내지 못한 것입니다.
7. 도착률을 고정한(open) 실행을 하나 더 돌려 /root/lt2/open.txt 에 저장합니다. 그리고 /root/lt2/model.md 에 다음을 씁니다: closed(고정 동시성) 모델 설명, open(고정 도착률) 모델 설명, coordinated omission 설명, 그리고 두 줄 target_rps=(설정한 목표 처리량)와 actual_rps=(open.txt 에서 읽은 실제 처리량).
8. /root/lt2/max-safe.txt 에 두 줄을 씁니다: max_safe_concurrency=, max_safe_rps=. ramp.csv 에서 p95 가 0.2초 이하이고 errors 가 0인 행 중 가장 큰 동시성과 그때의 RPS 입니다.
참고
- open 모델 근사:
hey의-q는 워커당 초당 요청 수를 제한합니다.hey -z 30s -c 50 -q 4 http://127.0.0.1:8085/는 목표 200 RPS(50 × 4)를 의미하므로target_rps=200으로 적고 실제 값과 비교하면 됩니다. - 스크립트 뼈대:
for c in 1 2 5 10 20; do hey -n 300 -c "$c" http://127.0.0.1:8085/ > "$1/c$c.txt" 2>&1; done. 앞에mkdir -p "$1"을 넣으세요. errors는awk '/Status code distribution/{f=1;next} f && /responses/ {gsub(/[][]/," "); if($1<200||$1>=300) e+=$2} END{print e+0}'로 셉니다. 에러가 없으면0을 적습니다.- 흔한 실수 1:
ramp.sh안에 출력 경로를 하드코딩하는 것. 첫 인자를 쓰지 않으면 채점에서 걸립니다. - 흔한 실수 2: 5단계에서 "가장 RPS 가 높은 동시성"을 무릎으로 적는 것. 무릎은 최고점이 아니라 증가가 멎기 시작하는 지점의 직전입니다.
- 흔한 실수 3: 각 단계를 너무 짧게 돌리는 것. 100건 미만이면 분위수가 의미를 갖지 못합니다.
단계 8개
- 부하 사다리 계획서 쓰기
- 실행 스크립트 작성
- 다섯 단계 실행
- 결과표 만들기
- 포화 지점 찾기
- 리틀의 법칙으로 실행 검증
- 개방/폐쇄 모델 비교
- SLA 를 지키는 최대 지점 산출