Cloud & Infrastructure 도구

2 vCPU·4GB 서버는 어디까지 사용할 수 있을까: 추측 대신 측정하는 법

핵심 답변

2 vCPU·4GB 서버가 감당할 수 있는 방문자 수는 사양표만 보고 계산할 수 없다. 같은 월 방문자 수라도 대부분이 캐시된 공개 페이지를 보는 서비스와 로그인·검색·결제·파일 업로드를 반복하는 서비스의 부하는 전혀 다르다.

2 vCPU·4GB 서버는 어디까지 사용할 수 있을까: 추측 대신 측정하는 법 — IXC Insights 기술 일러스트

2 vCPU·4GB 서버가 감당할 수 있는 방문자 수는 사양표만 보고 계산할 수 없다. 같은 월 방문자 수라도 대부분이 캐시된 공개 페이지를 보는 서비스와 로그인·검색·결제·파일 업로드를 반복하는 서비스의 부하는 전혀 다르다.

서버 수용량은 다음과 같이 정의하는 편이 정확하다.

정해진 요청 구성에서 지연·오류·자원 압력·대기열 기준을 모두 지키며 지속적으로 처리할 수 있는 최대 부하

Google Cloud의 현재 부하 테스트 지침도 절대 최대 처리량 하나보다, 지연·처리량·자원 사용 등 여러 차원에서 허용 가능한 부하 임계점을 찾도록 안내한다. Google SRE 역시 트래픽, 지연, 오류, 포화를 함께 보도록 한다. (docs.cloud.google.com)

2 vCPU라는 표기도 동일한 연산 성능을 보장하지 않는다. 어떤 VM에서는 vCPU가 하드웨어 스레드이고 다른 상품군에서는 물리 코어에 대응할 수 있다. 공유 CPU는 같은 호스트의 다른 워크로드에 따라 사용할 수 있는 CPU 사이클이 달라질 수 있다. CPU 세대, SMT 구성, 스토리지 종류와 한도, 네트워크 처리량도 함께 기록해야 한다. (docs.digitalocean.com)

따라서 테스트 결과는 2 vCPU·4GB = 방문자 N명이 아니라 다음 형식으로 남겨야 한다.

워크로드 프로필 [버전]과 환경 [지문]에서 완료 처리량 [값][측정 구간] 동안 유지했다. 이때 경로별 P95 지연은 [값], 오류율은 [값], 최대 큐는 [값]이었고 첫 제한 신호는 [CPU·메모리·DB·I/O·네트워크·외부 연동]이었다. 예상 피크 대비 검증된 여유 용량은 [값]이며 이 결과는 [캐시 상태·데이터 크기·백그라운드 작업·Provider 조건]에서만 유효하다.

이 글에서는 빈칸을 임의의 숫자로 채우지 않는다. 실제 시스템을 측정해 채우는 방법을 다룬다.


방문자 수를 Request Mix로 바꿔야 한다

월 방문자 수는 부하의 시간 분포를 보여주지 않는다. 같은 10만 회 방문도 한 달에 고르게 분산될 수 있고 몇 분 안에 몰릴 수 있다. 한 번의 방문이 서버 요청 한 건을 의미하지도 않는다.

서버가 실제로 받는 작업은 보통 다음처럼 섞여 있다.

  • CDN이나 페이지 캐시에서 끝나는 공개 조회
  • 애플리케이션과 DB를 모두 거치는 로그인 사용자 조회
  • 데이터 변경과 트랜잭션이 필요한 쓰기 요청
  • 검색·필터·집계처럼 CPU와 DB 비용이 큰 요청
  • 이미지 변환, 파일 업로드, 보고서 생성
  • 외부 API 호출을 기다리는 요청
  • 큐에서 비동기로 처리되는 작업
  • Cron·백업·인덱싱 같은 백그라운드 작업

Google SRE는 웹 트래픽을 초당 요청 수로 측정하되 정적 콘텐츠와 동적 콘텐츠처럼 요청 성격별로 나눌 수 있다고 설명한다. 부하 테스트 도구 역시 서로 다른 함수와 스케줄을 가진 복수 시나리오로 실제 트래픽 구성을 모델링할 수 있다. (sre.google)

RPS는 하나가 아니라 세 개를 기록한다

부하 테스트에서는 적어도 다음 세 수치를 구분해야 한다.

  1. Offered RPS: 부하 발생기가 보내려고 한 요청률
  2. Completed RPS: 서버가 응답을 끝낸 처리량
  3. Successful RPS: 기능적으로 성공한 완료 처리량

