BACKEND

실시간 성능 모니터링과 자동 스케일링 — OTel·KEDA·SLO 알림

junetapa 2026. 10. 30 업데이트 15 min read

백엔드 서비스의 실시간 성능 모니터링과 자동 스케일링을 한 흐름으로 정리한다. 무엇을 잴지(골든 시그널), 어떻게 모을지(OpenTelemetry·Prometheus), 언제 사람을 부를지(SLO 번레이트 알림), 무엇을 기준으로 늘릴지(HPA·KEDA)를 Prometheus·Kubernetes·KEDA 공식 문서와 Google SRE 워크북 기준으로 다룬다.

무엇을 잴 것인가 — 골든 시그널과 SLI

실시간 성능 모니터링과 자동 스케일링은 더 이상 대형 서비스만의 이야기가 아니다. 트래픽이 분 단위로 출렁이고 서비스가 여러 개로 나뉜 환경에서는 «지금 무슨 일이 벌어지고 있는가»를 바로 알지 못하면 장애 대응이 늦어진다. 모니터링은 그래프 구경이 아니라, 문제를 사용자보다 먼저 알아채고 확장·복구를 거는 운영 체계의 출발점이다.

출발점은 Google SRE가 정리한 골든 시그널(golden signals)이다. 지연 시간(latency), 트래픽(traffic), 오류율(errors), 포화도(saturation) 네 가지다. 수백 개의 메트릭을 무작정 모으기 전에 이 넷을 SLI(Service Level Indicator)로 먼저 정의하고, 그 위에 «한 달 동안 요청의 99.9%가 성공» 같은 SLO(목표)를 정한다. SLO가 있어야 알림 기준(6절)과 확장 기준(4절)이 숫자로 정해진다.

TIP

한 신호를 두 도구가 동시에 긁지 않게 역할을 나눈다. 같은 메트릭을 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에도 붙어 서비스 단위로 거르기 쉽다.
yaml
# 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 형식으로 노출하는 최소 구현이다. 미들웨어 한 겹이면 모든 라우트가 계측된다.

javascript
// 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)이 경계 하나와 맞도록 잡아야 «기준 안에 든 요청 비율»을 정확히 셀 수 있다.

promql
# 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개가 된다. 쓸 수 있는 메트릭은 네 갈래다.

종류출처쓰는 곳
Resourcemetrics.k8s.io(보통 Metrics Server)파드별 CPU·메모리
Podscustom.metrics.k8s.io파드별 사용자 메트릭(초당 요청 수 등)
Objectcustom.metrics.k8s.io특정 오브젝트 하나의 메트릭(인그레스 요청률 등)
Externalexternal.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 확인).

yaml
# 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 → 1

Prometheus 스케일러의 query는 원소 하나짜리 벡터나 스칼라를 돌려줘야 한다. activationThreshold(기본 0)는 0개에서 깨어날 기준이고, threshold는 깨어난 뒤 몇 개로 늘릴지의 목표 값이다.

NOTE

0개까지 줄이면 비용은 줄지만 첫 요청이 콜드 스타트를 겪는다. 사용자 대면 API는 minReplicaCount를 1 넘게(최소 1개) 두고, 큐를 소비하는 워커·배치처럼 비동기 작업에만 0을 적용하는 편이 안전하다. 쿠버네티스 배포 기본은 Kubernetes 입문 실전 배포 가이드를 참고한다.

운영 스택과 카디널리티

여럿이 운영하는 모니터링 스택은 대개 비슷한 모양이 된다. 노드나 파드 옆에 Collector를 두고, 메트릭은 Prometheus(장기 보관은 원격 저장소), 트레이스와 로그는 각 저장소로 보낸 뒤 대시보드 한 화면에서 함께 본다. 아래는 쿠버네티스 파드 자동 발견과 Alertmanager 연동을 포함한 Prometheus 설정 골격이다.

yaml
# 파드 자동 발견 + 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로 잡으라고 권한다. 위 표의 세 줄이 모두 이 비율이다.

yaml
# 멀티 윈도우 번레이트 알림 (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와 서로 값을 덮어쓰며 다툰다. 사람이 판단할 일(트래픽 전환, 데이터 정리)은 런북으로 남긴다.

TIP

모든 호출 알림에는 런북을 연결한다. 한밤중에 호출받은 당직자가 «이 알림이 울리면 무엇을 확인하고 어떻게 복구하는지»를 바로 따라갈 수 있어야 한다. 대응 절차가 없는 알림은 소음이다.

정리 — 작게 시작하는 순서

실시간 성능 모니터링과 자동 스케일링은 하나의 흐름이다. 신호를 한 규격으로 모으고, 골든 시그널을 SLI로 만들고, SLO 번레이트로 의미 있는 순간에만 사람을 부르고, 부하 신호에 맞춰 자동으로 늘린다. 처음부터 전부 갖추려 하지 말고 아래 순서로 넓힌다.

순서할 일끝났다는 기준
1핵심 서비스 하나에 골든 시그널 4개 노출p99 지연·오류율 대시보드가 있음
2SLI·SLO 정하기(예: 99.9%, 300ms)지연 SLI 식이 버킷 경계와 맞음
3번레이트 알림 2줄(14.4·6) + 런북호출 알림마다 런북 링크
4HPA(CPU request 확인) → 필요하면 KEDA축소 안정화 창 안에서 진동 없음
5OTLP 수집으로 넓히기올리는 리소스 속성 목록이 정해짐

측정하지 못하면 개선할 수도 없다. 작게 시작하되, 공통 규격 위에서 시작하는 것이 핵심이다.

실시간 성능 모니터링과 자동 스케일링 대표 그림 — 왼쪽에 제목 글자가 있고, 오른쪽에 그래프 패널이 달린 관제 화면과 그 신호에 맞춰 늘어나는 서버 블록 줄이 있는 장면
실시간 성능 모니터링과 자동 스케일링 — OTel·KEDA·SLO 알림.

자주 묻는 것

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·라우트 패턴처럼 값의 종류가 정해진 것만 씁니다.

참고 자료

  1. Google SRE Book. Monitoring Distributed Systems — 네 가지 골든 시그널(지연·트래픽·오류·포화도)(2026-10-30 확인). sre.google
  2. 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
  3. Prometheus GitHub 릴리스 — 3.15.1(2026-10-21)·3.16.0-rc.2(2026-10-28)(2026-10-30 확인). github.com
  4. Kubernetes. Horizontal Pod Autoscaling — 계산식·15초 주기·메트릭 네 종류·축소 안정화 300초·허용 오차 10%·HPAScaleToZero v1.37 베타(2026-10-30 확인). kubernetes.io
  5. KEDA. Concepts · Prometheus scaler(2.21) — HPA에 외부 메트릭 공급, 0↔1은 오퍼레이터, CPU·메모리 트리거 0 불가, query·threshold·activationThreshold(2026-10-30 확인). keda.sh
  6. Google SRE Workbook. Alerting on SLOs — 멀티 윈도우·멀티 번레이트 권장 값, 짧은 창 = 긴 창의 1/12(2026-10-30 확인). sre.google
모니터링 자동 스케일링 DevOps 쿠버네티스 Prometheus 백엔드
junetapa(박준철)
개발자가 아니라 쓰는 사람. IT 현장 27년의 기준으로 AI 도구를 고르고 기록한다.
Twitter Facebook URL 복사