K8s 고급 #5 옵저버빌리티 — Prometheus / Grafana / Loki / OpenTelemetry

K8s 고급 시리즈의 다섯 번째 글입니다. 지금까지 다룬 CNI, RBAC / IRSA, Admission, CRD / Operator는 모두 클러스터의 동작을 만드는 컴포넌트였습니다. 이 모든 컴포넌트가 잘 굴러가는지를 들여다보는 한 층이 더 필요합니다 — 옵저버빌리티입니다. 어느 Pod가 메모리를 얼마나 쓰는가, 어느 Service의 latency가 평소보다 높은가, 한 요청이 마이크로서비스 사이를 어떻게 흘러갔는가. 이 세 차원이 메트릭,로그,트레이스이고, 그 위에 K8s 운영의 표준 스택이 자리 잡고 있습니다.

이번 시리즈는 K8s 고급 6편입니다.

옵저버빌리티의 세 축 #

옵저버빌리티는 흔히 세 가지 데이터 종류로 갈라 이야기합니다.

무엇인가질문
Metrics시간에 따른 수치 시계열“지금 무슨 일이 일어나고 있는가”
Logs이벤트의 텍스트 기록“그 이벤트의 자세한 사정은 무엇인가”
Traces한 요청이 여러 서비스를 거치는 경로“왜 이 요청이 느렸는가”

이 셋은 서로 보완 관계입니다. 메트릭으로 이상을 발견하고, 로그로 자세한 상황을 짚고, 트레이스로 요청 경로의 어느 구간이 문제인지 좁히는 흐름이 일상의 디버깅 패턴입니다. 운영 클러스터에서 셋을 모두 갖추는 게 표준이고, 각 축의 K8s 도구는 거의 굳어 있습니다.

Metrics — Prometheus 중심의 표준 스택 #

K8s 메트릭의 사실상 표준은 Prometheus입니다. CNCF 졸업 프로젝트이고, K8s 자체의 컴포넌트(API 서버, kubelet, controller-manager, scheduler)가 모두 Prometheus가 이해할 수 있는 형식으로 메트릭을 노출합니다. Prometheus의 모델은 단순합니다.

  • Pull 기반 — Prometheus가 정기적으로 각 타깃의 /metrics 엔드포인트를 HTTP로 긁어 옵니다.
  • 시계열 데이터베이스 — 긁어 온 데이터는 라벨이 붙은 시계열로 저장됩니다.
  • PromQL — 시계열을 질의하는 자체 쿼리 언어.

표준 스택의 컴포넌트 #

운영 클러스터에 Prometheus를 설치하면 거의 항상 다음 컴포넌트가 같이 들어옵니다.

컴포넌트역할
Prometheus Server메트릭 수집 + 저장 + 질의
kube-state-metricsK8s 객체(Deployment, Pod, Node 등)의 상태를 메트릭으로 노출
node-exporter각 노드의 시스템 메트릭(CPU, 메모리, 디스크) 노출. DaemonSet으로 노드마다 한 개.
Alertmanager알람 라우팅, 묶음, 침묵 처리
Pushgateway (선택)짧게 사는 Job의 메트릭을 push로 받음

이 묶음을 한 번에 설치해 주는 표준 매니페스트가 kube-prometheus-stack Helm 차트입니다. 운영 클러스터의 도입 첫 단계로 거의 표준이 되어 있습니다.

ServiceMonitor / PodMonitor — Prometheus Operator의 역할 #

Prometheus의 scrape 대상을 직접 매니페스트로 적는 대신, Prometheus Operator가 도입한 CRD가 두 개 있습니다.

servicemonitor-app.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app
  namespace: my-app
  labels:
    release: prometheus  # kube-prometheus-stack의 selector와 일치
spec:
  selector:
    matchLabels:
      app: my-app
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics

이 ServiceMonitor를 적용하면 Prometheus Operator가 자동으로 Prometheus 설정을 갱신해서 그 Service의 Pod에서 30초마다 메트릭을 긁어 옵니다. 매니페스트로 scrape 대상을 선언하는 K8s-native 방식입니다.

PodMonitor는 Service 없이 Pod에 직접 붙는 변형입니다. 이 두 CRD 덕에 애플리케이션 팀은 Service 옆에 ServiceMonitor 한 장을 같이 적기만 하면 메트릭 수집이 자동으로 시작됩니다.