Offered RPS가 증가하는데 Completed RPS가 더 이상 늘지 않는다면, 서버 앞이나 내부 어딘가에서 작업이 쌓이거나 버려지고 있을 가능성이 크다.

고정된 가상 사용자 수만 사용하는 폐쇄형 테스트에서는 서버가 느려질수록 다음 요청 시작도 늦어진다. 이 때문에 실제 처리 한계에 도달했는데도 입력 부하가 스스로 줄어드는 현상이 생길 수 있다. 목표 RPS를 검증할 때는 응답시간과 독립적으로 요청을 시작하는 개방형 도착률 모델을 함께 검토해야 한다. (grafana.com)

동시접속자보다 Concurrent Work를 본다

브라우저 탭을 열어 둔 사람 1,000명이 서버에서 작업 1,000개를 동시에 실행한다는 뜻은 아니다. 반대로 사용자 수는 적어도 느린 DB 쿼리나 외부 API 때문에 서버 내부 작업이 오래 남아 동시 작업량이 커질 수 있다.

안정적인 시스템에서는 평균 동시 작업량을 다음 식으로 검산할 수 있다.

text
평균 동시 작업량 L ≈ 완료 처리율 λ × 평균 체류시간 W

이는 Little’s Law의 L = λW를 적용한 것이다. 다만 정상 상태에 가깝고 평균값이 유한한 구간에서 사용하는 검산식이며 P95나 P99를 평균 체류시간 자리에 넣어서는 안 된다. (pubsonline.informs.org)

실제로는 다음 항목을 따로 본다.

  • 처리 중인 HTTP 요청
  • 사용 중인 애플리케이션 Worker
  • DB에서 실행 중인 Query
  • DB Connection Pool 대기자
  • 메시지 큐에 쌓인 Job
  • 외부 API 응답을 기다리는 작업

수용량을 먼저 정의하고 테스트한다

부하를 올린 뒤 그래프를 보고 “이쯤이면 괜찮다”고 판단하면 기준이 계속 바뀐다. 테스트 전에 Pass/Fail 기준을 선언해야 한다.

text
검증된 지속 수용량
=
정해진 Request Mix에서
지정한 측정 구간 동안
완료 처리량 목표를 충족하고
경로별 지연·오류 기준을 지키며
대기열이 계속 증가하지 않고
자원 압력·OOM·비정상 Throttling이 발생하지 않으며
부하 제거 후 정상 상태로 회복할 수 있는 최대 부하

Grafana k6의 Threshold도 테스트 메트릭에 대한 Pass/Fail 기준으로 정의된다. 문서의 P95 200ms, 오류율 1% 미만 같은 값은 사용법을 보여주는 예시일 뿐, 모든 서비스의 권장 기준이 아니다. 실제 기준은 서비스의 사용자 기대와 운영 목표로 정해야 한다. (grafana.com)

Headroom은 CPU 여유율 하나가 아니다

수요 대비 검증된 여유 용량은 다음처럼 계산할 수 있다.

text
수요 대비 Capacity Headroom
=
(검증된 지속 완료 처리량 ÷ 예상 피크 완료 처리량) - 1

이 계산은 Request Mix, 데이터 크기, 캐시 상태, 코드와 의존성이 같은 경우에만 의미가 있다.

필요한 여유율에 범용 정답은 없다. 다음 조건이 결정해야 한다.

  • 피크 수요 예측의 오차
  • 짧은 Spike의 크기와 지속시간
  • 증설 또는 자동 확장에 걸리는 시간
  • 배치·백업·인덱싱 작업과의 중첩
  • 한 인스턴스나 외부 연동 장애 시 남은 용량
  • 서비스가 허용할 수 있는 지연과 오류

Google Cloud도 최적 자원 사용률이 애플리케이션마다 다르며 시스템이 100%에 도달하기 전에 성능이 악화될 수 있다고 설명한다. (docs.cloud.google.com)

CPU Time으로 한 번 더 검산한다

컨테이너나 systemd cgroup을 사용한다면 cpu.statusage_usec로 테스트 구간의 CPU Time을 구할 수 있다. (docs.kernel.org)

text
요청당 CPU 시간
=
테스트 구간 CPU 사용시간(초) ÷ 완료 요청 수

CPU가 주된 병목이고 요청당 CPU 비용이 부하에 따라 크게 변하지 않는 구간이라면 다음 식을 보조적인 검산에 사용할 수 있다.

text
CPU 기반 이론 처리량
≈
가용 CPU 초/초 × 목표 CPU 사용 비율
÷ 요청당 CPU 시간

