← 목록으로 돌아가기

Kubernetes 오토스케일링 실전 운영: HPA·VPA·Cluster Autoscaler·KEDA를 함께 쓰는 법

DevOps

Kubernetes Autoscaling HPA VPA KEDA Production

오토스케일링은 자동이지만 자동으로 안전해지지는 않는다

Kubernetes를 쓰면 트래픽이 늘어날 때 Pod가 자동으로 늘어나고, 노드가 부족하면 클러스터도 알아서 커질 것처럼 느껴집니다. 실제로 HPA(Horizontal Pod Autoscaler), VPA(Vertical Pod Autoscaler), Cluster Autoscaler, KEDA 같은 도구는 훌륭합니다. 하지만 운영에서 오토스케일링은 "켜면 끝"이 아니라, 여러 제어 루프가 서로 충돌하지 않도록 설계하는 일에 가깝습니다.

우리 팀이 처음 HPA를 켰을 때도 기대는 단순했습니다. CPU가 70%를 넘으면 Pod를 늘리면 된다고 생각했습니다. 하지만 피크 트래픽에서 Pod는 늘어났는데 새 Pod는 Pending에 머물렀고, Cluster Autoscaler가 노드를 추가하는 동안 기존 Pod의 latency가 더 나빠졌습니다. 다른 서비스에서는 HPA가 Pod를 늘린 직후 CPU 평균이 낮아져 다시 줄이고, 잠시 후 요청이 몰려 다시 늘리는 진동(oscillation)이 생겼습니다. 오토스케일링이 장애를 해결하기보다 장애의 리듬을 만들고 있었습니다.

이 글에서는 Kubernetes 오토스케일링을 운영 관점에서 정리합니다. HPA와 VPA의 역할 분리, requests/limits 산정, Cluster Autoscaler 지연, KEDA 기반 이벤트 스케일링, scale down 안정화, SLO 기반 지표 선택까지 실제 프로덕션에서 부딪히는 문제를 기준으로 다룹니다.


1. HPA: Pod 수를 늘리는 제어 루프

HPA는 deployment나 statefulset의 replica 수를 조정합니다. 가장 흔한 기준은 CPU utilization입니다. 예를 들어 target CPU utilization을 70%로 두면, 현재 Pod들의 평균 CPU 사용률이 requests 대비 70%를 넘을 때 replica 수를 늘립니다. 여기서 중요한 단어는 "requests 대비"입니다. CPU request를 잘못 잡으면 HPA 판단도 틀어집니다.

CPU request가 너무 낮으면 실제로는 여유가 있는데 utilization이 높게 계산되어 Pod가 과도하게 늘어납니다. 반대로 request가 너무 높으면 Pod가 이미 바쁜데 utilization은 낮게 보여 scale out이 늦습니다. HPA는 실제 코어 사용량만 보는 것이 아니라 request와의 비율을 보기 때문에, request sizing은 오토스케일링의 입력값입니다.

CPU만으로 충분하지 않은 서비스도 많습니다. API 서버는 CPU보다 RPS, queue length, p95 latency, active requests가 더 좋은 신호일 수 있습니다. Worker는 처리 대기 메시지 수나 lag가 중요합니다. CPU 기반 HPA는 이미 일이 쌓인 뒤에 반응하는 후행 지표가 되기 쉽습니다. 트래픽 패턴이 급격한 서비스는 custom metric 또는 external metric을 함께 써야 합니다.

HPA의 scale up은 빠르게, scale down은 느리게 잡는 편이 안전합니다. Scale up이 늦으면 사용자가 바로 지연을 겪습니다. Scale down이 빠르면 잠깐의 트래픽 하락에 Pod를 줄였다가 다시 늘리는 진동이 생깁니다. behavior.scaleDown.stabilizationWindowSeconds를 충분히 두고, 감소 정책을 보수적으로 잡으면 비용은 조금 늘지만 안정성이 좋아집니다.


2. VPA: Pod 크기를 맞추는 도구

VPA는 replica 수가 아니라 Pod의 CPU/memory request를 추천하거나 조정합니다. HPA가 "몇 개를 띄울까"를 결정한다면, VPA는 "각 Pod가 얼마나 큰 자원을 요청해야 할까"를 다룹니다. 둘은 목적이 다르지만 같은 리소스 값을 건드리기 때문에 함께 쓸 때 주의가 필요합니다.

CPU 기반 HPA와 CPU request를 자동 변경하는 VPA를 동시에 강하게 적용하면 제어 루프가 꼬일 수 있습니다. VPA가 CPU request를 올리면 HPA utilization이 낮아져 replica를 줄이고, replica가 줄어 다시 부하가 올라갈 수 있습니다. 그래서 실무에서는 VPA를 recommendation 모드로 먼저 운영해 적정 request를 관찰하고, HPA 대상 workload에는 자동 update를 신중하게 적용합니다.

