•
7분 읽기

LLM 게이트웨이는 왜 균등 분산을 포기하는가: 프리픽스 인지 라우팅

앞선 두 글에서 KV 캐시가 왜 필요한지, 그리고 요청 사이에 그 캐시를 재사용하는 프리픽스 캐싱이 무엇인지 봤다. 이 글은 한 계층 위로 올라간다. 모델 백엔드가 여러 개가 되는 순간 그 앞에 놓이는 게이트웨이가 어떤 문제를 떠안는지에 대한 이야기다.

결론부터 쓰면, LLM 앞단에서는 “요청을 골고루 뿌린다”는 로드밸런싱의 기본 전제가 무너진다.

게이트웨이가 생기는 이유

처음에는 보통 백엔드가 하나다. 추론 엔진을 하나 띄우고 애플리케이션이 직접 호출한다. 그런데 운영을 시작하면 금방 여러 개가 된다.

  • 큰 모델과 작은 모델을 용도별로 나눠 쓴다
  • 자체 호스팅 모델과 상용 API를 함께 쓴다
  • 같은 모델을 여러 복제본으로 띄워 처리량을 늘린다

이때 애플리케이션마다 어느 백엔드를 부를지 하드코딩하면 모델을 바꿀 때마다 코드를 고쳐야 한다. 그래서 앞에 한 겹을 둔다.

이 계층이 성립하는 건 업계가 OpenAI의 HTTP API 모양을 사실상 공통 규격처럼 따라갔기 때문이다. vLLM도 OpenAI 호환 서버를 기본으로 제공한다. 요청 모양이 같으니 중간에 한 겹을 끼워도 앞단 코드가 눈치채지 못한다.

flowchart LR
  APP["애플리케이션 · CLI · 에이전트<br/>POST /v1/chat/completions"] --> GW["게이트웨이"]
  GW --> B1["자체 호스팅 복제본 #1"]
  GW --> B2["자체 호스팅 복제본 #2"]
  GW --> B3["상용 API"]

게이트웨이가 맡는 일은 대체로 이렇다.

기능내용
모델 별칭앱은 논리적 이름 하나만 부르고, 실제 백엔드는 설정에서 바꾼다
라우팅복제본 여럿 중 어디로 보낼지 고른다
폴백백엔드가 죽으면 다른 백엔드로 넘긴다
레이트리밋·쿼터키·팀 단위로 한도를 건다
계량요청·토큰 사용량을 기록한다

그런데 라운드로빈이 캐시를 깨뜨린다

문제는 라우팅이다. 일반적인 HTTP 서비스라면 요청을 균등하게 뿌리는 게 거의 언제나 옳다. 백엔드가 stateless하니 어디로 보내든 결과가 같기 때문이다.

LLM 서빙은 그렇지 않다. KV 캐시는 특정 복제본의 GPU 메모리 안에만 있다. 1번 복제본이 만들어 둔 캐시를 2번 복제본은 볼 수 없다.

flowchart TB
  subgraph RR["라운드로빈 — 대화가 복제본을 떠돈다"]
    direction LR
    R1["1턴<br/>복제본 A<br/>캐시 생성"] --> R2["2턴<br/>복제본 B<br/>캐시 없음 → 전체 prefill"] --> R3["3턴<br/>복제본 C<br/>캐시 없음 → 전체 prefill"]
  end
  subgraph PA["프리픽스 인지 — 같은 앞머리는 같은 곳으로"]
    direction LR
    P1["1턴<br/>복제본 A<br/>캐시 생성"] --> P2["2턴<br/>복제본 A<br/>재사용"] --> P3["3턴<br/>복제본 A<br/>재사용"]
  end

대화가 길어질수록 손해가 커진다. 10턴째 요청의 프롬프트가 2만 토큰이라면, 캐시를 못 쓰는 복제본으로 보내는 순간 2만 토큰어치 prefill을 처음부터 다시 돌린다. 균등 분산을 지키려다 전체 처리량을 깎아 먹는 상황이 된다.

그래서 LLM 게이트웨이는 요청 앞머리의 해시를 보고 같은 앞머리는 같은 복제본으로 보내는 전략을 따로 갖는다. 보통 프리픽스 인지(prefix-aware) 또는 KV 캐시 인지(cache-aware) 라우팅이라 부른다.

세 가지 전략은 서로를 방해한다

실제로는 전략 하나만 쓸 수 없다. 세 가지 목표가 서로 다른 방향을 가리키기 때문이다.

전략최적화 대상부작용
라운드로빈부하 균등캐시 지역성 파괴
최소 지연·최소 대기열응답 시간대기열이 짧다고 캐시가 있는 건 아니다
프리픽스 인지캐시 적중률인기 있는 앞머리를 가진 복제본에 부하가 몰린다

마지막 부작용이 현실적인 함정이다. 사내 에이전트 50개가 같은 시스템 프롬프트를 공유한다면, 앞머리 해시도 같으니 전부 한 복제본으로 몰린다. 캐시 적중률은 완벽하지만 그 복제본만 과부하가 걸리고 나머지는 논다.

