무엇을 잴 것인가 — 골든 시그널과 SLI
실시간 성능 모니터링과 자동 스케일링은 더 이상 대형 서비스만의 이야기가 아니다. 트래픽이 분 단위로 출렁이고 서비스가 여러 개로 나뉜 환경에서는 «지금 무슨 일이 벌어지고 있는가»를 바로 알지 못하면 장애 대응이 늦어진다. 모니터링은 그래프 구경이 아니라, 문제를 사용자보다 먼저 알아채고 확장·복구를 거는 운영 체계의 출발점이다.
출발점은 Google SRE가 정리한 골든 시그널(golden signals)이다. 지연 시간(latency), 트래픽(traffic), 오류율(errors), 포화도(saturation) 네 가지다. 수백 개의 메트릭을 무작정 모으기 전에 이 넷을 SLI(Service Level Indicator)로 먼저 정의하고, 그 위에 «한 달 동안 요청의 99.9%가 성공» 같은 SLO(목표)를 정한다. SLO가 있어야 알림 기준(6절)과 확장 기준(4절)이 숫자로 정해진다.
한 신호를 두 도구가 동시에 긁지 않게 역할을 나눈다. 같은 메트릭을 Prometheus가 스크레이프하고 OpenTelemetry로도 보내면 비용과 시계열 수만 두 배로 늘어난다.
신호를 모으는 구조 — OpenTelemetry와 Prometheus
세 가지 신호
관측 데이터는 세 종류다. 메트릭은 시계열 숫자로 추세와 임계치 알림에 강하고, 로그는 개별 사건의 맥락을 담고, 트레이스는 요청 하나가 여러 서비스를 거치는 경로를 따라간다. 셋을 OpenTelemetry(OTel) 규격으로 내보내고, 저장소는 신호별로 고르는 구성이 흔하다. 메트릭은 Prometheus, 로그와 트레이스는 각각 맞는 저장소로 보낸다.
OpenTelemetry Collector의 역할
Collector는 애플리케이션과 저장소 사이에 두는 중간 관문이다. 애플리케이션은 OTLP로 한 곳에 보내고, Collector가 묶기·거르기·속성 가공을 거쳐 저장소별로 나눈다. 코드를 고치지 않고 수집 대상이나 저장소를 바꿀 수 있다는 것이 핵심 이점이다.
Prometheus가 OTLP를 직접 받는다
Prometheus 공식 가이드 기준으로 Prometheus는 --web.enable-otlp-receiver 플래그로 OTLP 수신을 켜고(기본 꺼짐), /api/v1/otlp/v1/metrics 경로로 OTLP 메트릭을 받는다. 가이드가 권하는 설정은 세 가지다.
- 순서 어긋난 샘플 허용 —
storage.tsdb.out_of_order_time_window: 30m. 가이드는 대부분 이 값으로 충분했다고 적는다. - 리소스 속성을 라벨로 올리기 —
otlp.promote_resource_attributes에service.name·service.instance.id·k8s.pod.name등을 적는다. 기본값은 아무것도 올리지 않음이다. - 식별 속성 남기기 —
otlp.keep_identifying_resource_attributes: true면service.name·service.namespace·service.instance.id가target_info에도 붙어 서비스 단위로 거르기 쉽다.
# prometheus.yml — OTLP 수신 설정(실행 플래그: --web.enable-otlp-receiver)
storage:
tsdb:
out_of_order_time_window: 30m
otlp:
promote_resource_attributes:
- service.name
- service.instance.id
- k8s.namespace.name
- k8s.pod.name
keep_identifying_resource_attributes: true
# otel-collector.yaml — 메트릭은 Prometheus OTLP로, 트레이스는 트레이스 저장소로
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch: {}
exporters:
otlphttp/prometheus:
endpoint: http://prometheus:9090/api/v1/otlp
otlp/traces:
endpoint: traces-backend:4317
service:
pipelines:
metrics: { receivers: [otlp], processors: [batch], exporters: [otlphttp/prometheus] }
traces: { receivers: [otlp], processors: [batch], exporters: [otlp/traces] }메트릭 이름 변환 방식(otlp.translation_strategy)의 기본값은 밑줄 치환과 단위 접미사를 붙이는 방식이다. UTF-8 이름을 그대로 쓰는 방식은 UTF-8 지원을 켜야 한다. 델타 메트릭을 누적으로 바꾸는 기능은 아직 실험 단계(otlp-deltatocumulative 기능 플래그)다. 지금 Prometheus 최신 안정판은 3.15.1(2026-10-21)이고 3.16은 RC 단계다(2026-10-30 확인). Prometheus와 Grafana를 처음 세우는 순서는 Prometheus·Grafana 모니터링 구축 가이드에 따로 정리했다.
서비스 계측 — 히스토그램과 PromQL
실제 백엔드 서비스를 계측해 본다. 골든 시그널을 메트릭으로 내보내는 것이 핵심이다. 아래는 Node.js 서비스에서 HTTP 요청 수(트래픽·오류율)와 응답 지연을 Prometheus 형식으로 노출하는 최소 구현이다. 미들웨어 한 겹이면 모든 라우트가 계측된다.
// Node.js 골든 시그널 계측 (prom-client)
const express = require('express');
const client = require('prom-client');
const app = express();
client.collectDefaultMetrics(); // CPU·메모리·이벤트 루프 등 기본 메트릭
// 요청 수 (트래픽 + 오류율)
const httpRequests = new client.Counter({
name: 'http_requests_total',
help: 'HTTP 요청 총 수',
labelNames: ['method', 'route', 'status'],
});
// 응답 지연 — 히스토그램으로 분위수 계산
const httpDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP 응답 지연(초)',
labelNames: ['method', 'route', 'status'],
buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5],
});
app.use((req, res, next) => {
const end = httpDuration.startTimer();
res.on('finish', () => {
// route 는 패턴(/users/:id)으로 — 실제 경로를 넣으면 시계열이 끝없이 는다
const labels = { method: req.method, route: req.route?.path || 'unmatched', status: res.statusCode };
httpRequests.inc(labels);
end(labels);
});
next();
});
// Prometheus 스크레이프 엔드포인트
app.get('/metrics', async (_req, res) => {
res.set('Content-Type', client.register.contentType);
res.end(await client.register.metrics());
});
app.listen(3000);지연은 히스토그램으로 내보낸다. 평균 응답 시간은 꼬리 지연을 가리므로 p95·p99 같은 분위수를 봐야 실제 사용자가 겪는 느림이 보인다. Prometheus에서는 histogram_quantile(0.99, ...)로 p99를 계산한다. 버킷 경계는 SLO 기준값(예: 300ms)이 경계 하나와 맞도록 잡아야 «기준 안에 든 요청 비율»을 정확히 셀 수 있다.
# p99 응답 지연 (5분 창, 라우트별)
histogram_quantile(0.99,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m]))
)
# 오류율 (5xx 비율)
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# 지연 SLI — 300ms 안에 끝난 요청 비율
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))자동 스케일링 — HPA와 KEDA
HPA가 계산하는 방식
Kubernetes HPA(Horizontal Pod Autoscaler)는 기본 15초마다 메트릭을 보고 원하는 레플리카 = ceil[현재 레플리카 × (현재 값 ÷ 목표 값)]으로 개수를 정한다. 공식 문서 예로, 레플리카 4개의 CPU 사용률이 200%이고 목표가 100%면 ceil(4 × 2) = 8개가 된다. 쓸 수 있는 메트릭은 네 갈래다.
| 종류 | 출처 | 쓰는 곳 |
|---|---|---|
| Resource | metrics.k8s.io(보통 Metrics Server) | 파드별 CPU·메모리 |
| Pods | custom.metrics.k8s.io | 파드별 사용자 메트릭(초당 요청 수 등) |
| Object | custom.metrics.k8s.io | 특정 오브젝트 하나의 메트릭(인그레스 요청률 등) |
| External | external.metrics.k8s.io | 클러스터 밖 값(큐 길이 등) |
진동을 막는 장치도 기본으로 들어 있다. 축소 쪽 안정화 창 기본값은 300초(5분)라 그 사이 나온 권고 중 가장 큰 값을 따르고, 목표와 10% 안쪽 차이는 무시한다(기본 허용 오차). CPU 기준을 쓰려면 컨테이너에 CPU request가 있어야 한다. 없으면 HPA가 그 메트릭으로는 아무것도 하지 않는다.
0개까지 줄이기
Kubernetes 문서 기준으로 HPA의 minReplicas: 0은 HPAScaleToZero 기능 게이트로 v1.37부터 베타·기본 켜짐이다. 단, Object나 External 메트릭이 하나 이상 있어야 하고 CPU·메모리 같은 Resource 메트릭만으로는 0까지 내릴 수 없다. 실행 중인 파드가 없으면 잴 수 없기 때문이다. 쓰는 클러스터 판이 그보다 낮다면 이 기능은 쓸 수 없다.
KEDA — 이벤트 기준 확장
KEDA(Kubernetes Event-Driven Autoscaling)는 HPA를 대신하지 않고 HPA에 외부 메트릭을 먹여 CPU·메모리 밖의 이벤트 소스로 확장을 넓힌다. 문서 설명대로 0↔1 구간은 KEDA 오퍼레이터가 직접 맡고, 1↔N 구간은 HPA가 KEDA가 준 메트릭으로 정한다. CPU·메모리 트리거로는 0까지 줄일 수 없다는 제한도 문서에 있다. 문서 사이트 기준 최신판은 2.21이다(2026-10-30 확인).
# KEDA ScaledObject — Prometheus 메트릭 기준 + 일 없으면 0개
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-scaler
spec:
scaleTargetRef:
name: worker-deployment
minReplicaCount: 0 # 일이 없으면 0개
maxReplicaCount: 20
cooldownPeriod: 120 # 마지막 활성 뒤 0으로 내리기 전 대기(초)
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
query: sum(rate(jobs_enqueued_total{queue="mail"}[1m]))
threshold: "50" # 파드 하나가 맡을 목표 값
activationThreshold: "1" # 이 값을 넘어야 0 → 1Prometheus 스케일러의 query는 원소 하나짜리 벡터나 스칼라를 돌려줘야 한다. activationThreshold(기본 0)는 0개에서 깨어날 기준이고, threshold는 깨어난 뒤 몇 개로 늘릴지의 목표 값이다.
0개까지 줄이면 비용은 줄지만 첫 요청이 콜드 스타트를 겪는다. 사용자 대면 API는 minReplicaCount를 1 넘게(최소 1개) 두고, 큐를 소비하는 워커·배치처럼 비동기 작업에만 0을 적용하는 편이 안전하다. 쿠버네티스 배포 기본은 Kubernetes 입문 실전 배포 가이드를 참고한다.
운영 스택과 카디널리티
여럿이 운영하는 모니터링 스택은 대개 비슷한 모양이 된다. 노드나 파드 옆에 Collector를 두고, 메트릭은 Prometheus(장기 보관은 원격 저장소), 트레이스와 로그는 각 저장소로 보낸 뒤 대시보드 한 화면에서 함께 본다. 아래는 쿠버네티스 파드 자동 발견과 Alertmanager 연동을 포함한 Prometheus 설정 골격이다.
# 파드 자동 발견 + Alertmanager 연동
apiVersion: v1
kind: ConfigMap
metadata:
name: monitoring-config
data:
prometheus.yml: |
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "slo.rules.yml"
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
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093카디널리티가 비용을 정한다
Prometheus에서 시계열 하나는 «메트릭 이름 + 라벨 값 조합» 하나다. 라벨에 사용자 ID나 요청 ID처럼 값이 끝없이 늘어나는 항목을 넣으면 시계열이 폭증해 메모리와 저장 비용이 뛴다. 3절 예제에서 route에 실제 경로 대신 라우트 패턴을 넣은 이유가 이것이다. 높은 카디널리티 식별자는 메트릭 라벨이 아니라 트레이스·로그로 보낸다. OTLP로 받을 때도 2절의 promote_resource_attributes에 올리는 속성을 꼭 필요한 것만 고른다.
| 라벨 후보 | 판단 | 이유 |
|---|---|---|
| method·status·route 패턴 | 라벨로 둠 | 값의 종류가 정해져 있음 |
| service.name·namespace | 라벨로 둠 | 서비스 단위로 거르기 |
| pod 이름 | 필요할 때만 | 재배포마다 새 값이 생김 |
| 사용자 ID·요청 ID·전체 URL | 라벨 금지 → 트레이스·로그 | 값이 끝없이 늘어남 |
서비스가 여러 개로 나뉜 구조에서 컨테이너 자원 설정을 함께 다듬는 방법은 마이크로서비스 아키텍처와 Docker 컨테이너 최적화 — 이미지·메시·보안에 정리했다.
알림 — SLO와 멀티 윈도우 번레이트
좋은 알림은 «장애가 났다»가 아니라 «사용자가 영향받기 전에, 행동할 만한 신호만» 울린다. 그래서 단일 임계치 대신 SLO의 에러 버짓에 기댄 멀티 윈도우·멀티 번레이트 알림을 쓴다. 번레이트는 에러 버짓을 얼마나 빨리 쓰는지를 나타낸다. 1이면 기간이 끝날 때 버짓을 정확히 다 쓰는 속도이고, 14.4면 30일 버짓을 약 2일(30 ÷ 14.4) 만에 다 쓴다.
워크북 권장 값(99.9% SLO)
| 버짓 소진 | 긴 창 | 짧은 창 | 번레이트 | 알림 |
|---|---|---|---|---|
| 2% | 1시간 | 5분 | 14.4 | 호출(page) |
| 5% | 6시간 | 30분 | 6 | 호출(page) |
| 10% | 3일 | 6시간 | 1 | 티켓 |
긴 창은 계속되는 문제를 잡고, 짧은 창은 «지금도 진행 중인지»를 확인해 이미 끝난 문제로 울리는 오탐을 거른다. 워크북은 짧은 창을 긴 창의 1/12로 잡으라고 권한다. 위 표의 세 줄이 모두 이 비율이다.
# 멀티 윈도우 번레이트 알림 (99.9% SLO → 버짓 0.001)
groups:
- name: slo-burn-rate
rules:
- alert: ErrorBudgetBurnFast
# 1시간 창과 5분 창이 모두 14.4배를 넘을 때만
expr: |
(slo:error_ratio:rate1h > (14.4 * 0.001))
and
(slo:error_ratio:rate5m > (14.4 * 0.001))
labels: { severity: page }
annotations:
summary: "에러 버짓 빠르게 소진 중 (이 속도면 약 2일 안에 바닥)"
runbook: "https://runbooks.example.com/error-budget-burn"
- alert: ErrorBudgetBurnSlow
expr: |
(slo:error_ratio:rate6h > (6 * 0.001))
and
(slo:error_ratio:rate30m > (6 * 0.001))
labels: { severity: page }slo:error_ratio:rate1h 같은 이름은 3절의 오류율 식을 창 길이별로 미리 계산해 두는 기록 규칙(recording rule)이다. 알림 식에서 매번 긴 창을 다시 계산하지 않게 해 준다.
자동 복구는 좁게
알림 뒤 자동 조치를 붙일 때는 범위를 좁힌다. 확장은 4절의 HPA·KEDA에 맡기고, 재시작은 Kubernetes의 liveness 프로브처럼 플랫폼이 이미 가진 장치를 쓴다. 직접 만든 스크립트가 레플리카 수를 바꾸면 HPA와 서로 값을 덮어쓰며 다툰다. 사람이 판단할 일(트래픽 전환, 데이터 정리)은 런북으로 남긴다.
모든 호출 알림에는 런북을 연결한다. 한밤중에 호출받은 당직자가 «이 알림이 울리면 무엇을 확인하고 어떻게 복구하는지»를 바로 따라갈 수 있어야 한다. 대응 절차가 없는 알림은 소음이다.
정리 — 작게 시작하는 순서
실시간 성능 모니터링과 자동 스케일링은 하나의 흐름이다. 신호를 한 규격으로 모으고, 골든 시그널을 SLI로 만들고, SLO 번레이트로 의미 있는 순간에만 사람을 부르고, 부하 신호에 맞춰 자동으로 늘린다. 처음부터 전부 갖추려 하지 말고 아래 순서로 넓힌다.
| 순서 | 할 일 | 끝났다는 기준 |
|---|---|---|
| 1 | 핵심 서비스 하나에 골든 시그널 4개 노출 | p99 지연·오류율 대시보드가 있음 |
| 2 | SLI·SLO 정하기(예: 99.9%, 300ms) | 지연 SLI 식이 버킷 경계와 맞음 |
| 3 | 번레이트 알림 2줄(14.4·6) + 런북 | 호출 알림마다 런북 링크 |
| 4 | HPA(CPU request 확인) → 필요하면 KEDA | 축소 안정화 창 안에서 진동 없음 |
| 5 | OTLP 수집으로 넓히기 | 올리는 리소스 속성 목록이 정해짐 |
측정하지 못하면 개선할 수도 없다. 작게 시작하되, 공통 규격 위에서 시작하는 것이 핵심이다.
자주 묻는 것
HPA만으로 파드를 0개까지 줄일 수 있나요?
Kubernetes 문서 기준으로 HPAScaleToZero 기능이 v1.37부터 베타로 기본 켜져 있어 minReplicas 0을 쓸 수 있습니다. 다만 Object나 External 메트릭이 하나 이상 있어야 하고, CPU·메모리 메트릭만으로는 0까지 내릴 수 없습니다. 그보다 낮은 판이라면 KEDA로 0과 1 사이를 맡깁니다.
Prometheus가 OpenTelemetry 메트릭을 직접 받을 수 있나요?
받을 수 있습니다. --web.enable-otlp-receiver 플래그로 OTLP 수신을 켜면 /api/v1/otlp/v1/metrics 경로로 받습니다. 순서 어긋난 샘플 허용 창 30분과 필요한 리소스 속성만 라벨로 올리는 설정을 함께 둡니다.
번레이트 14.4는 어떻게 나온 숫자인가요?
99.9% SLO에서 30일 에러 버짓의 2%를 1시간 안에 쓰는 속도입니다. 이 속도가 이어지면 약 2일 만에 버짓이 바닥납니다. Google SRE 워크북은 1시간 긴 창과 5분 짧은 창이 모두 이 값을 넘을 때 호출하도록 권합니다.
메트릭 라벨에 사용자 ID를 넣으면 안 되나요?
넣지 않습니다. 라벨 값 조합마다 시계열이 하나씩 생기므로 값이 끝없이 늘어나는 사용자 ID·요청 ID는 시계열을 폭증시킵니다. 이런 식별자는 트레이스나 로그로 보내고, 라벨은 method·status·라우트 패턴처럼 값의 종류가 정해진 것만 씁니다.
참고 자료
- Google SRE Book. Monitoring Distributed Systems — 네 가지 골든 시그널(지연·트래픽·오류·포화도)(2026-10-30 확인). sre.google
- Prometheus. Using Prometheus as your OpenTelemetry backend — --web.enable-otlp-receiver, /api/v1/otlp/v1/metrics, out_of_order_time_window 30m, promote_resource_attributes, keep_identifying_resource_attributes, translation_strategy(2026-10-30 확인). prometheus.io
- Prometheus GitHub 릴리스 — 3.15.1(2026-10-21)·3.16.0-rc.2(2026-10-28)(2026-10-30 확인). github.com
- Kubernetes. Horizontal Pod Autoscaling — 계산식·15초 주기·메트릭 네 종류·축소 안정화 300초·허용 오차 10%·HPAScaleToZero v1.37 베타(2026-10-30 확인). kubernetes.io
- KEDA. Concepts · Prometheus scaler(2.21) — HPA에 외부 메트릭 공급, 0↔1은 오퍼레이터, CPU·메모리 트리거 0 불가, query·threshold·activationThreshold(2026-10-30 확인). keda.sh
- Google SRE Workbook. Alerting on SLOs — 멀티 윈도우·멀티 번레이트 권장 값, 짧은 창 = 긴 창의 1/12(2026-10-30 확인). sre.google