이 글에서 다루는 것
Triton 서빙 환경에서 dev/prod 알럿을 완전히 분리하고, PrometheusRule/Alertmanager/Grafana를 하나의 판단 흐름으로 고정하는 GitOps 기반 Alerting 운영 표준 설계
선수지식
이 단계에서 해결하려는 문제
Observability는 대시보드가 아니라, 사고를 막는 운영 정책입니다. 이 문서는 dev/prod 알럿을 완전히 분리하고, 라벨 실수로 인한 교차 전송까지 구조적으로 차단하며, Triton 서빙 품질을 모델 실행 관점에서 감지하도록 설계된 GitOps 기반 Alerting 운영입니다.
알럿 이후 즉시 운영 판단이 가능하도록, Alertmanager 라우팅과 Grafana 대시보드를 하나의 판단 흐름으로 고정했습니다.
🎯 핵심 요약
- PrometheusRule: “언제 이상인가” 정의
- Alertmanager: “어디로 보낼지/차단할지” 최종 통제
- Slack: 단순 수신자
- 기본 receiver는 반드시
"null" - Slack 전송은 namespace 정규식 match로만 허용
- Triton 알럿은 p95 대신 mean latency 기반으로 시작
- Grafana: 알럿 이후 “원인 확인/판단” 표준 (avg latency + queue + pending)
- 알럿-대시보드는 한 세트로 운영 (Detect → Diagnose → Act)
1️⃣ 전체 아키텍처
[Workload (Triton/FastAPI/...)]
↓ metrics
[Prometheus (dev/prod)]
↓ PrometheusRule: "언제 이상인가"
[Alertmanager (dev/prod)]
↓ Routing Policy: "어디로 보낼지/차단할지"
[Slack (#dev / #prod)] → [Grafana Dashboard: Diagnose (30s)]
핵심 원칙
- Alertmanager가 최종 통제 지점
- PrometheusRule은 “알럿 발생"만 담당
- “기본 전송 금지(Null default)“가 안전한 운영의 출발점
- 알럿은 감지, Grafana는 원인 확인(진단) 표준
2️⃣ 운영 정책 (Policy)
| 항목 | 정책 |
|---|---|
| Default receiver | "null" |
| Slack 전송 | routes + matchers로만 허용 |
| 교차(dev↔prod) | 무조건 차단 |
| 예외 | 문서/코드로 명시적으로 추가한 것만 허용 |
| Dashboard | 알럿 발생 시 Grafana로 즉시 원인 확인 (30초 루틴) |
Part A. Alertmanager Dev/Prod 분리 & 철벽 라우팅
A-1) DEV Alertmanager 최종 설정
/tmp/alertmanager-dev.yaml
global:
resolve_timeout: 5m
route:
receiver: "null"
group_by: ["alertname","namespace","service","severity"]
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
routes:
- receiver: "slack-dev"
matchers:
- namespace=~".*-dev|monitoring-dev|kube-system"
receivers:
- name: "slack-dev"
slack_configs:
- api_url: <SECRET>
send_resolved: true
title: "[{{ .Status | toUpper }}][DEV] {{ .CommonLabels.alertname }}"
text: >
*Namespace:* {{ or .CommonLabels.namespace "N/A" }}
*Service:* {{ or .CommonLabels.service "N/A" }}
*Severity:* {{ or .CommonLabels.severity "none" }}
*Summary:* {{ or .CommonAnnotations.summary "No summary" }}
# ... (이하 생략)
- name: "null"
A-2) PROD Alertmanager 최종 설정
/tmp/alertmanager-prod.yaml
global:
resolve_timeout: 5m
route:
receiver: "null"
group_by: ["alertname","namespace","service","severity"]
group_wait: 30s
group_interval: 5m
repeat_interval: 12h
routes:
- receiver: "slack-prod"
matchers:
- namespace=~".*-prod|monitoring-prod|kube-system"
receivers:
- name: "slack-prod"
slack_configs:
- api_url: <SECRET>
send_resolved: true
title: "[{{ .Status | toUpper }}][PROD] {{ .CommonLabels.alertname }}"
text: >
*Namespace:* {{ or .CommonLabels.namespace "N/A" }}
*Service:* {{ or .CommonLabels.service "N/A" }}
*Severity:* {{ or .CommonLabels.severity "none" }}
*Summary:* {{ or .CommonAnnotations.summary "No summary" }}
# ... (이하 생략)
- name: "null"
A-3) SealedSecret 생성 (GitOps 필수)
DEV
kubectl -n monitoring-dev create secret generic alertmanager-config-dev \
--from-file=alertmanager.yaml=/tmp/alertmanager-dev.yaml \
--dry-run=client -o yaml \
| kubeseal \
--controller-namespace kube-system \
--controller-name sealed-secrets \
--namespace monitoring-dev \
--name alertmanager-config-dev \
--format yaml \
> envs/dev/sealed-secrets/monitoring/alertmanager-config-dev.yaml
PROD
kubectl -n monitoring-prod create secret generic alertmanager-config-prod \
--from-file=alertmanager.yaml=/tmp/alertmanager-prod.yaml \
--dry-run=client -o yaml \
| kubeseal \
--controller-namespace kube-system \
--controller-name sealed-secrets \
--namespace monitoring-prod \
--name alertmanager-config-prod \
--format yaml \
> envs/prod/sealed-secrets/monitoring/alertmanager-config-prod.yaml
A-4) 배포 & 반영 절차
git add envs/**/sealed-secrets/monitoring/alertmanager-config-*.yaml
git commit -m"fix(alertmanager): route by namespace + null default"
git push
argocd app sync monitoring-dev
argocd app sync monitoring-prod
kubectl -n monitoring-dev rollout restart sts/alertmanager-monitoring-dev-kube-promet-alertmanager
kubectl -n monitoring-prod rollout restart sts/alertmanager-monitoring-prod-kube-prome-alertmanager
A-5) 최종 검증 (운영 표준)
설정 반영 확인
curl -sk https://alert-dev.local/api/v2/status | jq -r'.config.original'
curl -sk https://alert-prod.local/api/v2/status | jq -r'.config.original'
Cross namespace 차단 확인
curl -sk https://alert-dev.local/api/v2/alerts/groups \
| jq'.[] | select(.labels.namespace=="airflow-prod") | .receiver.name'
기대값: "null"
A-6) 운영 주의사항
- 기본 receiver를
slack-*로 두는 구성 금지- 기본은 “null” -> 허용된 것만 Slack으로 보냄
- Slack 전송은 routes + matchers로만 허용
- 기본은 “null” -> 조건 맞으면 Slack으로 보냄
- 템플릿은
default대신or사용 권장or는 “비었으면 대체값"을 주는 방식 (항상 해석 가능)default는 사용 가능한 환경/버전에 따라 동작이 다를 수 있음
- Alertmanager가 참조하는 Secret을 항상 확인
kubectl -n monitoring-dev get alertmanager -o jsonpath='{.items[0].spec.configSecret}';echo
kubectl -n monitoring-prod get alertmanager -o jsonpath='{.items[0].spec.configSecret}';echo
Part B. Triton Serving Alert 구축 & 검증
B-1) 목적
- Pod up/down이 아니라 모델 실행 관점의 품질 이상 감지
- dev/prod 분리 구조 위에서 namespace 라벨 라우팅으로 자동 연동
B-2) Triton 메트릭 특성
| 메트릭 | 타입 | 의미 |
|---|---|---|
nv_inference_request_success | counter | 모델 실행 성공 누적 |
nv_inference_request_failure | counter | 모델 실행 실패 누적 |
nv_inference_count | counter | inference 총 요청 수 |
nv_inference_request_duration_us | sum | 처리 시간 합 |
Triton의 기본 Prometheus latency 메트릭은 sum/count 형태이므로, histogram 기반 p95를 전제로 하기보다는 mean latency 기반으로 진행했습니다.
B-3) PrometheusRule (DEV 예시)
envs/dev/monitoring/rules/triton-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: triton-alerts
namespace: monitoring-dev
labels:
release: monitoring-dev
spec:
groups:
- name: triton.rules
rules:
- alert: TritonHighErrorRate
expr: |
(
sum(rate(nv_inference_request_failure{job="triton"}[5m]))
/
clamp_min(
sum(rate(nv_inference_request_success{job="triton"}[5m]))
+ sum(rate(nv_inference_request_failure{job="triton"}[5m])),
1
)
) > 0.02
for: 2m
labels:
severity: warning
namespace: triton-dev
service: triton
# ... (annotations 생략)
- alert: TritonMeanLatencyHigh
expr: |
(
sum(rate(nv_inference_request_duration_us{job="triton"}[5m]))
/
clamp_min(sum(rate(nv_inference_count{job="triton"}[5m])), 1)
) / 1e6 > 0.000001
for: 10s
labels:
severity: warning
namespace: triton-dev
service: triton
# ... (annotations 생략)
B-4) 라우팅 전제 조건 (가장 흔한 함정)
“Alertmanager matcher에 namespace를 허용했는데 Slack이 안 온다?”
- matcher는 알럿이 firing일 때만 적용됩니다.
- 즉, Prometheus에 먼저 firing 알럿이 있어야 Alertmanager가 라우팅을 판단합니다.
확인:
curl -sk https://prometheus-dev.local/api/v1/alerts \
| jq'.data.alerts[] | select(.labels.alertname|test("Triton"))'
B-5) 테스트 절차 (운영 표준)
1) 클러스터 내부에서 inference 발생
kubectl -n triton-dev run loadgen --rm -i --restart=Never \
--image=curlimages/curl:8.6.0 -- \
sh -lc '
for i in $(seq 1 20); do
curl -sS http://triton.triton-dev.svc.cluster.local:8000/v2/models/best_model/infer \
-H "Content-Type: application/json" \
-d "{\"inputs\":[{\"name\":\"input\",\"shape\":[1,4],\"datatype\":\"FP32\",\"data\":[1,2,3,4]}]}" \
>/dev/null || true
done
echo done
'
2) Prometheus에서 firing 확인
curl -sk https://prometheus-dev.local/api/v1/alerts \
| jq'.data.alerts[] | select(.labels.alertname=="TritonMeanLatencyHigh")'
3) Alertmanager receiver 확인
curl -sk https://alert-dev.local/api/v2/alerts/groups \
| jq'.[] | select(.labels.alertname=="TritonMeanLatencyHigh") | .receiver.name'
기대값: "slack-dev"
B-6) HighErrorRate 테스트가 어려운 이유
nv_inference_request_failure는 모델 실행 단계에 들어간 요청 중 실패만 카운트합니다.
따라서 HTTP 실패여도 failure가 안 오를 수 있습니다.
| 실패 유형 | failure 증가 |
|---|---|
| 존재하지 않는 모델 호출 | X |
| JSON 파싱 오류 | X |
| input/output 스펙 오류 | X |
| shape mismatch | X |
| HTTP 4xx/5xx | X |
HighErrorRate는 “테스트용"이 아니라 운영 감시용으로 유지합니다. HTTP 실패율이 필요하면 Triton이 아니라 Gateway 계층(Nginx/Envoy/Ingress) 메트릭으로 별도 알럿을 둬야 합니다.
B-7) 운영 권장값
| 알럿 | 테스트 | 운영 |
|---|---|---|
| Mean latency | > 0.000001s | > 0.05 ~ 0.1s |
| Error rate | 테스트 비권장 | > 2% |
| for | 10s | 2~5m |
Part C. Grafana (Triton Serving) – 알람 이후 ‘판단 표준’
C-1) 목적
알람은 “이상"을 알려주지만, 운영에서 중요한 건 즉시 원인 확인입니다.
Triton은 기본 latency 메트릭이 histogram이 아니라 sum/count 기반이므로, 본 시리즈에서는 p95 대신 avg latency(ms)를 표준으로 삼고, avg latency + queue time + pending requests 같은 핵심 지표로 원인 확인을 가능하게 했습니다.
C-2) Dashboard 설계 원칙
- Triton 기본 metrics만으로 100% 동작
- “No data” 패널이 나오지 않도록 rate + avg(ms) 기반으로 설계
- 운영자가 바로 판단 가능한 신호만 남김
- RPS(success/failure)
- Failure rate(%)
- Pod Up
- Latency avg(ms)
- Avg Queue Time(ms)
- Pending requests
C-3) Dashboard Import (JSON)
Grafana -> Dashboard -> Import -> JSON 붙여넣기
- UID:
triton-serving-model-kpis-v2 - Title:
Triton Serving - Model KPIs (v2, avg-latency) - Variables:
job / instance / model / version
model / version 변수는 MLflow -> Triton 버전 디렉터리(version 라벨) 흐름과 연결되어 모델 버전별 성능 비교, 롤백 판단에 바로 사용 가능합니다.
C-4) “No data 패널” 방지용 최소 테스트
짧은 순간 부하에서는 Stat이 0으로 떨어질 수 있으므로 1~2분 수준의 짧은 지속 부하로 패널을 채웁니다.
kubectl -n triton-dev run loadgen --rm -i --restart=Never \
--image=curlimages/curl:8.6.0 -- \
sh -lc '
end=$((SECONDS+90))
while [ $SECONDS -lt $end ]; do
curl -sS http://triton.triton-dev.svc.cluster.local:8000/v2/models/best_model/infer \
-H "Content-Type: application/json" \
-d "{\"inputs\":[...]}" \
>/dev/null || true
done
echo done
'
Grafana Time range는 Last 5 minutes로 두면 확인이 가장 빠릅니다.
C-5) 운영에서의 사용법(알람 뜬 뒤 30초 루틴)
- Pod Up으로 “플랫폼 장애 vs 모델/성능 이슈” 1차 분기
- Failure rate / Failure RPS로 “실행 실패” 여부 확인
- Latency Avg(ms) 가 튀면
- Avg Queue Time으로 “대기열 병목 여부"를 먼저 확인하고, Pending requests와 함께 “밀림(큐) vs 단순 지연"을 1차 분기
- Pending requests가 증가하면 HPA/동시성/리소스 병목 대응으로 연결
설계 판단 (Why This Way?)
Default receiver를 null로 고정하여 라벨 실수로 인한 알럿 오전송을 구조적으로 차단하는 화이트리스트 방식을 채택했습니다. Triton 기본 메트릭이 histogram이 아닌 sum/count 형태이므로 p95 대신 mean latency + queue time + pending requests 조합으로 병목을 판단합니다.
다음에 읽을 글
→ Triton 서빙 플랫폼 - dynamic_batching + instance_group — 배치 최적화와 인스턴스 튜닝