콘텐츠로 이동
Study Note관측

2. 메트릭 — Prometheus와 알림

알림이 없는 메트릭은 사고가 난 뒤에 보는 부검 자료다 — 이 장의 절반이 알림인 이유다

이 장에서 처음 나오는 말3개
pull 모델
Prometheus가 대상에 직접 접속해 긁어 오는(scrape) 방식. 대상이 보내 주는(push) 방식과 반대다. "긁으러 갔는데 없더라"로 죽은 것도 알 수 있다는 게 큰 차이다.
카운터 · 게이지 · 히스토그램Counter · Gauge · Histogram
메트릭의 세 가지 성격. 늘기만 하는 것 · 오르내리는 것 · 분포를 담는 것이다. 성격을 모르고 질의하면 그래프가 조용히 거짓말을 한다.
PromQLPrometheus Query Language
메트릭 질의 언어. rate()로 속도를 내고 sum by()로 묶는 두 동작이 전체의 8할이다.

Prometheus는 대상이 보내 주기를 기다리지 않고 직접 긁으러 간다. 이 선택이 운영 감각을 꽤 바꾼다.

pull (Prometheus)push
대상이 죽으면긁기 실패가 곧 신호다 — up == 0안 오는 건지 죽은 건지 구분이 어렵다
대상 목록Prometheus가 안다 (서비스 디스커버리)대상이 주소를 알아야 한다
방화벽Prometheus → 대상 방향만 열면 된다반대
배치 작업곤란하다 — 짧게 살다 죽으면 못 긁는다 → Pushgateway자연스럽다
Prometheus가 서비스와 익스포터의 메트릭을 직접 긁고, 저장한 시계열을 Grafana와 Alertmanager에 제공하는 공식 구성도
지금은 가운데 Prometheus에서 왼쪽 대상들로 향하는 pull 화살표만 본다. 나머지 구성 요소는 이 장 뒤에서 하나씩 만난다.출처: Prometheus 공식 문서 — Overview

Prometheus가 긁어 오는 것은 결국 텍스트 몇 줄이다. 앱의 /metrics 주소를 그냥 curl하면 이게 나온다.

# HELP http_requests_total 처리한 HTTP 요청 수
# TYPE http_requests_total counter
http_requests_total{method="POST",path="/orders",status="200"} 12044
http_requests_total{method="POST",path="/orders",status="500"} 37
# HELP process_resident_memory_bytes 프로세스가 쓰고 있는 메모리
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 4.1943e+08

한 줄의 구조는 이름{라벨=값, ...} 숫자 다. 여기서 두 가지가 따라 나온다.

  • 저장되는 건 숫자 하나뿐이다. 에러 메시지도, 어느 사용자였는지도 여기 없다 — 그건 로그와 트레이스의 몫이다
  • 라벨 조합 하나가 시계열 하나다. 위 예에서 status만 다른 두 줄은 서로 다른 시계열 둘이고, Prometheus는 각각에 대해 (시각, 값) 수열을 따로 쌓는다

타입 넷 — 성격을 모르면 그래프가 거짓말을 한다

섹션 제목: “타입 넷 — 성격을 모르면 그래프가 거짓말을 한다”

같은 숫자라도 어떻게 변하는 값인지에 따라 읽는 법이 다르다. 이걸 타입이라 부른다.

타입성격예어떻게 읽나
counter늘기만 한다. 프로세스가 재시작하면 0으로 돌아간다http_requests_total · ..._errors_total절대 그냥 보지 않는다. rate()로 속도를 낸다
gauge오르내린다..._memory_bytes · kube_pod_status_ready그대로 본다. 추세는 deriv()(초당 변화율)·predict_linear()(추세를 연장한 예측값)
histogram관측값을 구간별로 센다http_request_duration_secondshistogram_quantile()로 p95·p99를 뽑는다
summary앱이 미리 계산한 분위수일부 라이브러리의 기본값파드가 여럿이면 합칠 수 없다. 아래 함정

이름 규칙이 타입을 알려 준다 — _total로 끝나면 counter, _bytes·_seconds로 끝나는 현재값은 대개 gauge, _bucket·_sum·_count 세 쌍이 보이면 histogram이다.

스크레이프는 순간의 스냅샷이다

섹션 제목: “스크레이프는 순간의 스냅샷이다”

앱은 자기가 언제 긁히는지 모른다. /metrics가 내놓는 것은 언제나 지금 이 순간의 값이다 — counter는 시작 이후의 누적 총합, gauge는 현재값. “이 구간 동안 얼마나 변했나”는 저장할 때 만들어지는 것이 아니라 질의할 때 rate()가 계산한다.

