Prometheus/Grafana로 인프라와 모델 드리프트를 한 화면에서 보기
Prometheus/Grafana 기반 시계열 관측에 데이터 드리프트 감지와 모델 성능 추적을 결합해 SLI/SLO로 운영하는 방법을 정리한다.
2026-08-12 · 최초 발행 2025-10-14
서버 CPU도 정상, 응답 지연도 정상인데 추천 모델의 클릭률이 조금씩 떨어지고 있다면 기존 인프라 모니터링으로는 이 문제를 볼 수 없다. 문제는 시스템이 아니라 모델이 보는 데이터의 분포가 바뀌었거나, 모델 자체의 예측 정확도가 흔들리고 있는 것이기 때문이다. Prometheus/Grafana 기반의 실시간 모니터링에 데이터 드리프트 감지와 모델 성능 추적을 얹어야 하는 이유가 여기에 있다 — 인프라 관측(SLI/SLO)과 모델 관측을 하나의 파이프라인으로 묶어야 "장애"와 "성능 저하"를 모두 잡아낼 수 있다.
여러 층위의 관측이 필요하다
Prometheus는 pull 기반으로 시계열을 수집하고, Grafana가 그걸 대시보드와 경보로 시각화한다. Exporter나 Client SDK로 메트릭을 노출하면 Prometheus가 주기적으로 긁어가고, Alertmanager가 규칙에 따라 알림을 라우팅하는 구조다. 이 위에 두 가지 층을 더 얹는다.
하나는 데이터 드리프트 — 입력 특성의 분포가 기준 대비 유의하게 변하는 현상을 KS·PSI·KL 같은 통계 검정이나 임베딩 거리로 점수화하는 것이다. 다른 하나는 모델 성능 추적 — 예측과 실측(라벨)을 비교해 정확도·AUC·F1·캘리브레이션 같은 품질 지표를 지속적으로 관측하는 것인데, 라벨이 실시간으로 오지 않는 경우가 대부분이라 지연 보정과 오프라인/온라인 혼합 측정이 필요하다.
이 세 층위는 성격이 다르다. 인프라/애플리케이션 지표는 초당 수십만 시계열을 밀리초~초 단위로 처리할 수 있고 append-only 구조라 쿼리 일관성이 강하다. 데이터 드리프트는 윈도우·빈 개수에 따라 계산 비용이 달라지고 기준 갱신에 지연이 있어 최종 일관성에 가깝다. 모델 성능 추적은 라벨 도착 지연에 크게 의존해서 최종 일관성을 받아들일 수밖에 없고, 백필과 버전 관리가 별도로 필요하다.
SLI/SLO도 이 세 층위에 맞춰 각각 정의한다. 예를 들어 요청 지연 p95<200ms(99% 준수), 드리프트 PSI<0.1(95% 준수), 주간 AUC≥0.85 같은 식이다.
드리프트 점수와 모델 성능 지표가 만들어지는 경로
메트릭 수집 계층에서는 애플리케이션·모델 서비스에 Prometheus Client SDK를 심어 지연·에러·큐 길이·스루풋·캐시 히트율 같은 핵심 SLI를 노출하고, Node/Process/GPU/Python Exporter로 시스템·런타임 지표를 보강한다. 스크레이프 주기와 보존 기간, 리레이블링 정책은 미리 정해두어야 하고, 특히 라벨 카디널리티가 커지면 저장·쿼리 비용이 급격히 늘어나므로 관리가 필요하다.
데이터 드리프트 감지 파이프라인은 입력 특성 스냅샷을 스트림이나 배치로 수집해 기준 분포와 비교하고, 드리프트 점수를 계산해 게이지 메트릭으로 반영하는 흐름이다. 통계적 유의수준과 다중 검정 보정(FDR)을 적용하고, 스파이크·결측 증가·범주 신규 유입 같은 패턴별로 다르게 대응해야 한다.
모델 성능 피드백 루프는 예측 로그와 지연 도착 라벨을 조인해 기간별로 집계하고 성능 지표를 계산해 대시보드·경보에 반영하는 구조다. 라벨이 늦게 오는 구간에는 스코어 분포 안정성이나 캘리브레이션 ECE 같은 온라인 프록시 지표로 공백을 메운다.
온라인 메트릭은 즉시성을 우선하고 오프라인 성능 지표는 최종 일관성을 받아들이는 것이 이 아키텍처의 기본 전제다. 고카디널리티 라벨은 제한을 두고, 지연 도착 라벨은 백필로 처리하며, 드리프트 기준을 자동으로 갱신하는 경우에는 승인 워크플로를 거치게 하는 편이 안전하다.
코드로 보는 실제 노출 방식
FastAPI 서비스에서 요청 카운터, 에러 카운터, 지연 히스토그램과 함께 드리프트 점수·캘리브레이션 게이지를 함께 노출하면 다음과 같은 모양이 된다. (Python 3.9+, FastAPI 0.103+, prometheus_client 0.16+, Uvicorn 0.23+, Prometheus 2.45+, Grafana 10+ 기준.)
# pip install fastapi uvicorn prometheus_client numpy
from fastapi import FastAPI
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
import time, random
import numpy as np
from starlette.responses import Response
app = FastAPI()
REQS = Counter("inference_requests_total", "총 예측 요청 수", ["model", "route"])
ERRS = Counter("inference_errors_total", "예측 에러 수", ["model", "route"])
LAT = Histogram("inference_latency_seconds", "예측 지연(초)", ["model", "route"], buckets=[0.05,0.1,0.2,0.3,0.5,1,2])
DRIFT = Gauge("drift_psi", "PSI 기반 드리프트 점수", ["feature", "model"])
CAL = Gauge("calibration_ece", "캘리브레이션 ECE", ["model"])
@app.get("/predict")
def predict():
model, route = "recsys-v2", "/predict"
REQS.labels(model, route).inc()
start = time.time()
try:
# dummy inference
time.sleep(random.uniform(0.02, 0.2))
score = random.random()
# online proxy: ECE 근사(데모용)
CAL.labels(model).set(abs(0.5 - score))
return {"score": score}
except Exception:
ERRS.labels(model, route).inc()
return {"error": "inference failure"}
finally:
LAT.labels(model, route).observe(time.time() - start)
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
# 주기 작업 예: 드리프트 점수 업데이트(배치/스트림 결과 반영)
def update_drift(feature, model, ref, cur):
# 간단 PSI 구현(데모)
bins = np.percentile(ref, [0,10,20,30,40,50,60,70,80,90,100])
rh, _ = np.histogram(ref, bins=bins, density=True)
ch, _ = np.histogram(cur, bins=bins, density=True)
rh, ch = np.where(rh==0, 1e-6, rh), np.where(ch==0, 1e-6, ch)
psi = np.sum((rh - ch) * np.log(rh / ch))
DRIFT.labels(feature, model).set(float(abs(psi)))
이 메트릭을 Prometheus가 15초 간격으로 긁어가고, p95 지연·드리프트 점수·에러율에 대한 경보 규칙을 함께 건다.
# prometheus.yml 일부
scrape_configs:
- job_name: "model_service"
scrape_interval: 15s
static_configs:
- targets: ["model-svc:8000"]
# alerting rules
groups:
- name: model-alerts
rules:
- alert: HighLatencyP95
expr: histogram_quantile(0.95, sum(rate(inference_latency_seconds_bucket[5m])) by (le)) > 0.3
for: 5m
labels: { severity: page }
annotations:
summary: "p95 지연 임계 초과"
- alert: DriftPSIHigh
expr: max_over_time(drift_psi[15m]) > 0.2
for: 15m
labels: { severity: ticket }
annotations:
summary: "드리프트 점수 상승"
- alert: InferenceErrorRate
expr: sum(rate(inference_errors_total[5m])) / sum(rate(inference_requests_total[5m])) > 0.01
for: 10m
labels: { severity: page }
지연 경보는 severity: page로 즉시 대응이 필요하다는 걸 표시하고, 드리프트 경보는 severity: ticket으로 낮춰 잡음을 줄인 점을 눈여겨볼 만하다 — 드리프트는 대체로 즉각 대응보다 조사와 재학습 판단이 먼저이기 때문이다.
도메인마다 SLI와 조치가 다르다
전자상거래 추천이라면 p95<150ms, CTR 프록시(스코어 분포·ECE), feature_price_norm의 PSI<0.1을 SLI로 잡고, PSI가 0.2를 넘는 상태가 30분 이상 지속되면 피처 스케일을 재적합하거나 최신 거래량 가중 기준 분포로 전환하는 식으로 대응한다. 결제 이상탐지는 p99<80ms, 주간 오프라인 Recall, 과검(false positive) 경보율을 본다 — 라벨이 48시간 정도 지연된다는 전제 아래, 주간 Recall이 0.9 아래로 떨어지면 임계치를 올리거나 최근 피처 엔지니어링 변경을 롤백하는 조치를 취한다. 콜센터 NLP 라우팅은 주간 Intent F1, 온라인 캘리브레이션 ECE<0.05, 언어 분포 드리프트를 보고, 언어 감지 분포가 크게 바뀌면 다국어 모델로 자동 전환하거나 트래픽을 분할하는 식으로 대응한다.
세 사례 모두 공통점은 "즉시 대응해야 할 인프라 문제"와 "조사 후 재학습·롤백을 판단해야 할 모델 문제"를 SLI 설계 단계에서부터 구분해두었다는 점이다.
운영에서 계속 신경 써야 할 것들
라벨 카디널리티는 반드시 제한해야 한다 — user_id 같은 고유값을 라벨로 쓰면 저장·쿼리 비용이 감당할 수 없이 커진다. 상세한 추적성과 비용은 트레이드오프 관계다. 장기 보존이 필요하면 Thanos·Cortex·Mimir 같은 리모트스토리지를 채택하되, 운영 복잡도가 늘어나는 대가로 과거 데이터 분석이 가능해진다는 점을 감안해야 한다. 드리프트 기준 스냅샷은 버전 관리를 하고 자동 갱신에는 승인 단계를 넣어야 민첩성과 오탐/미탐 리스크 사이의 균형을 잡을 수 있다. 라벨 지연에는 백필 파이프라인과 지표 재계산을 지원해야 하고, Grafana/Prometheus 접근에는 RBAC을, 경보 채널에는 서명과 감사 로그를 둬서 보안과 운영 편의 사이의 균형도 맞춰야 한다.
이런 체계를 갖추면 MTTR(장애 복구 시간)이 3050% 단축되고 근본 원인 파악 시간이 40% 이상 줄어든다. 드리프트를 조기에 잡으면 모델 성능 저하 기간이 70% 축소돼 비즈니스 손실도 함께 줄고, 경보 소거·집계·중복 억제로 경보 노이즈가 3040% 절감되면서 온콜 효율이 개선된다. SLO 준수율은 5~10pp 향상되고 릴리즈 속도도 안정화된다.
Prometheus/Grafana로 인프라 가시성을 확보하는 것과, 데이터 드리프트·모델 성능 추적을 같은 파이프라인에 태우는 것은 별개의 작업처럼 보이지만 결국 하나의 질문 — "지금 이 서비스가 정상인가"에 대한 답을 완전하게 만들기 위한 두 축이다. 온라인 지표의 즉시성과 오프라인 성능의 최종 일관성을 조화시키는 설계, 표준화된 SLI/SLO와 경보·대시보드·백필 파이프라인을 갖추는 것이 이 통합 관측 체계의 핵심이다.