Flagger MetricTemplate로 LLM p99 레이턴시 기반 카나리 자동 롤백 구성하기
모델 버전을 업그레이드한 직후, 프로덕션 p99 레이턴시가 400ms에서 3.2초로 급등했다. 사용자들은 타임아웃 에러를 겪고 있었지만, HTTP 상태 코드는 여전히 200을 반환하고 있어 모니터링 대시보드는 침묵을 유지했다. p99 레이턴시가 슬그머니 3초를 넘어도 일반적인 에러율 기반 롤백은 작동하지 않는다. 이것이 LLM 서비스에 _레이턴시 전용 카나리 분석_이 필요한 이유다.
이 글에서는 Flagger의 MetricTemplate CRD로 LLM 툴 호출 p99 레이턴시를 롤백 트리거로 연결하는 파이프라인의 설정 코드를 완성한다. Python 계측 코드부터 ServiceMonitor, MetricTemplate, Canary YAML까지 순서대로 따라가면 MetricTemplate 파이프라인을 완성할 수 있다.
이 글을 읽기 전 알면 좋은 것들: Kubernetes Pod/Deployment/Service 기초, Prometheus 메트릭 수집 개념(scrape, label), Python FastAPI 기초. Flagger 설치는 공식 설치 가이드를, Prometheus Operator 설치는 kube-prometheus-stack Helm 차트를 참고한다.
핵심 개념
이 세 가지를 이해하면 아래 예시를 바로 따라할 수 있다.
Progressive Delivery와 Flagger
Progressive Delivery: 새 버전을 소수의 사용자에게 먼저 노출하면서 안정성을 검증한 뒤 점진적으로 롤아웃하는 배포 전략. 블루/그린과 달리 트래픽을 퍼센트 단위로 세밀하게 제어하며, 메트릭 분석 결과에 따라 자동 프로모션 또는 롤백이 이루어진다.
Flagger는 CNCF Graduated 프로젝트로, Kubernetes 위에서 카나리 배포·A/B 테스트·블루/그린 배포를 자동화하는 Progressive Delivery 오퍼레이터다. 핵심 동작 원리는 다음과 같다.
- 새 버전 배포 감지 → 카나리 파드 생성
- 트래픽을 설정된
stepWeight씩 점진적으로 카나리로 이동 - 매
interval마다 메트릭 분석 실행 - 임계치 통과 → 프로모션 / 실패 연속 → 롤백
MetricTemplate: Flagger의 CRD로, Prometheus·Datadog·CloudWatch 등에 PromQL/쿼리를 실행해
float64값을 반환받아 임계치와 비교하는 분석 단위.{{ namespace }},{{ target }},{{ interval }},{{ variables.xxx }}같은 템플릿 변수로 쿼리를 동적으로 구성할 수 있다.
Prometheus Operator와 ServiceMonitor
Prometheus Operator는 Kubernetes 위에서 Prometheus 인스턴스를 선언적으로 관리한다. 직접 prometheus.yml을 편집할 필요 없이, ServiceMonitor CRD를 배포하면 레이블 셀렉터 기반으로 스크레이프 타겟이 자동 감지된다.
# Prometheus Operator가 관리하는 서비스 디스커버리 흐름
ServiceMonitor (레이블 셀렉터)
→ Prometheus Operator가 감지
→ prometheus.yml scrape_configs 자동 생성
→ Prometheus가 /metrics 엔드포인트 수집주의:
MetricTemplate의provider.address에 지정하는prometheus-operated는kube-prometheus-stackHelm 차트의 기본 서비스 이름이다. 설치 방법에 따라 이름이 달라질 수 있으므로,kubectl get svc -n monitoring으로 실제 서비스 이름을 반드시 확인해야 한다.
LLM 툴 호출 레이턴시를 왜 롤백 기준으로 삼는가?
LLM 에이전트에서 툴(function) 호출이 포함된 턴은 LLM 추론 + 툴 실행 + 재추론이 연쇄된다. 이 때문에 레이턴시가 일반 대화 턴의 최소 2배 이상이며, 새 버전에서 툴 호출 스키마가 변경되거나 파싱 로직에 회귀가 생기면 p99가 급등한다. HTTP 에러율은 정상이지만 사용자 경험은 이미 파괴된 상태다.
Histogram Quantile: Prometheus의
histogram_quantile(φ, ...)함수는 버킷 데이터를 선형 보간해 φ 분위수를 추정한다. p99 (φ=0.99)는 상위 1% 요청의 레이턴시 상한을 나타낸다. 정확한 보간을 위해 롤백 임계치(예: 3초)를 히스토그램 버킷 경계에 반드시 포함시켜야 한다.
rate() 함수는 카운터 메트릭을 초당 증가율로 변환한다. sum(rate(histogram_bucket[2m])) by (le) 패턴에서 [2m]은 PromQL의 range selector로, 최근 2분간의 증가율을 계산한다. Flagger의 interval: 2m 설정은 분석 주기를 의미하며, 쿼리의 [{{ interval }}]에 이 값이 그대로 삽입된다.
실전 적용
예시 1: Python FastAPI 서버에서 툴 호출 레이턴시 계측
FastAPI 앱에 prometheus_client로 히스토그램을 추가하고 /metrics 엔드포인트를 노출한다. 버킷 설계가 핵심이다 — 롤백 임계치(예: 3초)를 버킷 경계에 반드시 포함시켜야 histogram_quantile의 보간 오차를 최소화할 수 있다.
from prometheus_client import Histogram, make_asgi_app
from contextlib import contextmanager
from fastapi import FastAPI
app = FastAPI()
# /metrics 엔드포인트 마운트
metrics_app = make_asgi_app()
app.mount("/metrics", metrics_app)
tool_call_latency = Histogram(
"llm_tool_call_duration_seconds",
"LLM tool call duration in seconds",
["tool_name", "model"],
# 임계치인 3.0을 버킷에 명시적으로 포함
buckets=[0.1, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0]
)
@contextmanager
def track_tool_call(tool_name: str, model: str):
with tool_call_latency.labels(
tool_name=tool_name,
model=model
).time():
yield
# 사용 예시
# with 블록 안에 await가 있어도 time()은 정상 동작한다.
# contextmanager는 동기이지만 내부 await는 이벤트 루프를 블로킹하지 않는다.
async def call_web_search(query: str) -> str:
with track_tool_call("web_search", "gpt-4o"):
return await search_api(query)서버 실행 후 /metrics에서 메트릭이 노출되는지 확인한다.
uvicorn main:app --reload
curl http://localhost:8000/metrics | grep llm_tool_call_duration| 코드 포인트 | 설명 |
|---|---|
make_asgi_app() |
FastAPI에 /metrics 엔드포인트 마운트 |
buckets=[..., 3.0, ...] |
롤백 임계치를 버킷 경계에 포함 → 보간 정확도 확보 |
["tool_name", "model"] |
레이블로 툴별·모델별 분리 집계 가능 |
contextmanager + time() |
with 블록 진입~종료 시간을 자동 측정 |
예시 2: Kubernetes Service와 ServiceMonitor 구성
ServiceMonitor가 타겟을 감지하려면 먼저 metrics 포트를 노출하는 Service 리소스가 필요하다.
# Service — metrics 포트 노출
apiVersion: v1
kind: Service
metadata:
name: llm-agent
namespace: production
labels:
app: llm-agent # ServiceMonitor의 matchLabels와 일치해야 함
spec:
selector:
app: llm-agent
ports:
- name: http
port: 8000
targetPort: 8000
- name: metrics # ServiceMonitor가 참조하는 포트 이름
port: 8001
targetPort: 8001# ServiceMonitor — /metrics 엔드포인트 자동 수집
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: llm-agent-monitor
namespace: production
labels:
app: llm-agent
spec:
selector:
matchLabels:
app: llm-agent # Service의 레이블과 일치해야 함 (Deployment 레이블 아님)
endpoints:
- port: metrics # Service의 포트 이름
path: /metrics
interval: 15s주의:
ServiceMonitor.spec.selector.matchLabels는Service리소스의 레이블과 정확히 일치해야 한다.Deployment레이블이 아님에 유의.
예시 3: MetricTemplate — p99 레이턴시 쿼리 (기본형 / 확장형)
MetricTemplate CRD를 monitoring 네임스페이스에 배포한다. Flagger가 카나리 분석 시 이 쿼리를 실행해 float64 값을 반환받는다.
기본형 — 카나리 파드 전체의 p99를 측정한다.
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
name: llm-tool-call-p99-latency
namespace: monitoring
spec:
provider:
type: prometheus
# kubectl get svc -n monitoring 으로 실제 서비스 이름 확인 필요
address: http://prometheus-operated.monitoring:9090
query: |
histogram_quantile(0.99,
sum(
rate(
llm_tool_call_duration_seconds_bucket{
namespace="{{ namespace }}",
pod=~"{{ target }}-[0-9a-zA-Z]+.*"
}[{{ interval }}]
)
) by (le)
)Flagger의 카나리 파드 이름은 {deployment-name}-{canary|primary}-{replicaset-hash}-{pod-hash} 규칙을 따른다. pod=~"{{ target }}-[0-9a-zA-Z]+.*" 패턴은 이 규칙에 따라 카나리 파드와 프라이머리 파드를 모두 포함하지만, 카나리 분석 컨텍스트에서 {{ target }}은 카나리 Deployment를 가리키므로 해당 파드만 필터링된다. 이 필터 없이 프라이머리 파드까지 포함하면 카나리의 레이턴시 악화가 희석되어 롤백이 트리거되지 않는다.
확장형 — templateVariables로 특정 툴의 p99만 측정한다. pod 필터와 tool_name 필터를 함께 적용해 카나리 파드 내 특정 툴만 격리 분석한다.
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
name: llm-tool-call-p99-latency
namespace: monitoring
spec:
provider:
type: prometheus
address: http://prometheus-operated.monitoring:9090
query: |
histogram_quantile(0.99,
sum(
rate(
llm_tool_call_duration_seconds_bucket{
namespace="{{ namespace }}",
pod=~"{{ target }}-[0-9a-zA-Z]+.*",
tool_name="{{ variables.toolName }}"
}[{{ interval }}]
)
) by (le)
)| 템플릿 변수 | 값 예시 | 설명 |
|---|---|---|
{{ namespace }} |
production |
Canary 리소스의 네임스페이스 |
{{ target }} |
llm-agent |
Canary의 targetRef.name |
{{ interval }} |
2m |
분석 주기 (Canary spec에서 주입) |
{{ variables.toolName }} |
web_search |
Canary의 templateVariables에서 주입 |
예시 4: Canary 리소스 — 툴별 임계치 분석 정책 선언
확장형 MetricTemplate을 사용해 툴별로 다른 임계치를 적용하는 Canary 설정이다.
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: llm-agent
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: llm-agent
analysis:
interval: 1m
threshold: 5 # 5회 연속 실패 시 자동 롤백
maxWeight: 50 # 카나리 최대 50% 트래픽
stepWeight: 10 # 매 interval마다 10%씩 증가
metrics:
- name: web-search-latency
templateRef:
name: llm-tool-call-p99-latency
namespace: monitoring
templateVariables:
toolName: "web_search"
thresholdRange:
max: 5.0 # web_search p99 > 5초 → 분석 실패
interval: 2m # 쿼리의 [{{ interval }}]에 2m이 삽입됨
- name: code-exec-latency
templateRef:
name: llm-tool-call-p99-latency
namespace: monitoring
templateVariables:
toolName: "code_exec"
thresholdRange:
max: 10.0 # code_exec p99 > 10초 → 분석 실패
interval: 2m장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 자동 안전망 | 운영자 수동 개입 없이 품질 저하를 메트릭으로 감지·롤백 |
| 선언적 GitOps 통합 | YAML 한 장으로 분석 정책 코드화, Flux CD와 자연스럽게 연동 |
| 커스텀 메트릭 유연성 | HTTP 에러율 외에 툴 호출 성공률·토큰 처리량 등 LLM 특화 지표 조합 가능 |
| templateVariables 재사용성 | MetricTemplate 하나로 툴별·서비스별 임계치를 파라미터만 바꿔 적용 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 콜드 스타트 | 카나리 초기 트래픽 부족으로 히스토그램 샘플 불충분 | interval을 최소 2분 이상으로 설정 |
| 히스토그램 버킷 설계 오류 | 임계치가 버킷 경계에 없으면 보간 오차 발생 | 롤백 임계치를 buckets 배열에 명시적으로 포함 |
| Prometheus 주소 오설정 | 서비스 이름이 설치 환경에 따라 다름 | kubectl get svc -n monitoring으로 실제 이름 확인 |
| threshold 튜닝 미흡 | 너무 낮으면 일시 스파이크에 과민 반응, 너무 높으면 롤백 지연 | 기존 프로덕션 p99 분포 분석 후 설정 |
| LLM 레이턴시 고유 변동성 | 입력 길이·모델 상태에 따라 레이턴시 자체가 크게 변동 | 임계치를 기준선 p99 × 2 수준의 상대값으로 설정 고려 |
실무에서 가장 흔한 실수
- 버킷에 임계치 미포함:
buckets=[0.5, 1.0, 2.0, 5.0]으로 설정한 뒤thresholdRange.max: 3.0을 걸면,histogram_quantile이 2.0~5.0 구간을 보간해 실제보다 낮거나 높은 값을 반환한다.3.0을 반드시 버킷에 추가해야 한다. - 카나리 파드 필터 미적용:
pod=~"{{ target }}-[0-9a-zA-Z]+.*"패턴 없이 프라이머리 파드까지 쿼리 범위에 포함하면 카나리의 레이턴시 악화가 희석되어 롤백이 트리거되지 않는다. - ServiceMonitor 레이블 불일치:
ServiceMonitor.spec.selector.matchLabels가Service레이블이 아닌Deployment레이블을 참조해 스크레이프 타겟이 0개가 되는 경우.kubectl get servicemonitor -o yaml로 타겟 감지 상태를 반드시 확인하라.
트러블슈팅
메트릭이 수집되지 않거나 Flagger 분석이 실패할 때 확인해야 할 포인트다.
| 증상 | 확인 명령어 | 체크 포인트 |
|---|---|---|
| ServiceMonitor 타겟 미감지 | kubectl get servicemonitor -n production -o yaml |
matchLabels가 Service 레이블과 일치하는지 |
| Prometheus에서 메트릭 없음 | Prometheus UI → Status > Targets |
해당 타겟의 상태가 UP인지 |
| Canary 분석 실패 로그 | kubectl describe canary llm-agent -n production |
Events 섹션의 메트릭 조회 오류 메시지 |
| histogram_quantile이 NaN 반환 | PromQL: llm_tool_call_duration_seconds_count |
버킷 샘플 수가 충분한지 확인 |
마치며
LLM 서비스의 카나리 배포, 이제 에러율이 아닌 레이턴시로 수호하라. HTTP 200을 반환하면서도 사용자 경험을 파괴하는 레이턴시 회귀를 Flagger MetricTemplate과 Prometheus Operator로 선언적으로 감지하고 자동 롤백할 수 있다.
지금 바로 시작할 수 있는 3단계:
- 계측부터: FastAPI 앱에
prometheus_clientHistogram을 추가하고buckets에 롤백 임계치(예:3.0)를 포함한 뒤,uvicorn main:app --reload로 서버를 기동해curl localhost:8000/metrics | grep llm_tool_call으로 메트릭이 노출되는지 확인한다. - ServiceMonitor 배포: 위 예시 2의 Service와 ServiceMonitor YAML을 클러스터에 적용한 뒤, Prometheus UI의
Status > Targets에서 타겟이UP상태인지 검증한다. - MetricTemplate + Canary 적용: 예시 3·4의 YAML을 순서대로 배포하고,
kubectl describe canary llm-agent -n production으로Events섹션의 분석 로그를 실시간으로 모니터링한다.
다음 편
Grafana 대시보드로 Flagger 카나리 분석 현황을 실시간 시각화하고, AlertManager로 롤백 이벤트를 Slack에 알림 연동하는 법을 다룬다.
참고 자료
- Metrics Analysis | Flagger 공식 문서
- Canary analysis with Prometheus Operator | Flagger
- Canary analysis with Prometheus Operator | Flux
- How it works | Flagger
- vLLM Metrics 공식 문서
- An Introduction to Observability for LLM-based applications using OpenTelemetry
- Canary analysis metrics templating · Issue #418 · fluxcd/flagger
- Prometheus Operator 공식 문서