그래서 실제 구현은 적중 가능성과 현재 부하를 함께 점수화한다. 캐시를 가진 복제본을 선호하되 그 복제본이 임계치를 넘으면 다른 곳으로 보내고, 보낸 곳에서 새 캐시가 만들어지면 그쪽도 후보가 되는 식이다.

Kubernetes 생태계에서는 Gateway API Inference Extension이 이 문제를 표준화하려 하고 있다. 일반적인 Service 로드밸런싱과 달리 모델 서버의 내부 상태(대기열 길이, KV 캐시 사용률, 적재된 어댑터)를 보고 엔드포인트를 고르는 확장 지점을 정의한다. Envoy AI Gateway나 NVIDIA Dynamo도 같은 방향이다.

핵심은 “백엔드는 다 똑같다”는 가정이 깨졌다는 점이다. 라우팅이 백엔드 내부 상태를 알아야 하는 계층이 됐다.

계량이 하필 여기서 일어나는 이유

게이트웨이가 토큰 집계를 맡는 것도 우연이 아니다. 두 조건이 동시에 만족되는 지점이 여기뿐이다.

  1. 모든 요청이 지나간다. 백엔드마다 따로 세면 상용 API와 자체 호스팅을 한 장부에 합칠 수 없다.
  2. 응답에 사용량이 이미 적혀 있다. OpenAI 호환 응답에는 usage 객체가 들어 있어 입력·출력 토큰 수를 그대로 읽을 수 있다. 게이트웨이가 토크나이저를 돌려 다시 셀 필요가 없다.
{
  "choices": [ "..." ],
  "usage": {
    "prompt_tokens": 1820,
    "completion_tokens": 240,
    "total_tokens": 2060
  }
}

앞 글에서 본 캐시 적중률이 비용 지표라는 이야기가 여기서 운영 도구로 이어진다. 캐시에서 읽은 입력 토큰은 단가가 다르므로, 집계할 때 전체 입력 토큰과 캐시된 입력 토큰을 나눠 기록해야 금액이 맞는다. 제공자별 필드 이름과 할인율은 OpenAI와 Anthropic 공식 문서에서 확인하는 쪽이 안전하다.

집행은 데이터 경로에, 관제는 옆에

마지막으로 게이트웨이를 설계할 때 자주 등장하는 구조가 하나 있다. 한도를 실제로 막는 일과 정책을 정하고 집계하는 일을 분리하는 것이다.

flowchart TB
  CTRL["관제 — 키 발급 · 팀별 한도 정책 · 사용량 집계"]
  GW["게이트웨이 — 요청 경로 위<br/>키 검증 · 라우팅 · 한도 집행 · 토큰 기록"]
  CTRL -- "정책·키 배포" --> GW
  GW -- "사용량 메타데이터" --> CTRL
  APP["애플리케이션"] --> GW --> MODELS["모델 백엔드"]

요청은 게이트웨이만 지나고 관제 쪽은 지나지 않는다. 이렇게 두면 관제 컴포넌트가 멈춰도 서빙은 계속된다. 매 요청마다 중앙에 “이 키 유효한가”를 물으면 그 중앙이 전체 서비스의 단일 장애점이 되기 때문에, 보통 키를 해시 형태로 미리 내려보내고 게이트웨이가 로컬에서 대조한다. 정책 변경은 짧은 전파 지연을 감수한다.

정리

  • 백엔드가 여러 개가 되면 앞에 게이트웨이가 생기고, 그게 가능한 건 OpenAI 호환 API가 사실상 공통 규격이 됐기 때문이다.
  • KV 캐시는 복제본 로컬 상태라, 균등 분산이 캐시를 깨뜨린다. 긴 대화일수록 손해가 커진다.
  • 그래서 프리픽스 인지 라우팅이 필요하지만, 그것만 쓰면 인기 있는 앞머리를 가진 복제본에 부하가 몰린다. 적중 가능성과 부하를 함께 보는 것이 실제 구현이다.
  • 라우팅이 백엔드 내부 상태를 참조하는 계층이 됐고, Gateway API Inference Extension 같은 표준화 시도가 이 지점을 겨냥한다.
  • 토큰 계량이 게이트웨이에서 일어나는 건 모든 요청이 지나는 자리이면서 응답에 usage가 이미 들어 있기 때문이다. 캐시된 입력 토큰은 따로 집계해야 금액이 맞는다.
  • 한도 집행은 요청 경로에, 정책·집계는 그 옆에 두면 관제가 멈춰도 서빙이 산다.

세 편에 걸쳐 토큰 하나가 만들어지는 과정에서 시작해 게이트웨이까지 올라왔다. 관통하는 사실은 하나다. LLM 서빙에서 상태는 GPU 메모리 안에 있고, 그 사실이 위 계층의 설계를 계속 끌어당긴다.