한 줄짜리 PromQL 예시 #

PromQL은 그 자체로 깊은 주제이지만, 운영에서 가장 자주 쓰는 패턴 몇 가지를 짚어 두겠습니다.

네임스페이스의 Pod 메모리 사용률 합계
sum(container_memory_usage_bytes{namespace="payments"}) by (pod)
지난 5분간 5xx 응답 비율
sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m]))
P95 latency (히스토그램)
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)

rate()는 시계열의 초당 증가량을, histogram_quantile()은 히스토그램에서 특정 분위수를 계산합니다. 이 두 함수가 PromQL 활용의 70% 정도를 덮습니다.

Logs — Loki 중심의 새 스택 #

로그 수집의 옛 표준은 EFK 스택(Elasticsearch + Fluentd + Kibana)이었습니다. 강력하지만 무겁습니다 — Elasticsearch는 모든 로그 본문을 풀텍스트 인덱싱하므로 디스크와 메모리가 많이 듭니다.

Loki는 Grafana Labs가 만든 가벼운 대안입니다. 모델이 다릅니다 — 로그 본문은 인덱싱하지 않고, 라벨만 인덱싱합니다. 검색 시에는 라벨로 좁힌 뒤 본문을 그룹 검사합니다. Prometheus와 비슷한 라벨 모델을 로그에 가져온 셈입니다.

Loki 스택의 컴포넌트 #

컴포넌트역할
Loki로그 저장 + 질의
Promtail (또는 Fluent Bit)각 노드에서 로그를 읽어 Loki로 보냄. DaemonSet.
GrafanaLogQL 쿼리 + 시각화

Promtail은 노드의 /var/log/containers/를 읽어서 K8s 메타데이터(Pod 이름, 네임스페이스, 컨테이너 이름, 라벨)를 자동으로 라벨로 붙여 Loki로 전송합니다. 별도 애플리케이션 변경 없이 모든 컨테이너의 stdout/stderr가 그대로 수집됩니다.

LogQL — Loki의 쿼리 언어 #

PromQL과 비슷한 결입니다.

payments 네임스페이스의 ERROR 로그
{namespace="payments"} |= "ERROR"
특정 Pod의 에러 비율 (메트릭으로 변환)
sum(rate({pod="checkout-abc123"} |= "ERROR" [5m]))

{...}로 라벨 필터, |=로 본문 substring 매칭, |~로 정규식 매칭. rate()로 메트릭처럼 다룰 수 있어서 Grafana 대시보드에서 메트릭 차트와 같이 그릴 수 있습니다.

Loki vs EFK — 선택의 결 #

차원LokiEFK
인덱싱라벨만전체 본문
디스크 비용낮음높음
풀텍스트 검색그룹 검사 (느림)빠름
운영 부담낮음높음 (Elasticsearch 클러스터 운영)
Grafana 통합1급가능

신규 도입은 Loki가 표준에 가깝습니다. 풀텍스트 검색이 핵심 요구라면 EFK 또는 OpenSearch가 더 적합하지만, K8s 운영의 일상적인 디버깅에는 Loki의 라벨 + 그룹 모델이 충분합니다.

Traces — OpenTelemetry 중심의 통합 #

분산 추적의 옛 표준은 두 갈래로 나뉘어 있었습니다 — OpenTracingOpenCensus. 두 프로젝트가 합쳐져 만들어진 것이 OpenTelemetry(OTel) 입니다. 지금은 분산 추적,메트릭,로그를 같이 다루는 단일 표준이고, CNCF에서도 가장 활발한 프로젝트 중 하나입니다.

OpenTelemetry의 핵심 개념은 셋입니다.

  • Instrumentation 라이브러리 — 각 언어별 SDK가 애플리케이션 코드에 삽입되어 trace를 만듭니다. 자동 instrumentation 도구(Java agent 등)가 코드 변경 없이 붙는 경우도 많습니다.
  • OpenTelemetry Collector — 애플리케이션이 보내는 데이터를 받아서 처리,라우팅. 일반적으로 K8s에 DaemonSet 또는 Deployment로 띄웁니다.
  • 백엔드 — 실제 trace를 저장하고 시각화. Jaeger, Tempo, Datadog, Honeycomb 등.

