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

  • headroom은 왜 평균 부하가 아니라 장애와 피크 기준으로 잡아야 하는가?
  • autoscaling이 있어도 사전 용량 계획이 필요한 이유는 무엇인가?
  • backlog가 쌓였을 때 복구 시간을 어떻게 계산하고 제한하는가?

개요

capacity planning은 평균 부하를 처리할 수 있다를 확인하는 일이 아니다. 피크, 장애, retry, 배치, 배포 중 흔들림을 포함해도 핵심 기능이 무너지지 않는지 확인하는 일이다. headroom은 남는 자원이 아니다. 장애 중에도 선택한 SLO를 지키기 위해 의도적으로 비워 둔 공간이다. 다만 headroom을 크게 잡는다고 자동으로 안전해지는 것은 아니다. scaling 정책, pool 제한, queue 제한, downstream quota가 맞지 않으면 남는 CPU가 있어도 장애는 난다.

이 문서가 바꾸는 설계 축

바뀌는 내용
성능정상 피크와 장애 피크를 기준으로 instance, pool, queue 크기를 정한다.
정합성backlog 처리 중 중복, 순서, 만료된 작업을 어떻게 다룰지 정한다.
장애 격리한 영역 장애나 downstream 지연이 전체 capacity를 잠식하지 않게 분리한다.
운영 복잡도autoscaling, alert, load test, capacity review 절차가 필요해진다.

먼저 정할 질문

capacity planning은 다음 질문에서 시작한다.

질문설계 영향
정상 peak QPS는 얼마인가?기본 instance 수와 pool 크기를 정한다.
retry factor는 얼마인가?장애 중 2차 부하를 계산한다.
한 zone이 빠져도 버틸 것인가?N+1 또는 multi-zone headroom을 잡는다.
autoscaling 반응 시간은 얼마인가?burst를 버틸 선행 용량을 둔다.
queue backlog는 얼마나 오래 허용되는가?consumer 수와 drop 정책을 정한다.
어떤 기능을 먼저 줄일 것인가?rate limit, degradation, feature flag를 준비한다.
평균 부하가 낮아도 위 질문에 답하지 못하면 장애 때 판단할 근거가 없다.

대표 시나리오

알림 발송 API와 worker를 운영한다고 하자.

항목
정상 peak API 요청2,000 QPS
정상 worker 처리량4,000 msg/sec
이벤트 캠페인 peak6,000 QPS
client retry factor1.4
zone 수3
한 zone 장애 허용
autoscaling 반응 시간3분
이 숫자를 보면 정상 peak 2,000 QPS만으로는 부족하다.
캠페인 peak, retry, zone 장애를 각각 반영해야 한다.

계획 QPS 계산

먼저 이벤트 peak와 retry를 더한다.

event_peak_qps = 6,000
retry_factor = 1.4
planned_inflow_qps = 6,000 * 1.4
                   = 8,400 QPS

한 zone이 빠져도 버티려면 남은 2개 zone이 8,400 QPS를 처리해야 한다.

capacity_per_zone_needed = 8,400 / 2
                         = 4,200 QPS
total_normal_capacity = 4,200 * 3
                      = 12,600 QPS

3개 zone이 모두 살아 있을 때는 12,600 QPS capacity를 가진다. 실제 사용량 6,000 QPS 대비 여유가 커 보이지만, 한 zone 장애와 retry를 동시에 고려한 값이다.

headroom을 해석한다

headroom은 비율 하나로 끝내지 않는다.

headroom 종류의미
Traffic Headroom갑작스러운 요청 증가를 버티는 여유
Failure Headroomnode, zone, downstream 장애를 버티는 여유
Retry Headroomtimeout 후 재시도로 늘어난 부하를 버티는 여유
Operational Headroom배포, migration, batch 중 성능 흔들림을 흡수하는 여유
모든 headroom을 합치면 비용이 커진다.
그래서 SLO가 높은 핵심 경로와 그렇지 않은 경로를 나눈다.
핵심 API는 failure headroom을 크게 잡고, 리포트 batch는 지연을 허용할 수 있다.

autoscaling의 한계

autoscaling은 평균 부하가 천천히 증가할 때 유용하다. 하지만 갑작스러운 이벤트 피크에는 늦을 수 있다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: notification-api
spec:
  minReplicas: 12
  maxReplicas: 80
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

minReplicas가 낮으면 첫 3분 동안 queue와 connection pool이 먼저 포화될 수 있다. CPU 기준만 쓰면 DB wait이나 downstream timeout을 놓칠 수 있다. 피크가 예고되어 있으면 사전 scale-out과 cache warm-up이 필요하다.

pool 크기는 따로 제한한다

instance를 늘리면 DB connection도 같이 늘어난다. 이때 전체 connection 수가 DB 한계를 넘으면 scale-out이 장애를 키운다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      connection-timeout: 500
server:
  tomcat:
    threads:
      max: 200

