1. Prometheus Data Model and Scraping

time series와 label, scrape, TSDB, 그리고 쿠버네티스에서 대상을 찾는 법

  • Haram Lee
  • 2026-09-28
  • studies / Topics / Observability

1.1 What Prometheus Does

공식 문서는 Prometheus를 한 문장으로 요약한다.

Prometheus collects and stores its metrics as time series data, i.e. metrics information is stored with the timestamp at which it was recorded, alongside optional key-value pairs called labels.

메트릭을 시간 순서대로 쌓되, 각 값에 label이라는 key-value 쌍을 붙여 둔다는 뜻이다. 나머지 특징은 여기서 따라 나온다. 메트릭 이름과 label로 시계열을 구분하는 다차원 데이터 모델을 쓰고, 이 데이터를 조회하는 PromQL이라는 언어가 있다. 서버 하나가 분산 저장소에 기대지 않고 독립적으로 동작한다.

수집 방향은 pull이다. 앱이 서버로 값을 밀어 넣는 대신, Prometheus가 HTTP로 대상에 찾아가 값을 가져온다. 이것을 scrape라고 한다. 대상은 쿠버네티스 같은 service discovery나 정적 설정으로 찾는다.

flowchart LR
  app["앱<br/>/metrics 노출"] -- "scrape (pull)" --> prom["Prometheus 서버"]
  prom --> tsdb[("TSDB")]
  prom -- "PromQL" --> grafana["Grafana, 알림"]

구성 요소는 몇 개 더 있다. 앱이 메트릭을 내보내게 하는 client library, 다른 시스템의 지표를 Prometheus 형식으로 바꿔 주는 exporter, 알림을 보내는 Alertmanager, 잠깐 실행되고 끝나는 batch 잡을 위한 Pushgateway다. 시각화는 대개 Grafana가 맡는다.

1.2 Time Series, Labels, and Samples

Prometheus의 저장 단위는 time series다. 문서의 정의는 이렇다.

Every time series is uniquely identified by its metric name and optional key-value pairs called labels.

표기는 이름{label="값", ...}이다.

text
api_http_requests_total{method="POST", handler="/messages"}

메트릭 이름은 따로 취급되는 것처럼 보이지만, 내부적으로는 __name__이라는 label이다. 그래서 위 시계열은 이렇게 써도 같다.

text
{__name__="api_http_requests_total", method="POST", handler="/messages"}

이 사실은 2편에서 중요해진다. 이름도 label이므로, label을 고치는 설정으로 메트릭 이름까지 바꿀 수 있다.

label 값이 하나라도 다르면 다른 시계열이다. 문서는 label을 추가하거나 지우는 것도 마찬가지라고 적는다.

The change of any label’s value, including adding or removing labels, will create a new time series.

시계열에 쌓이는 값 하나는 sample이다. sample은 float64 값 하나와 밀리초 단위 timestamp로 이뤄진다.

이름 규칙도 있다. 메트릭 이름은 [a-zA-Z_:][a-zA-Z0-9_:]*, label 이름은 [a-zA-Z_][a-zA-Z0-9_]*를 따르는 것이 좋고, 콜론은 recording rule용으로 남겨 둔다. __로 시작하는 label 이름은 Prometheus 내부용이다. 이름 짓기 문서는 메트릭 이름이 “측정하는 대상의 일반적인 특징"을 나타내야 한다고 말한다. http_requests_total처럼 무엇을 세는지가 이름이고, 어느 서버, 어느 경로인지는 label이다. 실행할 때마다 바뀌는 값이 이름에 들어가면 이 원칙이 깨지는데, 2편에서 그런 사례를 본다.

1.3 Metric Types

client library는 네 가지 타입을 제공한다. 서버는 native histogram을 빼면 모두 float 시계열로 저장하므로, 타입은 주로 값을 어떻게 해석하고 조회할지 정해 둔 약속이다.