2 vCPU는 스케줄링 조건이 충분할 때 초당 최대 약 2 CPU초를 제공하는 형태로 생각할 수 있다. 그러나 공유 CPU, CPU quota, throttling, SMT, 단일 스레드 병목, DB·네트워크 대기 때문에 이 식이 실제 처리량을 보장하지는 않는다. 최종 판정은 항상 부하 테스트 결과로 해야 한다.


먼저 서버의 ‘환경 지문’을 기록한다

다른 Provider의 2 vCPU·4GB 서버를 비교하거나 같은 테스트를 재현하려면 아래 조건이 함께 남아 있어야 한다.

구분 기록할 내용
Compute Provider, Region·Zone, 상품명, vCPU 수, CPU 모델·세대, 아키텍처
CPU 배분 Shared / Dedicated, SMT 여부, vCPU 대 코어 매핑, CPU quota
CPU 이상 신호 throttling time, steal time, 단일 코어 편중
Memory 실제 사용 가능 메모리, Container/cgroup limit, Swap 크기와 정책
Storage Local·Block·Network Storage, SSD·NVMe, 볼륨 크기, IOPS·Throughput 한도
Network 인스턴스 네트워크 한도, 패킷·연결 제한, 부하 발생기와의 경로
Topology 앱·DB·캐시·큐가 같은 서버인지, 별도 서버인지
Software OS, Kernel, Runtime, Web Server, DB, Cache 버전
Process Model Worker·Thread·Process 수, Connection Pool과 Queue 한도
Data 데이터 크기, 인덱스 크기, 값 분포, 테스트 계정 수
Cache Cold·Warm 여부, TTL, 사전 예열 방법
Build Commit, Image Digest, 설정 버전, Feature Flag
Dependencies 외부 API, 메시징, 스토리지, 인증 서비스와 Mock 여부
Load Generator 도구·버전, 위치, 자체 CPU·메모리·네트워크 사양

Google Compute Engine은 일부 머신 계열에서 vCPU가 코어에 대응하지만 다른 계열에서는 기본적으로 코어당 두 vCPU를 사용한다. AWS의 여러 EC2 인스턴스에서는 한 CPU 스레드가 한 vCPU로 표현된다. DigitalOcean도 공유 CPU와 전용 CPU의 접근 보장 수준, CPU 세대, NVMe, 네트워크 성능을 구분한다. 따라서 2 vCPU·4GB라는 네 숫자만으로 서로 다른 VM을 동등 비교해서는 안 된다. (docs.digitalocean.com)


반드시 함께 측정할 값

Google SRE의 네 신호만으로 시작할 수 있지만 용량 판정에는 그 원인을 분리할 내부 지표가 더 필요하다. 성공 요청과 실패 요청의 지연도 분리해야 한다. (sre.google)

측정값 반드시 기록할 값 판정에 사용할 질문
Request Mix 경로·작업별 비중, 읽기·쓰기, 인증 여부, Payload, 캐시 상태, DB·외부 호출 수 실제 운영 트래픽과 같은 비용 분포인가
Requests/sec Offered, Started, Completed, Successful RPS를 경로별로 기록 입력을 높였을 때 완료 처리량도 계속 증가하는가
Concurrent Work In-flight Request, Active Worker, DB Active Query, Pool Waiter, Queue Depth 처리 중인 작업이 안정되는가, 계속 쌓이는가
CPU Utilization Host·cgroup·Process·코어별 사용률, user/system, steal, throttling CPU가 실제로 바쁜가, 한 코어나 quota에 막혔는가
CPU Time 구간 CPU초, 요청·작업당 CPU ms 처리량 증가에 따라 단위 작업 CPU 비용이 변하는가
Memory Working Set Warm-up 후 안정 구간의 cgroup 사용량·RSS·Heap·Page Cache·Peak·Refault 필요한 메모리가 평형에 도달하는가, 계속 증가하는가
Swap / OOM Swap in/out, major fault, reclaim, memory.events의 high·max·oom·oom_kill 메모리 부족을 Swap 지연이나 OOM으로 지불하고 있는가
DB Query QPS/TPS, 경로별 Query 수, Query P50·P95, Slow Query, Lock·Wait Event 앱보다 DB가 먼저 포화되는가
DB I/O Read·Write IOPS, bytes, latency, fsync, temp write 디스크 처리량보다 I/O 대기가 먼저 증가하는가
DB Connection Active·Idle·Waiting, 최대 연결, Pool 사용률과 대기시간 연결 한도나 Pool 대기가 요청 큐를 만드는가
Cache Hit CDN·Reverse Proxy·Application·Redis·DB Buffer별 hit/miss, eviction 테스트가 실제 서비스가 아니라 캐시 한 계층만 측정한 것은 아닌가
Network In/Out bytes, packets, 새 연결률, 연결 오류·재전송, 응답 크기 CPU가 남아 있어도 네트워크나 연결 처리가 제한되는가
Queue Depth Web/App Worker Queue, DB Pool Wait, Message Queue Lag, Oldest Age 큐가 일정 수준에서 안정되는가, 시간에 따라 증가하는가
P50 Latency 경로별 성공·실패 요청 중앙값 일반적인 요청 경험은 유지되는가
P95 Latency 경로별 성공·실패 요청 95번째 백분위 느린 상위 요청이 허용 기준을 넘는가
P99 Latency 장꼬리 위험이 중요한 경로에서 충분한 표본으로 측정 드문 지연이 결제·인증·저장 같은 중요 작업을 위협하는가
Error Rate 5xx, Timeout, Connection Error, Reject, 기능 실패, 잘못된 결과 빠른 실패가 평균 지연을 낮춰 보이게 하지는 않는가
Saturation CPU·Memory·I/O PSI, throttling, queue, lock, pool wait 사용률 100% 이전에 실제 작업 정지가 발생하는가
Headroom 예상 피크, 검증된 지속 용량, 첫 기준 위반점, 반복 편차 피크·변동·복구에 필요한 여유가 남아 있는가