Trace의 모델 — Span의 트리 #

분산 추적의 단위 데이터는 Span입니다. 한 요청이 여러 서비스를 거치는 동안 각 단계마다 span 한 개가 만들어지고, 부모-자식 관계로 묶여 트리를 이룹니다.

한 요청의 span 트리 예시
[gateway] /api/orders POST  (200ms)
 ├─ [orders-service] create order   (180ms)
 │   ├─ [postgres] INSERT orders    (15ms)
 │   ├─ [postgres] INSERT items     (12ms)
 │   └─ [kafka] publish order.created (45ms)
 └─ [auth-service] verify token     (10ms)

이 트리를 보면 200ms 중 어느 구간에서 시간이 가장 많이 들었는지가 한눈에 들어옵니다. P99 latency가 평소보다 높을 때 어느 서비스가 원인인지 좁히는 데 trace가 결정적입니다.

Tempo — Loki와 같은 결의 trace 저장소 #

Grafana Labs는 trace 저장소도 가벼운 모델로 만들었습니다 — Tempo입니다. Loki가 로그에 한 일을 trace에 한 셈입니다. 인덱스를 최소화하고 객체 스토리지(S3 / GCS)에 trace 본문을 저장합니다. trace ID로 직접 조회하는 데 최적화되어 있고, Loki,Prometheus와 같이 쓸 때 Grafana에서 메트릭 → 로그 → trace로 자연스럽게 흘러가는 흐름이 만들어집니다.

Grafana — 시각화의 표준 #

세 축의 데이터를 한곳에서 들여다보는 도구가 Grafana입니다. Prometheus, Loki, Tempo, Elasticsearch, CloudWatch 등 거의 모든 데이터 소스를 한 대시보드에 묶을 수 있고, 각 패널이 자기 쿼리 언어로 데이터를 가져옵니다.

운영 클러스터의 표준 대시보드 셋은 보통 다음 정도로 구성됩니다.

  • 클러스터 개요 — 노드별 CPU,메모리,디스크, Pod 개수, 네임스페이스별 자원 사용
  • 워크로드 개요 — Deployment / StatefulSet 별 replica 상태, 재시작 횟수, OOMKilled
  • API 서버 헬스 — request rate, error rate, P99 latency, etcd lag
  • 각 애플리케이션 — 비즈니스 메트릭 + 4 golden signals (latency, traffic, errors, saturation)

kube-prometheus-stack을 도입하면 클러스터,워크로드,API 서버 대시보드는 사전 구성된 상태로 같이 들어옵니다. 애플리케이션 대시보드만 도메인에 맞춰 새로 만들면 됩니다.

Alerting — Alertmanager의 역할 #

메트릭이 어떤 조건을 만족할 때 알람을 보내는 일은 Prometheus 자체가 아니라 Alertmanager가 담당합니다. Prometheus가 알람 룰을 평가해서 발생한 알람을 Alertmanager로 보내고, Alertmanager가 라우팅,묶음,침묵,반복 처리를 합니다.

PrometheusRule — 알람 정의의 CRD #

prometheusrule-high-error-rate.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: app-alerts
  namespace: my-app
  labels:
    release: prometheus
spec:
  groups:
    - name: my-app
      rules:
        - alert: HighErrorRate
          expr: |
            sum(rate(http_requests_total{status=~"5..", app="my-app"}[5m]))
              / sum(rate(http_requests_total{app="my-app"}[5m])) > 0.05
          for: 5m
          labels:
            severity: warning
            team: payments
          annotations:
            summary: "High 5xx rate on my-app ({{ $value | humanizePercentage }})"
            description: "5xx rate over the last 5 minutes is above 5%."

expr이 Prometheus에서 평가되는 조건, for: 5m이 “이 조건이 5분 연속 참이어야 알람 발생"이라는 의미입니다. labelsseverityteam이 Alertmanager에서 라우팅 키로 쓰입니다.

Alertmanager의 라우팅 #

Alertmanager의 설정에서는 라벨 기반으로 알람을 어디로 보낼지 정합니다.

alertmanager.yaml — 단순화
route:
  receiver: default
  group_by: ['alertname', 'team']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: pagerduty
    - match:
        team: payments
      receiver: payments-slack