타입뜻예
Counter늘기만 하는 누적값. 프로세스가 재시작하면 0으로 돌아간다처리한 요청 수, 에러 수
Gauge오르내리는 현재값메모리 사용량, 동시 요청 수
Histogram관측값을 구간(bucket)별로 세고 합계도 함께 준다요청 지연 시간 분포
Summaryclient 쪽에서 분위수를 계산해 준다지연 시간 p99

1.4 Jobs, Instances, and Scrapes

scrape할 수 있는 endpoint 하나를 instance라고 하고, 같은 목적을 가진 instance의 묶음을 job이라고 한다.

In Prometheus terms, an endpoint you can scrape is called an instance, usually corresponding to a single process. A collection of instances with the same purpose … is called a job.

scrape할 때 Prometheus는 두 label을 자동으로 붙인다. job은 설정에 적은 job 이름이고, instance는 긁은 대상의 <host>:<port>다.

scrape할 때마다 Prometheus가 스스로 만드는 시계열도 있다. 대상이 무엇을 내놓든 상관없이 생기는 시계열이고, 수집 상태와 cardinality를 볼 때 가장 먼저 찾게 된다.

시계열뜻
up대상에 닿았으면 1, 실패하면 0
scrape_duration_secondsscrape에 걸린 시간
scrape_samples_scraped대상이 노출한 sample 수
scrape_samples_post_metric_relabelingmetric relabel을 거친 뒤 남은 sample 수
scrape_series_added이번 scrape에서 새로 생긴 시계열 수(근사값)

scrape_samples_scraped와 scrape_samples_post_metric_relabeling을 비교하면 relabel로 얼마나 버렸는지 알 수 있다. scrape_series_added는 새 시계열이 얼마나 자주 생기는지, 즉 churn을 직접 보여 준다. 이 시계열들에는 metric relabel이 적용되지 않는다는 점도 기억해 둘 만하다. 그래서 2편에서 label을 지워도 up에는 대상의 label이 그대로 남는다.

1.5 Storage: The Local TSDB

Prometheus는 로컬 디스크에 자체 TSDB를 둔다. 들어온 sample은 2시간 단위 block으로 묶인다. block마다 세 가지가 들어 있다. sample 데이터를 담은 chunk, 메트릭 이름과 label로 시계열을 찾는 index, 메타 파일이다. 지금 받고 있는 block은 메모리에 두고, 장애에 대비해 sample을 먼저 WAL(write-ahead log)에 기록한다.

flowchart LR
  scrape["scrape된 sample"] --> wal["WAL"]
  scrape --> head["head block<br/>(메모리, 최근 2시간)"]
  head -- "2시간마다" --> b1[("block<br/>chunks + index + meta")]
  b1 --> compact["compaction<br/>더 큰 block으로"]

보관 기간은 --storage.tsdb.retention.time으로 정하고, 따로 정하지 않으면 15일이다.

index가 메트릭 이름과 label로 시계열을 찾는 구조라는 점이 중요하다. 이름이나 label 조합이 폭발하면 index와 메모리가 함께 커진다. 공식 문서가 label을 아껴 쓰라고 거듭 말하는 이유다.

로컬 저장소에는 한계도 분명하다.

Prometheus’s local storage is limited to a single node’s scalability and durability.

여러 Prometheus의 데이터를 모아 오래 보관하거나 한 번에 조회하려면 remote write로 원격 저장소에 보낸다. Thanos가 대표적인 선택지다. Thanos는 네 구성 요소로 이뤄진다. remote write를 받아 object storage에 block을 올리는 Receive, object storage의 과거 block을 조회하게 해 주는 Store Gateway, block을 합치고 downsampling하는 Compactor, 여러 소스를 한 번에 조회하는 Query다. 이렇게 데이터를 모은 곳에서는 여러 Prometheus의 메트릭 이름이 한 목록에 합쳐지므로, 한 곳에서 생긴 이름 폭발이 전체 조회를 느리게 만들 수 있다.

1.6 Kubernetes Service Discovery

