~/infra-dev.md
INFRA_DEV

HPA는 왜 CPU보다 큐 깊이를 봐야 할까: Kubernetes custom metrics로 배우는 백프레셔 오토스케일링

10분 읽기·2026.07.28·출처 5·00
오늘의 질문

워커가 느려질 때 Pod CPU가 30%라면, 우리는 정말 “여유가 있다”고 판단해도 될까?

infra-dev.md
HPA queue metrics

HPA는 왜 CPU보다 큐 깊이를 봐야 할까: Kubernetes custom metrics로 배우는 백프레셔 오토스케일링

1. 왜 지금 봐야 하나

Kubernetes 공식 블로그는 2026년 7월 14일 “custom metrics exporter”를 처음부터 만드는 글을 올렸다. 글의 출발점은 단순하다. Kubernetes가 기본으로 잘 아는 신호는 CPU와 메모리지만, 실제 운영에서 스케일링을 결정하는 신호는 종종 그 밖에 있다는 것이다.

예를 들면 이런 값들이다.

  • 대기 중인 메시지 수, 즉 queue depth
  • Kafka consumer lag 또는 Pub/Sub backlog
  • 마지막 배치 작업 처리 시간
  • 활성 WebSocket 연결 수
  • 초당 처리한 job 수와 실패 수

워커 시스템을 운영해 본 사람이라면 바로 체감할 수 있다. 메시지가 10만 개 쌓여 있는데 Pod CPU가 낮을 수 있다. 왜냐하면 워커가 외부 API rate limit을 기다리거나, DB lock에 막히거나, 단일 파티션 순서 보장을 위해 일부만 처리하고 있을 수 있기 때문이다. 반대로 CPU가 높아도 큐가 비어 있다면 더 늘리는 것이 낭비일 수 있다.

그래서 오늘의 핵심은 이것이다.

오토스케일링은 “서버가 바쁜가”가 아니라 “사용자나 downstream이 기다리는 일이 늘고 있는가”를 봐야 한다.

이번 글은 Kubernetes custom metrics exporter 글을 계기로, HPA(Horizontal Pod Autoscaler)를 CPU 자동 증설 장치가 아니라 백프레셔 제어 루프로 이해하는 연습이다. 특히 작은 팀이 큐·워커·배치 처리 시스템을 운영할 때 어떤 지표를 만들고, Prometheus와 adapter를 어떻게 신뢰 경로로 묶고, 어떤 실패 조건을 runbook에 넣어야 하는지 정리한다.

2. 핵심 개념

HPA는 Deployment나 StatefulSet 같은 scalable workload의 replica 수를 조정하는 Kubernetes 컨트롤러다. 공식 문서에 따르면 HPA는 연속적으로 “감”을 보는 프로세스가 아니라, 기본적으로 --horizontal-pod-autoscaler-sync-period 주기마다 목표 리소스와 metric API를 조회하는 간헐적 control loop다. 기본 주기는 15초다.

가장 단순한 계산식은 다음과 같다.

zsh — 생존확인.sh
desiredReplicas = ceil(currentReplicas × currentMetricValue / desiredMetricValue)

현재 metric이 목표보다 두 배 크면 replica를 대략 두 배로 늘리고, 절반이면 줄인다. 여기까지는 쉽다. 어려운 부분은 “currentMetricValue를 어디서 가져오느냐”다.

Kubernetes HPA는 여러 metric API를 사용할 수 있다.

  • metrics.k8s.io: CPU·메모리 같은 resource metrics. 보통 Metrics Server가 제공한다.
  • custom.metrics.k8s.io: Pod, Deployment 같은 Kubernetes 객체와 연결된 사용자 정의 metric.
  • external.metrics.k8s.io: queue depth, cloud metric, SaaS 지표처럼 Kubernetes 객체 바깥의 metric.

공식 custom metrics exporter 글은 exporter를 “/metrics endpoint 하나로 애플리케이션 또는 외부 시스템 상태를 Prometheus 형식으로 노출하는 작은 HTTP 서버”로 설명한다. Prometheus는 이를 주기적으로 scrape하고, query와 alert, autoscaling rule의 입력으로 저장한다.

