Prometheus 고급 가이드 — 메트릭 수집·PromQL·알림·SLO까지
이 글의 핵심
Prometheus에서 고부하 환경의 메트릭을 안정적으로 수집하고, PromQL·Recording Rule로 쿼리 비용을 줄이며, Alertmanager와 연동한 실전 알림·SLO 모니터링까지 한 번에 정리합니다.
옛날 팀이었는데, 모니터링 없이 결제 API만 docker logs -f로 뒤집고 있었어요. 장애 티가 난 건 점심때쯤인데, “어제 배포 뭐 올렸지?”부터 시작해서, 결국 새벽 3시에야 DB 커넥션 풀이 바닥난 걸 눈치챘죠. 그때도 로그에는 “connection refused”만 반복이고, 몇 건이 얼마나 쌓이고 있었는지, 어느 시점부터 p99가 갈렸는지는 전혀 안 보였어요. 이후로는 솔직히, 로그만으론 부족하다고 팀에 못 박았습니다. 집계된 숫자(메트릭)랑, 알림, 그다음이 로그·트레이스 순이에요. Prometheus는 그 첫 판에 꽤 잘 맞는 도구고요.
Prometheus는 풀(pull)로 시계열을 모으는 쪽이라, “우리 팀이 익숙한 게 뭐냐”에 따라 셋업 난이도는 갈리지만, 한번 자리 잡으면 up, rate(), histogram_quantile만으로도 반은 간다는 느낌이 납니다. 다만 rate()가 비거나 그래프가 이상할 때—스크랩 간격이랑 [5m] 윈도 길이가 안 맞거나, 파드 갈아끼울 때 SD가 밀리면—그냥 “Prometheus가 이상한 거 아님?” 하기 쉬운데, 대부분은 타겟/라벨/윈도 설정 쪽이에요.
카운터로 RPS를 볼 때 rate()는 윈도 안에서 평균 초당 증가율을 줘서, 스파이크는 좀 눌리지만 SLO·대시보드용으로는 보통 이쪽이 맞고요. irate()는 마지막 두 샘플만 써서 들쭉날쭉해서, “지금 뭐 터졌냐” 잠깐 볼 때말곤 잘 안 씁니다.
# HTTP 요청 초당 건수 (5분 윈도)
rate(http_requests_total{job="api"}[5m])
지연은 히스토그램으로 받는 경우가 많고, _bucket / _sum / _count 조합이 기본이에요. histogram_quantile은 버킷이 촘촘해야 의미가 있어요. SLO 상한(1초, 2초 이런 거)이랑 맞춰서 버킷을 설계 안 하면, 꼬리 지연이 잘릴 수 있어요.
# API 지연 p99 (job 기준, 환경에 맞게 좁혀 쓰기)
histogram_quantile(
0.99,
sum by (le, job) (
rate(http_request_duration_seconds_bucket{job="api"}[5m])
)
)
라벨이 다른 시계열을 붙이려면 on(), ignoring()에 group_left 같은 걸 쓰는데, 이런 조인이 많아지면 카디널리티랑 쿼리 비용이 같이 뛰어서, 자주 쓰는 조합은 Recording Rule로 먼저 떨궈 두는 쪽이 정신 건강에 좋아요.
# 예: pod_cpu와 pod_memory 조인 (메트릭명은 환경에 맞게)
sum by (pod, namespace) (rate(container_cpu_usage_seconds_total[5m]))
* on (pod, namespace) group_left
sum by (pod, namespace) (container_memory_working_set_bytes)
과거랑 비교하거나 offset 쓰는 것도 가능하고, predict_linear은 주기성 있는 트래픽이면 오탐 나기 쉬워서 임계값이랑 같이 스테이징에서 한번 돌려보는 게 맞고요.
# 1주 전 대비 에러율 변화 (예시)
(
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
)
-
(
sum(rate(http_requests_total{status=~"5.."}[5m] offset 1w))
/
sum(rate(http_requests_total[5m] offset 1w))
)
Recording / Alert을 나누는 감은 단순해요. 똑같은 PromQL을 Grafana랑 알림이 백날 꺼내면, Recording으로 한 번만 계산하게 만드는 거고, 사람이 봐야 할 조건은 Alert Rule이에요. interval이랑 scrape_interval이 정수 배로 맞는 게 해석할 때 편하다는 건, 해보면 다들 동의해요.
# recording_rules.yml 예시
groups:
- name: api_recording
interval: 30s
rules:
- record: job:http_requests:rate5m
expr: |
sum by (job) (rate(http_requests_total[5m]))
- record: job:http_errors:ratio5m
expr: |
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))
알림은 for로 스파이크를 좀 걸러도, 너무 길면 복구될 때까지 페이지가 늦게 오니까, 팀이랑 “이 알림은 몇 분 참고 볼까”를 애초에 합의하는 게 낫고요. 메시지에 런북 링크 넣는 건 진짜로 시간을 아껴줘요. 아래 annotations.description에 이상한 문장 끼어 있으면(예전에 템플릿 꼬인 것 같음) 반드시 지우고 팀 톤에 맞게 쓰는 게 맞아요.
# alert_rules.yml 예시
groups:
- name: api_alerts
rules:
- alert: HighErrorRate
expr: |
(
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))
) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "높은 HTTP 5xx 비율 (job={{ $labels.job }})"
description: "5분 윈도 기준 에러 비율이 5%를 넘었습니다. 대시보드/런북 링크를 여기에."
K8S면 정적 targets보다 kubernetes_sd_configs 쪽이 현실적이에요. 파드/서비스 메타를 라벨로 붙이고, 어노테이션으로 스크랩 여부·포트·패스를 조절하는 패턴 흔하죠. relabel_configs는 “뭘 수집할지”, metric_relabel_configs는 “저장할 때 라벨을 어떻게 깎을지”에 가깝고, user_id, order_id 같은 걸 메트릭 라벨에 심는 건 TSDB 죽이는 지름길이에요.
# prometheus.yml 발췌 — kubernetes_sd_configs
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
Consul 쓰는 조직이면 consul_sd_configs로 서비스/태그 읽고 relabel로 metrics 태그 달린 것만 남기는 식. 여기서 헬스체크가 통과해도 스크랩 준비가 안 맞는 경우가 있어서, 배포랑 타이밍을 맞춰야 해요.
scrape_configs:
- job_name: consul-services
consul_sd_configs:
- server: consul:8500
services: []
relabel_configs:
- source_labels: [__meta_consul_service]
target_label: job
- source_labels: [__meta_consul_tags]
regex: '.*,metrics,.*'
action: keep
Exporter는 node_exporter, kube-state-metrics, postgres/redis 쪽 표준 패턴이 정해져 있으니, “우리 SLO에 뭘 걸지” 먼저 합의하고 늘리는 게 맞고요. 앱 메트릭은 RED(요청/에러/지연)나 USE(이용·포화·에러) 틀에 맞추면 대시보드 그리기 쉬워요. Go면 대략 이런 느낌—진짜로는 promhttp로 /metrics 열고 미들웨어에서 잘 붙이면 됩니다.
// 개념 예시: Counter + Histogram (실서비스는 promhttp.Handler() 등으로 노출)
var (
httpRequests = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "path", "status"},
)
httpDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP latency",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
Grafana는 “한 화면에 다 때려 넣지 말고”, 인시던트용 짧은 요약이랑 원인용 느슨한 상세를 나누는 걸 개인적으론 추천해요. 템플릿 변수로 namespace / job / pod만 잘 잡혀도 반은 이김. PromQL을 Grafana에만 복붙해대면 숫자가 팀마다 달라지니까, Recording Rule에 정의를 모아두는 쪽이 덜 싸워요.
HA 얘기 나오면 “Prometheus 두 대 띄웠다”로 끝나는 경우가 있는데, 같은 타겟을 둘 다 스크랩하면 시계열이 겹쳐서 그래프·알림이 꼬여요. 샤딩, 리더 한 명만 스크랩, Thanos·Mimir·VictoriaMetrics로 장기/글로벌 뷰—이 중 뭐라도 의도가 있어야 하고요. Federation은 /federate로 집계된 것만 끌어올리는 거라, “전체 복제”가 아니에요. 크로스 리전에서 다 복제하려다 네트워크·단일장애점만 키우는 케이스 많이 봤어요.
# 상위 prometheus.yml 발췌
scrape_configs:
- job_name: federate-region-a
honor_labels: true
metrics_path: /federate
params:
match[]:
- '{job="api"}'
static_configs:
- targets: ['prometheus-region-a:9090']
SLO는 SLI(뭘 재나), SLO(목표), 에러 버짓(남은 나쁜 구간)으로 말이 많이 정리돼 있고, HTTP로 성공률을 이렇게 잡는 식이 흔해요. 30일 전체를 매번 쿼리로 긁으면 비싸서, 일 단위 기록이나 별도 시스템이랑 섞는 경우가 많고요.
# 성공 요청 비율 (실무에선 5xx/timeout 규칙에 맞게 조정)
sum(rate(http_requests_total{status!~"5.."}[30d]))
/
sum(rate(http_requests_total[30d]))
번 레이트(burn rate)는 “에러 버짓을 비정상적으로 빨리 태우고 있냐”를 보는 패턴이에요. 아래 14.4랑 0.999는 예시라서, Google 책/팀 SLO 문서에 맞춰서 다시 튜닝해야 하고, 숫자 복붙만 하다가 온콜이 죽는 건 저도 봤어요.
# 예시: 임계는 환경마다 다름
groups:
- name: slo_burn
rules:
- alert: ErrorBudgetBurnFast
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))
)
>
14.4 * (1 - 0.999)
for: 5m
labels:
severity: critical
annotations:
summary: "에러 버짓 소진이 빠름 (예시 임계)"
트러블슈팅은 표로 정리해봤자 나중에 안 읽는 경우가 많아서, 그냥 흐름으로 말할게요. rate()가 이상하면 스크랩 간격·윈도·SD 갱신부터. 히스토그램 분위수가 이상하면 버킷이랑 라벨 카디널리티—le에 path·유저id까지 겹쳐 넣는 순간 끝났어요. HA에서 알림이 두 개씩 오면 중복 스크랩/외부 라벨이랑 Alertmanager 그룹·인히비션이랑 같이 봐야 하고, 스크랩 타임아웃이 잦으면 타겟 CPU나 TLS·handshake 쪽을 scrape_duration_seconds로 같이 봅니다. 디스크가 선형으로 차면 retention이랑 이미 쌓인 시계열 수(prometheus_tsdb_head_series 같은 것)을 같이 봐요—“메트릭 하나만 더”가 아니라 기준선부터 잡는 게 맞고요. 간헐적 실패는 DNS·타임아웃·레이스 쪽, 성능은 N+1·락·동기 I/O, 메모리 누수는 캐시 무한·리스너·커넥션 안 닫힘, 배포만 깨지면 env·권한·lockfile, 설정 불일치면 프로필/시크릿/리전, 데이터 불일치면 멱등·부분쓰기·캐시 무효—이런 건 표가 아니라 한 번 겪어보면 머리에 남아요. 고칠 땐 최소재현 → 최근 변경 줄이기 → 환경 차이 → 관측으로 가설 → 수정 후 부하, 순이 무난해요.
공식 docs는 읽을 거리가 많아서, 링크만 남겨둘게요: Prometheus 문서, PromQL 기본, Configuration, Alertmanager, Grafana Prometheus 데이터 소스.
끝으로 한 번 더 말할게요. 로그만으론 부족해요. 메트릭이랑 알림·SLO가 있어야 “얼마나, 언제부터, 누가”가 한 화면에 잡히고, 그다음에 로그 grep이 의미가 생겨요. 그 새벽 3시짜리 재발만 안 하면 이 글도 본전이죠.