TT Lab
시작하기
배우기 러닝패스 코스

부하 테스트

부하 사다리와 포화 지점

TT Lab 에서 이어서 보기

목표

동시성을 사다리처럼 올려 가며 처리량과 지연의 관계를 측정하고, 포화 지점과 SLA 를 지키는 최대 지점을 숫자로 찾습니다. 그리고 리틀의 법칙으로 그 측정 자체가 유효했는지 검증합니다. 산출물은 /root/lt2/ 아래에 둡니다.

왜 중요한가

부하 테스트에서 가장 조용한 실패는 "부하 생성기가 목표 부하를 내지 못한 것"입니다. 응답 하나가 2초를 잡아먹으면 동기식 생성기는 그 2초 동안 보냈어야 할 요청을 보내지 않습니다. 시스템이 가장 느렸던 구간의 샘플이 통째로 사라지고, 생성기는 자신이 만들어 낸 백프레셔에 협조해 버립니다. 그 결과 부하를 올려도 p99 가 움직이지 않고, 프로덕션에서만 꼬리가 나쁩니다. 그래서 모든 실행에 한 줄짜리 수용 검사를 붙입니다. 보고된 실제 처리량이 설정한 목표 처리량과 일치하는가. 일치하지 않으면 그 실행의 지연 분포는 신뢰할 수 없습니다. 이 실습의 6·7단계가 정확히 그 검사입니다.

준비 — 부하 대상 띄우기

이 실습은 우분투 24.04 VM 에서 돌고 도커도 함께 깔려 있습니다. 그런데도 대상을 컨테이너가 아니라 파이썬 프로세스로 띄웁니다. 못 해서가 아니라 그쪽이 맞기 때문입니다.

부하 대상은 아무거나 띄우면 안 됩니다. 요청 하나에 드는 비용이 뚜렷한 대상이어야 사다리가 사다리답게 보이고, 6단계의 리틀의 법칙 검산(rps × 평균 지연 ≈ 동시성)도 성립합니다. nginx 로 정적 파일만 돌려주면 한 건이 1ms 도 걸리지 않아서, 재는 것이 서버가 아니라 부하 생성기 자신이 됩니다 — 사다리를 올려도 무릎이 나타나지 않고, 나타나더라도 그것은 대상의 한계가 아니라 hey 의 한계입니다.

아래 대상은 요청 하나에 50ms 를 쓰도록 만들어 두었습니다. 그 50ms 가 있어야 동시성을 올릴 때 처리량이 오르다 멎는 지점이 눈에 보입니다.

mkdir -p /root/lt2
cat > /root/lt2/target.py <<'PY'
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
BODY = 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): pass
ThreadingHTTPServer(("127.0.0.1", 8085), H).serve_forever()
PY
setsid 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 입니다.

참고

부하 사다리 계획서 쓰기

/root/lt2/plan.md 에 사다리 계획을 씁니다. 반드시 포함할 것: 동시성 단계 1, 2, 5, 10, 20 다섯 개, 각 단계의 지속 시간, 워밍업 계획, 그리고 성공 기준(예: p95 임계값).

/root/lt2/plan.md 에 다섯 단계의 동시성, 각 단계의 지속 시간, 워밍업, 성공 기준을 적습니다. 무엇을 보면 통과인지 미리 정하지 않으면 결과를 보고 기준을 만들게 됩니다.

실행 스크립트 작성

/root/lt2/ramp.sh 를 만들고 chmod +x 합니다. 출력 디렉터리를 첫 인자($1)로 받아 동시성 1, 2, 5, 10, 20 을 차례로 실행하고, 결과를 $1/c<동시성>.txt(예: c10.txt) 로 저장해야 합니다.

/root/lt2/ramp.sh 는 출력 디렉터리를 첫 인자로 받아 다섯 단계를 차례로 실행하고 결과를 c<동시성>.txt 로 저장합니다. 경로를 하드코딩하면 다른 디렉터리로 다시 돌릴 수 없습니다. chmod +x 를 잊지 마세요.