P50은 중앙값이고 P95는 전체 관측값 중 95%가 그 값 이하라는 뜻이다. 여러 인스턴스나 구간을 합쳐야 한다면 계측 방식과 Histogram bucket 설계도 함께 검토해야 한다. (prometheus.io)

Linux에서는 CPU·메모리·I/O 경합으로 작업이 멈춘 시간을 PSI로 확인할 수 있다. cgroup v2는 CPU 사용시간과 throttling, 전체 메모리 사용량, OOM 이벤트, 읽기·쓰기 바이트와 I/O 횟수를 노출한다. (docs.kernel.org)

text
# cgroup v2 예시. 실제 workload cgroup 경로로 바꾼다.
CGROUP=/sys/fs/cgroup/<workload-cgroup>

cat "$CGROUP/cpu.stat"
cat "$CGROUP/memory.current"
cat "$CGROUP/memory.stat"
cat "$CGROUP/memory.events"
cat "$CGROUP/io.stat"

cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

memory.current는 cgroup이 사용하는 전체 메모리이지 워킹셋을 직접 계산해 주는 값은 아니다. 지속 부하에서 안정되는 사용량, refault, major fault, Swap, PSI와 OOM 이벤트를 함께 보고 운영상 워킹셋을 추정해야 한다.

PostgreSQL이라면 numbackends, blks_read, blks_hit, I/O 시간, active time, wait event를 활용할 수 있다. 다른 DBMS에서는 같은 질문에 답하는 대응 지표를 선택한다. (postgresql.org)

Redis를 캐시로 쓴다면 keyspace_hits, keyspace_misses, evicted_keys를 기록한다. 전체 hit ratio만 보지 말고 테스트 단계별 miss와 eviction 증가를 함께 확인한다. (redis.io)


Benchmark Protocol

1. 테스트가 답해야 할 질문부터 쓴다

나쁜 목표:

이 서버는 몇 명까지 가능한가?

좋은 목표:

운영 피크와 같은 Request Mix에서 완료 처리량 [목표]을 유지하면서 경로별 지연·오류·큐·자원 압력 기준을 만족하는가?

확인하려는 것이 평시 피크인지, 순간 Spike인지, 장시간 지속 부하인지도 구분한다.

2. Pass/Fail과 중단 조건을 먼저 정한다

부하를 보내기 전에 다음을 확정한다.

  • 경로별 P50·P95·필요 시 P99 목표
  • 기능 오류와 시스템 오류의 허용 기준
  • 완료 처리량 목표
  • 허용 Queue Depth·Pool Wait
  • CPU throttling과 자원 압력 기준
  • Swap·OOM 금지 조건
  • DB Connection·Query·I/O 기준
  • 테스트 중단 조건
  • 부하 제거 후 회복 기준

서비스별 목표가 없다면 먼저 임시 운영 기준을 만들되, 이를 업계 표준처럼 표현하지 않는다.

3. 환경을 고정하고 지문을 저장한다

애플리케이션 Build, 설정, Worker 수, DB·캐시 버전, 데이터 크기, VM 상품과 CPU 모델을 기록한다. 테스트 중 Auto Update, 배포, 백업, 데이터 마이그레이션이 실행되지 않도록 통제한다.

운영에서 백업이나 배치가 피크 시간과 실제로 겹친다면, 별도의 중첩 시나리오로 다시 테스트한다.