쿠버네티스에서는 scrape 대상이 계속 바뀐다. pod가 뜨고 죽을 때마다 주소가 달라진다. Prometheus의 kubernetes_sd_config는 pod, service, endpoints 같은 쿠버네티스 객체를 지켜보다가 대상 목록을 갱신하고, 그 객체의 정보를 __meta_kubernetes_* label로 넘겨준다. 어떤 pod를 긁을지, 그 pod의 무엇을 label로 붙일지는 이 __meta_* label을 보고 정한다.

실무에서는 Prometheus 설정 파일을 직접 쓰기보다 Prometheus Operator를 쓰는 경우가 많다. Operator는 쿠버네티스 custom resource로 선언한 내용을 읽어 Prometheus 설정을 만들고, Prometheus가 그 설정을 다시 읽어 들이게 한다.

CRD역할
PrometheusPrometheus 인스턴스를 띄운다
ServiceMonitorlabel selector로 service를 골라 scrape 설정을 만든다
PodMonitorservice 없이 pod를 직접 골라 scrape한다
PrometheusRule알림 규칙과 recording rule을 정의한다
ScrapeConfig쿠버네티스 밖의 대상이나 특수한 설정을 다룬다

ServiceMonitor가 대상을 찾는 과정은 selector가 두 번 이어지는 구조다.

flowchart LR
  pod["Pod<br/>app=my-streaming-job"] -- "label selector" --> svc["Service"]
  svc --> ep["Endpoints"]
  sm["ServiceMonitor"] -- "label selector" --> svc
  prom["Prometheus CR<br/>serviceMonitorSelector"] -- "고름" --> sm
  prom -- "scrape" --> ep

service가 label selector로 pod를 고르면 endpoints가 생긴다. ServiceMonitor는 다시 label selector로 그 service를 고르고, 어느 port와 path를 긁을지 정한다. Prometheus 리소스는 serviceMonitorSelector로 어떤 ServiceMonitor를 쓸지 고른다.

yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-streaming-job
spec:
  selector:
    matchLabels:
      app: my-streaming-job
  podTargetLabels:
    - app
  endpoints:
    - port: spark-ui
      path: /metrics/executors/prometheus/
      interval: 60s

podTargetLabels는 pod의 쿠버네티스 label을 시계열 label로 옮긴다. 쿠버네티스 label 이름의 -는 _로 바뀐다. ServiceMonitor는 이 밖에도 namespace, service, pod, container, endpoint, instance, job label을 기본으로 붙인다.

prometheus.io/scrape: "true" 같은 annotation을 본 적이 있을 것이다. 이 annotation은 Prometheus의 공식 기능이 아니라 널리 쓰이는 관례라서, Prometheus 설정에 “이 annotation이 붙은 pod를 긁어라"라는 scrape job이 있을 때만 동작한다. ServiceMonitor는 annotation과 상관없이 label로 대상을 고른다.

1.7 PromQL Basics

기본 조회는 이름과 label로 시계열을 고르는 것이다.

promql
# 이름으로 조회 (instant vector: 각 시계열의 최신 값)
up

# label로 거르기
up{job="api-server"}

# 이름을 정규식으로 거르기 (__name__도 label이라 가능)
{__name__=~"http_.+_total"}

# 최근 5분 구간의 값들 (range vector)
scrape_series_added[5m]

cardinality를 살필 때 쓸 만한 쿼리도 있다.

promql
# job별 노출 sample 수와 relabel 뒤 남은 sample 수
sum by (job) (scrape_samples_scraped)
sum by (job) (scrape_samples_post_metric_relabeling)

# 최근 1시간 동안 새로 생긴 시계열 수 (churn)
sum by (job) (sum_over_time(scrape_series_added[1h]))

{__name__=~".+"}처럼 모든 이름에 걸리는 조건은 전체 index를 훑기 때문에 매우 무겁다. 이름이 이미 폭발한 환경에서는 이런 쿼리 하나가 조회 서버를 멈추게 할 수도 있다. 다음 편에서는 그 폭발이 어떻게 생기고, relabel로 어디까지 막을 수 있는지를 본다.

Discussion