LabHub
배우기 러닝패스 코스

PCA — 프로메테우스 인증 어소시에이트 · 타임스탬프·도함수·계측 API·스팬 · 이론

시각을 값으로 다루기 — timestamp(), 도함수, 계측 API, 스팬

LabHub 에서 이어서 보기

한 줄 요약

timestamp() 는 샘플의 시각을, time() 은 평가 시각을 값으로 꺼내 주므로 "언제부터" 를 뺄셈으로 계산할 수 있습니다. deriv()predict_linear() 는 게이지의 기울기로 추세를 읽고, 클라이언트 라이브러리는 Counter·Gauge·Histogram 을 레지스트리에 등록해 /metrics 로 노출하며, 트레이스는 trace_id 를 공유하고 parent_id 로 이어진 스팬의 트리입니다.

왜 이게 필요했나

"이 프로세스가 마지막으로 재시작한 게 언제인가", "배포된 지 얼마나 됐나", "디스크가 언제 찰까" 는 모두 시각을 계산해야 답이 나옵니다. Prometheus 계측 지침은 여기서 한 가지 원칙을 세웁니다. 경과 시간이 아니라 사건이 일어난 Unix 시각을 내보내라는 것입니다. "마지막 성공으로부터 N초" 를 애플리케이션이 직접 갱신하면 갱신 로직이 멈췄을 때 값도 멈춥니다. 시각을 내보내면 time() - my_timestamp_metric 으로 언제나 올바른 경과 시간이 나오고, 갱신 로직이 멈추는 문제에서 벗어납니다.

PCA 의 PromQL 도메인(28%)에는 "timestamp metrics" 항목이, Instrumentation 도메인(16%)에는 "client libraries" 가, Observability Concepts(18%)에는 "tracing and spans" 가 있습니다. 세 가지 모두 앞 모듈에서 짧게만 다루었던 것이라 여기서 한 번에 정리합니다.

어떻게 동작하나

time(), timestamp(), 그리고 시작 시각 지표

time() 은 1970-01-01 UTC 부터의 초를 돌려주는데, 문서는 이것이 현재 시각이 아니라 표현식이 평가되는 시각이라고 강조합니다. 과거 구간을 그래프로 그릴 때 각 점마다 그 점의 시각이 들어간다는 뜻입니다. timestamp(v) 는 인스턴트 벡터의 각 샘플이 찍힌 시각을 초로 돌려주며 float 과 히스토그램 샘플을 같은 방식으로 다룹니다.

클라이언트 라이브러리가 표준으로 내보내는 process_start_time_seconds 는 "프로세스가 시작한 Unix 시각(초)" 입니다. 이 지표 하나로 재시작을 잡을 수 있습니다.

# 지금 기준으로 프로세스가 동작한 시간(초)time() - process_start_time_seconds# 최근 1시간 안에 시작 시각이 바뀐(=재시작한) 인스턴스changes(process_start_time_seconds[1h]) > 0# 스크레이프가 얼마나 오래됐나 — 샘플 시각과 평가 시각의 차이time() - timestamp(up)

changes() 는 범위 안에서 값이 바뀐 횟수를 세므로 시작 시각이 바뀌면 재시작으로 읽힙니다. 카운터라면 resets() 가 감소 횟수를 세어 같은 목적에 씁니다. 마지막 식은 스테일니스(staleness)를 이해해야 읽힙니다. 쿼리 시각은 실제 샘플과 무관하게 정해지고, Prometheus 는 각 시리즈에 대해 lookback 기간(기본 5분, --query.lookback-delta 로 조정) 안의 가장 새 샘플을 그 시각의 값으로 씁니다. 대상이 사라지면 시리즈는 곧 stale 로 표시되어 결과에서 빠집니다. 그래서 timestamp() 로 본 샘플 시각이 평가 시각보다 몇 분 뒤처져 있으면 스크레이프가 지연되고 있다는 신호입니다.

deriv() 와 predict_linear() — 게이지 전용 도함수

