원문 캡처 · github.com
원문 캡처 · github.com

Sturnus는 지연 시간과 오류율을 바탕으로 여러 대규모 언어 모델(LLM) 제공업체에 요청을 자동 분산하는 정적 바이너리다. OpenAI 호환 API를 제공하며, 애플리케이션 옆에서 실행하는 사이드카로 사용할 수 있다. 별도의 인프라를 두지 않고도 현재 가장 빠르고 이용 가능한 제공업체로 트래픽을 보낸다.

GitHub 저장소에서 라이선스, 릴리스, 컨테이너 이미지(ghcr.io/sturnus-dev/sturnus)를 확인할 수 있다. Sturnus는 MIT 라이선스이며, 모든 기능을 사용자 인프라 안에서 실행한다.

설치와 사용

Sturnus를 실행하려면 config.toml이 필요하다. config.example.toml을 복사한 뒤 제공업체 정보를 추가한다.

Docker는 프로덕션 배포와 쿠버네티스 사이드카에 적합하다.

bash
docker run -v ./config.toml:/config.toml \\
 -p 4000:4000 \\
 ghcr.io/sturnus-dev/sturnus:latest

Rust 툴체인이 있다면 로컬 테스트를 위해 cargo install을 사용할 수 있다.

bash
cargo install sturnus
sturnus --config config.toml

Linux와 macOS용 사전 빌드 정적 바이너리는 x86_64와 aarch64 아키텍처로 제공되며, 각 릴리스에 첨부된다.

OpenAI 호환 SDK에서는 기본 URL만 Sturnus 주소로 바꾸면 된다.

diff
- client = OpenAI(base_url="https://api.openai.com/v1", api_key="sk-...")
+ client = OpenAI(base_url="http://127.0.0.1:4000/v1", api_key="unused")
python
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:4000/v1", api_key="unused")
response = client.chat.completions.create(
 model="fast", # resolved by sturnus to the fastest available candidate
 messages=[{"role": "user", "content": "Hello"}],
)

fast는 설정에 등록한 별칭이며, Sturnus가 현재 가장 빠르게 이용할 수 있는 후보로 연결한다.

설정과 제공업체

아래 예시는 수신 주소, 제공업체, 모델 별칭, 라우팅 설정을 지정한다. Vertex AI는 GKE Workload Identity를 사용하므로 API 키가 필요하지 않다.

toml
listen = "127.0.0.1:4000"

# Optional: a second listener serving /metrics, /health, /healthz, /status
# Bind it to a routable address so Prometheus can scrape it.
# metrics_listen = "0.0.0.0:4040"

# Providers: where to send requests
[provider.openai]
base_url = "https://api.openai.com/v1"
api_key = "${OPENAI_API_KEY}"

# Vertex AI via GKE Workload Identity (no API key needed)
[provider.vertex]
vertex_ai = { project_id = "my-gcp-project", location = "us-central1" }

# Model map: aliases the client uses → provider+model candidates
[model]
fast = [
 { provider = "openai", model = "gpt-4o-mini" },
 { provider = "vertex", model = "google/gemini-2.5-flash" },
]

[routing]
ewma_alpha = 0.3 # smoothing for the latency and success-rate EWMAs (higher = more reactive)
error_threshold = 0.5 # error-rate EWMA above which a session-affinity pin is broken (routing weights are unaffected)

모든 제공업체와 설정 옵션은 config.example.toml에 나와 있다. 예시에는 Groq, Azure, Google AI Studio, Anthropic, 로컬 OpenAI 호환 제공업체가 포함된다.

${VAR} 형식의 환경 변수는 설정을 불러올 때 치환된다. KEY=VALUE 형식의 .env 파일을 사용한다면 --env-file로 지정한다.

bash
sturnus --env-file /secrets/.env

각 제공업체에는 모든 외부 요청에 함께 보낼 헤더를 headers 맵으로 지정할 수 있다. Vertex 프로비저닝 처리량 같은 상위 서비스의 라우팅 설정에 활용할 수 있다.

toml
[provider.vertex-pt]
vertex_ai = { project_id = "my-project", location = "us-central1" }
headers = { "X-Vertex-AI-LLM-Request-Type" = "dedicated" }

헤더는 모델 별칭이 아니라 제공업체별로 설정한다. 같은 제공업체를 가리키는 별칭 둘은 같은 헤더를 사용하므로, 별칭별 헤더를 다르게 하려면 제공업체를 따로 설정해야 한다.