여기서 metric 타입 선택이 중요하다.

  • Counter: worker_jobs_processed_total처럼 계속 증가하는 총량. autoscaling에는 보통 rate()로 변환해서 쓴다.
  • Gauge: worker_queue_depth처럼 현재값이 오르내리는 값. 큐 깊이, 활성 연결 수에 적합하다.
  • Histogram: worker_job_duration_seconds처럼 처리 시간 분포를 볼 때 쓴다. p95/p99 지연을 alert에 연결하기 좋다.

즉 “워커가 밀린다”는 말을 운영 가능한 언어로 바꾸면 최소 세 가지 metric이 된다.

zsh — 생존확인.sh
worker_queue_depth              # 지금 얼마나 쌓였나
worker_jobs_processed_total     # 얼마나 처리하고 있나
worker_job_duration_seconds     # 한 건 처리에 얼마나 걸리나

큐 깊이만 있으면 “쌓인다”는 건 알 수 있지만 처리량이 줄었는지, 작업 자체가 오래 걸리는지 구분하기 어렵다. 처리량 counter만 있으면 backlog가 사용자 대기 시간으로 커지는지 모른다. 처리 시간 histogram만 있으면 대기열이 얼마나 위험한지 놓친다. 좋은 오토스케일링 지표는 하나의 숫자가 아니라 큐 상태, 처리율, 처리 지연의 조합에서 나온다.

3. 최신 이슈와 연결

Kubernetes 블로그 예제는 exporter를 만들고, Deployment와 Service, ServiceMonitor를 붙여 Prometheus가 scrape하게 만든 뒤, 다음 단계로 Prometheus Adapter를 통해 HPA가 worker_queue_depth 같은 custom metric을 참조할 수 있다고 설명한다.

Prometheus Operator 문서도 같은 운영 모델을 보여준다. 애플리케이션이 8080 같은 port로 metric을 노출하면, Service가 target을 안정적으로 묶고, ServiceMonitor 또는 PodMonitor가 “무엇을 scrape할지” 선언한다. Prometheus 리소스는 selector로 어떤 ServiceMonitor를 선택할지 정한다. 여기서 중요한 점은 metric 수집도 Kubernetes 리소스 계약이라는 것이다.

Prometheus Adapter는 그 다음 다리다. README는 이 프로젝트를 Prometheus를 이용해 Kubernetes Custom, Resource, External Metrics API를 구현하는 adapter라고 설명한다. HPA가 Prometheus를 직접 query하는 것이 아니라, Kubernetes API의 custom.metrics.k8s.io 또는 external.metrics.k8s.io로 묻고, adapter가 Prometheus query로 바꿔 답한다.

Adapter 설정은 네 단계를 갖는다.

  1. Discovery: 어떤 Prometheus series를 metric 후보로 볼 것인가
  2. Association: 그 series를 어떤 Kubernetes 리소스와 연결할 것인가
  3. Naming: HPA에 어떤 metric 이름으로 노출할 것인가
  4. Querying: 실제 값을 어떤 PromQL로 계산할 것인가

예를 들어 counter인 http_requests_total은 cumulative metric이라 그대로 autoscaling에 쓰기 어렵다. adapter walkthrough는 이를 rate()로 계산해 http_requests_per_second 같은 이름으로 노출하는 방식을 설명한다.

큐 기반 워커라면 선택지가 둘로 갈린다.

  • 워커 Pod별 metric으로 노출할 수 있으면 custom.metrics.k8s.io 경로를 쓴다.
  • 큐가 외부 서비스이고 특정 Pod에 속하지 않으면 external.metrics.k8s.io 경로를 쓴다.

Prometheus Adapter external metrics 문서는 queue_consumer_lagqueue_depth 같은 metric을 external rule로 구성하는 예를 보여준다. 또 HPA는 namespaced 리소스이므로 external metric query에도 namespace 조건이 자동으로 붙을 수 있고, 다른 namespace의 큐 metric을 보려면 namespaced: false 같은 설정을 명시적으로 이해해야 한다고 설명한다.

