Cluster Autoscaler — Cơ Chế Scale-Up & Scale-Down
Cluster Autoscaler nhìn vào cái gì
HPA, VPA, MPA đều thao tác ở chiều Pod — chúng thay đổi số lượng và kích thước Pod. Nhưng Pod cần node để chạy. Khi HPA tạo thêm Pod mà cluster không còn chỗ, Pod chỉ Pending. Đây là nơi Cluster Autoscaler (CA) bước vào: nó là control loop ở chiều node, chịu trách nhiệm đảm bảo có đủ node để chứa Pod, và không nhiều hơn mức cần.
Điểm cốt lõi định hình mọi hành vi của CA: CA ra quyết định dựa trên requests của Pod, không phải usage thật của node (GKE Cluster autoscaler). Nó không quan tâm node đang dùng bao nhiêu CPU thật; nó quan tâm tổng requests của các Pod trên node so với allocatable của node. Hệ quả trực tiếp: nếu requests đặt sai (file 1, 3), CA ra quyết định sai — request quá cao thì CA giữ node không cần thiết, request quá thấp thì CA đóng gói quá chặt rồi Pod bị throttle/OOM.
Một hiểu lầm phổ biến cần xóa ngay: CA không phải HPA cho node. Nó không nhìn metric CPU/memory để scale. Nó nhìn trạng thái scheduling: có Pod nào Pending vì thiếu chỗ không (scale-up), và có node nào đủ trống để dồn Pod đi nơi khác rồi xóa không (scale-down). CA và scheduler dùng chung ngôn ngữ — đây là lý do Chương 8 là tiên quyết.
Scale-up: từ Pod Pending tới node mới
Trigger
Theo tài liệu GKE, nếu Pod không thể được lập lịch trên bất kỳ node hiện có nào, CA thêm node, tới mức tối đa của node pool (GKE Cluster autoscaler). Theo FAQ của Cluster Autoscaler open-source, CA quét tìm Pod unschedulable mỗi 10 giây (cấu hình qua --scan-interval) (CA FAQ).
Quan trọng: trigger là Pod Pending vì lý do thiếu tài nguyên/không tìm được node phù hợp, không phải "node đầy". Một Pod Pending vì lý do khác (ví dụ requiredDuringScheduling anti-affinity không thể thỏa, PVC chưa bind, taint không có toleration) không nhất thiết kích hoạt scale-up có ích — CA sẽ mô phỏng và thấy node mới cũng không giải quyết được, nên không scale (và ghi noScaleUp, file 7).
Mô phỏng "fake scheduling" trên template node
Đây là phần thông minh nhất của CA. Khi có Pod Pending, CA không đoán mò node nào sẽ giúp. Nó dựng một "template node" cho mỗi node group (đại diện cho một node giả định nếu group đó scale lên), rồi chạy chính logic scheduling của Kubernetes để mô phỏng: nếu thêm một node kiểu này, các Pod Pending có lập lịch được lên đó không? (CA FAQ)
Cơ chế này nghĩa là CA tôn trọng mọi ràng buộc scheduling khi quyết định: nodeSelector, affinity, taints/tolerations, topology spread, resource requests. Một node group mà template node của nó fail predicate (ví dụ NodeResourcesFit vì Pod cần GPU mà group không có) sẽ bị loại — và CA ghi no.scale.up.mig.failing.predicate với tên predicate (file 7).
Hệ quả thiết kế: để CA scale-up được cho một loại Pod, phải tồn tại ít nhất một node group mà template của nó thỏa mọi ràng buộc của Pod. Nếu không node pool nào có nhãn/cấu hình phù hợp, CA bó tay — đây là lúc cần Node Auto-Provisioning (file 6) để CA tự tạo pool mới.
Expander: chọn node group nào để scale
Khi nhiều node group đều có thể chứa Pod Pending, CA phải chọn group nào để scale. Thuật toán chọn gọi là expander, đặt qua flag --expander (có thể nối nhiều cái bằng dấu phẩy để phá hòa) (CA FAQ):
| Expander | Hành vi | Khi nào hợp lý |
|---|---|---|
random | Chọn ngẫu nhiên khi không có ưu tiên | Mặc định "vô tư", ít dùng có chủ đích |
most-pods | Chọn group lập lịch được nhiều Pod nhất | Khi cần dọn một loạt Pending nhanh; hữu ích với nodeSelector |
least-waste | Chọn group còn ít CPU rảnh nhất sau scale-up (hòa thì xét memory) | Mặc định của CA — tối ưu đóng gói, giảm lãng phí |
least-nodes | Tối thiểu tổng số node sau scale-up | Khi muốn ít node to thay vì nhiều node nhỏ |
price | Chọn group rẻ nhất (chỉ GCE/GKE/Equinix) | Tối ưu chi phí trực tiếp |
priority | Theo ưu tiên do người dùng đặt cho từng group | Kiểm soát rõ ràng: ưu tiên reservation/Spot/region nhất định |
least-waste là mặc định và là lựa chọn đúng cho hầu hết trường hợp: nó chọn node group mà sau khi thêm node, lượng CPU thừa là nhỏ nhất — tức đóng gói chặt nhất. priority đặc biệt mạnh cho chiến lược production phức tạp: ví dụ ưu tiên scale node pool Spot trước (rẻ), chỉ fallback sang on-demand khi Spot không đủ — bằng cách gán priority cao cho pool Spot.
priority expander: chiến lược Spot-trước-on-demand
priority expander đáng nói riêng vì nó là công cụ chính cho chiến lược chi phí phổ biến nhất: ưu tiên Spot, fallback on-demand. Cấu hình qua một ConfigMap cluster-autoscaler-priority-expander gán điểm ưu tiên cho từng node group theo regex:
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-priority-expander
namespace: kube-system
data:
priorities: |-
50:
- .*spot.*
10:
- .*on-demand.*Số lớn hơn = ưu tiên cao hơn. CA sẽ thử scale pool Spot (điểm 50) trước; chỉ khi Spot không cấp được (hết công suất) mới rơi xuống pool on-demand (điểm 10). Kết hợp expander chained (priority,least-waste) để khi cùng mức ưu tiên thì phá hòa bằng least-waste. Đây là pattern production cho phép tiết kiệm lớn nhờ Spot mà vẫn có lưới an toàn on-demand khi Spot cạn.
Tốc độ scale-up: bao nhiêu node một lúc
CA không thêm node từng cái một khi cần nhiều. Khi xác định cần N node để dọn hết Pod Pending, nó thêm song song trong một lần (CA FAQ). Nhưng có trần: CA giới hạn số node tối đa nó cân nhắc trong một lần scan để tránh quyết định khổng lồ. Hệ quả: với một spike rất lớn, có thể cần vài chu kỳ scan (mỗi ~10s) để scale tới mức cần — một lý do nữa khiến scale-up không tức thì và capacity buffer (file 7) có giá trị. Quan trọng hơn nữa là độ trễ tạo node thật (gọi API Compute Engine, boot, join, kéo image) thường lớn hơn nhiều so với chu kỳ scan của CA — bottleneck thường ở khâu provision node, không phải ở tốc độ ra quyết định của CA.
location_policy: scale qua các zone
Một node pool GKE thường trải qua nhiều zone. Khi scale-up, CA phải chọn zone nào. Theo tài liệu GKE, location_policy có hai giá trị (từ GKE 1.24.1-gke.800+) (GKE Cluster autoscaler):
BALANCED(mặc định cho node on-demand/reserved): phân bố node của pool qua các zone đã chọn đều nhất có thể. Đây là lựa chọn cho độ bền — Pod trải đều qua zone, mất một zone không sập toàn bộ.ANY(mặc định cho Spot/flex-start): ưu tiên reservation chưa dùng và zone còn đủ công suất. Đây là lựa chọn cho khả năng cấp được — với Spot, việc lấy được node quan trọng hơn việc cân bằng zone hoàn hảo.
Trade-off: BALANCED cho độ bền tốt hơn nhưng có thể chậm/khó cấp khi một zone cạn công suất; ANY cấp dễ hơn nhưng có thể dồn node vào ít zone, giảm độ bền. Với workload critical dùng on-demand, giữ BALANCED và đảm bảo topology spread ở tầng Pod (Chương 8) để độ bền thực sự được đảm bảo cả hai tầng.
Scale-down: phần khó và nguy hiểm hơn nhiều
Scale-up sai chỉ tốn tiền. Scale-down sai có thể gây outage — xóa nhầm node đang chạy Pod quan trọng, hoặc xóa node làm Pod không còn chỗ. Vì thế CA cực kỳ thận trọng với scale-down, với nhiều lớp điều kiện và độ trễ.
Điều kiện để một node đủ điều kiện xóa
Theo CA FAQ, một node trở thành ứng viên xóa khi tất cả điều kiện sau đồng thời đúng (CA FAQ):
Utilization dưới ngưỡng: tổng
requests(CPU + memory) của các Pod trên node nhỏ hơn 50% allocatable.- Flag:
--scale-down-utilization-threshold, mặc định 0.5. - Công thức:
(tổng CPU requests + tổng memory requests) / allocatable < ngưỡng. - Nhấn mạnh lại: đây là requests, không phải usage. Một node với Pod request 60% allocatable nhưng thực dùng 5% sẽ không được xem là under-utilized — vì CA nhìn requests.
- Flag:
Đủ lâu ở trạng thái không cần thiết: node phải under-utilized liên tục tối thiểu 10 phút.
- Flag:
--scale-down-unneeded-time, mặc định 10m0s.
- Flag:
Đã qua các cửa sổ trễ:
- Kể từ lần scale-up gần nhất:
--scale-down-delay-after-add, mặc định 10m0s. (Ngăn xóa node ngay sau khi vừa thêm — chống flapping.) - Kể từ lần scale-down thất bại:
--scale-down-delay-after-failure, mặc định 3m0s.
- Kể từ lần scale-up gần nhất:
Không có annotation cấm: node không mang
cluster-autoscaler.kubernetes.io/scale-down-disabled: "true".
Tổng hợp lại: ngay cả khi một node hoàn toàn trống về requests, mặc định mất ít nhất 10 phút trước khi nó bị xóa, và lâu hơn nếu cluster vừa scale-up. Đây là thiết kế đúng — nó cho hệ thống thời gian ổn định trước khi rút công suất, và là lý do "vì sao node trống của tôi vẫn chưa bị xóa".
Điều gì chặn scale-down (rất quan trọng)
Đây là phần gây nhiều "tại sao node không xóa được" nhất. Một node không bị xóa nếu nó chứa bất kỳ loại Pod nào sau (CA FAQ):
- Pod kube-system không có PodDisruptionBudget (hoặc có PDB hạn chế tới mức không evict được). Đây là thủ phạm số một: các Pod hệ thống (kube-dns, metrics-server...) chạy trên node mà không có PDB cho phép di dời → node "kẹt". GKE quản lý PDB cho nhiều add-on, nhưng add-on tự cài thường thiếu.
- Pod có local storage (
hostPath, hoặcemptyDirkhông phảimedium: Memory) — trừ khi annotatecluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes: "vol1,vol2". - Pod có PodDisruptionBudget hạn chế ngăn mọi eviction (ví dụ
minAvailablebằng số replica hiện tại). - Pod không thuộc controller nào (không phải từ Deployment/StatefulSet/Job/ReplicaSet) — không có gì tạo lại nó nếu evict.
- Pod có annotation
cluster-autoscaler.kubernetes.io/safe-to-evict: "false". - Pod không thể lập lịch lại nơi khác do ràng buộc (node affinity, không đủ tài nguyên ở chỗ khác).
Pattern phòng thủ production: mọi workload không-stateless quan trọng nên có PDB, và mọi add-on tự cài chạy ở mọi node nên có PDB phù hợp — nếu không chúng âm thầm chặn scale-down toàn cluster, khiến hóa đơn không bao giờ giảm. Khi debug "scale-down không xảy ra", đọc noScaleDown reasons trong log visibility (file 7) để biết chính xác Pod nào chặn.
Drain sequence: cách CA rút Pod khỏi node
Khi quyết định xóa một node, CA thực hiện drain có trật tự (CA FAQ):
- Cordon node (đánh dấu unschedulable).
- Evict các Pod, tôn trọng PodDisruptionBudget. Đây là khác biệt then chốt so với node-pressure eviction (Chương 8) — CA tôn trọng PDB, node-pressure eviction thì không.
- Pod được lập lịch lại lên node khác (đã được mô phỏng đảm bảo có chỗ trước khi quyết định).
- Nếu drain không hoàn tất trong thời gian cho phép, node bị terminate cưỡng bức. Thời gian này là 1 giờ với GKE 1.32.7-gke.1079000+, và 10 phút với phiên bản cũ hơn.
--max-graceful-termination-sec(mặc định 600s) kiểm soát thời gian chờ graceful shutdown của Pod.- Eviction DaemonSet Pod kiểm soát riêng:
--daemonset-eviction-for-empty-nodes(mặc định tắt) và--daemonset-eviction-for-occupied-nodes(mặc định bật); có thể override per-Pod bằng annotationcluster-autoscaler.kubernetes.io/enable-ds-eviction.
Multiple node pools: CA quản lý từng pool
Một cluster GKE thường có nhiều node pool (general-purpose, GPU, Spot, memory-optimized...). CA quản lý mỗi pool như một node group riêng với min/max riêng:
--min-nodes/--max-nodes: giới hạn mỗi zone cho một pool.--total-min-nodes/--total-max-nodes(GKE 1.24+): giới hạn toàn cluster-wide cho pool, độc lập với số zone.
Khi scale-up, CA dùng expander để chọn giữa các pool đủ điều kiện (như trên). Khi scale-down, mỗi pool được đánh giá độc lập theo điều kiện utilization của nó.
Pattern production: tách workload theo pool với taint (Chương 8), và đặt min/max phù hợp từng pool. Ví dụ: pool GPU đắt nên min-nodes: 0 (scale về 0 khi không có job GPU), pool system min-nodes đủ cao để luôn có chỗ cho add-on. CA scale từng pool về đúng nhu cầu của loại workload nó phục vụ.
Autoscaling profiles: balanced vs optimize-utilization
GKE cung cấp hai autoscaling profile ảnh hưởng tới cả scheduler (Chương 8) lẫn hành vi scale-down của CA (GKE Cluster autoscaler):
balanced(mặc định): ưu tiên giữ nhiều tài nguyên sẵn sàng hơn cho Pod sắp tới. Scale-down thận trọng. Đây là lựa chọn cho độ tin cậy — có đệm công suất.optimize-utilization: scale-down mạnh tay hơn, đóng gói chặt hơn (scheduler chuyển sang bin-packing, Chương 8). Đây là lựa chọn cho chi phí.
gcloud container clusters update CLUSTER_NAME \
--autoscaling-profile optimize-utilizationTrade-off cốt lõi: optimize-utilization tiết kiệm tiền khi tải ổn định, nhưng vì nó scale-down nhanh và đóng gói chặt, khi có spike đột ngột cluster có ít đệm hơn → nhiều Pod Pending hơn trong lúc CA cấp node → scale-up chậm biểu hiện rõ hơn. Với workload có spike khó đoán và yêu cầu latency nghiêm ngặt, balanced (cộng capacity buffer, file 7) thường đúng hơn. Đừng bật optimize-utilization chỉ vì "muốn tiết kiệm" mà không cân nhắc hồ sơ tải.
Một cách dùng kết hợp hợp lý: giữ balanced ở cấp cluster cho workload phục vụ traffic, nhưng đặt workload batch/nền vào pool riêng nơi đóng gói chặt không gây rủi ro latency. Như vậy bạn được lợi ích chi phí của bin-packing ở đúng chỗ chấp nhận được, mà không hy sinh đệm công suất cho workload nhạy với spike.
Real-world: thiết kế node pool cho một cluster đa workload
Xét một cluster production điển hình với bốn lớp workload, minh họa cách CA quản lý nhiều pool độc lập:
- Pool
system(general-purpose, on-demand,min-nodes: 3): chạy add-on hệ thống, ingress controller, monitoring.mincao để luôn có chỗ cho hạ tầng;BALANCEDqua 3 zone cho độ bền. CA hiếm khi scale-down dưới min này. - Pool
web(general-purpose, on-demand,min: 3, max: 30): web/API service.balancedprofile + capacity buffer (file 7) để chịu spike. Topology spread (Chương 8) đảm bảo Pod trải đều zone. - Pool
batch(compute-optimized, Spot,min: 0, max: 100): worker xử lý hàng đợi, chịu được preemption.min: 0để scale về 0 lúc nhàn rỗi;ANYlocation policy ưu tiên cấp được. Taint Spot + toleration đảm bảo chỉ workload chịu preempt mới vào. - Pool
gpu(GPU, on-demand,min: 0, max: 8): training/inference.min: 0vì node GPU đắt — chỉ tồn tại khi có job. Taint GPU đẩy workload thường ra.
CA quản lý mỗi pool theo điều kiện riêng: pool batch và gpu co về 0 khi không có việc (tiết kiệm lớn), pool system và web giữ mức nền cho độ tin cậy. Khi một training job xuất hiện, CA scale gpu từ 0; khi hàng đợi phình, CA scale batch; cả hai độc lập với nhau và với pool phục vụ traffic. Đây là sức mạnh của multiple node pool: mỗi lớp workload có chính sách autoscaling phù hợp bản chất của nó, thay vì một chính sách chung không tối ưu cho ai.
Điểm tinh tế: việc tách pool qua taint (Chương 8) không chỉ để isolation mà còn để CA scale đúng loại node cho đúng workload. Nếu trộn mọi workload vào một pool, CA mất khả năng scale GPU về 0 (vì web service vẫn giữ node), và bin-packing kém hơn nhiều. Thiết kế pool là tiền đề của autoscaling hiệu quả.
Anti-patterns thường gặp
- Add-on tự cài chạy mọi node mà không có PDB. Âm thầm chặn scale-down toàn cluster; node không bao giờ xóa được, hóa đơn không giảm. Luôn thêm PDB.
requestsquá cao làm CA giữ node thừa. CA nhìn requests, không nhìn usage — request thừa nghĩa là node "trông" đầy dù thực dùng ít. Right-size requests (file 3) là tiền đề của scale-down hiệu quả.- Kỳ vọng scale-down tức thì. Mặc định tối thiểu 10 phút (
scale-down-unneeded-time) cộngdelay-after-add. "Node trống chưa xóa" thường là đúng theo thiết kế. - Dùng
emptyDir/hostPathcho workload mà không nhận ra nó chặn scale-down. DùngemptyDirmedium: Memoryhoặc annotatesafe-to-evict-local-volumesnếu chấp nhận mất dữ liệu tạm. - Bật
optimize-utilizationcho workload latency-critical có spike. Mất đệm công suất → spike thành Pending → outage. Cân nhắcbalanced+ buffer. - Đặt
max-nodesquá thấp. Scale-up đụng trần âm thầm, Pod Pending kéo dài. Theo dõinoScaleUp(file 7) và đặt trần đủ rộng. - Quên rằng CA cần một node group phù hợp. Nếu không pool nào thỏa ràng buộc của Pod, CA không scale được — cần NAP (file 6).