이 설계 덕분에 수집 타이밍이 흔들려도 괜찮다.

  • 샘플에는 실제로 긁힌 시각이 타임스탬프로 붙고, rate()는 고정 주기가 아니라 그 타임스탬프 간격으로 나눈다. 15초 주기가 몇 초 밀려도 계산이 틀어지지 않는다
  • 스크레이프를 한 번 통째로 놓쳐도 counter는 누적이라 그동안의 증가분이 다음 샘플에 담겨 있다. 잃는 것은 데이터가 아니라 해상도다 — 앱이 “구간의 변화량”을 보내는 설계였다면 놓친 구간이 통째로 증발했을 것이다

지연 시간은 평균이 거의 쓸모없다. 평균 200ms인데 100명 중 1명이 5초를 겪고 있으면 그 1명이 문제다. 그래서 지연은 분포로 저장한다.

# 히스토그램 하나는 실제로 이렇게 여러 줄로 나온다
http_request_duration_seconds_bucket{le="0.1"} 9800 # 0.1초 이하가 9800건
http_request_duration_seconds_bucket{le="0.5"} 9950
http_request_duration_seconds_bucket{le="1"} 9990
http_request_duration_seconds_bucket{le="+Inf"} 10000 # 전체
http_request_duration_seconds_sum 1240.5 # 걸린 시간 합
http_request_duration_seconds_count 10000 # 건수

le는 “less or equal”, 즉 누적 구간이다. 이 구조 덕분에 파드가 열 개여도 버킷끼리 그냥 더하면 전체 분포가 되고, 거기서 p99를 다시 계산할 수 있다.

라벨 조합 하나가 시계열 하나이므로, 라벨 설계가 곧 용량 설계다. method(5종) × path(20종) × status(6종)이면 시계열 600개 — 괜찮다. 여기에 user_id가 붙으면 사용자 수만큼 곱해진다.

하면 안 되는 라벨대신
user_id · request_id · trace_id로그·트레이스에 넣는다 (3장 · 4장)
pod_ip · 파드 이름(단명 파드)deployment·service 단위로
URL 전체 (/orders/12345)경로 템플릿 (/orders/:id)

이미 들어오는 라벨은 긁는 단계에서 잘라 낸다. 앱 배포를 기다릴 필요가 없다.

# ServiceMonitor의 endpoints 아래
metricRelabelings:
- action: labeldrop
regex: "id|uuid|request_id"

trace_id를 라벨에 넣지 못한다면, 메트릭에서 트레이스로는 어떻게 건너가나. 그 자리를 맡는 것이 exemplar다 — 히스토그램 관측값에 대표 trace ID 하나를 매달아 두는 것. 시계열의 라벨이 아니라 옆에 붙는 표본이라 카디널리티를 늘리지 않으면서, 그래프에서 튄 지점의 “실제로 느렸던 요청 하나”로 바로 갈 수 있게 한다.

# OpenMetrics 노출 형식 — 값 뒤의 # {...}가 exemplar다
http_request_duration_seconds_bucket{le="1"} 9990 # {trace_id="4bf92f3577b3..."} 0.94

연결에는 세 조각이 필요하다 — ① 앱(OTel SDK)이 관측값에 trace ID를 매달고, ② Prometheus가 exemplar 저장을 켜고(--enable-feature=exemplar-storage), ③ Grafana 데이터소스가 그 ID를 Tempo로 잇는다. ③의 설정과 “점이 안 보일 때 어디부터 보나”는 5장에 있다.

PromQL — 다섯 형태면 대부분 쓴다

섹션 제목: “PromQL — 다섯 형태면 대부분 쓴다”

PromQL은 문법이 넓지만, 실전에서 쓰는 모양은 사실상 다섯 개다. 이 다섯을 조합하는 것이 전부이고, 남의 대시보드에 있는 긴 쿼리도 대개 이것의 겹침이다.

up{job="node-exporter"} # 라벨로 고른다
up{job=~"node.*"} # =~ 정규식, != 부정, !~ 정규식 부정
node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}

이렇게 고른 결과가 순간 벡터 — “지금 시점의 값들”이다. 그래프에 그대로 그릴 수 있는 건 gauge뿐이라는 걸 기억한다.