4. 실제 Request Mix를 재구성한다

운영 로그나 분석 자료에서 경로·작업별 비중을 구한다. 개인정보와 Credential은 제거하고 테스트 데이터가 한두 개의 캐시 키에만 몰리지 않도록 분포를 재현한다.

운영 자료가 없다면 제품 흐름을 기준으로 가설 프로필을 만든다. 이 경우 결과에 synthetic workload라고 명시한다.

5. Cold와 Warm을 분리한다

캐시가 비어 있는 상태와 예열된 상태를 한 결과로 평균내지 않는다.

  • Cold start
  • Warm steady state
  • Cache eviction 이후
  • 배포 직후
  • 재시작 직후

이 중 실제 운영 위험과 관련 있는 프로필만 선택해 각각 결과를 남긴다.

6. 부하 발생기가 병목이 아닌지 확인한다

부하 발생기의 CPU, 메모리, 네트워크, 열린 파일 수와 요청 누락을 함께 본다. 부하 발생기가 포화되면 SUT가 아니라 테스트 장비의 한계를 측정하게 된다.

k6의 대규모 테스트 지침은 발생기 CPU가 완전히 포화되면 결과가 저하될 수 있다고 경고하며 도구 측 가이드로 CPU 사용률을 80% 이내로 유지하는 예를 제시한다. 이는 테스트 대상 서버의 권장 CPU 임계값이 아니라 부하 발생기 검증 기준이다. (grafana.com)

7. Idle Baseline과 Smoke Test를 수행한다

부하가 없을 때 CPU·메모리·DB·큐를 기록한다. 이어서 기능 성공 여부와 계측 누락을 확인할 만큼 작은 부하를 보낸다.

Smoke 단계에서 기능 오류나 관측 누락이 있으면 용량 테스트로 넘어가지 않는다.

8. 부하를 단계적으로 올리고 각 단계를 유지한다

목표 RPS 또는 작업률을 작은 단계부터 올린다. 각 단계는 순간 피크값만 얻고 끝내지 말고 다음 상태가 드러날 만큼 유지한다.

  • 완료 처리량이 안정되는가
  • P95가 계속 상승하는가
  • 워킹셋이 평형에 도달하는가
  • 큐가 일정 수준에서 안정되는가
  • DB·외부 연동 지연이 누적되는가

목표 RPS를 검증할 때는 개방형 도착률 모델을 사용하고 고정된 동시 세션을 검증할 때는 폐쇄형 모델을 사용할 수 있다. 둘의 결과를 같은 의미로 해석하지 않는다. (grafana.com)

9. 첫 기준 위반점을 기록한다

서버가 완전히 멈출 때까지 기다릴 필요는 없다.

다음 중 하나가 처음 지속적으로 발생한 지점을 기록한다.

  • 경로별 지연 기준 위반
  • 오류율 기준 위반
  • 완료 처리량 정체
  • Queue의 지속 증가
  • CPU throttling 또는 PSI 증가
  • Memory reclaim·Swap·OOM 위험
  • DB Pool·Lock·I/O 대기 증가
  • Cache hit 급락과 eviction 증가
  • 외부 연동 timeout

이 지점 바로 아래에서 모든 기준을 만족한 마지막 단계를 검증된 지속 용량 후보로 삼는다.

10. 통제된 Overload 동작을 확인한다

비운영 환경에서 안전 중단 조건을 설정하고 용량을 초과했을 때 시스템이 어떻게 실패하는지 확인한다.

  • 모든 요청이 함께 느려지는가
  • 일부 요청을 명시적으로 거절하는가
  • Timeout이 누적되는가
  • Queue가 메모리를 소비하는가
  • 부하를 내린 뒤 회복하는가

Google Cloud는 과부하 시 모든 요청을 늦추는 것보다 일부 요청을 거절해 나머지 요청의 성능을 보존하는 Load Shedding을 권고한다. 다만 실제 적용 정책은 서비스의 업무 특성과 재시도 안전성을 고려해 설계해야 한다. (docs.cloud.google.com)

11. Soak와 Recovery를 확인한다

예상 피크 수준에서 충분히 지속해 다음과 같은 시간 의존 문제를 찾는다.

  • 메모리 누수
  • Connection 누수
  • Queue backlog 증가
  • 캐시 eviction
  • DB temp file·WAL·Checkpoint 영향
  • 로그와 디스크 증가
  • 주기적 GC와 Batch 간섭

부하를 제거한 뒤에는 다음을 확인한다.

  • 큐가 정해진 시간 안에 비워지는가
  • 지연과 오류가 Baseline 수준으로 돌아오는가
  • 메모리가 안정 구간으로 돌아오는가
  • Connection과 Worker가 정상화되는가
  • OOM이나 프로세스 재시작이 없었는가

