이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 트래픽 급증 상황에서 증설보다 먼저 확인해야 할 병목은 무엇인가?
- traffic shed, cache, queue, rate limit 중 무엇을 언제 선택해야 하는가?
- 급한 완화와 근본 설계 변경을 어떻게 분리하는가?
급증 대응의 출발점
트래픽 급증 대응은 서버를 더 붙이는 절차가 아니다.
어떤 자원이 포화되었고, 어떤 요청을 줄이면 사용자 피해가 가장 작아지는지 결정하는 절차다.
예시는 이벤트 오픈 직후 상품 상세와 주문 API가 동시에 느려지는 상황이다.
이때 무작정 애플리케이션 인스턴스를 늘리면 DB connection과 Redis hot key를 더 세게 압박할 수 있다.
이 runbook은 성능을 직접 바꾸지만, 정합성, 장애 격리, 운영 복잡도도 함께 바꾼다.
- 성능: 포화된 자원 앞에서 요청을 줄이거나 우회한다.
- 정합성: 재고, 주문, 결제 같은 명령은 캐시 응답으로 대체하지 않는다.
- 장애 격리: 비핵심 조회, 추천, 로깅, 알림을 핵심 주문 경로에서 떼어낸다.
- 운영 복잡도: 제한 정책, 예외 고객, 복구 조건을 운영자가 이해해야 한다.
첫 5분에 확인할 것
원인 분석을 끝내기 전에 사용자 영향부터 줄인다.
다만 아무 기능이나 끄면 안 된다.
다음 순서로 확인한다.
- 영향 범위: 전체 사용자, 특정 API, 특정 region, 특정 tenant 중 어디인가?
- 되돌릴 수 없는 작업: 주문, 결제, 정산, 포인트 차감처럼 중복 실행 위험이 있는가?
- 포화 지표: app CPU, DB CPU, connection pool, Redis CPU, queue age, external quota 중 무엇이 올라갔는가?
- 트래픽 모양: 신규 유입, retry, bot, batch, push 알림 fanout 중 무엇이 급증했는가?
- 즉시 줄일 수 있는 경로: 추천, 검색 자동완성, 이미지 변환, 통계 적재처럼 끊어도 되는 기능이 있는가?
이 다섯 가지가 보이면 증설 여부도 더 정확해진다.
증설은 포화 지점 뒤에 있는 자원을 늘릴 때만 의미가 있다.
병목별 판단
| 병목 | 먼저 할 일 | 잘못된 대응 |
|---|---|---|
| App CPU | 비싼 endpoint와 serialization 비용을 찾는다. | DB가 병목인데 app만 늘린다. |
| DB connection | pool 대기, slow query, lock wait를 본다. | connection max를 크게 올려 DB를 더 죽인다. |
| Redis hot key | key별 QPS와 miss storm을 본다. | TTL을 모두 동시에 늘리거나 지운다. |
| Queue lag | oldest age와 ingress/egress를 비교한다. | consumer만 늘려 downstream quota를 넘긴다. |
| 외부 API quota | timeout, 429, retry rate를 본다. | retry를 늘려 quota 소진을 가속한다. |
평균 latency가 아니라 포화 지표를 봐야 한다.
급증 상황에서는 평균이 정상이어도 특정 key, partition, customer tier가 먼저 무너질 수 있다.
traffic shed 기준
traffic shed는 장애를 숨기는 기능이 아니다.
핵심 기능을 살리기 위해 덜 중요한 요청을 명시적으로 줄이는 설계 선택이다.
우선순위는 다음처럼 둔다.
- 1순위 보존: 주문 생성, 결제 승인, 이미 결제한 주문 조회
- 2순위 제한: 상품 상세 조회, 장바구니 갱신, 쿠폰 조회
- 3순위 차단: 추천, 개인화 배너, 실시간 랭킹, 분석 이벤트
HTTP 응답도 다르게 설계한다.
명령 경로는 429 Retry-After나 503 temporary_unavailable로 명확히 거절한다.
조회 경로는 stale cache나 축약 응답을 줄 수 있지만, 재고나 가격처럼 사용자가 결정을 내리는 값은 출처와 시점을 관리해야 한다.
정책 예시
traffic_policy:
order_create:
priority: critical
rate_limit: user:5/min
fallback: reject_with_retry_after
product_detail:
priority: high
cache: stale_while_revalidate_30s
recommendation:
priority: low
mode: disabled_during_incident정책은 코드보다 운영 절차와 더 가깝다.
누가 언제 recommendation을 끌 수 있는지, 끈 뒤 어떤 지표가 회복되어야 다시 켤 수 있는지가 함께 있어야 한다.
캐시를 쓸 때의 선
캐시는 조회 급증에 효과적이다.
하지만 명령 경로의 정합성을 대체하지 않는다.
상품 상세의 설명, 이미지, 리뷰 요약은 stale 응답을 허용할 수 있다.
재고 차감, 결제 금액, 쿠폰 사용 가능 여부는 최종 확인을 DB나 결제 시스템 기준으로 해야 한다.
급증 중 cache miss가 늘면 DB가 더 위험해질 수 있다.
이때는 TTL 연장, 사전 warm-up, single flight, negative cache를 검토한다.
단, 잘못된 값을 오래 보여주는 비용이 작은 화면에만 적용한다.
queue로 미룰 수 있는 것
queue는 느린 후속 작업을 분리할 때 유용하다.
주문 생성 응답 안에서 알림, 추천 업데이트, 검색 색인 갱신, 통계 적재를 모두 끝낼 필요는 없다.
하지만 queue가 무한 완충재는 아니다.
oldest message age가 사용자가 기대하는 반영 시간보다 커지면 이미 사용자 경험 장애다.
급증 중에는 producer를 제한하지 않고 consumer만 늘리는 대응을 피한다.
consumer가 downstream DB나 외부 API를 더 세게 호출해 2차 장애를 만들 수 있기 때문이다.
운영 중 의사결정 기록
장애 중에는 다음 형식으로 결정을 남긴다.
time: 10:05
signal: order_create p95 8s, DB pool wait 2s
decision: recommendation API disabled, product_detail stale cache 30s allowed
owner: commerce-oncall
rollback_condition: DB pool wait < 100ms for 10m이 기록은 사후 보고서가 아니라 다음 결정을 돕는 작업 로그다.
나중에 어떤 조치가 효과가 있었는지 비교할 수 있어야 한다.
근본 설계 변경 후보
급한 완화가 끝나면 같은 조치를 상시 정책으로 둘지 판단한다.
매번 추천 API를 끄고 살아난다면 추천이 핵심 경로에 너무 가까운 것이다.
매번 DB connection pool이 먼저 찬다면 slow query, transaction 길이, connection 사용 시간을 줄이는 편이 증설보다 중요할 수 있다.
매번 Redis hot key가 터진다면 key 분산, local cache, precompute, 요청 합치기를 검토한다.
매번 외부 API quota가 먼저 찬다면 timeout, bulkhead, circuit breaker, 사전 승인 quota가 필요하다.
하지 말아야 할 대응
- 병목 확인 없이 app instance부터 늘린다.
- connection pool 최대값을 크게 올려 DB 동시성을 무제한으로 만든다.
- 모든 cache key를 동시에 삭제한다.
- retry 설정을 늘려 순간 성공률을 올리려 한다.
- 장애 중 batch, marketing push, data export를 그대로 둔다.
- 운영자 판단 없이 자동으로 모든 기능을 fallback으로 전환한다.
확인 질문
확인 질문
- 확인 질문: 트래픽 급증 때 가장 먼저 묻는 질문은 무엇인가?
- 답변: 어떤 사용자가 어떤 기능에서 실패를 보고 있고, 되돌릴 수 없는 작업이 포함되는지다.
- 확인 질문: 이 runbook이 바꾸는 설계 축은 무엇인가?
- 답변: 성능 완화가 중심이지만 정합성 보존, 비핵심 기능 격리, 운영 정책 복잡도를 함께 바꾼다.
- 확인 질문: 증설이 위험한 경우는 언제인가?
- 답변: 병목이 DB lock, Redis hot key, 외부 quota, downstream 처리량인데 app만 늘려 호출 압력을 키울 때다.