서비스 장애의 상당수는 “성능이 조금 느려진 상태"를 오래 방치하다가, 어느 순간 임계점을 넘으면서 발생합니다. 보통 이 시점에는 CPU나 DB 사용률보다 먼저 대기열 길이(queue depth) 와 p95/p99 지연시간이 비정상적으로 튀기 시작합니다. 즉, 문제의 본질은 처리 로직 그 자체보다도 “입구에서 얼마나 요청을 받아들일지"를 통제하지 못한 데 있습니다.
Rate Limiter가 “초당 요청 수"를 제한하는 도구라면, Admission Control은 “지금 이 순간 시스템이 감당 가능한 동시 작업 수"를 통제하는 도구입니다. 둘은 비슷해 보이지만 보호 대상과 작동 지점이 다릅니다. 실무에서는 두 장치를 함께 써야 장애 전파를 막을 수 있습니다.
이 글에서 얻는 것
- Admission Control과 Concurrency Limit의 차이를 운영 관점에서 구분할 수 있습니다.
- p95 지연, 큐 길이, 에러율 기반으로 제한값을 잡는 실무 의사결정 기준을 얻습니다.
- API/비동기 워커/DB 앞단에서 어디에 어떤 제한을 둘지 우선순위를 정리할 수 있습니다.
핵심 개념/이슈
1) Rate Limit만으로는 포화를 막지 못한다
Rate Limit은 유입량을 매끈하게 만들지만, 요청 하나가 오래 붙잡고 있는 리소스(예: DB connection, 외부 API slot)가 고갈되면 여전히 무너집니다. 특히 평균은 멀쩡해도 tail latency가 커지면, 동시 실행 중인 요청 수가 빠르게 누적되어 포화가 가속됩니다.
관련 배경은 API 레이트 리밋과 백프레셔 심화와 Thread Pool 튜닝에서 다뤘듯이, 핵심은 “유입 제한"과 “처리량 한계"를 분리해서 관리하는 것입니다.
2) Concurrency Limit은 시스템의 “현재 체력"을 반영해야 한다
고정값(예: 동시 200개)만 두면 트래픽 패턴이나 다운스트림 상태 변화에 대응이 어렵습니다. 그래서 권장되는 방식은 아래 두 단계입니다.
- 초기 고정값으로 안전하게 시작
- p95 지연/에러율 기반으로 점진적 자동 조정(AIMD 계열)
예시 기준:
- 최근 1분 p95 < 250ms, 에러율 < 1%: limit +5%
- 최근 1분 p95 > 500ms 또는 에러율 > 2%: limit -20%
- 3분 연속 악화 시 강제 보호 모드(신규 요청 일부 즉시 거절)
이 기준은 SLO/SLI/Error Budget 운영과 연결됩니다. 성능 지표를 “보기"만 하면 늦고, 제한값 조정에 직접 연결해야 의미가 있습니다.
3) 거절 전략(Reject Policy)이 없으면 제한은 반쪽짜리다
Concurrency Limit을 걸었는데, 초과 요청을 무조건 대기열에 쌓아두면 결국 타임아웃 폭탄으로 돌아옵니다. 따라서 초과 요청 처리 정책을 명확히 해야 합니다.
- 즉시 거절: 429/503 + Retry-After
- 우선순위 큐: 결제/인증/쓰기 요청 우선
- 오래된 요청 폐기: 큐 대기 1초 초과 시 드롭
실무에서는 “모든 요청을 공평하게 살리기"보다 “핵심 요청을 확실히 살리기"가 정답인 경우가 많습니다. 이는 Timeout/Retry/Backoff 설계와 같이 묶어 설계해야 효과가 납니다.
실무 적용
1) 어디부터 제한할지 우선순위 정하기
아래 순서로 적용하면 실패 비용이 낮고 효과가 빠릅니다.
- 다운스트림 바로 앞: DB pool, 외부 API client, 메시지 브로커 producer
- 애플리케이션 입구: API Gateway 또는 서비스 ingress
- 워크플로우 내부 단계: 고비용 연산 구간(이미지 처리, 대용량 조인)
의사결정 기준:
- 장애 시 전사 영향 큰 경로(결제/주문/로그인) 먼저
- 평균 200ms 이하라도 p99 1초 초과 구간 우선
- 재시도 트래픽 비율이 15% 이상이면 즉시 적용
2) 숫자로 시작하는 초기 설정값 템플릿
팀이 처음 도입할 때는 아래처럼 단순한 시작점이 좋습니다.
- API 동시 처리 한도:
core 수 x 2 ~ 4에서 시작 - DB 커넥션의 70~80%를 상한으로 예약
- 큐 대기 최대 시간: 100
300ms (핵심 API), 5001000ms (비핵심) - 보호 모드 진입: p95 2배 악화가 2분 지속될 때
예: 8코어 서비스, DB pool 60개라면
- API concurrency 초기값 24
- DB-bound endpoint 전용 제한 40
- 큐 대기 200ms 초과 시 503 반환
핵심은 “정답값"을 찾는 게 아니라, 폭주 시 무한 대기를 막는 안전장치를 먼저 세우는 것입니다.
3) 관측 포인트를 최소 5개는 반드시 수집
운영 대시보드에 아래 항목이 없으면 튜닝이 감으로 흐릅니다.
- current concurrency (현재 동시 실행 수)
- queued requests (대기열 길이)
- reject rate (거절 비율)
- queue wait p95/p99
- downstream timeout/error rate
관측성 베이스라인 글처럼 지표를 수집하는 데서 끝내지 말고, 알람 조건까지 붙여야 합니다. 예를 들어 “reject rate 5% 초과 + p95 500ms 초과” 조합은 즉시 대응 신호로 쓰기 좋습니다.
트레이드오프/주의점
너무 낮은 제한값은 인위적 병목을 만든다
안전하게 시작하는 건 맞지만, 제한값이 과도하게 낮으면 정상 트래픽에서도 429/503이 증가합니다. 초기 1~2주 동안은 시간대별 트래픽 분포를 보며 단계적으로 상향해야 합니다.재시도 정책과 충돌하면 오히려 폭주한다
서버에서 거절했는데 클라이언트가 즉시 재시도하면 자기증폭 루프가 생깁니다. Retry-After, 지수 백오프, 최대 재시도 횟수 제한을 반드시 동반해야 합니다.단일 글로벌 제한값은 멀티테넌트 환경에서 불공정하다
특정 테넌트가 슬롯을 점유하면 다른 고객이 피해를 봅니다. 테넌트별 최소 보장 슬롯 또는 가중치 기반 큐가 필요합니다.“에러를 줄이기 위해 거절을 숨기는” 운영은 금물
타임아웃으로 늦게 실패시키는 것보다 빠른 거절이 전체 시스템 안정성에 유리합니다. 대시보드의 단기 성공률보다 사용자 체감 복구시간을 기준으로 판단해야 합니다.
체크리스트 또는 연습
운영 체크리스트
- 핵심 경로(결제/로그인/주문)에 동시성 제한이 분리 적용되어 있다.
- 초과 요청의 거절 정책(상태코드, 메시지, Retry-After)이 정의되어 있다.
- 재시도 클라이언트의 최대 횟수/백오프가 서버 정책과 정합적이다.
- 큐 길이, 거절율, 대기시간 p95/p99 알람이 설정되어 있다.
- 제한값 변경 이력과 장애 후 회고 결과가 문서화되어 있다.
연습 과제
- 지난 2주간 API 지표에서 p95 급등 시점의 동시 실행 수와 큐 길이를 함께 그래프로 그려보세요. 임계점이 보이면 그 수치를 초기 제한값 후보로 삼을 수 있습니다.
- 고비용 엔드포인트 3개를 골라 endpoint별 제한(전역 공유 금지)을 적용하고, 적용 전후의 timeout 비율을 비교해보세요.
- 보호 모드 진입/해제 조건을 runbook으로 작성하고, 게임데이에서 초당 트래픽 3배 상황을 재현해 검증해보세요.
Admission Control은 “요청을 막는 기술"이 아니라, 시스템 전체를 살리기 위해 실패를 통제하는 기술입니다. 장애가 난 뒤에 추가하면 너무 늦습니다. 지금 트래픽이 안정적일 때 제한값, 거절 정책, 관측 지표를 먼저 정의해두는 게 가장 싸고 강한 투자입니다.
💬 댓글