이 글에서 다루는 것

dev/prod 간 메트릭이 교차 수집되는 문제를 라벨 기반으로 완전 분리하고, Prometheus TSDB를 local-path로 전환하여 성능과 안정성을 개선하는 과정을 다룹니다.

선수지식


이 단계에서 해결하려는 문제

dev와 prod는 분리돼 있어야 하는데, 정작 관측 데이터에서는 서로 섞여 보이는 문제가 계속 생겼다. Prometheus/KSM/Kubelet의 라벨과 셀렉터가 조금만 어긋나도 dev에서 prod 메트릭을 읽는 심각한 혼선이 발생한다. 이번 단계에서는 관측 스택을 라벨 기반으로 완전히 절단하는 데 집중한다. 그리고 TSDB는 로컬 디스크로 옮겨 속도와 안정성을 끌어올리고, NFS는 나머지 관측 도구 전용으로 재정비한다.


🎯 핵심 요약

  • 문제
    • prometheus-dev.local에서 airflow-prod, fastapi-prod 메트릭이 섞여서 나옴
    • kube-state-metrics / kubelet(cAdvisor) / ServiceMonitor 라벨이 꼬여 dev<->prod 교차 수집
    • NFS 기반 Prometheus TSDB로 worker2 종료 지연 + I/O 부담 체감
  • 목표
    • dev/prod 메트릭 수집 완전 분리
    • Prometheus TSDB를 local-path로 전환(속도 및 안정성 개선)
    • NFS는 관측 도구(Grafana/Alertmanager/로그 등)에만 사용

1️⃣ 문제 상황 정리 — “왜 dev에서 prod가 보이지?”

1-1. 실제로 겪은 증상

prometheus-dev.local에서 아래 쿼리를 날리면:

curl -skG 'https://prometheus-dev.local/api/v1/query' \
  --data-urlencode 'query=count by(namespace)(kube_pod_info)'

결과에 airflow-prod, fastapi-prod, monitoring-prod 같은 prod 네임스페이스가 같이 등장한다. 겉으로 보기엔 스택은 dev/prod로 나눠져 있는데, 관측 데이터 레벨에서는 섞여 있는 상태였다.

1-2. 진짜 원인들

  1. kube-state-metrics.prometheus.monitor.additionalLabels.release 값을 잘못 넣어서 prod 쪽 KSM도 release=monitoring-dev로 찍혀 버림
  2. Prometheus의 serviceMonitorSelector.matchLabels.release가 dev/prod 간 일관성이 없음
  3. kubelet(cAdvisor) 메트릭에 namespace 필터가 안 들어가 있어서 노드 기준으로 떠 있는 모든 컨테이너 메트릭이 섞여 들어옴

2️⃣ dev/prod 완전 분리 설계 — “라벨로 자르는 기준 만들기”

핵심 개념: 모든 관측 리소스에는 release=monitoring-{env} 라벨을 강제하고, Prometheus는 자기 env의 release 값만 본다.

2-1. Prometheus 셀렉터 통일

dev (apps/monitoring-dev.yaml):

prometheus:
  prometheusSpec:
    serviceMonitorSelector:
      matchLabels:
        release: monitoring-dev
    podMonitorSelector:
      matchLabels:
        release: monitoring-dev
    ruleSelector:
      matchLabels:
        release: monitoring-dev

prod (apps/monitoring-prod.yaml):

prometheus:
  prometheusSpec:
    serviceMonitorSelector:
      matchLabels:
        release: monitoring-prod
    podMonitorSelector:
      matchLabels:
        release: monitoring-prod
    ruleSelector:
      matchLabels:
        release: monitoring-prod

이 셋이 통일돼야 한다: ServiceMonitor, PodMonitor, PrometheusRule 모두 release 라벨이 환경에 맞아야 한다.


3️⃣ kube-state-metrics (KSM) — dev/prod 라벨 & 필터링

3-1. 잘못됐던 부분

# (오류) apps/monitoring-prod.yaml
kube-state-metrics:
  prometheus:
    monitor:
      additionalLabels:
        release: monitoring-dev   # prod인데 dev로 들어가 있었음

prod KSM의 ServiceMonitor도 dev Prometheus가 보는 release=monitoring-dev 라벨 세트를 타버렸다.

3-2. 수정본 (핵심)

# apps/monitoring-prod.yaml
kube-state-metrics:
  prometheus:
    monitor:
      enabled: true
      additionalLabels:
        release: monitoring-prod
      metricRelabelings:
        - action: keep
          sourceLabels: [namespace]
          regex: '.*-prod|monitoring-prod|kube-system'

dev도 동일 구조로 release: monitoring-dev, regex: '.*-dev|monitoring-dev|kube-system'을 적용한다.

3-3. 검증 쿼리

curl -skG 'https://prometheus-dev.local/api/v1/query' \
  --data-urlencode 'query=count by(namespace)(kube_pod_info)'
  • dev: dev, monitoring-dev, kube-system만 등장
  • prod: prod, monitoring-prod, kube-system만 등장

4️⃣ kubelet / cAdvisor — “노드는 공유인데, 메트릭도 공유되면 큰일”

4-1. 문제 포인트

kubelet의 /metrics/cadvisor 엔드포인트는 노드 전체 파드 메트릭을 노출한다. dev/prod Prometheus 둘 다 kubelet을 스크랩하면 한쪽에서 다른 쪽 네임스페이스 메트릭까지 같이 보게 된다. 기본 metricRelabelings/metrics용이고, /metrics/cadvisor에는 적용되지 않는다.

4-2. 해결: cAdvisorMetricRelabelings 추가

dev 예시:

kubelet:
  service:
    labels:
      stack: dev
  serviceMonitor:
    namespaceSelector:
      matchNames: [kube-system]
    selector:
      matchLabels:
        stack: dev
    metricRelabelings:
      - action: keep
        sourceLabels: [namespace]
        regex: '.*-dev|monitoring-dev|kube-system'
    cAdvisorMetricRelabelings:
      - action: keep
        sourceLabels: [namespace]
        regex: '.*-dev|monitoring-dev|kube-system'

prod는 stack: prod, monitoring-prod, .*-prod로 동일 구조.

4-3. 검증 쿼리

curl -skG 'https://prometheus-dev.local/api/v1/query' \
  --data-urlencode 'query=count by(namespace)(container_cpu_cfs_periods_total{job="kubelet", __metrics_path__="/metrics/cadvisor"})'

dev 쿼리 결과에 prod namespace가 보이면 아직 어딘가 구멍이 있는 것이다.


🧩 팁 — “stale metric & 잡소음 알람 정리”

5-1. stale metric 정리

교차 수집 이슈를 해결했어도 이전에 수집된 시계열이 잠깐 남아 있을 수 있다. 환경을 깨끗하게 리셋하고 싶다면:

kubectl -n monitoring-dev  rollout restart deploy/monitoring-dev-kube-state-metrics
kubectl -n monitoring-prod rollout restart deploy/monitoring-prod-kube-state-metrics
kubectl -n monitoring-dev  rollout restart statefulset prometheus-monitoring-dev-kube-promet-prometheus
kubectl -n monitoring-prod rollout restart statefulset prometheus-monitoring-prod-kube-prome-prometheus

5-2. Watchdog 알람 Slack 소음 제거

기본 Watchdog 알람은 “파이프라인이 살아있는지 확인하는 용도"라 항상 firing 된다.

route:
  routes:
    - match:
        alertname: Watchdog
      receiver: "null"

6️⃣ Prometheus TSDB 스토리지 — NFS -> local-path 전환

6-1. 왜 Prometheus만 local-path로 옮겼나?

  • TSDB 특성상 쓰기/읽기 I/O가 많고 민감함
  • NFS 위에 올리면: worker 노드 종료 시 unmount 대기로 종료 지연, 네트워크 레이턴시가 관측 성능에 영향
  • Grafana/Alertmanager는 I/O량이 훨씬 적으므로 NFS로도 충분

구조 정리: Prometheus는 local-path(노드 로컬 디스크), Grafana/Alertmanager는 NFS(nfs-monitoring).

6-2. values 수정

prometheus:
  prometheusSpec:
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: local-path
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi

6-3. 실제 전환 절차 (데이터 초기화 전제)

# (1) 값 반영
kubectl apply -f apps/monitoring-dev.yaml

# (2) Prometheus 일시 중단
kubectl -n monitoring-dev scale statefulset prometheus-monitoring-dev-kube-promet-prometheus --replicas=0

# (3) 기존 NFS PVC 삭제
kubectl -n monitoring-dev delete pvc -l app.kubernetes.io/name=prometheus

# (4) Prometheus 재기동 (local-path 기반 PVC 생성)
kubectl -n monitoring-dev scale statefulset prometheus-monitoring-dev-kube-promet-prometheus --replicas=1

prod도 동일하게 진행한다. ArgoCD selfHeal 때문에 레플리카가 다시 올라오지 않도록 타이밍에 신경 써야 한다.

6-4. PVC 확인

kubectl -n monitoring-dev get pvc | grep prometheus
# STORAGECLASS 열이 local-path 여야 정상

7️⃣ Prometheus TSDB 실제 위치 — “데이터가 디스크 어디에 있나”

7-1. PVC -> PV -> 호스트 경로 추적

kubectl -n monitoring-dev get pvc <prometheus-pvc-name> -o yaml | grep volumeName
kubectl get pv pvc-xxxxxx -o yaml | grep path
# path: /opt/local-path-provisioner/pvc-xxxxxx

컨테이너 내부에서는 같은 볼륨이 /prometheus로 마운트되어 있다. 노드에서 직접 확인하면 chunks_head, wal, snapshots 등 TSDB 구조가 보인다.


8️⃣ NFS는 어디에 쓰나? — 관측 스택 나머지(PVC) 전략

8-1. NFS 동적 프로비저너 (observability 전용)

Loki/Promtail, Grafana, Alertmanager 등 로그/대시보드 계열은 공유 스토리지가 편해서 NFS를 유지한다.

helm upgrade --install nfs-prov \
  nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  -n storage \
  --set nfs.server=192.168.18.141 \
  --set nfs.path=/mnt/nfs_share/mlops/observability \
  --set storageClass.name=nfs-observability \
  # ... (이하 생략)

nfs-observability라는 StorageClass를 생성하여 Loki / Grafana / Alertmanager에 사용한다.

8-2. Monitoring 스택에서 NFS 사용하는 영역

  • Grafana: 대시보드/설정
  • Alertmanager: 알람 히스토리
  • Loki: 로그 보관

Prometheus TSDB는 이미 local-path로 분리했으므로 읽기/쓰기 많은 시계열은 로컬, 공유가 편한 설정/로그는 NFS라는 역할 분리가 된다.


설계 판단 (Why This Way?)

노드 수가 제한된 환경에서 물리적 클러스터 분리 대신 release 라벨 + metricRelabelings로 논리적 메트릭 격리를 구현했습니다. Prometheus TSDB는 고 IOPS가 필수이므로 local-path를, Grafana/Loki 등 I/O가 적은 워크로드는 NFS를 사용하여 스토리지를 워크로드 특성별로 분리했습니다.


다음에 읽을 글

Observability 4단계: Loki/Promtail 로그 파이프라인 구축 — 중앙 집중 로그 수집