Vertex 제공업체는 요청에 사이드카가 관리하는 labels를 넣어 GCP Billing Export에서 비용을 태그할 수 있다. 배포 식별 정보 등은 최상위 [attribution] 블록에 두며, 보통 환경 변수에서 값을 가져온다. 기능을 사용하도록 설정한 Vertex 제공업체의 요청 본문에 라벨이 병합된다.

toml
[attribution]
service = "${SERVICE_NAME}"
owner = "${OWNER}"
env = "${ENV}"

[provider.vertex]
vertex_ai = { project_id = "my-project", location = "us-central1", attribution = true }

사이드카 키는 같은 이름의 클라이언트 제공 labels 키보다 우선하며, 이름이 겹치지 않는 클라이언트 키는 보존한다. 이 기능은 현재 Vertex에만 적용된다. 키와 값은 Vertex의 이름 규칙([a-z][a-z0-9_-]{0,62})을 따라야 한다.

라우팅과 세션 고정

Sturnus는 요청 본문에서 model 필드만 바꾸고 나머지는 바이트 단위로 그대로 전달한다. 키 순서, 숫자 정밀도, 서식을 보존하며, 응답도 도착하는 대로 전달한다. 여기에는 SSE text/event-stream 청크가 포함된다.

후속 요청을 같은 제공업체에 보내려면 응답의 x-session-affinity 헤더를 다음 요청에 돌려보낸다. 여러 차례 이어지는 대화에서 문맥이 제공업체별로 달라질 때 유용하다.

python
response = client.chat.completions.create(
 model="fast",
 messages=[{"role": "user", "content": "Hello"}],
)
affinity = response.headers["x-session-affinity"] # e.g. "openai/gpt-4o-mini"

response = client.chat.completions.create(
 model="fast",
 messages=[{"role": "user", "content": "Follow-up"}],
 extra_headers={"x-session-affinity": affinity},
)

세션 고정은 상태를 저장하지 않으며, 여러 파드에서도 공유 상태 없이 작동한다. 고정된 후보의 오류율 지수이동평균(EWMA)이 error_threshold를 넘을 때까지 해당 후보를 사용한다. 기본 평활화 설정에서는 대략 연속 두 번 오류가 발생하면 고정이 해제된다. 이때 헤더는 무시되고 새 제공업체를 선택하며, 응답에서 갱신된 x-session-affinity를 확인할 수 있다. 알 수 없거나 형식이 잘못된 헤더는 일반 라우팅으로 처리한다.

라우팅은 다음과 같이 작동한다.

  • 클라이언트가 "model": "fast"를 포함한 POST /v1/chat/completions 요청을 보낸다.
  • 사이드카가 fast 별칭의 후보를 찾고, 후보별 유효 지연 시간을 계산한다. 유효 지연 시간은 지연 시간 EWMA를 성공률 EWMA로 나눈 값이다. 오류 확률이 p인 후보는 성공 한 번을 얻기까지 약 1/(1-p)회의 시도가 필요하므로, 오류도 느린 응답처럼 유효 지연 시간을 늘린다.
  • 후보마다 (best_effective / its_effective)^k에 따라 가중치를 준다. 가장 나은 후보가 대부분의 트래픽을 받고, 성능이 낮은 후보는 점점 적지만 0은 아닌 몫을 받는다. 결정론적 저불일치 수열인 황금비 베일 수열을 사용해 가중치를 실제 선택으로 바꾼다.
  • 성능이 낮은 후보에도 일부 트래픽이 계속 들어가므로 EWMA를 최신 상태로 유지한다. 응답이 빨라지거나 오류가 멈춘 제공업체는 자동으로 트래픽을 되찾는다. 지연 시간 데이터가 없는 후보는 첫 표본을 얻을 때까지 가장 나은 후보의 4분의 1 비율로 탐색되며, 성공률에 따라 조정된다.
  • model 필드를 실제 모델 이름으로 바꾸고 인증 헤더를 설정한 뒤 요청을 전달한다.
  • 첫 응답 청크가 도착한 시점의 시간 간격(TTFC)을 측정해 EWMA에 반영한다. 응답 상태 코드도 성공률 EWMA에 반영하며, 업스트림 4xx를 포함해 2xx가 아닌 응답은 모두 오류로 센다.

