이 글에서 다루는 것

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_successcounter모델 실행 성공 누적
nv_inference_request_failurecounter모델 실행 실패 누적
nv_inference_countcounterinference 총 요청 수
nv_inference_request_duration_ussum처리 시간 합

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 mismatchX
HTTP 4xx/5xxX

HighErrorRate는 “테스트용"이 아니라 운영 감시용으로 유지합니다. HTTP 실패율이 필요하면 Triton이 아니라 Gateway 계층(Nginx/Envoy/Ingress) 메트릭으로 별도 알럿을 둬야 합니다.


B-7) 운영 권장값

알럿테스트운영
Mean latency> 0.000001s> 0.05 ~ 0.1s
Error rate테스트 비권장> 2%
for10s2~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초 루틴)

  1. Pod Up으로 “플랫폼 장애 vs 모델/성능 이슈” 1차 분기
  2. Failure rate / Failure RPS로 “실행 실패” 여부 확인
  3. Latency Avg(ms) 가 튀면
  4. Avg Queue Time으로 “대기열 병목 여부"를 먼저 확인하고, Pending requests와 함께 “밀림(큐) vs 단순 지연"을 1차 분기
  5. 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 — 배치 최적화와 인스턴스 튜닝