작은 차이처럼 보이지만 운영에서는 크다. production worker HPA가 default namespace의 queue_depth만 보고 있었다면 실제 payments namespace backlog를 무시할 수 있다. 반대로 namespace를 꺼버리면 여러 tenant의 queue depth가 합쳐져 특정 서비스가 과도하게 scale-out될 수 있다. metric label과 namespace는 단순 태그가 아니라 스케일링 권한 경계다.

4. 개발자 관점 해석

개발자가 HPA를 CPU 기준으로만 설정하는 이유는 편하기 때문이다. Metrics Server만 있으면 되고, YAML 예제도 많다. 하지만 큐·워커 시스템에서 CPU는 종종 후행 지표다.

예를 들어 이미지 변환 워커를 생각해 보자.

  1. 사용자가 이미지를 업로드한다.
  2. API 서버가 작업을 큐에 넣는다.
  3. 워커가 큐에서 메시지를 가져와 변환한다.
  4. 결과를 object storage와 DB에 기록한다.

이 시스템의 사용자 경험은 “API 서버 CPU”보다 “큐에서 얼마나 오래 기다리는가”에 가깝다. 장애도 보통 큐에서 먼저 보인다.

  • 외부 스토리지 지연 때문에 처리 시간이 늘어난다.
  • 특정 파일 포맷에서 변환 실패가 반복된다.
  • retry가 폭증해 정상 작업이 뒤로 밀린다.
  • 워커는 살아 있지만 downstream API rate limit 때문에 처리율이 낮다.

CPU 기반 HPA는 이런 상황을 늦게 보거나 아예 못 볼 수 있다. 반면 queue depth 또는 consumer lag를 target으로 두면 “대기열이 목표보다 커졌다”는 사실을 직접 replica 수 조정에 반영할 수 있다.

하지만 custom metrics autoscaling은 만능이 아니다. 새 실패 조건이 생긴다.

첫째, metric freshness 문제다. Prometheus scrape 주기, exporter polling 주기, adapter relist interval, HPA sync period가 서로 어긋나면 HPA가 오래된 값을 보고 scale할 수 있다. Prometheus Adapter README는 metrics-max-age가 Prometheus scrape interval보다 짧으면 metric이 adapter에서 가끔 사라질 수 있다고 주의한다.

둘째, metric cardinality 문제다. queue_depth{user_id=...}처럼 고카디널리티 label을 붙이면 Prometheus 비용과 query 지연이 커진다. HPA에 필요한 label은 보통 service, queue, namespace, environment 정도다.

셋째, causal loop 문제다. backlog가 크다고 무조건 Pod를 늘리면 downstream DB나 외부 API가 더 빨리 망가질 수 있다. 백프레셔 오토스케일링은 “무한 증설”이 아니라 maxReplicas, rate limit, circuit breaker, retry budget과 같이 설계해야 한다.

넷째, ownership 문제다. exporter가 잘못된 queue를 읽거나, ServiceMonitor selector가 빠지거나, adapter rule이 metric 이름을 잘못 바꾸면 HPA는 조용히 잘못된 숫자를 믿는다. 그래서 autoscaling metric은 dashboard용 metric보다 더 엄격한 변경 관리가 필요하다.

5. 내 프로젝트에 적용할 체크포인트

큐 기반 workload가 있다면 아래 순서로 점검해 보자.

1) 먼저 “기다림”을 정의한다

CPU나 replica 수가 아니라 사용자가 체감하는 기다림을 정한다.

  • 이메일 발송: 발송 대기 메시지 수와 oldest message age
  • 이미지 처리: 변환 대기열 깊이와 p95 처리 시간
  • 결제 후처리: consumer lag와 실패 retry 수
  • WebSocket fanout: active connection 수와 broadcast queue depth

가능하면 queue_depth 하나보다 oldest_message_age_seconds를 같이 둔다. queue depth 1,000개라도 처리량이 충분하면 괜찮을 수 있지만, 가장 오래된 메시지가 20분째 대기 중이면 사용자 영향이 크다.