12. 동일 조건에서 반복한다

한 번의 최고 결과를 선택하지 않는다. 동일 조건으로 반복해 중앙값과 실행 간 범위를 남긴다.

공유 CPU VM에서 반복 편차가 크다면 평균만 제시하지 말고 최저·중앙·최고 실행과 CPU steal·throttling·스토리지 지연을 함께 확인한다.


Capacity Test Worksheet

아래 양식의 빈칸은 실제 측정값으로 채운다.

A. Test Identity & Environment

구역 필드 기록값
식별 Test ID [입력]
식별 목적·판정할 질문 [입력]
식별 실행일·담당자 [입력]
SUT Commit / Image Digest [입력]
SUT 설정·Feature Flag 버전 [입력]
Compute Provider / Region / Zone / Plan [입력]
Compute vCPU / RAM [입력]
Compute CPU 모델·세대·아키텍처 [입력]
Compute Shared / Dedicated / SMT / Core Mapping [입력]
Limits cgroup·Container CPU quota [입력]
Limits Memory limit / Swap / PID·File limit [입력]
Storage 종류·크기·IOPS·Throughput 한도 [입력]
Network 인스턴스 한도·테스트 경로 [입력]
Software OS / Kernel / Runtime / Web Server [입력]
Process Worker·Thread·Queue 설정 [입력]
Database 위치·버전·크기·Connection Pool [입력]
Cache 위치·버전·용량·TTL·Eviction [입력]
Data 레코드·인덱스 크기와 분포 [입력]
Dependencies 외부 API·스토리지·인증·Mock 범위 [입력]
Generator 도구·버전·사양·위치 [입력]
Telemetry Metric·Log·Trace Dashboard [입력]
Safety 실행 창·중단 조건·복구 담당자 [입력]

B. Request Mix Profile

Scenario ID 사용자 작업 Route·Protocol Mix % Arrival Model Payload Auth Read/Write Cache State DB Query/Request External Call 비고
[S-01] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력]
[S-02] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력]
[S-03] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력] [입력]

선택한 프로필 안에서 Mix 합계는 100%가 되어야 한다. Normal Peak, Write-heavy, Cold Cache, Batch Overlap처럼 프로필이 다르면 별도 버전으로 관리한다.

C. Pass / Fail Criteria

영역 Metric 기준 측정 Window 적용 범위
Throughput Completed / Successful RPS [입력] [입력] 전체·경로별
Latency P50 [입력] [입력] 경로별
Latency P95 [입력] [입력] 경로별
Latency P99 [필요한 경로만 입력] [입력] 경로별
Error 시스템 오류율 [입력] [입력] 전체·경로별
Error 기능 실패율 [입력] [입력] 핵심 작업
CPU Utilization / PSI / Throttling [입력] [입력] Host·cgroup
CPU CPU ms/request [입력] [입력] 전체·경로별
Memory Working-set estimate / PSI [입력] [입력] Host·cgroup
Memory Swap / OOM [입력] [입력] Host·cgroup
DB Query P95 / Connection Wait [입력] [입력] DB·Pool
DB IOPS / I/O latency / Lock [입력] [입력] DB·Storage
Cache Hit / Miss / Eviction [입력] [입력] 계층별
Network Throughput / Error / Retransmit [입력] [입력] App·DB 경로
Queue Depth / Oldest Age / Pool Wait [입력] [입력] Queue별
Recovery Baseline 복귀 시간 [입력] 부하 종료 후 전체
Headroom 예상 Peak 대비 여유율 [입력] 최종 판정 Workload Profile

D-1. Stage Result — Service

Stage Offered RPS Completed RPS Successful RPS Concurrent Work P50 P95 P99 Error Rate Queue Depth Stable
Baseline
Warm-up
L1
L2
L3
Overload
Recovery

D-2. Stage Result — Resource & Dependency

Stage CPU % CPU ms/req Throttle·Steal Working Set Swap·OOM DB QPS·P95 DB Conn·Wait IOPS·Latency Cache Hit·Evict Network PSI 첫 위반
Baseline
Warm-up
L1
L2
L3
Overload
Recovery

E. Final Capacity Statement

text
Test ID:
Workload Profile Version:
Application Build:
Environment Fingerprint:

Expected Peak Demand:
Validated Sustainable Completed Throughput:
Validated Successful Throughput:

P50:
P95:
P99:
Error Rate:
Maximum Queue Depth:

CPU Utilization:
CPU Time per Request:
Memory Working-set Estimate:
Swap / OOM:
DB Query / I/O / Connection:
Cache Hit / Eviction:
Network:
PSI / Saturation:

First Breached Criterion:
Likely Bottleneck:
Capacity Headroom:

Recovery Result:
Run-to-run Variance:

This result is valid only when:
- Request Mix:
- Cache State:
- Data Size:
- Background Work:
- Provider / VM Class:
- Dependency Conditions:

Retest Required When:
- Code or Runtime changes
- Data size or distribution changes
- Request Mix changes
- DB, Cache or Worker configuration changes
- VM Provider, CPU class, Storage or Network changes

Result Interpretation Table

아래 표의 병목은 확정 원인이 아니라 다음 검증을 위한 가설이다. 여러 자원이 동시에 포화될 수도 있다.

관측 패턴 우선 가설 다음 확인 피해야 할 결론
Offered RPS는 증가하지만 Completed RPS가 정체되고 Queue가 계속 증가 지속 처리 한계 도달 Worker·DB Pool·Message Queue별 대기 위치 “오류가 적으니 아직 여유가 있다”
CPU·CPU PSI·throttling이 증가하고 CPU Time/request가 비교적 일정 Compute 포화 코어별 사용률, 단일 스레드, quota, steal CPU 평균만 보고 DB와 Queue를 제외
전체 CPU는 낮지만 한 코어만 포화되고 지연이 증가 단일 스레드·Lock·직렬 구간 Runtime profile, lock wait, event loop lag vCPU 수를 늘리면 선형 개선된다고 가정
워킹셋이 계속 증가하고 GC·refault·Swap·Memory PSI가 함께 증가 메모리 누수 또는 부족 Heap, Page Cache, Connection·Buffer 누수 RAM 증설만으로 누수를 해결했다고 판단
App CPU는 낮지만 DB Query P95·Pool Wait·Lock·I/O가 증가 DB 병목 Top SQL, 실행계획, connection, wait event App 인스턴스를 늘리면 해결된다고 판단
Cache Hit가 낮아지고 DB QPS·I/O가 동시에 증가 캐시 churn·키 분포·예열 문제 계층별 miss, TTL, eviction, test data “DB가 느리다”로만 결론
P50은 안정적인데 P95·P99만 급격히 증가 장꼬리 경합 GC, lock, connection wait, 외부 API, 느린 Query 평균 응답시간만으로 통과
오류율이 증가하는데 평균 지연은 오히려 낮아짐 빠른 실패·거절·연결 오류 성공·실패 지연 분리, 상태코드, 기능 성공 지연이 개선됐다고 판단
CPU·DB는 남지만 Network 처리량·연결 오류가 한계에 접근 네트워크·연결 처리 병목 Packet, retransmit, connection rate, payload vCPU 부족으로 단정
App 수를 늘렸지만 총 처리량이 거의 증가하지 않음 공용 DB·Cache·Queue·외부 연동 병목 각 하위 시스템의 saturation 더 많은 App 복제본을 계속 추가
같은 이미지와 부하인데 실행별 성능 편차가 큼 Shared CPU·스토리지·네트워크 변동 CPU steal, throttling, 디스크 지연, Host class 최고 실행 한 번을 서버 성능으로 공표
CPU 사용률은 높지만 지연·오류·Queue·PSI가 안정적 현재 부하는 처리 중이나 여유 검토 필요 Spike·Recovery·반복 편차 높은 CPU만으로 즉시 실패 판정
부하 종료 뒤 Queue·메모리·Connection이 정상화되지 않음 회복 실패·누수·재시도 폭주 Queue drain, retry, connection lifecycle 부하 구간의 P95만 보고 통과

Linux PSI는 CPU·메모리·I/O 경합으로 실제 작업이 정지한 시간을 보여주므로 단순 사용률과 포화를 구분하는 데 유용하다. PostgreSQL의 연결·I/O·wait event와 Redis의 hit·miss·eviction도 앱 서버 외부의 병목을 분리하는 데 사용할 수 있다. (docs.kernel.org)


Scale-up / Scale-out Trigger Template

Scale-up과 Scale-out은 CPU 그래프 하나로 결정하지 않는다.

Scale-up이 우선 후보가 되는 경우

  • 필요한 메모리 워킹셋이 현재 인스턴스의 실용 한계를 넘지만 누수는 아닌 경우
  • 단일 인스턴스의 CPU·I/O 성능이 명확한 첫 병목이며 더 높은 등급에서 개선 가능성이 있는 경우
  • 상태나 로컬 데이터 때문에 즉시 수평 분산하기 어려운 경우
  • 공유 CPU 변동성이 문제여서 전용 CPU나 다른 성능 등급으로 옮겨야 하는 경우