rate() 가 카운터의 초당 증가율이라면, deriv(v range-vector)단순 선형 회귀로 구한 초당 도함수이고 predict_linear(v, t) 는 같은 회귀로 t초 뒤의 값을 예측합니다. 두 함수 모두 문서에 "게이지에만 써야 하며 float 샘플에서만 동작한다" 고 적혀 있고, 범위 안에 float 샘플이 둘 이상 있어야 계산됩니다. +Inf 나 -Inf 가 끼면 결과는 NaN 입니다. 게이지의 차이가 필요하면 delta() 를 쓰는데, 이것은 첫 값과 마지막 값의 차이를 범위 전체로 외삽하므로 정수 샘플에서도 소수가 나올 수 있습니다. 계측 지침은 게이지에 rate() 를 쓰지 말라고 명시합니다.

# 4시간 추세로 볼 때 6시간 뒤 남은 디스크가 0 이하가 되는가predict_linear(node_filesystem_avail_bytes{mountpoint="/data"}[4h], 6 * 3600) < 0

클라이언트 라이브러리 — 등록하고, 노출하고, 스크레이프 때 읽힌다

공식 라이브러리는 Go, Java/Scala, Node.js, Python, Ruby, Rust 이고, 라이브러리는 스크레이프 시점에 추적 중인 모든 지표의 현재 상태를 내보냅니다. 값을 밀어 보내는 것이 아니라 읽어 가는 구조입니다. 네 가지 핵심 타입 중 Counter 는 오르기만 하고 재시작에 0 으로 돌아가며(내려갈 수 있는 값에는 쓰지 말 것), Gauge 는 오르내리고, Histogram 은 관측값을 버킷에 세어 _bucket{le}, _sum, _count 세 시리즈(고전 히스토그램 기준)로 노출합니다.

from prometheus_client import Counter, Histogram, start_http_serverREQS = Counter("requests_total", "Total requests",               labelnames=["method"], namespace="myapp")LAT = Histogram("request_duration_seconds", "HTTP request latency",                labelnames=["method", "endpoint"], namespace="myapp",                buckets=[.01, .05, .1, .25, .5, 1, 2.5, 5])def handle(method, endpoint):    REQS.labels(method=method).inc()    with LAT.labels(method=method, endpoint=endpoint).time():        ...start_http_server(8000)   # /metrics 노출

Python 에서 Counter 는 이름 끝의 _total 을 떼었다가 노출할 때 다시 붙입니다(OpenMetrics 가 _total 을 요구하기 때문). namespace·subsystem·name 은 밑줄로 이어져 전체 이름이 되고, registry 는 기본 REGISTRY 이며 None 을 주면 등록하지 않습니다(시험 코드용). Histogramle 는 예약된 라벨이라 라벨 이름으로 쓸 수 없고, buckets 는 오름차순이어야 하며 +Inf 는 항상 자동으로 붙습니다. 기본 버킷은 .005 .01 .025 .05 .075 .1 .25 .5 .75 1 2.5 5 7.5 10 입니다.

Go 는 prometheus.NewRegistry() 로 레지스트리를 만들고 reg.MustRegister(collectors.NewGoCollector(), collectors.NewProcessCollector(...)) 로 런타임·프로세스 수집기를 등록한 뒤, promauto.With(reg).NewCounter(prometheus.CounterOpts{Name: ..., Help: ...}) 로 지표를 만들고 promhttp.HandlerFor(reg, ...)/metrics 에 겁니다. HistogramOptsBuckets 를 비우면 DefBuckets(.005 .01 .025 .05 .1 .25 .5 1 2.5 5 10)가 쓰이는데, 문서는 이것이 네트워크 서비스 응답 시간을 넓게 재도록 맞춘 값이라 대부분 자기 용도에 맞는 버킷을 정의해야 할 것이라고 적습니다.

버킷 설계

