dev notes

ECS에서 EKS로 전환하며 LLM API Rate Limit 영향을 검증해봤다

2026-06-1120 min read
공유

기존 ECS 환경에서 운영하던 서비스들을 EKS로 전환하는 작업을 준비하고 있었습니다.

당시 여러 서비스에서 OpenAI와 Anthropic API를 사용하고 있었고, 일부 기능은 MapReduce 체인을 통해 사용자 요청 하나가 여러 번의 LLM 호출로 이어지는 구조였습니다.

검증하고 싶었던 부분은 EKS로 전환하게되면서 기존보다 pod을 유연하게 늘릴 수 있고, HPA를 통해 트래픽에 따라 더 많은 인스턴스가 동시에 요청을 처리할 수 있기에 애플리케이션이 동시에 처리할 수 있는 요청량이 커지면 외부 LLM API로 나가는 호출량도 순간적으로 크게 증가하지 않을까? 였습니다.

서비스별 API Key는 나뉘어 있었지만 provider의 rate limit은 상위 계정 단위에서 공유되고 있었기 때문에, 여러 서비스가 동시에 scale-out했을 때 특정 서비스의 부하가 다른 서비스의 LLM 호출에도 영향을 줄 수 있다고 봤습니다.

특히 MapReduce를 사용하는 기능은 사용자 요청 하나가 10회 이상의 LLM 호출로 증폭될 수 있어서, EKS 전환 이후 처리량이 커졌을 때 Rate Limit에 어떤 영향을 주는지 마이그레이션 전에 확인해 보기로 했습니다.

EKS 전환 이후 예상했던 흐름#

기존 ECS 환경에서는 인스턴스 수가 비교적 고정되어 있었기 때문에 순간적으로 처리할 수 있는 요청량에도 어느 정도 상한이 있던 반면 EKS에서는 HPA가 빠르게 replica를 늘릴 수 있으니, 애플리케이션 내부의 병목이 먼저 나타나기보다 외부 provider의 RPM이나 TPM 한도에 먼저 도달할 가능성이 있다고 생각했습니다.

일전에 추가한 MapReduce 기능에서는 이 영향이 더 클 수 있었습니다.

text
사용자 요청 1건

입력 분할

여러 번의 LLM 호출

최종 Reduce 호출

사용자 요청 수와 실제 provider 호출 수가 동일하지 않기 때문에, pod 수 증가와 요청 증폭이 겹치면 예상보다 빠르게 rate limit을 소진할 수 있다고 봤습니다.

따라서 문서상 계산만으로 판단하기보다는 실제 HPA가 이 부하에서 어떻게 동작하는지 확인할 필요가 있었습니다.

부하 테스트 환경 구성#

실제 OpenAI나 Anthropic에 수백 명의 동시 요청을 보내면서 테스트할 수는 없었기 때문에, provider의 특성을 어느 정도 재현할 수 있는 Mock LLM 서버를 만들었습니다.

전체 구성은 다음과 같았습니다.

  • Mock OpenAI / Anthropic 서버
    • Token Bucket 기반 rate limit
    • 2~5초 응답 지연
    • Prometheus 메트릭 제공
    • 런타임에 rate limit 변경 가능
  • simple-service
    • 요청당 LLM 1회 호출
    • Provider Fallback 적용
  • mapreduce-service
    • 사용자 요청 하나가 여러 LLM 호출로 이어지는 구조
  • minikube 3노드 클러스터
  • k6
    • 동시 사용자 부하 발생
  • Prometheus / Grafana
    • pod, CPU, 요청량, 429 모니터링
Loading diagram...

테스트에서는 먼저 replica를 2개로 고정한 상태에서 부하를 발생시키고, 이후 동일한 조건에서 HPA를 활성화해 pod 수와 provider 호출량, 429 발생 여부가 어떻게 달라지는지를 비교했습니다.

고정 replica에서의 테스트#

먼저 pod을 2개로 고정해놓고 k6에서 최대 300명의 동시 사용자를 발생시켰습니다.

항목결과
4290건
에러율0.53%
응답 시간 p9512분 44초
처리량3.95 req/s

pod이 처리할 수 있는 양보다 많은 요청이 들어오면서 대기 요청이 쌓였고, 응답 시간은 크게 늘어났습니다.

다만 Mock LLM에 설정한 rate limit에는 도달하지 않아 429는 발생하지 않았습니다.

애플리케이션이 처리량을 충분히 끌어올리지 못한 채 요청을 내부에서 대기시키고 있는 상태에 가까웠습니다.

HPA를 활성화하여 재테스트#

첫 테스트에서는 replica가 3개까지 증가했고 처리량도 크게 높아졌습니다.

항목결과
4290건
에러율0.00%
응답 시간 p955.27초
처리량30.2 req/s

처리량은 3.95 req/s에서 30.2 req/s까지 올라갔고 응답 시간도 크게 줄었습니다.

하지만 예상했던 429는 발생하지 않았습니다.