Scale-out이 우선 후보가 되는 경우

  • 개별 인스턴스의 병목이 재현 가능하고 요청을 여러 인스턴스에 독립적으로 분배할 수 있는 경우
  • 세션·파일·작업 상태가 외부화됐거나 일관되게 공유되는 경우
  • Load Balancer와 Health Check가 실제 부하에서 검증된 경우
  • DB·Cache·Queue·외부 연동에 추가 App 부하를 받을 여유가 있는 경우
  • 배포·장애 시 일부 인스턴스가 빠져도 목표 처리량을 유지해야 하는 경우

먼저 최적화하거나 구조를 고쳐야 하는 경우

  • Slow Query, Lock, Connection Pool 대기가 첫 병목
  • Cache Hit 붕괴나 eviction이 원인
  • 메모리 누수나 Connection 누수
  • 무제한 Queue와 재시도 폭주
  • 외부 API rate limit 또는 timeout
  • 단일 전역 Lock이나 직렬 처리 구간
  • 부하 발생기 자체가 포화된 경우

Trigger Record

text
decision_id: ""
measured_at: ""
owner: ""

workload_profile_version: ""
application_build: ""
environment_fingerprint: ""

expected_peak:
  offered_rate: ""
  completed_rate: ""
  request_mix: ""

validated_capacity:
  completed_rate: ""
  successful_rate: ""
  duration: ""
  repeated_runs: ""
  run_variance: ""

headroom:
  formula: "(validated_sustainable_throughput / expected_peak_throughput) - 1"
  result: ""
  required_headroom_rationale:
    - forecast_error
    - burst_size_and_duration
    - scaling_lead_time
    - background_work_overlap
    - failure_or_recovery_capacity

first_breached_criterion:
  metric: ""
  threshold: ""
  observed_value: ""
  window: ""
  scenario: ""

bottleneck_class:
  primary: "cpu | memory | db | storage | cache | network | queue | dependency | unknown"
  supporting_evidence:
    - ""
    - ""
  contradictory_evidence:
    - ""

scale_up_trigger:
  condition: >
    [metric] exceeds [service-defined threshold] for [window]
    in [required repeated runs], while [correlated evidence] is present.
  candidate_action: ""
  expected_effect: ""
  invalidation_condition: ""
  retest_required: true

scale_out_trigger:
  condition: >
    Per-instance latency, errors and queue remain within criteria,
    but aggregate expected demand exceeds validated multi-instance capacity.
  prerequisites:
    - load_distribution_verified
    - health_check_verified
    - state_and_session_handling_verified
    - downstream_headroom_verified
    - deployment_and_rollback_verified
  candidate_action: ""
  expected_effect: ""
  retest_required: true

optimize_before_scaling_when:
  - database_wait_is_first_bottleneck
  - cache_hit_collapse
  - memory_or_connection_leak
  - unbounded_queue_or_retry
  - external_dependency_limit
  - generator_saturation

rollback_condition:
  - ""
  - ""

post_change_validation:
  workload_profile: ""
  benchmark_protocol_version: ""
  acceptance_criteria: ""
  scheduled_at: ""

결론

2 vCPU·4GB 서버는 어떤 서비스에는 충분하고 다른 서비스에는 시작부터 부족할 수 있다. 그 차이를 만드는 것은 월 방문자 수가 아니라 요청의 비용과 분포다.

좋은 용량 판정에는 다음이 함께 들어간다.

  • 어떤 요청을 어떤 비율로 보냈는가
  • Offered·Completed·Successful RPS가 각각 얼마였는가
  • 동시에 처리되거나 대기한 작업이 얼마나 됐는가
  • 경로별 P50·P95·필요 시 P99가 기준을 지켰는가
  • CPU 사용률뿐 아니라 요청당 CPU Time과 throttling이 어땠는가
  • 메모리 워킹셋·Swap·OOM·Pressure가 안정적이었는가
  • DB·Cache·Storage·Network·Queue 중 어디가 먼저 포화됐는가
  • 예상 피크 이후에도 필요한 Headroom이 남았는가
  • 부하가 사라진 뒤 정상 상태로 회복했는가

마지막 문장은 몇 명까지 가능하다가 아니라 다음과 같아야 한다.

이 서버는 이 워크로드 프로필과 환경에서 이 처리량까지 검증됐으며 첫 제한 자원은 이것이고 예상 피크 대비 이만큼의 여유가 있다.

코드, 데이터, 요청 구성, 캐시 정책, Provider, CPU 등급, 스토리지나 네트워크가 바뀌면 그 문장도 다시 측정해야 한다.