2) metric 타입을 고른다

  • 현재 대기 수: Gauge
  • 총 처리 수/실패 수: Counter + rate()
  • 처리 시간: Histogram
  • consumer lag: Gauge 또는 외부 시스템 metric

Counter를 HPA에 직접 연결하지 말고 adapter rule에서 rate로 바꾼다. Gauge도 label을 최소화한다.

3) scrape 경로를 코드처럼 리뷰한다

Prometheus Operator를 쓴다면 Service, ServiceMonitor, Prometheus selector가 모두 이어져야 한다.

  • Service port 이름과 ServiceMonitor endpoint port가 일치하는가?
  • ServiceMonitor label이 Prometheus의 selector에 잡히는가?
  • /metrics/healthz를 분리했는가?
  • exporter resource request/limit이 너무 작아 scrape 자체가 실패하지 않는가?

4) adapter rule을 운영 계약으로 본다

Prometheus Adapter rule은 단순 변환 파일이 아니다. HPA가 믿는 API를 정의한다.

  • Discovery query가 필요한 series만 선택하는가?
  • Kubernetes resource label association이 맞는가?
  • counter는 rate/window를 명시했는가?
  • external metric의 namespace 자동 주입을 이해했는가?
  • metrics-max-age와 scrape interval이 충돌하지 않는가?

5) scale-out 상한과 downstream 보호를 같이 둔다

큐가 쌓일 때 replica를 늘리는 것은 downstream에 더 많은 요청을 보낸다는 뜻이다.

  • maxReplicas는 DB connection pool과 외부 API rate limit 기준으로 정한다.
  • retry는 exponential backoff와 jitter를 둔다.
  • poison message는 DLQ로 분리한다.
  • idempotency key 없이 retry scale-out을 켜지 않는다.
  • scale-down stabilization으로 출렁임을 줄인다.

6. 오늘 10분 액션

오늘 바로 할 수 있는 실험은 “우리 서비스에 맞는 HPA metric 후보 표”를 만드는 것이다.

zsh — 생존확인.sh
워크로드: image-worker
사용자 영향: 변환 완료 지연
핵심 metric: image_queue_depth, oldest_image_age_seconds
보조 metric: image_jobs_processed_total{status}, image_job_duration_seconds
HPA 후보: external metric image_queue_depth / target average 50
상한: maxReplicas 20, DB connection pool 100, 외부 API rate limit 300rpm
실패 조건: metric missing, adapter query error, DLQ 증가, oldest age 10분 초과

그 다음 staging에서 아래 세 가지를 확인한다.

  1. kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1 또는 external metrics API에 metric이 보이는가?
  2. Prometheus에서 HPA가 쓰는 PromQL을 직접 실행했을 때 값이 기대와 맞는가?
  3. 큐에 테스트 메시지를 넣었을 때 HPA event와 replica 변화가 metric 변화보다 늦게라도 따라오는가?

이 실험의 목표는 완벽한 자동 스케일링이 아니다. 목표는 “우리가 어떤 숫자를 믿고 Pod를 늘리는지”를 팀이 설명할 수 있게 만드는 것이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 infra_dev 글을 확인했다. 최근 글은 etcd control plane 보안 패치, PostgreSQL 18 logical replication과 WAL retention, Docker Content Trust 종료, Cloud Run multi-region health, GKE rollout sequencing, GitHub Linked Artifacts, Kubernetes In-place Pod Resize, Terraform state secret, GitHub Actions step-level parallelism, OpenTelemetry semantic convention 카디널리티, RDS delayed replica를 다뤘다. 이번 글은 Kubernetes와 관측성을 다시 언급하지만 etcd·Pod resize·GKE upgrade·OpenTelemetry convention이 아니라, 2026년 7월 Kubernetes custom metrics exporter 글을 계기로 HPA를 queue depth/consumer lag 기반 백프레셔 제어 루프로 설계하는 방법과 Prometheus Adapter의 discovery·association·naming·querying 계약에 집중해 중복을 피했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)