방문자 수를 Request Mix로 바꿔야 한다
2 vCPU·4GB 서버가 감당하는 방문자 수는 사양표만 보고 계산되지 않습니다. 같은 월 방문자 수라도 대부분 캐시된 공개 페이지를 보는 서비스와 로그인·검색·결제·파일 업로드를 반복하는 서비스의 부하는 전혀 다릅니다. 서버 수용량은 정해진 요청 구성에서 지연과 오류, 자원 압력, 대기열 기준을 모두 지키며 지속적으로 처리하는 최대 부하로 정의하는 편이 정확합니다. Google Cloud의 부하 테스트 지침도 절대 최대 처리량 하나보다 지연과 처리량, 자원 사용 등 여러 차원에서 허용 가능한 임계점을 찾도록 안내합니다.[1] Google SRE 역시 트래픽과 지연, 오류, 포화를 함께 보도록 합니다.[3]
2 vCPU라는 표기도 같은 연산 성능을 뜻하지 않습니다. 어떤 VM에서는 vCPU가 하드웨어 스레드이고 다른 상품군에서는 물리 코어에 대응합니다. 공유 CPU는 같은 호스트의 다른 워크로드에 따라 쓸 수 있는 CPU 사이클이 달라집니다. CPU 세대와 SMT 구성, 스토리지 종류와 한도, 네트워크 처리량도 함께 적어야 합니다.[2]
따라서 테스트 결과는 “2 vCPU·4GB = 방문자 N명”이 아니라 다음 형식으로 남깁니다. 워크로드 프로필 [버전]과 환경 [지문]에서 완료 처리량 [값]을 [측정 구간] 동안 유지했다. 이때 경로별 P95 지연은 [값], 오류율은 [값], 최대 큐는 [값]이었고 첫 제한 신호는 [CPU·메모리·DB·I/O·네트워크·외부 연동]이었다. 예상 피크 대비 검증된 여유 용량은 [값]이며 이 결과는 [캐시 상태·데이터 크기·백그라운드 작업·Provider 조건]에서만 유효하다.
월 방문자 수는 부하의 시간 분포를 보여주지 않습니다. 같은 10만 회 방문도 한 달에 고르게 흩어지거나 몇 분 안에 몰립니다. 한 번의 방문이 서버 요청 한 건을 뜻하지도 않습니다. 서버가 실제로 받는 작업은 보통 여덟 갈래로 섞입니다. CDN이나 페이지 캐시에서 끝나는 공개 조회, 애플리케이션과 DB를 모두 거치는 로그인 사용자 조회, 데이터 변경과 트랜잭션이 필요한 쓰기 요청입니다. 나머지는 검색·필터·집계처럼 CPU와 DB 비용이 큰 요청, 이미지 변환과 파일 업로드와 보고서 생성입니다. 외부 연동과 비동기 작업에는 외부 API 호출을 기다리는 요청, 큐에서 비동기로 처리되는 작업, 그리고 Cron·백업·인덱싱 같은 백그라운드 작업입니다.[3]
부하 테스트에서는 적어도 세 수치를 갈라야 합니다. Offered RPS가 늘어나는데 Completed RPS가 더는 늘지 않는다면, 서버 앞이나 내부 어딘가에서 작업이 쌓이거나 버려지고 있습니다. 고정된 가상 사용자 수만 쓰는 폐쇄형 테스트에서는 서버가 느려질수록 다음 요청 시작도 늦어집니다. 이 때문에 실제 처리 한계에 닿았는데도 입력 부하가 스스로 줄어드는 현상이 생깁니다. 목표 RPS를 검증할 때는 응답시간과 독립적으로 요청을 시작하는 개방형 도착률 모델을 함께 검토해야 합니다.[7]
동시접속자보다 Concurrent Work를 봅니다. 브라우저 탭을 열어 둔 사람 1,000명이 서버에서 작업 1,000개를 동시에 실행한다는 뜻은 아닙니다. 반대로 사용자 수는 적어도 느린 DB 쿼리나 외부 API 때문에 서버 내부 작업이 오래 남아 동시 작업량이 커집니다. 안정적인 시스템에서는 평균 동시 작업량 L이 완료 처리율 λ와 평균 체류시간 W의 곱과 같다는 Little's Law로 검산합니다. 다만 정상 상태에 가깝고 평균값이 유한한 구간에서 쓰는 검산식이며 P95나 P99를 평균 체류시간 자리에 넣어서는 안 됩니다.[5] 실제로는 처리 중인 HTTP 요청, 사용 중인 애플리케이션 Worker, DB에서 실행 중인 Query, DB Connection Pool 대기자, 메시지 큐에 쌓인 Job, 외부 API 응답을 기다리는 작업을 따로 봅니다.
- Offered RPS
- 부하 발생기가 보내려고 한 요청률입니다.
- Completed RPS
- 서버가 응답을 끝낸 처리량입니다.
- Successful RPS
- 기능적으로 성공한 완료 처리량입니다.
수용량을 먼저 정의하고 테스트한다
부하를 올린 뒤 그래프를 보고 “이쯤이면 괜찮다”고 판단하면 기준이 계속 바뀝니다. 테스트 전에 Pass/Fail 기준을 선언해야 합니다. 세 개념의 뜻은 다음과 같습니다.
Grafana k6의 Threshold도 테스트 메트릭에 대한 Pass/Fail 기준으로 정의됩니다. 문서의 P95 200ms, 오류율 1% 미만 같은 값은 사용법을 보여주는 예시일 뿐 모든 서비스의 권장 기준이 아닙니다. 실제 기준은 서비스의 사용자 기대와 운영 목표로 정합니다.[4]
Headroom은 CPU 여유율 하나가 아닙니다. 여유율 계산은 Request Mix와 데이터 크기, 캐시 상태, 코드와 의존성이 같을 때만 의미가 있습니다. 필요한 여유율에 범용 정답도 없습니다. 피크 수요 예측의 오차, 짧은 Spike의 크기와 지속시간, 증설이나 자동 확장에 걸리는 시간, 배치·백업·인덱싱 작업과의 중첩, 한 인스턴스나 외부 연동 장애 시 남은 용량, 서비스가 허용하는 지연과 오류가 이를 정합니다. Google Cloud도 최적 자원 사용률이 애플리케이션마다 다르며 시스템이 100%에 닿기 전에 성능이 나빠진다고 설명합니다.[1]
CPU Time으로 한 번 더 검산합니다. 컨테이너나 systemd cgroup을 쓴다면 cpu.stat의 usage_usec로 테스트 구간의 CPU Time을 구합니다.[8] CPU가 주된 병목이고 요청당 CPU 비용이 부하에 따라 크게 변하지 않는 구간이라면, 가용 CPU 초를 목표 CPU 사용 비율로 곱한 뒤 요청당 CPU 시간으로 나눈 값을 보조 검산에 씁니다. 2 vCPU는 스케줄링 조건이 충분할 때 초당 최대 약 2 CPU초를 주는 형태로 생각합니다. 그러나 공유 CPU와 CPU quota, throttling, SMT, 단일 스레드 병목, DB·네트워크 대기 때문에 이 식이 실제 처리량을 말해 주지는 않습니다. 최종 판정은 항상 부하 테스트 결과로 합니다.
- 검증된 지속 수용량
- 정해진 Request Mix에서 지정한 구간 동안 처리량 목표를 채우고 지연·오류 기준을 지키며, 대기열이 계속 늘지 않고 자원 압력과 OOM, 비정상 Throttling이 없으며, 부하 제거 후 정상 상태로 돌아오는 최대 부하입니다.
- Capacity Headroom
- 검증된 지속 완료 처리량을 예상 피크 완료 처리량으로 나눈 값에서 1을 뺀 여유율입니다.
- 요청당 CPU 시간
- 테스트 구간의 CPU 사용시간을 완료 요청 수로 나눈 값이며, 부하가 올라갈 때 이 값이 변하는지를 봅니다.
운영 트래픽에서 개선 우선순위까지 — 실제 요청의 구성으로 부하를 만들고 병목의 원인 가설과 개선 효과를 재검증합니다.
먼저 서버의 ‘환경 지문’을 기록한다
다른 Provider의 2 vCPU·4GB 서버를 비교하거나 같은 테스트를 재현하려면 아래 조건이 함께 남아 있어야 합니다.
Google Compute Engine은 일부 머신 계열에서 vCPU가 코어에 대응하지만 다른 계열에서는 기본적으로 코어당 두 vCPU를 씁니다. AWS의 여러 EC2 인스턴스에서는 한 CPU 스레드가 한 vCPU로 표현됩니다. DigitalOcean도 공유 CPU와 전용 CPU의 접근 보장 수준, CPU 세대, NVMe, 네트워크 성능을 가릅니다. 따라서 2 vCPU·4GB라는 네 숫자만으로 서로 다른 VM을 동등 비교해서는 안 됩니다.[2]
구분 | 기록할 내용 |
|---|---|
| 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 SRE의 네 신호만으로 시작하되, 용량 판정에는 그 원인을 가를 내부 지표가 더 필요합니다. 성공 요청과 실패 요청의 지연도 갈라야 합니다.[3]
P50은 중앙값이고 P95는 전체 관측값 가운데 95%가 그 값 이하라는 뜻입니다. 여러 인스턴스나 구간을 합쳐야 한다면 계측 방식과 Histogram bucket 설계도 함께 검토합니다.[9]
Linux에서는 CPU·메모리·I/O 경합으로 작업이 멈춘 시간을 PSI로 확인합니다. cgroup v2는 CPU 사용시간과 throttling, 전체 메모리 사용량, OOM 이벤트, 읽기·쓰기 바이트와 I/O 횟수를 노출합니다. 워크로드 cgroup 경로에서 cpu.stat, memory.current, memory.stat, memory.events, io.stat을 읽고 /proc/pressure의 cpu·memory·io를 함께 봅니다.[6]
memory.current는 cgroup이 쓰는 전체 메모리이지 워킹셋을 계산해 주는 값이 아닙니다. 지속 부하에서 안정되는 사용량과 refault, major fault, Swap, PSI, OOM 이벤트를 함께 보고 운영상 워킹셋을 추정합니다. PostgreSQL이라면 numbackends, blks_read, blks_hit, I/O 시간, active time, wait event를 씁니다.[10] 다른 DBMS에서는 같은 질문에 답하는 대응 지표를 고릅니다. Redis를 캐시로 쓴다면 keyspace_hits, keyspace_misses, evicted_keys를 적고, 전체 hit ratio만 보지 말고 테스트 단계별 miss와 eviction 증가를 함께 확인합니다.[11]
측정값 | 반드시 기록할 값 | 판정에 사용할 질문 |
|---|---|---|
| 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 | 예상 피크, 검증된 지속 용량, 첫 기준 위반점, 반복 편차 | 피크와 변동, 복구에 필요한 여유가 남아 있는가 |
Benchmark Protocol
첫째, 테스트가 답해야 할 질문부터 씁니다. “이 서버는 몇 명까지 가능한가”는 나쁜 목표입니다. “운영 피크와 같은 Request Mix에서 완료 처리량 [목표]를 유지하면서 경로별 지연·오류·큐·자원 압력 기준을 만족하는가”가 좋은 목표입니다. 확인하려는 것이 평시 피크인지, 순간 Spike인지, 장시간 지속 부하인지도 가릅니다.
둘째, Pass/Fail과 중단 조건을 먼저 정합니다. 부하를 보내기 전에 다음 기준을 확정합니다. 경로별 P50·P95·필요 시 P99 목표, 기능 오류와 시스템 오류의 허용 기준, 완료 처리량 목표, 허용 Queue Depth와 Pool Wait를 정합니다. CPU throttling과 자원 압력 기준, Swap·OOM 금지 조건, DB Connection·Query·I/O 기준, 테스트 중단 조건, 부하 제거 후 회복 기준을 확정합니다. 서비스별 목표가 없다면 임시 운영 기준을 먼저 만들되 이를 업계 표준처럼 표현하지 않습니다.
셋째, 환경을 고정하고 지문을 저장합니다. 애플리케이션 Build와 설정, Worker 수, DB·캐시 버전, 데이터 크기, VM 상품과 CPU 모델을 적습니다. 테스트 중 Auto Update와 배포, 백업, 데이터 마이그레이션이 실행되지 않도록 통제합니다. 운영에서 백업이나 배치가 피크 시간과 실제로 겹친다면 별도의 중첩 시나리오로 다시 테스트합니다.
넷째, 실제 Request Mix를 재구성합니다. 운영 로그나 분석 자료에서 경로·작업별 비중을 구합니다. 개인정보와 Credential은 지우고, 테스트 데이터가 한두 개의 캐시 키에만 몰리지 않도록 분포를 재현합니다. 운영 자료가 없다면 제품 흐름을 놓고 가설 프로필을 만들고 결과에 synthetic workload라고 밝힙니다.
다섯째, Cold와 Warm을 가릅니다. 캐시가 비어 있는 상태와 예열된 상태를 한 결과로 평균내지 않습니다. Cold start, Warm steady state, Cache eviction 이후, 배포 직후, 재시작 직후 가운데 실제 운영 위험과 관련 있는 프로필만 골라 각각 결과를 남깁니다.
여섯째, 부하 발생기가 병목이 아닌지 확인합니다. 발생기의 CPU와 메모리, 네트워크, 열린 파일 수, 요청 누락을 함께 봅니다. 발생기가 포화되면 SUT가 아니라 테스트 장비의 한계를 재게 됩니다. k6의 대규모 테스트 지침은 발생기 CPU가 완전히 포화되면 결과가 나빠진다고 경고하며 CPU 사용률을 80% 이내로 유지하는 예를 듭니다. 이는 테스트 대상 서버의 권장 CPU 임계값이 아니라 부하 발생기를 검증하는 값입니다.[12]
일곱째, Idle Baseline과 Smoke Test를 수행합니다. 부하가 없을 때 CPU와 메모리, DB, 큐를 적습니다. 이어서 기능 성공 여부와 계측 누락을 확인할 만큼 작은 부하를 보냅니다. Smoke 단계에서 기능 오류나 관측 누락이 있으면 용량 테스트로 넘어가지 않습니다.
여덟째, 부하를 단계적으로 올리고 각 단계를 유지합니다. 목표 RPS나 작업률을 작은 단계부터 올립니다. 각 단계는 순간 피크값만 얻고 끝내지 말고 완료 처리량이 안정되는지, P95가 계속 오르는지, 워킹셋이 평형에 닿는지, 큐가 일정 수준에서 안정되는지, DB·외부 연동 지연이 쌓이는지가 드러날 만큼 유지합니다. 목표 RPS를 검증할 때는 개방형 도착률 모델을, 고정된 동시 세션을 검증할 때는 폐쇄형 모델을 씁니다. 둘의 결과를 같은 의미로 해석하지 않습니다.[4]
아홉째, 첫 기준 위반점을 적습니다. 서버가 완전히 멈출 때까지 기다릴 필요는 없습니다. 첫 기준 위반 지점을 적습니다. 확인할 항목은 경로별 지연 기준 위반, 오류율 기준 위반, 완료 처리량 정체, Queue의 지속 증가, CPU throttling이나 PSI 증가입니다. 나머지 위반 항목도 확인합니다. Memory reclaim·Swap·OOM 위험, DB Pool·Lock·I/O 대기 증가, Cache hit 급락과 eviction 증가, 외부 연동 timeout 가운데 하나가 처음 지속적으로 나타난 지점을 적습니다. 이 지점 바로 아래에서 모든 기준을 만족한 마지막 단계를 검증된 지속 용량 후보로 삼습니다.
열째, 통제된 Overload 동작을 확인합니다. 비운영 환경에서 안전 중단 조건을 걸고 용량을 넘겼을 때 시스템이 어떻게 실패하는지를 봅니다. 모든 요청이 함께 느려지는지, 일부 요청을 명시적으로 거절하는지, Timeout이 쌓이는지, Queue가 메모리를 먹는지, 부하를 내린 뒤 회복하는지입니다. Google Cloud는 과부하 시 모든 요청을 늦추기보다 일부 요청을 거절해 나머지 요청의 성능을 지키는 Load Shedding을 권합니다. 다만 실제 적용 정책은 서비스의 업무 특성과 재시도 안전성을 놓고 설계해야 합니다.[1]
열한째, Soak와 Recovery를 확인합니다. 예상 피크 수준에서 충분히 지속합니다. 메모리 누수, Connection 누수, Queue backlog 증가, 캐시 eviction, DB temp file·WAL·Checkpoint 영향, 로그와 디스크 증가, 주기적 GC와 Batch 간섭 같은 시간 의존 문제를 찾습니다. 부하를 제거한 뒤에는 큐가 정해진 시간 안에 비워지는지, 지연과 오류가 Baseline 수준으로 돌아오는지, 메모리가 안정 구간으로 돌아오는지, Connection과 Worker가 정상화되는지, OOM이나 프로세스 재시작이 없었는지를 확인합니다.
열두째, 동일 조건에서 반복합니다. 한 번의 최고 결과를 고르지 않습니다. 같은 조건으로 반복해 중앙값과 실행 간 범위를 남깁니다. 공유 CPU VM에서 반복 편차가 크다면 평균만 제시하지 말고 최저·중앙·최고 실행과 CPU steal, throttling, 스토리지 지연을 함께 확인합니다.
Capacity Test Worksheet
아래 양식의 빈칸은 실제 측정값으로 채웁니다. 워크시트는 다섯 블록으로 나뉩니다.
B의 Mix 합계는 선택한 프로필 안에서 100%가 되어야 합니다. Normal Peak, Write-heavy, Cold Cache, Batch Overlap처럼 프로필이 다르면 별도 버전으로 관리합니다.
D는 D-1의 처리량·지연·안정성과 D-2의 자원·병목 기록으로 나뉩니다. 아래 항목별 빈칸을 채웁니다. 두 양식 모두 Baseline, Warm-up, L1, L2, L3, Overload, Recovery 일곱 단계를 기록합니다.
E는 한 장으로 끝냅니다. Test ID와 Workload Profile 버전, Application Build, 환경 지문을 적습니다. 예상 피크 수요, 검증된 지속 완료 처리량과 성공 처리량, P50·P95·P99, 오류율, 최대 큐를 적습니다. CPU 사용률과 요청당 CPU 시간, 메모리 워킹셋 추정, Swap과 OOM, DB Query와 I/O와 Connection을 적습니다. Cache hit와 eviction, Network, PSI와 포화, 첫 기준 위반, 유력한 병목, 여유율, 회복 결과, 실행 간 편차를 적습니다. 마지막에는 이 결과가 유효한 조건과 재측정이 필요한 변경을 적습니다.
- A. Test Identity & Environment
- 무엇을 시험했는지 특정하는 식별 블록입니다. 아래 A 필드에 실제 환경값을 적습니다.
- B. Request Mix Profile
- 시나리오마다 사용자 작업과 Route, Mix 비율, Arrival Model, Payload, 인증, 읽기·쓰기, 캐시 상태, 요청당 DB Query 수, 외부 호출을 적는 블록입니다.
- C. Pass / Fail Criteria
- 처리량과 지연, 오류, CPU, 메모리, DB, 캐시, 네트워크, 큐, 회복, 여유율의 기준과 측정 Window, 적용 범위를 부하 전에 확정하는 블록입니다.
- D. Stage Result
- 단계마다 서비스 지표와 자원·의존성 지표를 나눠 적는 두 블록입니다. 앞은 RPS와 지연, 오류, 큐를, 뒤는 CPU와 메모리, DB, I/O, 캐시, 네트워크, PSI와 첫 위반을 담습니다.
- E. Final Capacity Statement
- 검증된 지속 처리량과 지연·오류·큐, 첫 제한 신호, 여유율, 그리고 이 결과가 유효한 조건과 재측정이 필요한 변경을 한 장에 적는 블록입니다.
- A / 식별 / Test ID
- [입력]
- A / 식별 / 목적·판정할 질문
- [입력]
- A / 식별 / 실행일·담당자
- [입력]
- A / SUT / Commit / Image Digest
- [입력]
- A / SUT / 설정·Feature Flag 버전
- [입력]
- A / Compute / Provider / Region / Zone / Plan
- [입력]
- A / Compute / vCPU / RAM
- [입력]
- A / Compute / CPU 모델·세대·아키텍처
- [입력]
- A / Compute / Shared / Dedicated / SMT / Core Mapping
- [입력]
- A / Limits / cgroup·Container CPU quota
- [입력]
- A / Limits / Memory limit / Swap / PID·File limit
- [입력]
- A / Storage / 종류·크기·IOPS·Throughput 한도
- [입력]
- A / Network / 인스턴스 한도·테스트 경로
- [입력]
- A / Software / OS / Kernel / Runtime / Web Server
- [입력]
- A / Process / Worker·Thread·Queue 설정
- [입력]
- A / Database / 위치·버전·크기·Connection Pool
- [입력]
- A / Cache / 위치·버전·용량·TTL·Eviction
- [입력]
- A / Data / 레코드·인덱스 크기와 분포
- [입력]
- A / Dependencies / 외부 API·스토리지·인증·Mock 범위
- [입력]
- A / Generator / 도구·버전·사양·위치
- [입력]
- A / Telemetry / Metric·Log·Trace Dashboard
- [입력]
- A / Safety / 실행 창·중단 조건·복구 담당자
- [입력]
- B / [S-01]
- 사용자 작업: [입력] · Route·Protocol: [입력] · Mix %: [입력] · Arrival Model: [입력] · Payload: [입력] · Auth: [입력] · Read/Write: [입력] · Cache State: [입력] · DB Query/Request: [입력] · External Call: [입력] · 비고: [입력]
- B / [S-02]
- 사용자 작업: [입력] · Route·Protocol: [입력] · Mix %: [입력] · Arrival Model: [입력] · Payload: [입력] · Auth: [입력] · Read/Write: [입력] · Cache State: [입력] · DB Query/Request: [입력] · External Call: [입력] · 비고: [입력]
- B / [S-03]
- 사용자 작업: [입력] · Route·Protocol: [입력] · Mix %: [입력] · Arrival Model: [입력] · Payload: [입력] · Auth: [입력] · Read/Write: [입력] · Cache State: [입력] · DB Query/Request: [입력] · External Call: [입력] · 비고: [입력]
- D-1 / Baseline
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / Warm-up
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / L1
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / L2
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / L3
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / Overload
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-1 / Recovery
- Offered RPS: [입력] · Completed RPS: [입력] · Successful RPS: [입력] · Concurrent Work: [입력] · P50: [입력] · P95: [입력] · P99: [입력] · Error Rate: [입력] · Queue Depth: [입력] · Stable: [입력]
- D-2 / Baseline
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / Warm-up
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / L1
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / L2
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / L3
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / Overload
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
- D-2 / Recovery
- CPU %: [입력] · CPU ms/req: [입력] · Throttle·Steal: [입력] · Working Set: [입력] · Swap·OOM: [입력] · DB QPS·P95: [입력] · DB Conn·Wait: [입력] · IOPS·Latency: [입력] · Cache Hit·Evict: [입력] · Network: [입력] · PSI: [입력] · 첫 위반: [입력]
영역 | 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 |
서비스 구조에서 운영 기준까지 — 네트워크와 서버를 설계하고 배포 경로, 접근 권한과 백업 기준을 함께 갖춥니다.
Result Interpretation Table
아래 표의 병목은 확정 원인이 아니라 다음 검증을 위한 가설입니다. 여러 자원이 동시에 포화되기도 합니다.
Linux PSI는 CPU·메모리·I/O 경합으로 실제 작업이 멈춘 시간을 보여주므로 단순 사용률과 포화를 가르는 데 유용합니다. PostgreSQL의 연결·I/O·wait event와 Redis의 hit·miss·eviction도 앱 서버 밖의 병목을 가르는 데 씁니다.[6]
관측 패턴 | 우선 가설 | 다음 확인 | 피해야 할 결론 |
|---|---|---|---|
| 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만 보고 통과 |
Scale-up / Scale-out Trigger Template
Scale-up과 Scale-out은 CPU 그래프 하나로 정하지 않습니다. 세 갈래 가운데 어느 쪽이 우선 후보가 되는지는 첫 병목이 무엇인지에 달려 있습니다.
판단은 기록으로 남깁니다. Trigger Record에는 결정 ID와 측정일, 담당자, Workload Profile 버전, Application Build, 환경 지문을 적습니다. 이어 예상 피크의 Offered·Completed 처리율과 Request Mix, 검증된 용량의 처리율과 지속 시간, 반복 횟수, 실행 간 편차를 적습니다. Headroom에는 계산식과 결과, 그리고 그 여유율이 필요한 이유를 예측 오차·Spike 크기·확장 소요 시간·백그라운드 작업 중첩·장애 시 잔여 용량으로 적습니다.
첫 위반 기준에는 지표와 임계값, 관측값, Window, 시나리오를 적습니다. 병목 분류에는 CPU·메모리·DB·스토리지·캐시·네트워크·큐·의존성 가운데 하나를 고르고 뒷받침하는 증거와 반대되는 증거를 함께 적습니다. Scale-up과 Scale-out 트리거에는 조건과 후보 조치, 기대 효과, 무효화 조건, 재측정 필요 여부를 적습니다. Scale-out 트리거에는 부하 분산과 헬스 체크, 상태·세션 처리, 하위 시스템 여유, 배포와 롤백이 검증됐는지를 전제로 답니다. 마지막에는 롤백 조건과 변경 후 재검증 계획을 적습니다.
- 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이나 직렬 처리 구간이 있을 때. 부하 발생기 자체가 포화된 때
결론
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 등급, 스토리지나 네트워크가 바뀌면 그 문장도 다시 재야 합니다.