Pod 50개에 pool size 20이면 최대 1,000 connection이다. DB가 안정적으로 처리할 수 있는 connection이 500이면 pool size나 Pod 수를 다시 잡아야 한다. capacity planning은 API 서버 수만 계산하지 않는다. thread, connection, queue, downstream quota를 함께 맞춘다.

backlog drain time 계산

비동기 시스템에서는 backlog가 얼마나 빨리 빠지는지가 중요하다.

incoming_rate = 8,000 msg/sec
consumer_capacity = 5,000 msg/sec
duration = 10 minutes
backlog = (8,000 - 5,000) * 600
        = 1,800,000 messages

피크가 끝난 뒤 consumer capacity를 10,000 msg/sec로 올릴 수 있다고 하자.

drain_rate = 10,000 - 3,000 normal_incoming
           = 7,000 msg/sec
drain_time = 1,800,000 / 7,000
           = 약 257초

약 4분 17초면 backlog를 해소할 수 있다. 하지만 메시지 TTL이 2분이면 이미 늦다. backlog 계산은 사용자에게 지연 상태를 보여줄지, 오래된 메시지를 버릴지 결정하게 만든다.

saturation 지표

CPU 50%만 보고 안전하다고 말하면 안 된다.

영역saturation 지표
API Serverthread active, request queue, p95 latency
DBactive connection, lock wait, write latency, replication lag
Queuedepth, oldest message age, consumer lag
Cachememory used, eviction, hit ratio, hot key
External APItimeout rate, quota used, circuit open count
saturation은 여유가 사라지는 순간을 보여준다.
error rate는 이미 사용자 영향이 발생한 뒤에 오른다.
capacity alert는 error보다 saturation을 먼저 봐야 한다.

기능 축소 기준

모든 기능에 같은 headroom을 줄 수 없다. 장애 중 줄일 수 있는 기능을 미리 정한다.

기능축소 가능 여부장애 중 정책
로그인낮음강한 보호, 별도 pool
주문 생성낮음rate limit, idempotency, 재고 보호
추천 피드높음stale data 또는 기본 목록
리포트 다운로드높음지연 또는 임시 중단
알림 발송중간queue 지연, TTL 초과 시 drop
이 기준이 없으면 장애 때 모든 기능이 같은 자원을 경쟁한다.
핵심 기능을 보호하려면 덜 중요한 기능을 늦추거나 닫아야 한다.

개인 프로젝트 기준

  • 예상 peak QPS와 instance 1대의 처리량을 간단히라도 비교한다.
  • DB connection pool 총합이 DB 허용 범위를 넘지 않는지 계산한다.
  • queue를 쓴다면 backlog drain time을 한 번 계산한다.
  • autoscaling이 없으면 수동 증설 기준과 장애 때 줄일 기능을 적는다.
  • timeout과 retry가 capacity를 몇 배로 키우는지 문서에 남긴다.

기업 운영 기준

  • capacity review는 출시, 대형 이벤트, 큰 기능 변경 전에 반복한다.
  • load test는 API latency뿐 아니라 DB, queue, cache, external API 지표를 함께 본다.
  • zone 장애, downstream 장애, retry storm을 별도 시나리오로 테스트한다.
  • min/max replica, pool size, queue length, rate limit 값의 owner를 정한다.
  • alert는 saturation, burn rate, oldest message age처럼 사전 신호를 포함한다.

위험 신호

  • 평균 QPS에 2배를 곱한 값만 capacity planning으로 사용한다.
  • autoscaling이 있으니 headroom이 필요 없다고 설명한다.
  • Pod 수를 늘리면서 DB connection 총합을 계산하지 않는다.
  • queue depth만 보고 oldest message age와 drain time을 보지 않는다.
  • retry 정책이 capacity 계산에 반영되지 않는다.
  • 장애 중 어떤 기능을 줄일지 정하지 않는다.
  • load test가 단일 API 성공률만 보고 downstream saturation을 보지 않는다.

확인 질문

확인 질문

  • 확인 질문: headroom은 왜 비용 낭비가 아닌가?
    • 답변: 피크, retry, zone 장애, 배포 중 흔들림을 흡수해 핵심 SLO를 지키기 위한 의도적 여유이기 때문이다.
  • 확인 질문: autoscaling이 있는데도 capacity planning이 필요한 이유는 무엇인가?
    • 답변: scaling 반응 전 burst와 downstream quota, connection pool 한계는 autoscaling만으로 해결되지 않기 때문이다.
  • 확인 질문: backlog drain time이 설계 결정에 주는 영향은 무엇인가?
    • 답변: 지연을 허용할지, consumer를 늘릴지, 오래된 작업을 버릴지, 사용자에게 pending을 보여줄지 결정하게 만든다.