Mock provider의 Rate Limit이 너무 넉넉한가 싶어서 RPM을 10,000에서 1,000으로, 다시 100까지 낮춰 테스트했습니다.

100 RPM에서는 429가 발생했지만 HPA 환경에서만 나타난 현상은 아니었습니다. pod을 고정한 테스트에서도 비슷하게 429가 발생했습니다.

그래서 HPA 상태를 다시 확인했습니다.

text
NAME                    TARGETS       REPLICAS
simple-service-hpa      cpu: 1%/50%   2

CPU 사용률은 약 1%였고 HPA의 scale-out 기준은 50%였습니다.

앞에서 replica가 3개까지 올라갔던 것은 이전 테스트 상태가 남아 있던 영향이었고, 테스트 환경을 초기화한 뒤 다시 확인하니 LLM 요청 부하만으로 CPU 기반 HPA가 적극적으로 scale-out하지 않았습니다.

처음 가정했던

text
트래픽 증가
  → CPU 증가
  → HPA Scale-out
  → LLM API 호출량 증가

라는 흐름이 실제 테스트에서는 나타나지 않은 것입니다.

외부 LLM 응답 대기가 CPU 사용률로 나타나지 않았다#

테스트한 서비스에서는 요청 처리 시간의 대부분을 외부 LLM API의 응답을 기다리는 데 사용하고 있었습니다.

Loading diagram...

JSON 직렬화나 HTTP 처리처럼 CPU를 사용하는 구간도 있지만, 전체 요청 처리 시간에서 외부 API 응답을 기다리는 시간이 훨씬 길었습니다.

동시 요청이 늘어나면 처리 중인 요청과 대기 중인 스레드는 증가하지만, 이 시간이 그대로 CPU 사용률 상승으로 이어지는 것은 아니었습니다.

실제로 테스트에서도 요청 부하는 높아졌지만 CPU는 약 1% 수준에 머물렀습니다.

text
예상
 
트래픽 증가
  → CPU 증가
  → HPA Scale-out
  → LLM 호출량 증가
 
 
실제 테스트
 
트래픽 증가
  → 외부 LLM 응답 대기 증가
  → CPU 사용률은 낮은 수준 유지
  → CPU 기반 HPA는 크게 반응하지 않음

따라서 이번처럼 외부 LLM API 호출이 요청 처리 시간의 대부분을 차지하는 서비스에서는 CPU 사용률만으로 실제 부하를 판단하기 어려울 수 있었습니다.

물론 제가 구성한 환경에서만 적용되는 조건이지 모든 LLM 서비스가 동일하다는 의미는 아닙니다. 로컬 추론이나 무거운 데이터 전처리처럼 CPU를 많이 사용하는 로직이 함께 있다면 HPA의 동작도 달라질 수 있습니다.

이번 테스트에서는 외부 API 응답 대기가 지배적이었고, CPU가 실제 병목을 나타내는 지표가 아니었습니다.

Rate Limit 리스크를 운영 데이터 기준으로 재확인#

CPU 기반 HPA가 Rate Limit을 직접적으로 키울 것이라는 처음 가정은 테스트에서 확인되지 않았기에 HPA와 pod 수를 제외하고, 운영 환경에서 실제로 발생하는 LLM 요청량을 기준으로 다시 계산했습니다.

특히 MapReduce를 사용하는 기능의 호출 증폭을 함께 봤습니다.

Loading diagram...

운영 로그의 요청 수와 평균적인 토큰 사용량을 기준으로 성수기 부하를 투영해 보니, 일부 기능에서는 RPM보다 TPM(Token Per Minute) 한도에 먼저 가까워졌습니다.

사용자 요청 한 건의 입력에 프롬프트와 컨텍스트가 포함되고, MapReduce 과정에서는 이 호출이 여러 번 발생하기 때문에 단순한 HTTP 요청 건수만으로 capacity를 판단하기 어려웠습니다.

운영 로그의 현재 사용량을 기준으로 요청량이 증가했을 때 provider의 어느 한도에 먼저 가까워지는지를 계산한 값이지 실제 429 장애를 관측한 결과는 아닙니다. 테스트를 진행하면서 외부 LLM Provider를 사용하는 서비스에서 capacity를 볼 때 함께 고려해야 할 항목들도 몇 가지 정리해봤습니다.

RPM만 보지 말고 TPM도 함께 고려하기#

LLM 기능에서는 사용자 요청 수와 실제 Provider 호출량이 일치하지 않을 수 있습니다.

예를 들어 MapReduce 구조에서는 하나의 사용자 요청이 입력 분할에 따라 여러 번의 LLM 호출로 이어집니다.

text
사용자 API 요청 수

실제 LLM Provider 호출 수

따라서 capacity를 계산할 때 서비스의 RPS나 Provider의 RPM(Requests Per Minute)만 보기보다, 요청당 LLM 호출 횟수와 토큰 사용량을 함께 확인할 필요가 있었습니다.