Memory는 CPU보다 더 조심해야 합니다. CPU는 throttling으로 느려질 수 있지만, memory limit을 넘으면 Pod는 OOMKilled됩니다. JVM, Node.js, Rust, Go 런타임마다 메모리 사용 패턴이 다르고, GC와 heap 설정도 영향을 줍니다. VPA 추천값을 맹신하지 말고 peak traffic, 배치 작업, warm-up 이후 steady state를 분리해서 봐야 합니다.

가장 현실적인 운영 방식은 VPA recommendation을 주기적으로 보고 request를 조정하는 것입니다. 특히 새 서비스 출시 초기에 request를 감으로 잡았다면, 며칠간 관측한 뒤 p95 또는 p99 사용량 기준으로 조정합니다. Request가 맞아야 scheduler가 올바르게 배치하고, HPA도 올바른 판단을 내립니다.


3. Cluster Autoscaler: Pending Pod를 보고 노드를 늘린다

HPA가 Pod를 늘려도 노드에 자리가 없으면 새 Pod는 Pending 상태가 됩니다. 이때 Cluster Autoscaler가 pending Pod를 보고 노드 그룹을 확장합니다. 하지만 노드 추가는 즉시 일어나지 않습니다. 클라우드 VM 생성, kubelet join, image pull, readiness probe 통과까지 시간이 걸립니다. 이 지연을 무시하면 scale out이 항상 한 박자 늦습니다.

Cluster Autoscaler는 "필요할 때 노드가 생긴다"가 아니라 "Pending Pod가 생긴 뒤 노드 추가가 시작된다"는 구조입니다. 따라서 급격한 트래픽 스파이크에는 미리 여유 capacity를 두거나, priority class와 overprovisioning pod를 사용해 완충 공간을 만들어야 합니다. 낮은 priority의 pause pod를 미리 띄워 노드 여유를 차지하게 하고, 실제 서비스 Pod가 오면 pause pod가 밀려나며 즉시 스케줄링되는 패턴이 있습니다.

Scale down도 위험합니다. 노드를 줄일 때 PodDisruptionBudget, local storage, daemonset, topology spread constraints 때문에 노드가 비워지지 않을 수 있습니다. 반대로 너무 공격적으로 줄이면 다음 피크에서 다시 노드를 늘리느라 지연이 생깁니다. 비용 최적화와 안정성은 항상 trade-off입니다. 야간 트래픽이 낮은 서비스라도 아침 피크 직전에는 node warm-up 전략이 필요할 수 있습니다.

Node group 설계도 중요합니다. 모든 workload를 하나의 거대한 노드 그룹에 넣으면 단순하지만, GPU/메모리 특화/spot/on-demand 정책을 나누기 어렵습니다. 반대로 너무 많은 노드 그룹은 autoscaler 판단과 bin packing을 복잡하게 만듭니다. 서비스의 자원 형태와 장애 격리 기준에 맞춰 몇 개의 명확한 pool로 나누는 편이 운영하기 쉽습니다.


4. KEDA: 큐와 이벤트를 기준으로 스케일링한다

Worker 계열 서비스는 CPU보다 queue lag가 더 좋은 스케일링 지표입니다. 메시지가 10만 개 쌓였는데 CPU가 낮다는 이유로 Pod를 늘리지 않으면 처리는 늦어집니다. KEDA는 Kafka lag, RabbitMQ queue length, Redis stream, Prometheus query, cron schedule 같은 외부 이벤트를 기준으로 Kubernetes workload를 scale out할 수 있게 해줍니다.

KEDA의 장점은 scale-to-zero입니다. 이벤트가 없을 때 worker를 0개까지 줄이고, 메시지가 들어오면 다시 늘릴 수 있습니다. 비용에는 유리하지만 cold start와 처리 지연을 고려해야 합니다. 이미지가 크거나 초기화가 느린 worker라면 scale-to-zero가 사용자 경험을 해칠 수 있습니다. 실시간성이 필요한 큐는 min replica를 1 이상으로 두는 편이 안전합니다.

Queue 기반 스케일링에서는 "메시지 수 ÷ Pod당 처리량"으로 목표 replica를 잡습니다. 하지만 Pod당 처리량은 고정이 아닙니다. 외부 API rate limit, DB connection, CPU, 메시지 크기에 따라 달라집니다. 따라서 단순 queue length뿐 아니라 oldest message age를 함께 봐야 합니다. 큐 길이가 짧아도 오래된 메시지가 있다면 특정 partition이 막혔거나 poison message가 있을 수 있습니다.

KEDA와 HPA를 같이 쓸 때는 ownership을 명확히 해야 합니다. KEDA는 내부적으로 HPA를 생성해 제어하는 방식이 많습니다. 같은 deployment에 사람이 만든 HPA와 KEDA가 만든 HPA가 동시에 붙으면 의도치 않은 충돌이 생깁니다. 이벤트 기반 workload는 KEDA가 replica를 책임지고, 일반 API workload는 HPA가 책임지는 식으로 경계를 나누는 편이 좋습니다.


