CA Troubleshooting, Capacity Buffers & Provisioning Requests
File 5 mổ xẻ cơ chế của Cluster Autoscaler, file 6 mở rộng sang Node Auto-Provisioning. File này khép lại chiều node với ba thứ quyết định sự khác biệt giữa một cluster "có autoscaling" và một cluster "vận hành autoscaling tốt": quan sát được quyết định của CA, cắt độ trễ scale-up bằng đệm công suất, và đặt trước công suất atomic cho batch/AI.
Vì sao quan sát được là điều kiện tiên quyết
Cluster Autoscaler ra quyết định trên control plane do Google quản lý — bạn không đọc được log verbose của nó như cluster tự dựng. Khi một Pod kẹt Pending hay một node trống không chịu xóa, không có visibility thì mọi chẩn đoán là phỏng đoán. GKE giải quyết bằng cách ghi mọi quyết định của CA vào Cloud Logging dưới dạng visibility events (View cluster autoscaler events). Đây là nguồn sự thật để debug autoscaling.
Hai nhóm event
- Status events: phát theo chu kỳ, mô tả kích thước node pool thực tế và mục tiêu —
autoscaledNodesCount,autoscaledNodesTarget,measureTime. Dùng để thấy "CA đang muốn cluster lớn cỡ nào". - Decision events: các quyết định cụ thể —
scaleUp,scaleDown,eventResult,nodePoolCreated,nodePoolDeleted.
Chi tiết các decision event (View cluster autoscaler events):
scaleUp: CA tăng node pool qua Managed Instance Group; liệt kê MIG bị ảnh hưởng và các Pod unschedulable đã kích hoạt (cắt còn 50 entry).scaleDown: CA xóa node under-utilized; gồmcpuRatiovàmemRatio(tỷ lệ utilization của node bị xóa).eventResult: kết quả hoàn thành của một thao tác scale, có message lỗi hoặc rỗng nếu thành công.nodePoolCreated/nodePoolDeleted: phát khi NAP tạo/xóa pool (file 6).
Đọc noScaleUp: vì sao không scale-up được
Khi có Pod Pending nhưng CA không scale-up được, nó phát event với reason string cụ thể. Học đọc các chuỗi này là kỹ năng debug quan trọng nhất (View cluster autoscaler events):
| Reason | Ý nghĩa | Hành động |
|---|---|---|
no.scale.up.nap.pod.zonal.resources.exceeded | NAP không cấp thêm được trong zone (vượt công suất/limit) | Kiểm tra resourceLimits, quota zone, đổi zone |
no.scale.up.nap.pod.gpu.no.limit.defined | Pod cần GPU nhưng cluster chưa định nghĩa limit GPU cho NAP | Thêm --max-accelerator cho loại GPU đó (file 6) |
no.scale.up.mig.failing.predicate | Template node của MIG fail một scheduling predicate (kèm tên, ví dụ NodeResourcesFit, NodeAffinity) | Sửa ràng buộc Pod hoặc tạo pool phù hợp |
no.scale.up.in.backoff | CA đang trong backoff tạm thời sau lỗi | Đợi; điều tra lỗi gốc trong eventResult |
Quy trình debug "Pod Pending, không scale-up":
- Xác nhận Pod Pending vì thiếu chỗ (đọc
kubectl describe podevents) chứ không phải lý do scheduling thuần (PVC, affinity bất khả thi). - Đọc
noScaleUpevent cho Pod đó → reason string cho biết chính xác rào cản. - Reason
failing.predicate+ tên predicate → đối chiếu Chương 8 để biết ràng buộc nào không thỏa. - Reason
gpu.no.limit.definedhayzonal.resources.exceeded→ vấn đề ở NAP limits/quota, không phải Pod.
Đọc noScaleDown: vì sao node không xóa được
Đối xứng với scale-up, khi một node under-utilized nhưng không xóa được, CA phát noScaleDown với reason (View cluster autoscaler events):
| Reason | Ý nghĩa | Hành động |
|---|---|---|
no.scale.down.node.pod.kube.system.unmovable | Pod kube-system (không phải DaemonSet) không có PDB trên node | Thêm PDB cho add-on đó |
no.scale.down.node.pod.not.backed.by.controller | Pod không thuộc controller nào (không tạo lại được) | Chạy Pod qua Deployment/Job; hoặc annotate safe-to-evict |
no.scale.down.node.pod.has.local.storage | Pod dùng local storage (emptyDir/hostPath) | Annotate safe-to-evict-local-volumes nếu chấp nhận; hoặc đổi storage |
no.scale.down.node.pod.not.enough.pdb | PDB hạn chế ngăn eviction | Nới minAvailable/maxUnavailable của PDB |
no.scale.down.node.no.place.to.move.pods | Không node nào khác nhận được Pod | Tăng công suất chỗ khác hoặc nới ràng buộc |
no.scale.down.in.backoff | CA đang backoff scale-down tạm thời | Đợi |
Đây là bảng tra cứu vàng cho "vì sao hóa đơn không giảm". Mọi node giữ lại không cần thiết đều có một reason string giải thích. Thủ phạm phổ biến nhất trong thực tế là kube.system.unmovable (add-on tự cài thiếu PDB) và not.enough.pdb (PDB đặt quá chặt).
Cloud Logging queries
Truy vấn các sự kiện này qua log container.googleapis.com/cluster-autoscaler-visibility (View cluster autoscaler events). Event nằm trong trường jsonPayload, timestamp UNIX giây.
Xem các quyết định scale-up/scale-down:
resource.type="k8s_cluster"
resource.labels.location="COMPUTE_REGION"
resource.labels.cluster_name="CLUSTER_NAME"
log_id("container.googleapis.com/cluster-autoscaler-visibility")
( "decision" NOT "noDecisionStatus" )Xem việc xóa node trống:
resource.type="k8s_cluster"
resource.labels.project_id="PROJECT_ID"
resource.labels.location="COMPUTE_REGION"
resource.labels.cluster_name="CLUSTER_NAME"
logName="projects/PROJECT_ID/logs/events"
( "Scale-down: removing empty node" )Lọc theo noScaleUp để gỡ Pod Pending, hay theo reason string cụ thể (ví dụ "no.scale.down.node.pod.kube.system.unmovable"), là cách nhanh nhất khoanh vùng vấn đề trên một cluster lớn. Trong production, nên build một dashboard/alert trên các reason noScaleUp để phát hiện sớm tình trạng "muốn scale mà không được".
Một mẹo vận hành: vì các event này nằm trong _Default bucket với retention giới hạn, nếu cần phân tích xu hướng dài hạn (ví dụ "tần suất noScaleUp theo tuần") nên thiết lập một log sink xuất các event này sang BigQuery. Khi đó bạn truy vấn được lịch sử quyết định autoscaler bằng SQL, dựng báo cáo định kỳ về sức khỏe autoscaling thay vì chỉ debug phản ứng từng sự cố.
Capacity buffer & overprovisioning: cắt độ trễ scale-up
Đây là một trong những pattern quan trọng nhất của cả chương, vì nó giải quyết failure mode tốn kém nhất: scale-up quá chậm khi spike.
Nhắc lại độ trễ end-to-end (từ index): HPA nhận ra (~15s) → Pod Pending → CA phát hiện (~10s) → gọi API tạo node (phút) → node join + kéo image (phút) → Pod Ready. Tổng có thể là vài phút. Trong vài phút đó, nếu không có công suất dự phòng, Pod mới chỉ Pending và service thiếu công suất — biểu hiện thành lỗi cho người dùng.
Vấn đề gốc: CA chỉ scale-up khi Pod đã Pending. Nó phản ứng, không dự phòng. Để có công suất sẵn sàng trước khi cần, ta cần lừa CA giữ một lượng node trống — và cách kinh điển là pause Pod với PriorityClass thấp (overprovisioning).
Cơ chế pause Pod (balloon Pod)
- Tạo một
PriorityClassvới giá trị âm (thấp hơn mọi workload thật):
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -10
globalDefault: false
description: "Pause Pods giữ chỗ, bị preempt khi workload thật cần"- Deploy một số "balloon Pod" chạy image
pause(gần như không tốn tài nguyên thật) nhưng request một lượng tài nguyên đáng kể (bằng kích thước đệm mong muốn):
apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 5
template:
spec:
priorityClassName: overprovisioning
containers:
- name: pause
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: "1"
memory: 1GiVì sao nó hoạt động
- Balloon Pod request tài nguyên → CA tính chúng vào nhu cầu → giữ đủ node để chứa chúng. Đệm công suất tồn tại sẵn dưới dạng node đang chạy balloon Pod.
- Khi workload thật cần chỗ và cluster đầy, vì balloon Pod có priority âm, scheduler preempt chúng (Chương 8) → balloon Pod bị đẩy ra, workload thật chiếm chỗ ngay lập tức trên node đã sẵn sàng (không phải đợi tạo node).
- Balloon Pod bị preempt trở thành Pending → CA scale-up để khôi phục đệm (lúc này độ trễ tạo node không ảnh hưởng người dùng, vì workload thật đã có chỗ).
Kết quả: bạn "trả trước" độ trễ tạo node bằng cách luôn giữ một lớp node đệm. Đánh đổi là chi phí của đệm đó. Kích thước đệm là quyết định kinh tế: đủ lớn để hấp thụ spike điển hình, không quá lớn để lãng phí. Đây là cách đúng để dung hòa "scale-up nhanh" với chi phí, thay vì over-provisioning toàn bộ workload thật.
Lưu ý: trên Autopilot, GKE cung cấp khái niệm capacity buffer/balloon Pod theo cách quản lý hơn (ví dụ qua compute class hoặc cấu hình buffer), giảm nhu cầu tự dựng balloon Deployment. Trên Standard, pattern pause Pod thủ công vẫn là cách phổ biến và minh bạch nhất.
Provisioning Requests & Dynamic Workload Scheduler
Capacity buffer giải quyết spike của workload phục vụ. Nhưng có một lớp workload khác với nhu cầu khác hẳn: batch và AI/ML — training job cần N node GPU/TPU cùng lúc, hoặc không gì cả. Đây là nơi Provisioning Requests vào cuộc.
Vấn đề "all-or-nothing"
Một distributed training job cần 64 GPU đồng bộ. Nếu CA cấp được 60 rồi kẹt 4 (hết công suất), 60 node GPU đắt đỏ ngồi không chờ 4 node còn lại — lãng phí khổng lồ, và job không thể bắt đầu. Cấp phát từng phần (incremental) là sai mô hình cho workload gang-scheduled.
ProvisioningRequest: cấp phát atomic
Theo tài liệu GKE (Provisioning Requests), ProvisioningRequest là một custom resource cho phép cấp phát tài nguyên atomic cho batch/AI: GKE cấp tất cả tài nguyên yêu cầu cùng lúc — hoặc tất cả, hoặc không gì. Điều này loại bỏ tình trạng cấp phát từng phần.
Cấu trúc chính:
apiVersion: autoscaling.x-k8s.io/v1
kind: ProvisioningRequest
metadata:
name: training-job-pr
namespace: default
spec:
provisioningClassName: queued-provisioning.gke.io
parameters:
maxRunDurationSeconds: "3600"
podSets:
- count: 64
podTemplateRef:
name: training-pod-templatePod tham chiếu request qua annotation:
metadata:
annotations:
autoscaling.x-k8s.io/consume-provisioning-request: training-job-pr
autoscaling.x-k8s.io/provisioning-class-name: "queued-provisioning.gke.io"Và PodTemplate cần label cloud.google.com/apply-warden-policies: "true" để GKE áp cùng validation/mutation như Pod thường.
Hai provisioning class
queued-provisioning.gke.io— dùng với flex-start với queued provisioning cho workload quy mô lớn cần thời điểm bắt đầu linh hoạt. Phù hợp Dynamic Workload Scheduler: bạn "xếp hàng" yêu cầu, GKE cấp khi có đủ công suất, job chạy trong thời lượng giới hạn (maxRunDurationSeconds). Đây là mô hình tối ưu chi phí cho training không gấp — chấp nhận chờ để có công suất GPU/TPU đảm bảo.check-capacity.gke.io— kiểm tra/đặt trước công suất, dùng khi cần xác nhận có đủ trước khi cam kết.
Status và ràng buộc thời gian
Các status quan trọng: Accepted=true, Provisioned=true, Failed=true. Ràng buộc then chốt: sau khi Provisioned=true, bạn có 10 phút để khởi động Pod tiêu thụ công suất đã cấp — nếu không, công suất bị thu hồi (Provisioning Requests). Vì thế Provisioning Requests thường dùng cùng một job queue/orchestrator (Kueue) để đảm bảo Pod được tạo ngay khi công suất sẵn sàng.
CA tích hợp với Provisioning Requests để tránh đếm trùng resource request giữa Pod thường và flex-start queued provisioning — đảm bảo tracking công suất chính xác.
Khi nào dùng
- Distributed training/inference cần gang scheduling (tất cả Pod cùng lúc): Provisioning Requests đảm bảo atomic.
- Workload GPU/TPU đắt đỏ: tránh node đắt ngồi không chờ phần còn lại.
- Batch không gấp tối ưu chi phí: dùng flex-start queued provisioning, chấp nhận chờ để có giá/độ sẵn sàng tốt hơn.
- Không dùng cho workload phục vụ traffic latency-thấp (dùng capacity buffer thay thế).
Kueue: hàng đợi job đặt trên autoscaler
Provisioning Requests cấp công suất atomic, nhưng nó không tự quản lý hàng đợi và thứ tự của nhiều job tranh nhau tài nguyên. Đó là vai trò của Kueue — một job queueing controller hoạt động cùng GKE autoscaling.
Mô hình: thay vì để mọi Job tạo Pod ngay (rồi tranh nhau và một nửa kẹt Pending), Kueue giữ Job trong hàng đợi và chỉ admit (cho phép tạo Pod) khi có đủ quota/công suất. Kueue tích hợp với Provisioning Requests: khi admit một job cần gang scheduling, nó tạo ProvisioningRequest, đợi Provisioned=true, rồi tạo Pod trong cửa sổ 10 phút — giải đúng ràng buộc thời gian ở trên.
Vì sao quan trọng cho batch/AI ở scale: không có hàng đợi, nhiều training job nộp cùng lúc sẽ tạo một biển Pod Pending, CA/NAP cố cấp node cho tất cả, và nếu công suất GPU không đủ cho tất cả thì không job nào chạy trọn vẹn — tài nguyên bị phân mảnh giữa các job dở dang. Kueue áp đặt kỷ luật: chạy hết job A rồi job B, thay vì cả hai cùng dở. Đây là chuyển dịch từ "scheduler best-effort" sang "batch system có hàng đợi và quota" — mô hình đúng cho cluster AI dùng chung.
Incident walkthrough: "hóa đơn tăng 40% mà không thêm workload"
Một tình huống production điển hình kết hợp nhiều khái niệm của file này. Triệu chứng: chi phí node tăng dần, số node không giảm dù tải đêm rất thấp.
Bước 1 — Xác nhận giả thuyết. Query status events: autoscaledNodesTarget không giảm vào ban đêm dù tải thấp. Vậy CA muốn giữ số node này — không phải bug cấp phát.
Bước 2 — Tìm node không xóa được. Query noScaleDown:
resource.type="k8s_cluster"
resource.labels.cluster_name="prod"
log_id("container.googleapis.com/cluster-autoscaler-visibility")
"no.scale.down"Kết quả: hàng loạt no.scale.down.node.pod.kube.system.unmovable cho các node mang Pod của một logging agent tự cài (DaemonSet-like nhưng deploy nhầm thành Deployment không PDB).
Bước 3 — Nguyên nhân gốc. Logging agent chạy trên gần như mọi node, không có PDB, không phải DaemonSet thật → mỗi node nó chạm trở thành "không drain được" → CA không xóa được node nào có nó → cluster kẹt ở kích thước cao điểm.
Bước 4 — Khắc phục. Chuyển agent sang DaemonSet đúng cách (DaemonSet Pod được xử lý riêng khi drain, file 5), hoặc thêm PDB phù hợp. Sau khi sửa, scale-down-unneeded-time 10 phút trôi qua, CA bắt đầu xóa node đêm, chi phí về đúng mức.
Bài học: một workload thiếu PDB có thể giữ cả cluster ở kích thước cao điểm 24/7. Đây là dạng lãng phí ẩn phổ biến nhất, và chỉ phát hiện được khi đọc noScaleDown reasons — không có cách nào khác để thấy nó từ metric chi phí thuần.
Đặt kích thước capacity buffer: bài toán kinh tế
Kích thước đệm không phải con số tùy ý — nó là một phép cân bằng định lượng được. Khung tư duy:
- Chi phí của thiếu đệm = xác suất spike vượt công suất × tác động (lỗi 5xx, vi phạm SLO, mất doanh thu) trong khoảng thời gian CA cấp node (vài phút).
- Chi phí của đệm = số node đệm × giá node × thời gian (thường trực 24/7).
Cách đặt thực tế:
- Đo độ dốc spike thật: từ dữ liệu lịch sử, xác định spike điển hình tăng bao nhiêu replica trong khoảng thời gian bằng độ trễ tạo node end-to-end (ví dụ "trong 3 phút, tải tăng tối đa 20 Pod"). Đó là lượng đệm cần để spike điển hình không phải đợi node mới.
- Quy ra tài nguyên: 20 Pod × request mỗi Pod = tổng CPU/memory đệm → số balloon Pod và request của chúng.
- Điều chỉnh theo độ quan trọng: service doanh thu cao/SLO chặt → đệm rộng hơn; service nội bộ chịu được chờ → đệm nhỏ hoặc không.
- Đệm động theo giờ: nhiều đội scale balloon Deployment theo lịch (CronJob/HPA trên balloon) — đệm lớn giờ cao điểm, nhỏ ban đêm. Đệm không cần cố định.
Sai lầm phổ biến ở cả hai cực: không đệm (mọi spike là cuộc đua với độ trễ tạo node, thường thua) và đệm quá lớn "cho yên tâm" (trả tiền cho công suất gần như không bao giờ dùng). Đo, đừng đoán.
Tổng hợp các reason string hay gặp nhất
Để tiện tra cứu khi trực sự cố, đây là các reason string xuất hiện nhiều nhất trong thực tế và hành động tương ứng — bổ sung cho hai bảng ở trên:
no.scale.down.node.pod.kube.system.unmovable→ add-on/Pod hệ thống thiếu PDB. Thủ phạm số một của chi phí không giảm.no.scale.down.node.pod.not.enough.pdb→ PDB đặt quá chặt (minAvailable= số replica). Nới PDB.no.scale.down.node.pod.has.local.storage→ Pod dùng emptyDir/hostPath. Annotatesafe-to-evict-local-volumeshoặc đổi storage.no.scale.up.nap.pod.gpu.no.limit.defined→ thiếu--max-accelerator. Thêm limit GPU (file 6).no.scale.up.mig.failing.predicate→ ràng buộc Pod không pool nào thỏa. Đối chiếu predicate với Chương 8.scale.down.error.failed.to.delete.node.min.size.reached→ đụngmin-nodes. Đúng theo cấu hình, không phải lỗi.
Đưa các chuỗi này vào một saved query/dashboard Cloud Logging là cách hiệu quả nhất để biến debug autoscaler từ phỏng đoán thành tra cứu.
Anti-patterns thường gặp
- Không có capacity buffer cho workload có spike. Mọi spike trở thành cuộc đua với độ trễ tạo node — thường thua. Pause Pod priority âm là pattern chuẩn.
- Đặt đệm quá lớn "cho an toàn". Đệm là chi phí thường trực. Đo spike thật và đặt đệm vừa đủ.
- Không build alert trên
noScaleUp/noScaleDownreasons. Tình trạng "muốn scale mà không được" diễn ra âm thầm cho tới khi thành sự cố. Giám sát reason string. - Dùng cấp phát thường (không atomic) cho gang-scheduled training. Node GPU ngồi không chờ nhau, lãng phí lớn. Dùng Provisioning Requests.
- Quên ràng buộc 10 phút của Provisioning Request. Công suất đã cấp bị thu hồi nếu Pod không khởi động kịp. Kết hợp với orchestrator (Kueue).
- Coi log visibility là tùy chọn. Đây là nguồn debug autoscaler duy nhất trên control plane managed. Không đọc = debug mù.