쓴 것무슨 일이 벌어지나이렇게
sum(http_requests_total)counter 원값의 합 — 계단만 올라간다sum(rate(...[5m]))
rate(sum(...))합친 뒤 속도를 내서 재시작 보정이 깨진다rate가 먼저, sum이 나중
avg(p99_latency)분위수의 평균은 아무 의미가 없다히스토그램 버킷을 합친 뒤 histogram_quantile
histogram_quantile(0.99, sum by (service) (...))le가 없어 결과가 빈다sum by (le, service)
up{job="x"} == 0대상이 아예 사라지면 시계열도 없어 알림이 안 뜬다absent(up{job="x"})를 함께 건다
이 절에서 처음 나오는 말2개
REDRate · Errors · Duration
서비스를 요청 수 · 에러 · 지연으로 보는 관례. 사용자가 겪는 증상이라 알림의 출발점이다.
USEUtilization · Saturation · Errors
CPU·디스크 같은 자원을 사용률 · 포화 · 오류로 보는 관례. 원인을 파고들 때 쓴다.

메트릭을 낼 수 있다고 아무거나 내면 시계열만 는다. 무엇을 재야 하는지에는 검증된 관례 둘이 있다 — 대상이 다르다.

RED — 요청을 처리하는 것USE — 자원
대상API · 서비스 · 큐 컨슈머CPU · 메모리 · 디스크 · 네트워크
무엇을Rate 요청 수 · Errors 에러 · Duration 지연Utilization 사용률 · Saturation 포화 · Errors 오류
답하는 것사용자가 지금 아픈가왜 아픈가 (어디가 모자란가)
알림을 건다면여기에 건다 — 증상이다조사용. 알림으로 걸면 시끄럽다
# RED — 서비스 하나에 이 셋이면 충분하다
sum by (service) (rate(http_requests_total[5m])) # Rate
sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (service) (rate(http_requests_total[5m])) # Errors
histogram_quantile(0.99,
sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))) # Duration
# USE — 자원 하나에 이 셋
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) # Utilization
node_load5 / on(instance) count by (instance) (node_cpu_seconds_total{mode="idle"}) # Saturation
rate(node_network_receive_errs_total[5m]) # Errors
설정에 들어가기 전에2개
exporter익스포터
노드나 시스템의 상태를 Prometheus가 읽을 수 있는 메트릭으로 내주는 작은 프로그램.
ServiceMonitor · PodMonitor
"이 Service·Pod를 긁어라"를 선언하는 쿠버네티스 리소스. Prometheus 설정 파일을 직접 고치지 않게 해 준다.
방식성격
kube-prometheus-stack (Helm)Prometheus Operator + Prometheus + Alertmanager + Grafana + node-exporter + kube-state-metrics + 기본 대시보드·규칙 한 벌. 온프렘의 기본 출발점
Prometheus Operator 단독위 묶음이 과할 때
바이너리·단독 컨테이너쿠버네티스 밖 대상을 긁을 때

이걸 쓰면 긁을 대상과 알림 규칙을 CRD로 추가하게 된다 — 설정 파일을 손대지 않는다.

ServiceMonitor와 PrometheusRule을 Operator가 설정으로 만들어 Prometheus가 대상들을 scrape하고 Alertmanager로 알림을 보내는 구조

기본 묶음에 들어오는 exporter 둘이 사실상 클러스터 감시의 바닥을 깐다 — node-exporter가 노드(CPU·메모리·디스크·네트워크), kube-state-metrics가 쿠버네티스 오브젝트 상태(파드 Ready인가, Deployment 몇 개 떴나) 를 낸다. kube_pod_status_ready 같은 이름이 보이면 후자에서 온 것이다.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-app
namespace: prod
labels:
release: kube-prometheus-stack # Prometheus가 고르는 라벨 — 안 맞으면 조용히 무시된다
spec:
selector:
matchLabels:
app: my-app # 이 라벨을 가진 Service를
endpoints:
- port: http-metrics # 포트 "이름"이다. 번호가 아니다
path: /metrics
interval: 30s

3.13이 LTS다 (2026-07). 2.x 설정은 대체로 그대로 통하지만, 새로 얻은 게 있다.

항목내용온프렘에서
OTLP 수신OpenTelemetry 프로토콜로 들어오는 메트릭을 직접 받는다앱이 OTel로 계측돼 있으면 exporter 없이 바로 (4장)
UTF-8 라벨라벨 이름·값에 UTF-8 허용OTel 속성 이름을 그대로 쓸 수 있다
새 UI쿼리·타겟 화면 개편
remote write 2.0장기 저장소로 보낼 때 효율 개선장기 저장을 붙일 때