receivers:
  - name: default
    slack_configs:
      - channel: '#alerts'
  - name: pagerduty
    pagerduty_configs:
      - service_key: ...
  - name: payments-slack
    slack_configs:
      - channel: '#payments-alerts'

이 모델 덕에 알람의 라우팅이 코드(매니페스트)로 관리되고, Slack / PagerDuty / Email 같은 채널로 분기됩니다.

운영 시 잡아 둘 원칙 #

옵저버빌리티 스택은 한 번 잘 갖춰 두면 클러스터의 시야가 단숨에 깊어지지만, 잘못 운영하면 클러스터 자원의 큰 부분을 잡아먹는 도구가 됩니다. 다음 네 가지를 잡아 두는 게 좋습니다.

1. 메트릭 카디널리티 폭발 주의 #

Prometheus는 라벨 조합 하나당 별도 시계열을 만듭니다. 라벨에 user ID, request ID, UUID 같은 고카디널리티 값을 넣으면 시계열 수가 폭발하고, Prometheus가 메모리를 빠르게 소진합니다. 운영 가이드의 첫 번째 원칙은 라벨에 고카디널리티 값을 넣지 마라입니다.

카디널리티 점검 — 시계열이 가장 많은 메트릭
topk(10, count by (__name__)({__name__=~".+"}))

이 쿼리로 시계열 수가 많은 메트릭을 주기적으로 점검하는 게 운영의 일부입니다.

2. 보존 기간과 원격 저장소 #

Prometheus의 로컬 저장소는 기본 15일 보존입니다. 그 이상 보관하려면 원격 저장소(Thanos, Cortex, Mimir, VictoriaMetrics)에 같이 보내야 합니다. 마찬가지로 Loki와 Tempo도 객체 스토리지(S3 / GCS)에 long-term storage를 두는 게 표준입니다.

보존 기간은 비용과 직결됩니다. 메트릭 6개월, 로그 30일, trace 7일 정도가 일반적인 출발점이고, 각 축의 보존을 따로 정할 수 있습니다.

3. 알람의 SNR — 너무 많은 알람은 무알람과 같다 #

알람을 너무 많이 만들면 운영자가 알람을 무시하기 시작하고, 정작 중요한 알람도 놓치게 됩니다. 알람 설계의 표준 원칙은 “알람 한 건 = 사람의 즉시 대응 한 번” 입니다.

  • Symptom-based — 원인이 아니라 증상에 알람. “DB 연결 풀이 80% 차 있다"가 아니라 “API의 5xx 비율이 5% 넘는다”.
  • for 기간으로 잡음 제거 — 짧은 스파이크에 알람이 울리지 않도록.
  • severity 갈래critical은 깨워야 하는 것, warning은 내일 아침에 보면 되는 것. 갈래가 흐릿해지면 무시되기 시작합니다.

4. golden signals를 표준으로 #

Google SRE 문화에서 출발한 4 golden signals는 거의 모든 워크로드 모니터링의 출발점입니다.

시그널의미
Latency요청 처리 시간 (P50 / P95 / P99)
Traffic초당 요청 수
Errors실패 비율
Saturation자원 포화도 (CPU, 메모리, 큐 길이)

이 넷을 모든 서비스에 같은 형태로 노출해 두면 대시보드도 알람도 표준화됩니다. 도메인 메트릭은 그 위에 얹습니다.

마무리 #

K8s 운영의 옵저버빌리티 스택을 한 사이클로 정리했습니다. 메트릭의 Prometheus + kube-state-metrics + node-exporter, 로그의 Loki(또는 EFK), 트레이스의 OpenTelemetry + Tempo, 시각화의 Grafana, 알람의 Alertmanager까지가 거의 굳은 표준 묶음이고, kube-prometheus-stack과 Loki / Tempo Helm 차트가 도입의 첫 단계입니다. 카디널리티 / 보존 / 알람 SNR / golden signals 네 원칙을 운영 단계의 가드레일로 잡아 두면 스택이 클러스터의 자원을 갉아먹지 않고 시야만 깊어집니다. 다음 글이자 K8s 고급 시리즈의 마지막 글에서는 매니페스트의 source of truth를 git에 두는 운영 모델 — ArgoCD와 Flux 기반의 GitOps를 다루겠습니다.

X