5. Readiness와 Warm-up: 늘어난 Pod가 바로 트래픽을 받아도 되는가

Scale out의 함정은 replica 수가 늘었다고 capacity가 즉시 늘어난 것이 아니라는 점입니다. 새 Pod가 Ready 상태가 되기 전에는 트래픽을 받지 않습니다. 더 중요한 것은 Ready가 됐다고 애플리케이션이 완전히 warm-up된 것도 아닙니다. JVM JIT, connection pool 초기화, model loading, cache warming, config fetch가 끝나지 않았는데 readiness probe가 통과하면 새 Pod가 첫 요청부터 느려질 수 있습니다.

Readiness probe는 "프로세스가 떠 있다"가 아니라 "트래픽을 받아도 된다"를 의미해야 합니다. DB connection pool이 준비됐는지, 필수 config를 읽었는지, 내부 cache가 최소한 초기화됐는지 확인해야 합니다. 단순히 /healthz에서 200을 반환하는 것은 liveness에는 충분할 수 있지만 readiness에는 부족합니다.

Scale down에서도 graceful shutdown이 필요합니다. Pod가 terminating 상태가 되면 endpoint에서 빠지고, 기존 요청을 마칠 시간을 줘야 합니다. terminationGracePeriodSeconds, preStop hook, 서버의 keep-alive drain 설정이 맞지 않으면 scale down 순간 499/502가 늘어납니다. 오토스케일링은 늘리는 것만큼 줄이는 과정도 사용자 트래픽과 연결됩니다.


6. 지표 선택: CPU보다 SLO에 가까운 신호를 본다

HPA 지표는 서비스 성격에 맞아야 합니다. CPU-bound 이미지 처리 서비스는 CPU가 좋은 기준입니다. I/O-bound API는 active requests, p95 latency, queue depth가 더 나을 수 있습니다. DB connection pool이 병목인 서비스는 Pod를 늘리면 오히려 DB 연결 수가 늘어 장애가 커질 수 있습니다. 이 경우 HPA target을 올리기 전에 downstream capacity를 확인해야 합니다.

SLO와 연결된 지표를 보조 신호로 두는 것도 중요합니다. Scale out이 시작됐는데 p95 latency가 계속 오른다면 병목이 Pod CPU가 아닐 가능성이 큽니다. 외부 API rate limit, DB lock, cache miss storm, node network 병목일 수 있습니다. 오토스케일링 대시보드에는 replica 수, CPU/memory, pending pod, node count뿐 아니라 latency, error rate, queue age, downstream saturation을 함께 배치해야 합니다.

예측 가능한 트래픽에는 scheduled scaling도 유용합니다. 매일 오전 9시에 트래픽이 오른다면 HPA가 반응할 때까지 기다릴 필요가 없습니다. 피크 10분 전에 min replica를 올려두면 cold start와 node scale-up 지연을 줄일 수 있습니다. 이벤트 기반 자동화와 시간 기반 사전 확장은 경쟁 관계가 아니라 보완 관계입니다.


7. 운영 체크리스트

  • HPA 기준 지표가 서비스 병목과 실제로 연결되어 있는가
  • CPU request가 HPA 판단을 왜곡하지 않도록 관측값 기반으로 조정됐는가
  • VPA는 recommendation부터 적용하고, HPA와 같은 리소스를 동시에 흔들지 않는가
  • Cluster Autoscaler 지연을 고려해 overprovisioning 또는 scheduled warm-up을 뒀는가
  • Pending Pod가 생겼을 때 원인이 노드 부족인지, taint/toleration인지, resource request 과다인지 구분할 수 있는가
  • Scale down stabilization window와 graceful shutdown이 설정되어 있는가
  • Queue worker는 CPU가 아니라 lag, oldest age, 처리량 기준으로 스케일링하는가
  • KEDA와 수동 HPA가 같은 workload를 동시에 제어하지 않는가
  • 오토스케일링 대시보드에 replica, pending pod, node count, latency, error rate, downstream saturation이 함께 있는가

결론: 제어 루프를 설계해야 자동화가 안전해진다

Kubernetes 오토스케일링은 강력하지만, 각각의 도구는 자기 시야 안에서만 판단합니다. HPA는 Pod 평균 지표를 보고, VPA는 resource request를 보고, Cluster Autoscaler는 Pending Pod를 보고, KEDA는 외부 이벤트를 봅니다. 운영자가 해야 할 일은 이 제어 루프들이 서로 엇박자를 내지 않도록 경계와 지표를 설계하는 것입니다.

좋은 오토스케일링은 비용을 줄이는 기능으로만 끝나지 않습니다. 트래픽 피크에서 사용자 지연을 줄이고, 장애 시 과도한 요청이 downstream을 무너뜨리지 않게 하며, 배포와 스케일 다운 과정에서도 연결을 부드럽게 빼줍니다. 자동화의 목표는 사람이 아무것도 보지 않는 것이 아니라, 사람이 봐야 할 신호가 더 명확해지는 것입니다.