고전 히스토그램에서 버킷은 계측 시점에 고정되고, 버킷마다 시리즈가 하나씩 생기며(비어 있어도), 나중에 바꾸면 다른 레이아웃끼리 집계가 되지 않아 큰 혼란이 생깁니다. 지침은 예상 값 범위와 하고 싶은 쿼리에 맞춰 버킷을 고르라고 합니다. 예를 들어 "요청의 95% 를 300ms 안에" 라는 SLO 가 있으면 0.3 에 경계를 두어야 _bucket{le="0.3"} 로 정확한 비율이 나옵니다. 분위수는 histogram_quantile(0.95, sum by (le) (rate(x_bucket[5m]))) 로 서버에서 계산하며, 추정 오차는 분위수가 속한 버킷의 폭 안으로 제한됩니다. 네이티브 히스토그램(Go·Java 지원)은 버킷을 고르지 않고 해상도만 정하므로 문서는 가능하면 그쪽을 권합니다. 또 하나의 지침은 없는 지표를 만들지 말라는 것입니다. 사건이 나기 전까지 시리즈가 없으면 쿼리가 어려우니 0 을 미리 내보내며, 라벨 없는 지표는 대부분 라이브러리가 자동으로 0 을 냅니다.

트레이스와 스팬

트레이스는 요청이 애플리케이션을 지나간 경로이고, 스팬은 그 안의 작업 단위입니다. 스팬에는 이름, 부모 스팬 ID(루트는 비어 있음), 시작·종료 시각, 스팬 컨텍스트(trace ID, span ID, trace flags, trace state), 속성(attributes), 이벤트, 링크, 상태가 들어갑니다. 같은 트레이스의 스팬은 같은 trace_id 를 공유하고, 자식의 parent_id 는 부모의 span_id 와 같습니다. 이 두 필드만으로 트리가 만들어집니다. OpenTelemetry 문서는 스팬을 "문맥·상관관계·계층이 들어간 구조화 로그" 로 비유합니다.

서비스 경계를 넘을 때는 컨텍스트 전파(context propagation)가 필요합니다. 기본 전파기는 W3C TraceContext 로, traceparent 헤더에 <version>-<trace-id>-<parent-id>-<trace-flags> 형식(예: 00-a0892f3577b34da6a3ce929d0e0e4736-f03067aa0ba902b7-01)으로 담아 보내고, 받는 쪽이 이를 꺼내 새 스팬의 부모로 삼습니다. 전파는 보통 계측 라이브러리가 자동으로 합니다. 지표와 트레이스를 잇는 고리가 exemplar 입니다. Python 의 observe(0.43, exemplar={"trace_id": "..."}) 처럼 관측값에 trace_id 를 붙일 수 있고, OpenMetrics 형식에서만 노출됩니다.

현장에서 만나는 모습

"메모리 누수 알림이 새벽마다 오는데 아침엔 정상" 인 경우가 있습니다. predict_linear 를 30분 범위로 걸어 두면 야간 배치가 도는 30분 동안의 기울기만으로 몇 시간 뒤를 외삽해 버립니다. 범위를 배치 주기보다 길게 잡거나 for 지속 시간을 두는 것이 지침에 맞는 대응입니다.

재시작 감지에서는 changes(process_start_time_seconds[1h]) 대신 up == 0 을 쓰다가 놓치는 일이 있습니다. 재시작이 스크레이프 간격 안에 끝나면 up 은 한 번도 0 이 되지 않습니다. 시작 시각 지표는 프로세스가 스스로 내보내는 값이라 짧은 재시작도 값이 바뀌어 남습니다.

다음 퀴즈에서 확인할 것

time() 이 돌려주는 시각의 정확한 뜻, timestamp() 와 시작 시각 지표로 만드는 식, lookback 기본값, deriv/predict_linear 가 요구하는 조건, Python·Go 계측 API 의 등록 방식과 기본 버킷, 버킷 설계 지침, 스팬의 필수 요소와 traceparent 헤더 형식을 묻습니다. 참고: [PromQL Functions](https://prometheus.io/docs/prometheus/latest/querying/functions/), [Querying basics — Staleness](https://prometheus.io/docs/prometheus/latest/querying/basics/), [Instrumentation practices](https://prometheus.io/docs/practices/instrumentation/), [Histograms and summaries](https://prometheus.io/docs/practices/histograms/), [client_python Histogram](https://prometheus.github.io/client_python/instrumenting/histogram/), [Instrumenting a Go application](https://prometheus.io/docs/guides/go-application/), [OpenTelemetry Traces](https://opentelemetry.io/docs/concepts/signals/traces/), [Context propagation](https://opentelemetry.io/docs/concepts/context-propagation/).