특히 긴 문서를 처리하는 기능은 한 번의 호출에서 사용하는 프롬프트와 컨텍스트가 크고, MapReduce 과정에서 이런 호출이 반복됩니다. 이런 경우에는 요청 횟수에는 아직 여유가 있어도 TPM(Tokens Per Minute) 한도에 먼저 가까워질 수 있습니다.

결국 기능별 capacity를 볼 때는 다음처럼 나눠 보는 편이 적절했습니다.

text
사용자 요청량
  × 요청당 LLM 호출 수
  × 호출당 평균 토큰 사용량

CPU가 낮아도 다른 리소스가 먼저 부족할 수 있다#

테스트한 서비스는 blocking 방식으로 외부 LLM API를 호출하고 있었습니다.

요청 처리 시간의 대부분을 외부 응답을 기다리는 데 사용했기 때문에 CPU 사용률은 낮게 유지됐지만, 그렇다고 애플리케이션에 충분한 처리 여유가 있다는 의미는 아니었습니다.

외부 응답을 기다리는 동안에도 요청을 처리하는 스레드와 HTTP connection 같은 리소스는 점유될 수 있기 때문입니다. 동시 요청이 계속 증가한다면 CPU보다 thread pool이나 connection pool이 먼저 병목이 될 수 있습니다.

text
CPU 사용률이 낮다

애플리케이션에 여유가 있다

이번 테스트에서 CPU 기반 HPA가 scale-out하지 않았다는 것과 서비스의 capacity가 충분하다는 것은 별개의 문제였습니다.

API Key를 분리해도 Rate Limit의 범위는 확인해야 한다#

서비스별로 API Key를 분리해서 사용하고 있더라도 각 서비스가 완전히 독립된 Provider capacity를 사용하는 것은 아닐 수 있습니다.

Rate Limit이 적용되는 범위는 Provider와 계정 설정에 따라 Organization, Project, Workspace 등으로 달라질 수 있기 때문에, Key를 몇 개로 나눴는지가 아니라 실제 Rate Limit이 어떤 단위로 적용되는지를 확인할 필요가 있었습니다.

여러 서비스가 같은 상위 Rate Limit을 공유하는 구조라면 한 서비스의 사용량이 급격하게 증가했을 때 다른 서비스가 사용할 수 있는 여유도 함께 줄어듭니다.

따라서 서비스별 요청량만 따로 보는 것보다, 같은 Provider capacity를 공유하는 서비스들의 전체 RPM과 TPM을 같이 보는 편이 적절했습니다.

429가 발생했을 때의 처리 방식도 확인하기#

Rate Limit에 실제로 도달했을 때 애플리케이션이 어떻게 동작하는지도 함께 확인했습니다.

당시에는 서비스마다 LLM 호출 코드가 따로 존재했고, 429 응답에 대한 처리 방식도 통일되어 있지 않았습니다. 재시도 가능한 응답인지 확인하거나 Provider가 제공하는 retry 관련 정보를 활용해 재시도 시점을 조절하고, 즉시 처리하기 어려운 요청을 지연시키는 등의 정책이 필요했습니다.

그렇지 않으면 Rate Limit에 도달하는 순간 Provider의 429가 그대로 사용자 요청 실패로 이어질 수 있습니다.

Provider Fallback 역시 Rate Limit 상황에서는 별도로 볼 필요가 있었습니다.

OpenAI 호출에 실패하면 Anthropic으로 넘길 수 있도록 구성했지만, Fallback Provider가 Primary Provider와 동일한 수준의 capacity를 가지고 있는 것은 아닙니다. 당시 사용 중인 계정의 한도를 비교했을 때도 Fallback Provider의 TPM 여유가 더 작았습니다.

따라서 Primary Provider가 수용하지 못한 요청을 그대로 Fallback Provider로 넘기는 것만으로는 Rate Limit에 대한 대응이 되기 어려웠습니다.

Fallback을 구성할 때도 단순히 “다른 Provider가 있다”는 것보다 그 Provider가 실제로 얼마나 많은 요청과 토큰을 추가로 수용할 수 있는지를 함께 확인해야 했습니다.

LLM 서비스의 Capacity를 볼 때 확인하는 값#

테스트 전에는 EKS 전환 이후의 pod 수와 CPU 사용률을 주요 지표로 보고 있었습니다.

부하 테스트와 운영 로그를 함께 확인한 뒤에는 다음 항목을 같이 보는 쪽으로 기준을 바꿨습니다.

Loading diagram...

외부 LLM API 의존도가 높은 서비스에서는 CPU 사용률이 낮더라도 외부 응답을 기다리는 요청이 계속 쌓일 수 있고, 애플리케이션의 CPU보다 내부 대기 리소스나 Provider의 RPM·TPM이 먼저 한계에 도달할 수도 있습니다.

따라서 이런 서비스의 capacity를 확인할 때는 CPU와 pod 수만 보기보다 실제 LLM 호출량, 토큰 사용량, 동시 요청 수, 외부 API latency와 thread/connection pool 상태를 함께 보는 편이 적절했습니다.

참고 자료

Connected Notes