LabHub

부하 테스트 · 테스트 설계와 워밍업 · 실습

부하 사다리와 포화 지점

LabHub 에서 이어서 보기

목표

동시성을 사다리처럼 올려 가며 처리량과 지연의 관계를 측정하고, 포화 지점과 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/sec95% in 줄이 남아 있어야 합니다.
4. /root/lt2/ramp.csv 를 만듭니다. 형식은 concurrency,rps,p95,errors 이고 동시성 1/2/5/10/20 다섯 행을 씁니다. rpsp95 는 해당 cN.txt 에서 읽은 값, errors 는 상태 코드 분포에서 2xx 가 아닌 응답 수(정수, 정확히 일치해야 함)입니다.
5. /root/lt2/knee.txtknee_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 입니다.

참고

단계 8개

  1. 부하 사다리 계획서 쓰기
  2. 실행 스크립트 작성
  3. 다섯 단계 실행
  4. 결과표 만들기
  5. 포화 지점 찾기
  6. 리틀의 법칙으로 실행 검증
  7. 개방/폐쇄 모델 비교
  8. SLA 를 지키는 최대 지점 산출