이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Capacity planning을 RPS 계산이 아니라 SLO와 headroom 관리로 어떻게 바꾸는가?
  • app instance를 늘릴 때 DB pool, NAT, DNS, external API quota가 왜 같이 위험해지는가?
  • 개인 프로젝트와 기업 운영에서 capacity 기준을 어떻게 다르게 잡는가?

개요

Capacity planning은 “우리 서버가 최대 몇 RPS까지 버티는가”를 자랑하는 일이 아니다. 목표 SLO를 지키면서 어느 traffic까지 안정적으로 처리할 수 있고, 장애나 배포 중에도 얼마나 여유가 있는지 아는 일이다. 백엔드 개발자는 CPU, memory뿐 아니라 connection, pool, queue, bandwidth, downstream quota까지 capacity의 일부로 봐야 한다.

  • 목표는 peak traffic에서 p95/p99와 error rate를 지키는 것이다.
  • 평균 RPS보다 burst, fan-out, retry, batch job, cache miss가 더 위험할 수 있다.
  • scale out은 app capacity를 늘리지만 shared downstream capacity를 더 빨리 소모한다.
  • headroom은 낭비가 아니라 장애 흡수 공간이다.

원리

Capacity는 다음 네 숫자로 시작한다.

traffic demand: 초당 들어오는 요청 수와 burst
service time: 요청 하나가 자원을 점유하는 시간
concurrency: 동시에 처리 중인 요청 수
limit: CPU, pool, queue, bandwidth, quota의 상한

Little’s Law로 단순화하면 concurrency = throughput * latency다. latency가 100ms에서 1s로 늘면 같은 RPS에서도 동시 점유량이 10배가 된다. tail latency가 길어지면 connection과 thread가 오래 묶이고, 이것이 다시 queue를 키운다.

Capacity Inventory

서비스마다 다음 표를 가지고 있어야 한다.

항목예시 질문
App instancepod/VM 수, CPU/memory limit, thread/event loop
HTTP connectionLB/proxy/app keep-alive, max connection
DB poolinstance당 pool size, 전체 DB max connection
CacheQPS, hot key, memory, eviction
Queuepartition, consumer 수, lag 허용치
External APIquota, rate limit, timeout, retry 정책
Networkegress bandwidth, NAT port, DNS resolver limit
Deploymentrolling update 중 감소하는 capacity

이 표가 없으면 장애 때 “서버를 늘리자”만 반복하게 된다.

DB Pool 계산

가장 흔한 실수는 instance 수와 pool size를 곱한 값이 DB 한계를 넘는 것이다.

app instances: 20
max pool size per instance: 30
possible DB connections: 600
DB max_connections: 500

이 경우 scale out이 오히려 DB 장애를 만든다. 운영에서는 app pool 합계, migration tool, admin session, background worker까지 포함해 여유를 둔다.

코드와 명령으로 확인하기

간단한 k6 smoke load 예시다.

import http from "k6/http";
import { sleep, check } from "k6";
 
export const options = {
  stages: [
    { duration: "1m", target: 50 },
    { duration: "3m", target: 200 },
    { duration: "1m", target: 0 }
  ],
  thresholds: {
    http_req_failed: ["rate<0.01"],
    http_req_duration: ["p(95)<300", "p(99)<800"]
  }
};
 
export default function () {
  const res = http.get("https://api.example.com/products");
  check(res, { "status is 200": r => r.status === 200 });
  sleep(1);
}

CLI로 짧은 부하를 걸 수 있다.

k6 run load-test.js
wrk -t4 -c200 -d60s https://api.example.com/products

테스트 중에는 서버 metric과 client generator metric을 모두 본다. 부하 도구가 먼저 CPU/network 한계에 걸리면 결과가 거짓이다.

인프라 협업 포인트

  • 인프라 팀에는 예상 peak RPS, payload 크기, connection 유지 시간, egress, burst 패턴을 전달한다.
  • 인프라 팀은 LB/L7 proxy limit, NAT gateway, subnet IP, DNS resolver, cloud quota를 공유해야 한다.
  • DB와 cache 담당자에게 app instance 증가 계획과 pool 합계를 미리 알려야 한다.
  • 이벤트 전에는 rollback보다 traffic shedding, waiting room, feature flag, cache warm-up 전략까지 논의한다.

실전 팁

  • capacity 문서는 현재값, 한계값, 알람 기준, 완화 방법을 같이 적는다.
  • p95/p99가 SLO를 넘기기 시작하는 RPS를 saturation point로 기록한다.
  • headroom은 최소 정상 peak의 30~50% 이상을 출발점으로 검토하되 비용과 장애 리스크로 조정한다.
  • batch, cron, cache warm-up은 사용자 traffic과 같은 downstream 자원을 쓸 수 있다.
  • 개인 프로젝트라도 “무료 DB connection 20개, app pool 10개, worker 2개”처럼 작은 숫자부터 써 두면 장애가 덜 신비롭다.

위험 신호!

  • instance 수만 늘리고 DB/cache/external API quota를 확인하지 않는다.
  • capacity 기준이 평균 RPS 하나뿐이다.
  • 부하 테스트 결과에 p95/p99, error, saturation metric이 없다.
  • 이벤트나 배포 중 줄어드는 capacity를 계산하지 않는다.

확인 질문

확인 질문

  • Capacity planning에서 headroom이 필요한 이유는 무엇인가?
    • traffic burst, 장애, 배포, retry, cache miss 같은 변동을 흡수하기 위한 여유가 필요하기 때문이다.
  • app instance를 늘릴 때 DB pool 합계를 계산해야 하는 이유는 무엇인가?
    • instance마다 pool을 열기 때문에 전체 DB connection이 DB 한계를 넘을 수 있기 때문이다.
  • Little’s Law가 tail latency와 연결되는 이유는 무엇인가?
    • latency가 늘면 같은 throughput에서도 동시에 점유되는 요청 수가 늘어 queue와 자원 포화를 키우기 때문이다.

참고 문서