가장 나은 제공업체를 집중적으로 사용하면서도 성능이 낮은 후보가 측정에 필요한 트래픽을 받도록 설계했다. 후보의 탐색 몫은 성능이 나쁠수록 줄지만 최소 1%를 유지한다. 이에 따라 복구된 제공업체를 다시 찾아내는 데 드는 비용은 최대 약 100회 요청이며, 장애 중에는 별칭 트래픽의 최대 약 1%가 실패 중인 후보에 배정된다.

라우팅은 지연 시간을 기준으로 하며 비용이나 품질을 기준으로 삼지 않는다. 별칭 하나에 속한 후보 모델은 대체로 서로 바꿔 쓸 수 있어야 한다. Sturnus는 속도를 위해 품질이나 비용을 맞바꾸지 않고, 이미 동등하다고 판단한 선택지 가운데 가장 빠른 모델을 고른다.

API와 관측 기능

프록시용 POST 경로는 listen에서만 제공한다. 관측용 GET 경로는 listen과, 설정한 경우 metrics_listen에서 제공한다.

메서드경로설명
POST/v1/chat/completions업스트림으로 전달하며 모델 별칭을 해석한다
POST/v1/embeddings업스트림으로 전달하며 모델 별칭을 해석한다
GET/health{"status":"ok"}를 반환한다
GET/status후보별 스트리밍·비스트리밍 EWMA, 오류율, 상태를 반환한다
GET/metrics아래 설명과 같은 Prometheus 메트릭을 제공한다

/metrics의 Prometheus 메트릭은 alias, provider, model 라벨을 가진다.

메트릭유형의미
sturnus_requests_totalcounter완료된 응답 수. status_code 라벨을 추가하며 업스트림 4xx/5xx를 포함한다
sturnus_ttfc_secondshistogram스트리밍 요청의 첫 청크 도착까지 걸린 시간
sturnus_latency_secondshistogram비스트리밍 요청의 전체 응답 시간
sturnus_errors_totalcounter응답을 만들지 못한 전송 실패 수. 시간 초과, 연결 실패, DNS 실패가 해당한다
sturnus_buffer_rejections_totalcounter전체 버퍼 예산이 찼을 때 429로 거부한 요청 수. 별칭별 라벨은 없다

연결 실패 메트릭은 시작 시 0으로 초기화한다. 따라서 메트릭 시계열이 없다는 이유로 오류가 없다고 잘못 판단하지 않는다.

로그는 tracing을 이용한 구조화 형식이다. 터미널에서는 색상 텍스트를 출력하며 NO_COLOR를 따른다. 출력이 파이프나 리디렉션으로 전달되면 줄 단위 JSON을 쓴다. 형식은 --log-format <auto|pretty|json> 또는 STURNUS_LOG_FORMAT으로, 로그 수준은 RUST_LOG로 설정한다. 기본 로그 수준은 sturnus=info다.

각 요청에는 request_id가 포함된 스팬이 생성된다. 클라이언트가 W3C traceparent를 제공하면 trace_id와 parent_span_id로 전달해 서비스 간 상관관계를 파악할 수 있다.

메모리와 배포 조건

요청 버퍼는 요청별·전체 합산 기준으로 제한된다. 메모리 예산을 넘는 요청은 파드가 OOM으로 종료되는 대신 429와 Retry-After를 반환하며 부하를 줄인다. 전체 요청 버퍼 예산의 기본값은 컨테이너 메모리 제한의 절반이다. 시작할 때 cgroups에서 값을 읽고, 값을 가져온 위치를 로그에 남긴다. 다른 상한을 원하면 routing.max_buffered_bytes를 설정한다.

listen은 프록시와 제공업체 인증 정보를 모두 다루므로 배포 환경이 허용하는 가장 제한적인 주소에 바인딩해야 한다. 쿠버네티스 네이티브 사이드카에서는 앱 컨테이너가 파드의 네트워크 네임스페이스를 공유하고 127.0.0.1:4000으로 Sturnus에 접속한다. 이 경우 listen = "127.0.0.1:4000"을 유지하면 인증 정보를 가진 프록시가 다른 파드에서 접근되지 않는다. 메트릭을 수집하려면 metrics_listen = "0.0.0.0:4040"으로 설정할 수 있다. 이 주소에서는 프록시가 아니라 /metrics, /health, /healthz, /status만 제공한다. 프록시가 요청 처리를 마칠 때까지 메트릭 수신기를 유지해 마지막 수집 구간이 사라지지 않게 한다. 파드 안의 컨테이너는 포트를 공유하므로 앱이 사용하지 않는 메트릭 포트를 골라야 한다.