리텐션 — 언제 장기 저장이 필요한가

섹션 제목: “리텐션 — 언제 장기 저장이 필요한가”

Prometheus는 시계열을 자체 저장 엔진(TSDB, Time Series Database)에 담는데, 이게 로컬 디스크에만 있다. 그래서 보존 기간은 PVC 크기와 직결된다.

spec:
retention: 30d
retentionSize: 180GB # 둘 중 먼저 닿는 쪽으로 지운다 — 둘 다 주는 게 안전하다
storage:
volumeClaimTemplate:
spec:
storageClassName: standard
resources: { requests: { storage: 200Gi } }

retentionSize를 PVC의 80~90% 선으로 잡아 둔다. 기간만 주면 시계열이 늘었을 때 디스크가 먼저 차고, 그때 Prometheus가 죽는다.

필요답
15~30일이면 충분로컬 TSDB 그대로. 대부분 여기서 끝난다
몇 달~몇 년 보존Thanos 또는 Mimir — 압축된 블록을 오브젝트 스토리지에 (온프렘 덱 6장)
클러스터 여러 개를 한 화면에마찬가지로 Thanos/Mimir. 또는 Grafana에서 데이터소스를 여러 개
HA (Prometheus 자체 이중화)같은 설정으로 2벌을 돌리고 Alertmanager가 중복을 제거하게 한다
알림 경로의 마지막 조각1개
Alertmanager
Prometheus가 발견한 알림을 묶고 · 중복을 없애고 · 담당자에게 보내는 별도 컴포넌트.

규칙 200개짜리 대시보드보다 잘 고른 알림 20개가 낫다. 원칙 넷.

원칙뜻
사람이 행동할 수 있는 것만받고도 할 일이 없는 알림은 다음부터 무시된다
증상으로 건다“노드 CPU 90%“보다 “요청 실패율 증가”가 낫다 — 위 RED·USE 절
for를 넉넉히깜빡임은 사람을 깨울 이유가 없다
runbook 링크를 단다새벽 3시에 처음 보는 알림에서 제일 필요한 건 “그래서 뭘 하지”다

온프렘에서 반드시 있어야 하는 알림

섹션 제목: “온프렘에서 반드시 있어야 하는 알림”

클라우드가 콘솔에서 알려 주던 것들이다. 없으면 아무도 안 알려 준다.

알림대략의 조건왜
대상 다운up == 0 (10m)가장 기본
노드 NotReadykube_node_status_condition{condition="Ready",status="true"} == 0
디스크 소진 예측predict_linear(...[6h], 24h) < 0온프렘은 증설에 시간이 걸린다
PVC 사용률kubelet_volume_stats_available_bytes / ..._capacity_bytes < 0.1Prometheus·Loki 자신이 여기 걸린다
인증서 만료 임박certmanager_certificate_expiration_timestamp_seconds - time() < 14*86400온프렘 덱 4장. 조용히 만료돼 전면 장애가 된다
etcd 이상etcd_server_has_leader == 0, fsync 지연클러스터 전체가 느려지는 원인
CNPG 백업 실패 · 복제 지연백업 나이, pg_stat_replication 지연온프렘 덱 7장. WAL이 쌓이면 DB가 멈춘다
오브젝트 스토리지 용량스토리지 exporter 기준차면 로그와 백업이 동시에 멈춘다
파드 재시작 반복rate(kube_pod_container_status_restarts_total[15m]) > 0
서비스 에러율RED의 E — > 0.05 (5m)사용자가 아픈 것을 직접 재는 유일한 줄
관측 스택 자체Prometheus TSDB 용량, Loki 인입 실패감시자가 조용히 죽는다
Watchdog항상 참인 규칙아래

Prometheus는 “규칙이 참이다”까지만 하고 누구에게 어떻게 보낼지는 전부 여기서 정한다.

route:
receiver: default
group_by: [alertname, namespace]
group_wait: 30s # 첫 알림 전 대기 — 관련된 것들이 같이 오게
group_interval: 5m # 같은 묶음에 새 알림이 붙었을 때 다시 보내는 간격
repeat_interval: 4h # 안 고쳐졌을 때 다시 깨우는 주기
routes:
- matchers: [ severity="critical" ]
receiver: oncall-phone
- matchers: [ alertname="Watchdog" ]
receiver: external-heartbeat # 죽은 스위치 감시용
repeat_interval: 5m
inhibit_rules:
# 노드가 통째로 죽었으면 그 위 파드 알림은 묻는다
- source_matchers: [ alertname="NodeDown" ]
target_matchers: [ severity="warning" ]
equal: [ node ]