다섯 단계 실행

스크립트를 /root/lt2/out 에 대해 실행합니다. /root/lt2/out/c1.txt, c2.txt, c5.txt, c10.txt, c20.txt 다섯 개가 만들어져야 하고, 각 파일의 총 응답이 100건 이상이며 Requests/sec 와 95% in 줄이 남아 있어야 합니다.

/root/lt2/out 아래에 c1 c2 c5 c10 c20 결과가 모두 있어야 하고 각 단계는 100건 이상이어야 합니다. 각 파일에 요약과 지연 분포 절이 그대로 남아 있어야 다음 단계에서 값을 읽을 수 있습니다.

결과표 만들기

/root/lt2/ramp.csv 를 만듭니다. 형식은 concurrency,rps,p95,errors 이고 동시성 1/2/5/10/20 다섯 행을 씁니다. rps 와 p95 는 해당 cN.txt 에서 읽은 값, errors 는 상태 코드 분포에서 2xx 가 아닌 응답 수(정수, 정확히 일치해야 함)입니다.

/root/lt2/ramp.csv 에 동시성별 rps, p95, errors 를 정리합니다. 값은 원본 출력에서 읽은 것이어야 하고, errors 는 2xx 가 아닌 응답 수입니다.

포화 지점 찾기

/root/lt2/knee.txt 에 knee_concurrency= 한 줄을 씁니다. 판정 규칙은 다음과 같습니다. 동시성을 1 → 2 → 5 → 10 → 20 순서로 보면서, 직전 단계 대비 RPS 증가율이 10% 미만이 되는 첫 지점을 찾고 그 직전 동시성을 답으로 씁니다. 끝까지 10% 이상 늘었다면 답은 20 입니다.

/root/lt2/knee.txt 에 knee_concurrency= 한 줄을 씁니다. 처리량이 더 이상 의미 있게 늘지 않기 시작하는 지점의 직전 동시성이 답이고, 판정 규칙은 지시문에 정확히 적혀 있습니다.

리틀의 법칙으로 실행 검증

/root/lt2/little.txt 에 네 줄을 씁니다: concurrency=10, rps=(out/c10.txt 의 Requests/sec), latency_avg_s=(같은 파일의 Average), product=(rps × 평균 지연, 소수 둘째 자리). 이 곱이 동시성 10 과 크게 다르면 그 실행은 목표 부하를 내지 못한 것입니다.

/root/lt2/little.txt 에 동시성 10 구간의 rps, 평균 지연, 그 곱을 적습니다. 곱이 동시성과 크게 다르면 부하 생성기가 목표 부하를 실제로 내지 못한 것입니다.

개방/폐쇄 모델 비교

도착률을 고정한(open) 실행을 하나 더 돌려 /root/lt2/open.txt 에 저장합니다. 그리고 /root/lt2/model.md 에 다음을 씁니다: closed(고정 동시성) 모델 설명, open(고정 도착률) 모델 설명, coordinated omission 설명, 그리고 두 줄 target_rps=(설정한 목표 처리량)와 actual_rps=(open.txt 에서 읽은 실제 처리량).

도착률을 고정한 실행을 따로 돌려 /root/lt2/open.txt 에 저장하고, model.md 에 두 모델의 차이와 coordinated omission 을 설명합니다. 목표 처리량과 실제 처리량을 나란히 적어 비교하세요.

SLA 를 지키는 최대 지점 산출

/root/lt2/max-safe.txt 에 두 줄을 씁니다: max_safe_concurrency=, max_safe_rps=. ramp.csv 에서 p95 가 0.2초 이하이고 errors 가 0인 행 중 가장 큰 동시성과 그때의 RPS 입니다.

/root/lt2/max-safe.txt 에 조건을 만족하는 가장 큰 동시성과 그때의 처리량을 적습니다. 판정 조건은 지시문에 있는 두 가지이고, 표에서 직접 골라야 합니다.