클라이언트가 별도 컨테이너에서 접속하는 독립형 Docker 배포에서는 컨테이너 IP를 통해 접속한다. 이 경우 127.0.0.1 바인딩은 요청을 받지 못하므로 listen을 0.0.0.0:4000으로 설정해야 한다.

쿠버네티스에서는 Sturnus를 네이티브 사이드카로 실행하도록 권장한다. restartPolicy: Always를 설정한 초기화 컨테이너 방식이며, 이 기능은 v1.29부터 안정화됐다. 그러면 앱 컨테이너보다 먼저 시작하고 앱이 종료된 뒤에 종료되므로, 앱의 첫 요청 전에 프록시가 준비되고 앱의 요청 처리가 끝날 때까지 프록시가 유지된다.

이미지는 ghcr.io/sturnus-dev/sturnus에서 멀티 아키텍처(amd64/arm64) scratch 컨테이너로 배포된다. 태그는 시맨틱 버전을 따른다. 예시는 :latest, :5.0, :5.0.0이다.

마운트한 .env 파일로 비밀 정보를 주입하려면 다음과 같이 실행한다.

bash
docker run -v ./config.toml:/config.toml \\
 -v ./secrets.env:/secrets/.env:ro \\
 -p 4000:4000 \\
 ghcr.io/sturnus-dev/sturnus:latest --env-file /secrets/.env

GKE에서는 Vertex Workload Identity를 자동으로 사용한다. GKE 외부에서는 다음 두 방식 가운데 하나로 인증 정보를 제공한다. 프로덕션에는 GOOGLE_APPLICATION_CREDENTIALS가 가리키는 서비스 계정 키를 권장한다.

bash
docker run -v ./config.toml:/config.toml \\
 -v ./sa-key.json:/sa-key.json:ro \\
 -e GOOGLE_APPLICATION_CREDENTIALS=/sa-key.json \\
 -p 4000:4000 \\
 ghcr.io/sturnus-dev/sturnus:latest

로컬 개발에서는 gcloud ADC를 사용할 수 있다. 이미지의 HOME은 /root로 설정되어 있으므로 해당 경로에 인증 파일을 마운트한다.

bash
docker run -v ./config.toml:/config.toml \\
 -v ~/.config/gcloud/application_default_credentials.json:/root/.config/gcloud/application_default_credentials.json:ro \\
 -p 4000:4000 \\
 ghcr.io/sturnus-dev/sturnus:latest

범위와 제약

Sturnus는 단일 정적 바이너리이며 Redis, 데이터베이스, 제어 평면이 필요하지 않다. 각 사이드카는 로컬에서 관찰한 정보만으로 독립적으로 라우팅하므로 공유 상태를 운영할 필요가 없다. 프로젝트는 호스팅 SaaS에 전체 트래픽과 키를 보내거나 기능 범위가 큰 애플리케이션을 두는 방식과 달리, 작고 감사 가능한 표면적을 가진 구성 요소로 제공된다고 설명한다. 자동 지연 시간 기반 라우팅은 많은 게이트웨이에서 엔터프라이즈 요금제에 포함되는 기능이라고 프로젝트는 덧붙인다.

Sturnus는 요청 단위의 장애 조치나 재시도를 하지 않는다. 투명한 프록시로서 업스트림 오류를 가공하지 않고 클라이언트에 그대로 전달한다. 오류 응답은 이후 요청의 라우팅 신호에는 반영되므로 불안정한 제공업체의 우선순위는 빠르게 낮아지지만, 이미 실패한 개별 요청은 그대로 반환된다. OpenAI, Anthropic, LangChain 등의 클라이언트 SDK에는 설정 가능한 재시도와 백오프 기능이 있으므로, 재시도는 SDK에서 설정하고 Sturnus가 상태가 더 나은 제공업체로 보내도록 하는 방식이다.

지출 추적, 프롬프트 관리, UI, 다양한 통합 기능을 갖춘 완전한 LLMOps 플랫폼이 필요하다면 Sturnus는 그런 제품이 아니다.

빌드와 라이선스

bash
# Development
cargo build

# Release (static binary with LTO)
cargo build --release

# Run tests
cargo test

라이선스는 MIT다. 관련 저장소 항목은 설정, 엔드포인트, 관측 기능, 세션 고정, 라우팅 방식, Docker, 빌드, 라이선스에서 확인할 수 있다.