inhibit_rules가 실전에서 특히 중요하다 — 노드 하나가 죽었을 때 알림 40개가 오면 아무도 안 읽는다. 계획된 작업 중에는 silence로 잠시 끄되, 만료 시각을 반드시 넣는다 — 영구 침묵은 알림을 지운 것과 같다.

터미널 창
# ① 대상이 다 잡혔나
kubectl -n observability port-forward svc/kube-prometheus-stack-prometheus 9090:9090
# http://localhost:9090/targets — DOWN인 대상의 Error 열을 본다
# ② 규칙이 로드됐나 (PrometheusRule 라벨이 틀리면 조용히 무시된다)
# http://localhost:9090/rules
# ③ 지금 발화 중인 알림
curl -s http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | {name:.labels.alertname, state}'
# ④ Alertmanager까지 실제로 가나 — 테스트 알림을 직접 쏴 본다
kubectl -n observability port-forward svc/kube-prometheus-stack-alertmanager 9093:9093
curl -XPOST http://localhost:9093/api/v2/alerts -H 'Content-Type: application/json' -d '[{
"labels": {"alertname":"SmokeTest","severity":"critical"},
"annotations": {"summary":"알림 경로 점검"}
}]'
# → 실제 채널(메신저·메일)에 도착하는지 확인
# ⑤ 무엇이 시계열을 많이 먹고 있나
# http://localhost:9090/tsdb-status — 시계열 수와 라벨 카디널리티 상위
증상흔한 원인확인
대상이 목록에 아예 없다release 라벨 · NS 셀렉터 불일치kubectl get servicemonitor -o yaml의 labels
대상이 DOWN이다포트 이름 오타 · NetworkPolicy · /metrics 경로/targets의 Error 열, 파드에서 직접 curl
그래프가 계단처럼 올라간다counter를 그냥 그렸다rate(...[5m])를 씌운다
histogram_quantile 결과가 빈다by에서 le가 빠졌다sum by (le, ...)
나눗셈 결과가 빈다양쪽 라벨 집합이 다르다양쪽을 같은 by로 묶는다
알림이 안 뜬다규칙 미로드 · 대상이 사라져 시계열 자체가 없음/rules, absent() 추가
알림이 뜨는데 안 온다receiver 설정 · inhibit · silence④의 테스트 알림, Alertmanager UI
메모리가 계속 는다카디널리티 폭발/tsdb-status 상위, metricRelabelings로 labeldrop
디스크가 찬다retentionSize 미설정PVC의 80~90% 선으로 넣는다
  • 메트릭 한 줄은 이름{라벨} 숫자 가 전부다. 라벨 조합 하나 = 시계열 하나
  • 타입을 알아야 읽는 법이 정해진다 — counter는 rate(), gauge는 그대로, 지연은 histogram. summary는 파드가 여럿이면 합쳐지지 않는다
  • 샘플은 구간의 변화량이 아니라 순간의 스냅샷이다. counter가 누적이라 스크레이프가 밀리거나 빠져도 해상도만 잃는다 — 대신 주기보다 짧은 스파이크는 안 보인다
  • PromQL은 고르기 · 속도 · 묶기 · 비율 · 분위수 다섯이면 대부분 된다. 순서는 언제나 rate → sum by → histogram_quantile
  • 무엇을 잴지는 RED(서비스)와 USE(자원). 알림은 RED에 걸고 USE는 조사용이다
  • 트레이스로는 exemplar로 건너간다 — trace_id는 라벨이 아니라 exemplar에 싣는다
  • pull 모델이라 up == 0 하나로 “죽었다”를 안다 — 온프렘 알림의 출발점
  • kube-prometheus-stack으로 시작하고, 대상은 ServiceMonitor로 추가한다. 안 긁히면 release 라벨 · 포트 이름 · 네임스페이스 셀렉터 셋을 본다
  • 보존은 로컬 TSDB로 15~30일이 기본. retention과 retentionSize를 둘 다 준다
  • 알림은 행동 가능한 것만, 증상으로, for를 넉넉히, runbook과 함께. 필수 목록에 디스크 소진 예측 · 인증서 만료 · CNPG 백업 실패가 들어간다
  • Watchdog을 외부로 라우팅한다 — 알림이 안 오는 것을 알아채는 유일한 방법이다