Scaling và AutoscalingScaling and Autoscaling
Part of the Kubernetes Roadmap knowledge base.
Tổng quan
Scaling là lý do chính khiến hầu hết các team chọn Kubernetes: lời hứa rằng capacity sẽ đi theo nhu cầu thực tế thay vì phải provision cho một mức đỉnh chỉ dựa trên phỏng đoán. Kubernetes cung cấp scaling ở ba tầng riêng biệt, và nhầm lẫn giữa chúng là nguyên nhân phổ biến nhất gây ra cấu hình autoscaling sai.
- Số lượng Pod (horizontal) — chạy nhiều hay ít bản sao của một workload. Thủ công (
kubectl scale,replicas:) hoặc tự động qua Horizontal Pod Autoscaler (HPA) và các công cụ event-driven như KEDA. - Kích thước Pod (vertical) — cấp cho mỗi Pod nhiều hay ít CPU/memory hơn. Thủ công (chỉnh
requests/limits) hoặc tự động qua Vertical Pod Autoscaler (VPA). - Số lượng node (hạ tầng) — thêm hoặc bớt máy để Pod có chỗ chạy. Tự động qua Cluster Autoscaler hoặc Karpenter.
Ba tầng này bổ trợ cho nhau và tạo thành một chuỗi: HPA tạo thêm Pod → các Pod đó ở trạng thái Pending vì không node nào còn chỗ → Cluster Autoscaler/Karpenter phát hiện các Pod không schedule được và thêm node → các Pod được schedule. Hiểu chuỗi này là điều thiết yếu, vì một lỗi thường gặp là “HPA đã scale up nhưng chẳng có gì xảy ra” — vấn đề thực sự là node scaling chưa bao giờ được cấu hình.
Sợi dây liên kết xuyên suốt tất cả là resource request. Request là thứ scheduler dùng để reserve capacity, là mẫu số mà phần trăm utilization của HPA được đo theo, và là căn cứ để Cluster Autoscaler quyết định một Pod có vừa một node hay không. Đặt request sai thì mọi tầng autoscaling đều lệch. Request là nền móng; mọi thứ khác được xây trên nó.
Kiến thức nền tảng
Scaling thủ công
Hình thức scaling đơn giản nhất là đặt số replica. Theo kiểu declarative, nó nằm trong workload spec:
spec:
replicas: 4 # Deployment / ReplicaSet / StatefulSet
Theo kiểu imperative, bạn có thể thay đổi trực tiếp:
kubectl scale deployment/web --replicas=6
kubectl scale statefulset/kafka --replicas=5
kubectl scale --replicas=3 -f deployment.yaml
# Scaling có điều kiện — chỉ khi đang ở một giá trị đã biết (tránh race)
kubectl scale deployment/web --current-replicas=4 --replicas=6
Một cạm bẫy quan trọng: nếu một HPA nhắm vào một Deployment, đừng đồng thời đặt replicas: trong Git. HPA sở hữu số replica và sẽ “đánh nhau” với manifest của bạn, tạo ra vòng lặp flapping trong đó GitOps reset về replicas: 3 còn HPA lập tức scale lên lại. Cách sửa là bỏ hẳn trường replicas khỏi mọi workload do HPA quản lý (hoặc trong Argo CD, cấu hình ignore differences trên trường đó). Khi tạo lần đầu, nếu bạn bỏ replicas, nó mặc định là 1 cho đến khi HPA tiếp quản.
Metrics pipeline
Autoscaling chỉ tốt bằng chất lượng metrics cung cấp cho nó. Có hai API quan trọng:
metrics.k8s.io— API resource metrics, được phục vụ bởi metrics-server. Cung cấp CPU/memory realtime theo từng Pod và node. Đây là nguồn chokubectl topvà cho HPA scale theo CPU/memory. metrics-server không được cài mặc định trên cluster thuần (dù các dịch vụ managed như GKE và AKS có bundle sẵn) — một HPA mới báo<unknown>/70%gần như luôn nghĩa là metrics-server thiếu hoặc không khỏe.custom.metrics.k8s.io/external.metrics.k8s.io— API custom và external metrics, phục vụ bởi một adapter (Prometheus Adapter, adapter của KEDA, hoặc của cloud provider). Chúng cho phép HPA scale theo metrics ứng dụng (requests per second, độ sâu queue) thay vì CPU thô.
metrics-server lấy mẫu qua Summary API của kubelet khoảng 15 giây một lần. Nó được thiết kế cho autoscaling, không phải cho monitoring hay dữ liệu lịch sử — đừng trỏ dashboard vào nó; hãy dùng Prometheus cho việc đó.
Vì sao request là nền tảng của scaling
Target phổ biến nhất của HPA, averageUtilization, là phần trăm của CPU/memory request của Pod, không phải của node hay của limit. Nếu một container request 100m CPU và đang dùng 70m, đó là 70% utilization bất kể node lớn cỡ nào.
Điều này có hệ quả rõ rệt:
- Không đặt request → HPA không tính được utilization và metric báo
<unknown>. HPA theo CPU/memory bắt buộc phải có request trên resource tương ứng. - Request đặt quá thấp → utilization trông cao giả tạo, HPA over-scale, lãng phí Pod.
- Request đặt quá cao → utilization trông thấp giả tạo, HPA không bao giờ kích hoạt, Pod bị throttle mà không được scale.
Cùng một request đó dẫn dắt Cluster Autoscaler: nó mô phỏng xem request của một Pod có vừa một node ứng viên hay không. Một Pod không có memory request trông như “miễn phí” với autoscaler nhưng thực tế có thể làm OOM cả node. Right-sizing request là công việc tiên quyết cho mọi autoscaler.
Khái niệm chính
Horizontal Pod Autoscaler (HPA)
HPA (autoscaling/v2) định kỳ (mặc định mỗi 15s, tham số --horizontal-pod-autoscaler-sync-period) đọc metrics và điều chỉnh số replica của một target có thể scale (Deployment, ReplicaSet, StatefulSet — bất cứ thứ gì hỗ trợ subresource scale). Thuật toán cốt lõi là một tỉ lệ đơn giản:
desiredReplicas = ceil( currentReplicas × ( currentMetricValue / desiredMetricValue ) )
Nếu 4 replica trung bình 90% CPU so với target 60%: ceil(4 × 90/60) = ceil(6) = 6 replica. Một dải dung sai ±10% ngăn thrashing quanh target.
Các loại metric mà API v2 hỗ trợ:
| Loại | Nguồn | Ví dụ |
|---|---|---|
Resource | metrics-server | Utilization CPU/memory hoặc giá trị tuyệt đối |
Pods | custom metrics adapter | metric ứng dụng theo Pod, tính trung bình trên các Pod (ví dụ RPS mỗi pod) |
Object | custom metrics adapter | một metric mô tả một object khác (ví dụ requests/s của Ingress) |
External | external metrics adapter | metric từ ngoài cluster (độ dài queue SQS, Kafka lag) |
Một HPA thực tế dùng cả CPU lẫn một custom metric, kèm block behavior:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # % của CPU *request*
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "500" # scale để giữ ~500 rps mỗi pod
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # scale up ngay lập tức
policies:
- type: Percent
value: 100 # tối đa gấp đôi
periodSeconds: 30
- type: Pods
value: 4 # hoặc thêm tối đa 4 pod
periodSeconds: 30
selectPolicy: Max # chọn policy cho phép nhiều hơn
scaleDown:
stabilizationWindowSeconds: 300 # đợi 5 phút tải thấp ổn định
policies:
- type: Percent
value: 20 # bớt tối đa 20% mỗi phút
periodSeconds: 60
Khi liệt kê nhiều metric, HPA tính số replica mong muốn cho từng metric và lấy giá trị cao nhất — nó scale để thỏa mãn tín hiệu “khắt khe” nhất, không bao giờ giảm dưới mức mà bất kỳ metric nào cần.
Block behavior (chỉ có ở v2) là thứ khiến HPA production trở nên tỉnh táo. scaleUp thường quyết liệt (phản ứng nhanh với tải); scaleDown được cố ý làm thận trọng với một stabilization window — HPA giữ khuyến nghị lớn nhất thấy được trong cửa sổ đó, nên một cú sụt tải ngắn không giết các Pod mà vài giây sau lại cần đến. Mặc định stabilization khi scale-down là 300s.
Cạm bẫy của HPA:
- HPA dựa trên memory thường là một cái bẫy: hầu hết ứng dụng không trả memory lại cho OS, nên utilization chỉ tăng dần và HPA không bao giờ scale down được. Với app bị giới hạn bởi memory, hãy ưu tiên CPU hoặc custom metric theo tốc độ request.
- HPA và một rolling deployment có thể tương tác không tốt; quyết định scaling trong lúc rollout dùng metrics trộn lẫn của cả Pod cũ lẫn mới.
minReplicas: 1cho phép single point of failure; dịch vụ web production nên đặt sàn ở 2–3.
Vertical Pod Autoscaler (VPA)
VPA điều chỉnh request CPU/memory của Pod (và tùy chọn cả limit) để khớp với mức sử dụng quan sát được, giải quyết bài toán “tôi không biết nên request bao nhiêu”. Nó là một component cài riêng (không có sẵn). Có ba mode qua updatePolicy.updateMode:
| Mode | Hành vi | Dùng cho |
|---|---|---|
Off | Chỉ tính và báo cáo khuyến nghị; không thay đổi gì | Tìm request đúng mà không gây gián đoạn |
Initial | Đặt request chỉ khi tạo Pod | Right-size lúc deploy, không xáo trộn khi đang chạy |
Auto / Recreate | Evict và tạo lại Pod để áp request mới | Tự động hoàn toàn (gây gián đoạn) |
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: worker-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: worker
updatePolicy:
updateMode: "Auto"
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed: { cpu: 50m, memory: 64Mi }
maxAllowed: { cpu: 2, memory: 2Gi }
controlledResources: ["cpu", "memory"]
Trước đây VPA ở mode Auto phải evict và tạo lại Pod để thay đổi request, gây gián đoạn — đó là lý do nhiều team chạy nó ở mode Off và áp khuyến nghị thủ công hoặc lúc deploy. Kubernetes v1.33 đã đưa in-place Pod resource resize lên beta, cho phép một số thay đổi resource mà không cần restart, giúp giảm bớt nỗi đau này về sau.
Xung đột HPA/VPA: đừng bao giờ chạy HPA và VPA trên cùng một resource metric. Nếu cả hai đều theo dõi CPU, VPA nâng request (làm giảm % utilization), khiến HPA tưởng cần scale down, làm tải mỗi Pod tăng, khiến VPA lại nâng request — một vòng dao động “đánh nhau”. Các kết hợp an toàn:
- HPA theo CPU + VPA ở mode
Off(chỉ khuyến nghị). - HPA theo một custom metric (ví dụ RPS hoặc độ sâu queue) + VPA theo CPU/memory. Đây là pattern được khuyến nghị để scale cả hai chiều: HPA lo “bao nhiêu cái” theo tải ứng dụng, VPA lo “to cỡ nào” theo mức dùng resource.
Cluster Autoscaler
Autoscaling ở tầng Pod trở nên vô dụng nếu không có chỗ đặt Pod. Cluster Autoscaler (CA) theo dõi các Pod kẹt ở Pending do thiếu resource và mở rộng node group (AWS ASG, GCP MIG, Azure VMSS) để chứa chúng; ngược lại nó gỡ bỏ các node bị dưới mức sử dụng trong một khoảng thời gian và có Pod có thể reschedule đi nơi khác.
Cơ chế và lưu ý chính:
- Nó scale theo request, không phải mức dùng thực. Một cluster đầy Pod request nhiều hơn mức dùng sẽ không bao giờ scale down, còn cluster đầy Pod request quá ít có thể bị nhồi vào quá ít node.
- Scale-down được thiết kế thận trọng: một node phải ở dưới ngưỡng utilization (mặc định 50%) trong một khoảng (mặc định 10 phút) và mọi Pod của nó phải di chuyển được. Pod chặn eviction nếu không có controller, dùng local storage, có PodDisruptionBudget hạn chế, hoặc mang annotation
cluster-autoscaler.kubernetes.io/safe-to-evict: "false". - Scale-from-zero: một node group có thể scale xuống 0 node rồi lại lên. Để CA biết một group đã về 0 có thể đáp ứng nhu cầu của một Pod đang chờ (label, taint, resource), group phải được gắn tag để CA suy ra hình dạng node mà không cần một máy đang chạy làm mẫu.
- Nó tôn trọng PodDisruptionBudget khi scale-down, và tuân theo
PriorityClasscho thứ tự eviction pod. - Nó cố ý chậm và an toàn; provision một node cloud mất vài phút. Để phản ứng nhanh hơn, hãy over-provision bằng các “pause” Pod ưu tiên thấp để chúng bị preempt bởi workload thật, mua thời gian trong khi một node đang lên.
Karpenter (AWS, và hơn thế)
Karpenter là một autoscaler provision node kiểu mới hơn (khởi nguồn từ AWS, nay là dự án CNCF) thay thế cả Cluster Autoscaler lẫn node group. Thay vì scale các group định sẵn, nó nhìn tổng resource request của các Pod đang chờ và provision node vừa kích cỡ đúng lúc (just-in-time), chọn loại instance, kích thước, zone và loại capacity (kể cả Spot) trực tiếp từ cloud API.
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| Đơn vị scale | Node group định sẵn (ASG/MIG/VMSS) | Từng node, chọn theo nhu cầu |
| Chọn instance | Cố định theo group | Linh hoạt, từ một NodePool các loại được phép |
| Tốc độ | Vài phút (qua ASG) | Nhanh hơn (nói chuyện trực tiếp với EC2) |
| Bin-packing | Bị giới hạn bởi hình dạng group | Chủ động consolidate xuống node rẻ hơn |
| Tính di động | Đa cloud | Chủ yếu AWS (hỗ trợ Azure đang nổi lên) |
Tính năng consolidation của Karpenter chủ động dồn cluster xuống ít/rẻ node hơn khi tải giảm, một khoản tiết kiệm chi phí lớn nhưng đồng nghĩa với nhiều node churn hơn — hãy đi kèm PodDisruptionBudget và graceful shutdown. Bạn mô tả ý định trong một NodePool (các họ instance được phép, zone, giới hạn, chính sách disruption) thay vì tự tay định cỡ group.
KEDA — autoscaling theo sự kiện
HPA scale theo resource hoặc metrics đã lộ ra, nhưng nhiều workload nên scale theo sự kiện: số message trong queue, lag trên một Kafka topic, số dòng trong database, một lịch cron. KEDA (Kubernetes Event-Driven Autoscaling) lấp khoảng trống này. Nó là một dự án CNCF hoạt động như một metrics adapter kiêm bộ tạo HPA: bạn khai báo một ScaledObject với các trigger, và KEDA tạo và quản lý một HPA bên dưới, cấp cho nó external metrics từ hơn 60 scaler (SQS, Kafka, RabbitMQ, Prometheus, Azure Service Bus, cron, và nhiều hơn).
Tính năng nổi bật của KEDA là scale-to-zero: khi không có sự kiện, nó scale Deployment về 0 replica (điều HPA thuần không làm được vì sàn minReplicas của nó là 1), rồi kích hoạt replica đầu tiên khi có sự kiện tới. Điều này lý tưởng cho consumer bùng nổ theo đợt và batch worker mà nếu không sẽ ngồi không đốt tài nguyên.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
spec:
scaleTargetRef:
name: order-consumer # Deployment cần scale
minReplicaCount: 0 # scale về 0 khi rảnh
maxReplicaCount: 50
cooldownPeriod: 300 # đợi 5 phút không có sự kiện trước khi về 0
pollingInterval: 15
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.eu-west-1.amazonaws.com/123456789012/orders
queueLength: "20" # nhắm ~20 message mỗi replica
awsRegion: eu-west-1
authenticationRef:
name: keda-aws-credentials
- type: kafka
metadata:
bootstrapServers: kafka:9092
consumerGroup: orders
topic: orders
lagThreshold: "100" # thêm một replica cho mỗi 100 message lag
Với hai trigger, KEDA scale để thỏa mãn cái nào đòi nhiều replica hơn. Lưu ý sự phân biệt: KEDA tự xử lý kích hoạt 0↔1, rồi giao scaling 1↔N cho HPA nó sinh ra. Biến thể ScaledJob của nó scale các Job thay vì Deployment, mỗi Job cho một batch sự kiện — lý tưởng cho xử lý chạy dài, không idempotent.
Scaling StatefulSet — các lưu ý
StatefulSet có thể được scale bằng kubectl scale hoặc HPA, nhưng workload có state hiếm khi scale gọn gàng như workload stateless:
- Thao tác tuần tự, từng cái một: mặc định StatefulSet tạo/xóa Pod tuần tự (
web-0,web-1,web-2…) và scale down gỡ ordinal cao nhất trước. Điều này chậm và, với một database, có thể không an toàn. - Rebalance dữ liệu không tự động: thêm một replica vào Kafka, Cassandra hay Elasticsearch cho bạn một thành viên rỗng — cluster phải rebalance partition/shard, điều Kubernetes hoàn toàn không biết. Autoscale số replica mà không có operator lo rebalance có thể làm hỏng hoặc mất cân bằng cluster.
- Scale-down rủi ro mất dữ liệu: gỡ một replica có thể bỏ rơi hoặc xóa dữ liệu của nó. PersistentVolumeClaim không bị xóa tự động khi scale-down (chúng được giữ lại theo
persistentVolumeClaimRetentionPolicy), nên một HPA ngây thơ scale database xuống rồi lên để lại các PVC lơ lửng và dữ liệu thiếu replica. - Hệ quorum có giới hạn cứng: các hệ đồng thuận (etcd, ZooKeeper) cần số thành viên lẻ để có quorum và không chịu được autoscaling tùy tiện.
Quy tắc chung: đừng đặt HPA lên một StatefulSet chống lưng cho một hệ có state trừ khi một operator chuyên dụng (ví dụ Strimzi Kafka operator, một database operator) quản lý thành viên và rebalance. Với các kho dữ liệu có state, hãy scale có chủ đích và thủ công, hoặc để operator làm.
Best Practices
-
Đặt resource request chính xác trước khi bật bất kỳ autoscaler nào. Request là mẫu số cho utilization của HPA, phép tính vừa-vặn của Cluster Autoscaler, và target của VPA. Request sai làm hỏng mọi tầng — right-size từ dữ liệu sử dụng thực (VPA ở mode
Offhoặc Prometheus) thay vì đoán. -
Cài và giám sát metrics-server như một điều kiện tiên quyết. HPA theo CPU/memory âm thầm báo
<unknown>nếu thiếu nó. Hãy coi nó là hạ tầng cốt lõi, chạy highly available, và cảnh báo khi nó không khỏe. -
Không bao giờ đặt
replicas:trong Git cho workload do HPA quản lý. HPA và manifest của bạn sẽ “đánh nhau”, gây flapping số replica. Bỏ trường đó đi (và cấu hình công cụ GitOps ignore nó). -
Dùng
autoscaling/v2và tinh chỉnh blockbehavior.scaleUpquyết liệt (phản ứng nhanh) cộng với cửa sổ stabilizationscaleDownthận trọng (tránh thrashing) là ranh giới giữa một autoscaler ổn định và một cái flapping. Mặc định scale-down stabilization là 5 phút — hãy giữ nó rộng rãi. -
Ưu tiên CPU hoặc custom metric hơn memory cho target HPA. Đa số runtime không trả memory lại OS, nên memory utilization chỉ leo lên và HPA không scale down được. Hãy scale theo CPU hoặc một tín hiệu ứng dụng (RPS, độ sâu queue).
-
Không bao giờ chạy HPA và VPA trên cùng một resource metric. Chúng tạo thành vòng phản hồi dao động. Chỉ kết hợp chúng dạng HPA-theo-custom-metric + VPA-theo-CPU/memory, hoặc chạy VPA ở mode
Offđể lấy khuyến nghị. -
Ghép Pod autoscaling với node autoscaling. Một HPA tạo Pod mà không node nào chứa được chỉ sinh ra Pod
Pending. Luôn triển khai Cluster Autoscaler hoặc Karpenter cùng với HPA, và kiểm tra toàn bộ chuỗi từ đầu đến cuối. -
Đặt sàn dịch vụ production ở 2–3 replica kèm một PodDisruptionBudget.
minReplicas: 1là single point of failure khi drain node và rollout. Một PDB ngăn autoscaling và bảo trì cluster đưa bạn về 0 Pod sẵn sàng. -
Dùng KEDA cho workload event-driven và scale-to-zero. Consumer queue, processor Kafka và batch job nên scale theo backlog, không theo CPU, và nên nghỉ ở 0 replica. Đặt
cooldownPeriodhợp lý để các khoảng rảnh ngắn không gây churn. -
Đặt
maxReplicasvà giới hạn node-group làm lan can chi phí và blast-radius. Autoscaling không có giới hạn trên biến một cú spike tải (hoặc một bug metric) thành hóa đơn cloud mất kiểm soát. Hãy cap replica và số node một cách tường minh. -
Bảo vệ các Pod không được phép bị evict khi scale-down. Dùng PodDisruptionBudget và annotation
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"cho Pod có state hoặc chạy dài để consolidation node không làm gián đoạn chúng. -
Cấu hình và gắn tag node group cho scale-from-zero. Thêm các tag resource/label/taint mà Cluster Autoscaler cần để suy luận về group rỗng, để một group đã về 0 vẫn có thể được chọn cho Pod đang chờ.
-
Đừng autoscale StatefulSet chống lưng cho hệ có state nếu không có operator. Autoscale replica ngây thơ cho database/message broker rủi ro mất dữ liệu, PVC lơ lửng và hỏng quorum. Hãy để một operator chuyên dụng quản lý thành viên và rebalance.
-
Load-test cấu hình autoscaling của bạn. Tinh chỉnh autoscaler (ngưỡng, cửa sổ, cooldown) chỉ được kiểm chứng dưới tải thực tế. Chạy soak và spike test, quan sát thời gian phản ứng và trạng thái ổn định, và lặp lại trước khi tin nó trên production.
-
Cân nhắc Karpenter cho provision node linh hoạt, hướng chi phí trên AWS. Node vừa cỡ just-in-time và consolidation chủ động cắt lãng phí so với node group cố định — nhưng hãy tính đến node churn thêm bằng PDB và graceful shutdown.
-
Thiết kế app để shutdown một cách graceful. Autoscaling nghĩa là Pod liên tục được tạo và hủy. Hãy xử lý
SIGTERM, drain công việc đang chạy dở, và đặtterminationGracePeriodSecondscùng mộtpreStophook để scale-down và consolidation không làm rớt request.
Tài liệu tham khảo
- Horizontal Pod Autoscaling
- HPA walkthrough
- Resource metrics pipeline & metrics-server
- Managing resources for containers (requests & limits)
- Vertical Pod Autoscaler
- Resize container resources in-place
- Cluster Autoscaler FAQ
- Karpenter documentation
- KEDA documentation · KEDA scalers
- StatefulSet basics & scaling
- StatefulSet PVC retention policy
- PodDisruptionBudget
- roadmap.sh — Kubernetes
Part of the Kubernetes Roadmap knowledge base.
Overview
Scaling is the reason most teams adopt Kubernetes in the first place: the promise that capacity follows demand instead of being provisioned for a guessed-at peak. Kubernetes offers scaling at three distinct layers, and confusing them is the single most common source of autoscaling misconfiguration.
- Pod count (horizontal) — run more or fewer copies of a workload. Manual (
kubectl scale,replicas:) or automatic via the Horizontal Pod Autoscaler (HPA) and event-driven tools like KEDA. - Pod size (vertical) — give each Pod more or less CPU/memory. Manual (edit
requests/limits) or automatic via the Vertical Pod Autoscaler (VPA). - Node count (infrastructure) — add or remove machines so Pods have somewhere to land. Automatic via the Cluster Autoscaler or Karpenter.
These layers are complementary and form a chain: the HPA creates more Pods → those Pods are Pending because no node has room → the Cluster Autoscaler/Karpenter notices the unschedulable Pods and adds nodes → the Pods schedule. Understanding this chain is essential, because a common failure mode is “the HPA scaled up but nothing happened” — the real problem being that node scaling was never configured.
The connective tissue across all of it is the resource request. Requests are what the scheduler reserves, what the HPA’s utilization percentage is measured against, and what the Cluster Autoscaler uses to decide whether a Pod fits on a node. Get requests wrong and every layer of autoscaling misbehaves. Requests are the foundation; everything else is built on top.
Fundamentals
Manual scaling
The simplest form of scaling is setting a replica count. Declaratively, it lives in the workload spec:
spec:
replicas: 4 # Deployment / ReplicaSet / StatefulSet
Imperatively, you can change it live:
kubectl scale deployment/web --replicas=6
kubectl scale statefulset/kafka --replicas=5
kubectl scale --replicas=3 -f deployment.yaml
# Conditional scaling — only if it is currently at a known value (avoids races)
kubectl scale deployment/web --current-replicas=4 --replicas=6
A critical gotcha: if an HPA targets a Deployment, do not also set replicas: in Git. The HPA owns the replica count and will fight your manifest, producing a flapping loop where GitOps resets it to replicas: 3 and the HPA immediately scales it back. The fix is to omit the replicas field entirely from any workload managed by an HPA (or, in Argo CD, ignore differences on that field). On first creation, if you omit replicas, it defaults to 1 until the HPA takes over.
The metrics pipeline
Autoscaling is only as good as the metrics feeding it. Two APIs matter:
metrics.k8s.io— the resource metrics API, served by metrics-server. Provides live CPU/memory per Pod and node. This powerskubectl topand the HPA’s CPU/memory scaling. metrics-server is not installed by default on vanilla clusters (though managed offerings like GKE and AKS bundle it) — a fresh HPA that reports<unknown>/70%almost always means metrics-server is missing or unhealthy.custom.metrics.k8s.io/external.metrics.k8s.io— the custom and external metrics APIs, served by an adapter (Prometheus Adapter, KEDA’s metrics adapter, or a cloud provider’s). These let the HPA scale on application metrics (requests per second, queue depth) rather than raw CPU.
metrics-server samples via the kubelet’s Summary API roughly every 15 seconds. It is designed for autoscaling, not for monitoring or historical data — do not point dashboards at it; use Prometheus for that.
Why requests are the basis of scaling
The HPA’s most common target, averageUtilization, is a percentage of the Pod’s CPU/memory request, not of the node or of a limit. If a container requests 100m CPU and is using 70m, that is 70% utilization regardless of how big the node is.
This has sharp consequences:
- No request set → HPA cannot compute utilization and the metric reports
<unknown>. CPU/memory HPAs require a request on the target resource. - Request set too low → utilization looks artificially high, the HPA over-scales, and you waste Pods.
- Request set too high → utilization looks artificially low, the HPA never triggers, and Pods are throttled without scaling.
The same request drives the Cluster Autoscaler: it simulates whether a Pod’s requests fit on a candidate node. A Pod with no memory request looks free to the autoscaler but can OOM a node in reality. Right-sizing requests is prerequisite work for every autoscaler.
Key Concepts
Horizontal Pod Autoscaler (HPA)
The HPA (autoscaling/v2) periodically (default every 15s, the --horizontal-pod-autoscaler-sync-period) reads metrics and adjusts the replica count of a scalable target (Deployment, ReplicaSet, StatefulSet — anything implementing the scale subresource). The core algorithm is a simple ratio:
desiredReplicas = ceil( currentReplicas × ( currentMetricValue / desiredMetricValue ) )
If 4 replicas average 90% CPU against a 60% target: ceil(4 × 90/60) = ceil(6) = 6 replicas. A ±10% tolerance band prevents thrashing around the target.
Metric types the v2 API supports:
| Type | Source | Example |
|---|---|---|
Resource | metrics-server | CPU/memory utilization or absolute value |
Pods | custom metrics adapter | per-Pod app metric averaged across Pods (e.g. RPS per pod) |
Object | custom metrics adapter | a single metric describing another object (e.g. Ingress requests/s) |
External | external metrics adapter | metric from outside the cluster (SQS queue length, Kafka lag) |
A worked HPA using both CPU and a custom metric, with a scaling behavior block:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # % of the CPU *request*
- type: Pods
pods:
metric:
name: http_requests_per_second
target:
type: AverageValue
averageValue: "500" # scale to keep ~500 rps per pod
behavior:
scaleUp:
stabilizationWindowSeconds: 0 # scale up immediately
policies:
- type: Percent
value: 100 # at most double
periodSeconds: 30
- type: Pods
value: 4 # or add at most 4 pods
periodSeconds: 30
selectPolicy: Max # take whichever allows more
scaleDown:
stabilizationWindowSeconds: 300 # wait 5 min of sustained low load
policies:
- type: Percent
value: 20 # remove at most 20% per minute
periodSeconds: 60
When multiple metrics are listed, the HPA computes a desired replica count for each and takes the highest — it scales to satisfy the most demanding signal, never dropping below what any metric needs.
The behavior block (v2 only) is what makes production HPAs sane. scaleUp is usually aggressive (react fast to load); scaleDown is deliberately conservative with a stabilization window — the HPA keeps the maximum recommendation seen over the window, so a brief traffic dip doesn’t prematurely kill Pods that will be needed again in seconds. The default scale-down stabilization is 300s.
HPA pitfalls:
- Memory-based HPAs are often a trap: most apps don’t release memory back to the OS, so utilization only ratchets up and the HPA can never scale down. Prefer CPU or a request-rate custom metric for memory-bound apps.
- The HPA and a rolling deployment can interact badly; scaling decisions during a rollout use metrics from a mix of old and new Pods.
minReplicas: 1allows single points of failure; production web services should floor at 2–3.
Vertical Pod Autoscaler (VPA)
The VPA adjusts a Pod’s CPU/memory requests (and optionally limits) to match observed usage, solving the “I don’t know what to request” problem. It is a separate installable component (not built in). It has three modes via updatePolicy.updateMode:
| Mode | Behavior | Use for |
|---|---|---|
Off | Only computes and reports recommendations; changes nothing | Discovering the right requests without disruption |
Initial | Sets requests only at Pod creation | Right-size at deploy, no in-life churn |
Auto / Recreate | Evicts and recreates Pods to apply new requests | Full automation (disruptive) |
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: worker-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: worker
updatePolicy:
updateMode: "Auto"
resourcePolicy:
containerPolicies:
- containerName: '*'
minAllowed: { cpu: 50m, memory: 64Mi }
maxAllowed: { cpu: 2, memory: 2Gi }
controlledResources: ["cpu", "memory"]
Historically the VPA in Auto mode had to evict and recreate Pods to change requests, causing disruption — which is why many teams run it in Off mode and apply its recommendations manually or at deploy time. Kubernetes v1.33 graduated in-place Pod resource resize to beta, allowing some resource changes without a restart, which reduces this pain going forward.
The HPA/VPA conflict: never run an HPA and a VPA on the same resource metric. If both watch CPU, the VPA raises the request (lowering utilization %), which makes the HPA think it should scale down, which raises per-Pod load, which makes the VPA raise requests again — an oscillating fight. Safe combinations:
- HPA on CPU + VPA in
Offmode (recommendations only). - HPA on a custom metric (e.g. RPS or queue depth) + VPA on CPU/memory. This is the recommended pattern for scaling both dimensions: HPA handles “how many” from app load, VPA handles “how big” from resource usage.
Cluster Autoscaler
Pod-level autoscaling is useless if there’s nowhere to put the Pods. The Cluster Autoscaler (CA) watches for Pods stuck in Pending due to insufficient resources and grows node groups (AWS ASGs, GCP MIGs, Azure VMSS) to fit them; conversely it removes nodes that have been underutilized for a period and whose Pods can be rescheduled elsewhere.
Key mechanics and caveats:
- It scales on requests, not actual usage. A cluster full of Pods requesting far more than they use will never scale down, and one full of Pods under-requesting can be packed onto too few nodes.
- Scale-down is conservative by design: a node must be under a utilization threshold (default 50%) for a period (default 10 min) and all its Pods must be movable. Pods block eviction if they have no controller, use local storage, have restrictive PodDisruptionBudgets, or carry the
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"annotation. - Scale-from-zero: a node group can scale down to zero nodes and back up. For the CA to know a zeroed group can satisfy a pending Pod’s needs (labels, taints, resources), the group must be tagged so the CA can infer the node shape without a running example.
- It respects PodDisruptionBudgets during scale-down, and honors
PriorityClassfor pod eviction ordering. - It is deliberately slow and safe; provisioning a cloud node takes minutes. For faster reaction, over-provision with low-priority “pause” Pods that get preempted by real workloads, buying time while a node spins up.
Karpenter (AWS, and beyond)
Karpenter is a newer, node-provisioning autoscaler (originating at AWS, now a CNCF project) that replaces both the Cluster Autoscaler and node groups. Instead of scaling predefined groups, it looks at the aggregate resource requests of pending Pods and provisions right-sized nodes just-in-time, choosing instance types, sizes, zones, and capacity types (including Spot) directly from the cloud API.
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| Unit of scaling | Predefined node groups (ASG/MIG/VMSS) | Individual nodes, chosen per need |
| Instance selection | Fixed per group | Flexible, from a NodePool of allowed types |
| Speed | Minutes (via ASG) | Faster (talks to EC2 directly) |
| Bin-packing | Limited by group shapes | Actively consolidates onto cheaper nodes |
| Portability | Multi-cloud | Primarily AWS (Azure support emerging) |
Karpenter’s consolidation actively repacks the cluster onto fewer/cheaper nodes as load drops, which is a major cost win but means more node churn — pair it with PodDisruptionBudgets and graceful shutdown. You describe intent in a NodePool (allowed instance families, zones, limits, disruption policy) rather than sizing groups by hand.
KEDA — event-driven autoscaling
The HPA scales on resource or exposed metrics, but many workloads should scale on events: messages in a queue, lag on a Kafka topic, rows in a database, a cron schedule. KEDA (Kubernetes Event-Driven Autoscaling) fills this gap. It is a CNCF project that acts as a metrics adapter and an HPA generator: you declare a ScaledObject with triggers, and KEDA creates and manages an HPA under the hood, feeding it external metrics from 60+ scalers (SQS, Kafka, RabbitMQ, Prometheus, Azure Service Bus, cron, and more).
KEDA’s headline feature is scale-to-zero: when there are no events, it scales the Deployment to 0 replicas (something a plain HPA cannot do, as its minReplicas floor is 1), then activates the first replica when an event arrives. This is ideal for bursty consumers and batch workers that would otherwise sit idle burning resources.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: order-consumer
spec:
scaleTargetRef:
name: order-consumer # the Deployment to scale
minReplicaCount: 0 # scale to zero when idle
maxReplicaCount: 50
cooldownPeriod: 300 # wait 5 min of no events before scaling to zero
pollingInterval: 15
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.eu-west-1.amazonaws.com/123456789012/orders
queueLength: "20" # target ~20 messages per replica
awsRegion: eu-west-1
authenticationRef:
name: keda-aws-credentials
- type: kafka
metadata:
bootstrapServers: kafka:9092
consumerGroup: orders
topic: orders
lagThreshold: "100" # add a replica per 100 messages of lag
With two triggers, KEDA scales to satisfy whichever demands more replicas. Note the distinction: KEDA handles 0↔1 activation itself, then hands 1↔N scaling to the generated HPA. Its ScaledJob variant scales Jobs instead of Deployments, one Job per event batch — ideal for long-running, non-idempotent processing.
Scaling StatefulSets — caveats
StatefulSets can be scaled with kubectl scale or an HPA, but stateful workloads rarely scale as cleanly as stateless ones:
- Ordered, one-at-a-time operations: by default a StatefulSet creates/deletes Pods sequentially (
web-0,web-1,web-2…) and scaling down removes the highest ordinal first. This is slow and, for a database, may not be safe. - Data rebalancing isn’t automatic: adding a replica to Kafka, Cassandra, or Elasticsearch gives you an empty member — the cluster must rebalance partitions/shards, which Kubernetes knows nothing about. Autoscaling replica count without an operator that handles rebalancing can corrupt or unbalance the cluster.
- Scale-down risks data loss: removing a replica can orphan or delete its data. PersistentVolumeClaims are not deleted automatically on scale-down (they’re retained per the
persistentVolumeClaimRetentionPolicy), so a naive HPA scaling a database down and up leaves dangling PVCs and under-replicated data. - Quorum systems have hard limits: consensus systems (etcd, ZooKeeper) need an odd member count for quorum and cannot tolerate arbitrary autoscaling.
Rule of thumb: don’t put an HPA on a StatefulSet backing a stateful system unless a purpose-built operator (e.g. the Strimzi Kafka operator, a database operator) manages membership and rebalancing. For stateful data stores, scale deliberately and manually, or let the operator do it.
Best Practices
-
Set accurate resource requests before enabling any autoscaler. Requests are the denominator for HPA utilization, the fit calculation for the Cluster Autoscaler, and the target for VPA. Wrong requests break every layer — right-size from real usage data (VPA in
Offmode or Prometheus) rather than guessing. -
Install and monitor metrics-server as a prerequisite. CPU/memory HPAs silently report
<unknown>without it. Treat it as core infrastructure, run it highly available, and alert on its health. -
Never set
replicas:in Git for an HPA-managed workload. The HPA and your manifest will fight, causing a flapping replica count. Omit the field (and configure your GitOps tool to ignore it). -
Use
autoscaling/v2and tune thebehaviorblock. AggressivescaleUp(react fast) plus a conservativescaleDownstabilization window (avoid thrashing) is the difference between a stable and a flapping autoscaler. Default scale-down stabilization is 5 minutes — keep it generous. -
Prefer CPU or custom metrics over memory for HPA targets. Most runtimes never release memory to the OS, so memory utilization only climbs and the HPA can’t scale down. Scale on CPU or an application signal (RPS, queue depth) instead.
-
Never run HPA and VPA on the same resource metric. They form an oscillating feedback loop. Combine them only as HPA-on-custom-metric + VPA-on-CPU/memory, or run VPA in
Offmode for recommendations. -
Pair Pod autoscaling with node autoscaling. An HPA that creates Pods no node can host just produces
PendingPods. Always deploy the Cluster Autoscaler or Karpenter alongside HPAs, and verify the whole chain end to end. -
Floor production services at 2–3 replicas with a PodDisruptionBudget.
minReplicas: 1is a single point of failure during node drains and rollouts. A PDB keeps autoscaling and cluster maintenance from taking you to zero available Pods. -
Use KEDA for event-driven and scale-to-zero workloads. Queue consumers, Kafka processors, and batch jobs should scale on backlog, not CPU, and should idle at zero replicas. Set a sensible
cooldownPeriodso brief idle gaps don’t cause churn. -
Set
maxReplicasand node-group limits as cost and blast-radius guardrails. Autoscaling without an upper bound turns a traffic spike (or a metric bug) into a runaway cloud bill. Cap replicas and node counts explicitly. -
Protect Pods that must not be evicted during scale-down. Use PodDisruptionBudgets and the
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"annotation for stateful or long-running Pods so node consolidation doesn’t disrupt them. -
Configure and tag node groups for scale-from-zero. Add the resource/label/taint tags the Cluster Autoscaler needs to reason about empty groups, so a zeroed group can still be selected for pending Pods.
-
Don’t autoscale StatefulSets backing stateful systems without an operator. Naive replica autoscaling of databases/message brokers risks data loss, dangling PVCs, and quorum failures. Let a purpose-built operator manage membership and rebalancing.
-
Load-test your autoscaling configuration. Autoscaler tuning (thresholds, windows, cooldowns) is only validated under realistic load. Run soak and spike tests, watch the reaction time and steady state, and iterate before trusting it in production.
-
Consider Karpenter for cost-driven, flexible node provisioning on AWS. Just-in-time right-sized nodes and active consolidation cut waste versus fixed node groups — but budget for the extra node churn with PDBs and graceful shutdown.
-
Design apps to shut down gracefully. Autoscaling means Pods are constantly created and destroyed. Handle
SIGTERM, drain in-flight work, and setterminationGracePeriodSecondsand apreStophook so scale-down and consolidation don’t drop requests.
References
- Horizontal Pod Autoscaling
- HPA walkthrough
- Resource metrics pipeline & metrics-server
- Managing resources for containers (requests & limits)
- Vertical Pod Autoscaler
- Resize container resources in-place
- Cluster Autoscaler FAQ
- Karpenter documentation
- KEDA documentation · KEDA scalers
- StatefulSet basics & scaling
- StatefulSet PVC retention policy
- PodDisruptionBudget
- roadmap.sh — Kubernetes