← Kubernetes← Kubernetes
KubernetesKubernetes19 Th7, 2026Jul 19, 202619 phút đọc16 min read

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.

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-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:

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ạiNguồnVí dụ
Resourcemetrics-serverUtilization CPU/memory hoặc giá trị tuyệt đối
Podscustom metrics adaptermetric ứng dụng theo Pod, tính trung bình trên các Pod (ví dụ RPS mỗi pod)
Objectcustom metrics adaptermột metric mô tả một object khác (ví dụ requests/s của Ingress)
Externalexternal metrics adaptermetric 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:

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:

ModeHành viDùng cho
OffChỉ 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 PodRight-size lúc deploy, không xáo trộn khi đang chạy
Auto / RecreateEvict và tạo lại Pod để áp request mớiTự độ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:

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:

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 AutoscalerKarpenter
Đơn vị scaleNode group định sẵn (ASG/MIG/VMSS)Từng node, chọn theo nhu cầu
Chọn instanceCố định theo groupLinh 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-packingBị giới hạn bởi hình dạng groupChủ động consolidate xuống node rẻ hơn
Tính di độngĐa cloudChủ 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:

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

  1. Đặ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 Off hoặc Prometheus) thay vì đoán.

  2. 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.

  3. 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ó).

  4. Dùng autoscaling/v2 và tinh chỉnh block behavior. scaleUp quyết liệt (phản ứng nhanh) cộng với cửa sổ stabilization scaleDown thậ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.

  5. Ư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).

  6. 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ị.

  7. 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.

  8. Đặt sàn dịch vụ production ở 2–3 replica kèm một PodDisruptionBudget. minReplicas: 1 là 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.

  9. 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 cooldownPeriod hợp lý để các khoảng rảnh ngắn không gây churn.

  10. Đặt maxReplicas và 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.

  11. 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.

  12. 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ờ.

  13. Đừ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.

  14. 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.

  15. 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.

  16. 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à đặt terminationGracePeriodSeconds cùng một preStop hook để scale-down và consolidation không làm rớt request.

Tài liệu tham khảo

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.

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-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:

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:

TypeSourceExample
Resourcemetrics-serverCPU/memory utilization or absolute value
Podscustom metrics adapterper-Pod app metric averaged across Pods (e.g. RPS per pod)
Objectcustom metrics adaptera single metric describing another object (e.g. Ingress requests/s)
Externalexternal metrics adaptermetric 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:

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:

ModeBehaviorUse for
OffOnly computes and reports recommendations; changes nothingDiscovering the right requests without disruption
InitialSets requests only at Pod creationRight-size at deploy, no in-life churn
Auto / RecreateEvicts and recreates Pods to apply new requestsFull 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:

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:

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 AutoscalerKarpenter
Unit of scalingPredefined node groups (ASG/MIG/VMSS)Individual nodes, chosen per need
Instance selectionFixed per groupFlexible, from a NodePool of allowed types
SpeedMinutes (via ASG)Faster (talks to EC2 directly)
Bin-packingLimited by group shapesActively consolidates onto cheaper nodes
PortabilityMulti-cloudPrimarily 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:

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

  1. 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 Off mode or Prometheus) rather than guessing.

  2. 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.

  3. 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).

  4. Use autoscaling/v2 and tune the behavior block. Aggressive scaleUp (react fast) plus a conservative scaleDown stabilization window (avoid thrashing) is the difference between a stable and a flapping autoscaler. Default scale-down stabilization is 5 minutes — keep it generous.

  5. 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.

  6. 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 Off mode for recommendations.

  7. Pair Pod autoscaling with node autoscaling. An HPA that creates Pods no node can host just produces Pending Pods. Always deploy the Cluster Autoscaler or Karpenter alongside HPAs, and verify the whole chain end to end.

  8. Floor production services at 2–3 replicas with a PodDisruptionBudget. minReplicas: 1 is a single point of failure during node drains and rollouts. A PDB keeps autoscaling and cluster maintenance from taking you to zero available Pods.

  9. 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 cooldownPeriod so brief idle gaps don’t cause churn.

  10. Set maxReplicas and 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.

  11. 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.

  12. 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.

  13. 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.

  14. 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.

  15. 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.

  16. Design apps to shut down gracefully. Autoscaling means Pods are constantly created and destroyed. Handle SIGTERM, drain in-flight work, and set terminationGracePeriodSeconds and a preStop hook so scale-down and consolidation don’t drop requests.

References