Chương 8: GKE Scheduler — Thuật Toán, Affinity & Resource Model
Vì sao chương này quyết định độ ổn định của cluster
Phần lớn sự cố "Pod kẹt Pending" mà các đội vận hành GKE gặp phải không phải do thiếu tài nguyên thật, mà do mô hình tinh thần sai về cách scheduler ra quyết định. Một engineer khai báo requests quá tay, đặt một podAntiAffinity bắt buộc trên topology key sai, hoặc quên rằng node pool đang mang một taint — và scheduler, vận hành đúng như thiết kế, từ chối đặt Pod. Triệu chứng nhìn giống "hết tài nguyên" nhưng nguyên nhân gốc là một ràng buộc do chính người dùng tạo ra.
kube-scheduler là một trong những control plane component được Google quản lý hoàn toàn trên GKE: bạn không SSH vào nó, không sửa file cấu hình của nó trên cluster Standard, và trên Autopilot bạn thậm chí không thấy node. Nhưng chính vì không chạm được vào scheduler, việc hiểu sâu thuật toán nội tại của nó càng quan trọng — vì cách duy nhất để điều khiển scheduler là qua spec của Pod (requests, affinity, tolerations, topology spread, priority) và qua cấu trúc node pool. Mọi đòn bẩy đều gián tiếp, và mọi đòn bẩy sai đều biểu hiện thành cùng một triệu chứng mơ hồ.
Theo tài liệu Kubernetes, kube-scheduler chọn node cho Pod qua một thao tác hai bước: filtering (lọc ra các node khả thi) và scoring (chấm điểm để chọn node tốt nhất) (kube-scheduler). Đằng sau hai bước đó là Scheduling Framework — kiến trúc plugin biên dịch thẳng vào scheduler, với hơn một chục extension point (Scheduling Framework). Chương này mổ xẻ toàn bộ guồng máy đó tới mức bạn có thể nhìn một Pod Pending và suy ra chính xác plugin nào đã loại node nào, vì sao, và sửa ở đâu.
Điều kiện tiên quyết
- Đã đọc Chương 6 (Node Lifecycle & Pool Management): node pool, taints mặc định của GKE,
max-pods-per-node, capacity của node. - Đã đọc Chương 7 (Networking Internals): scheduling quyết định Pod nằm ở node nào, ảnh hưởng same-node vs cross-node path và phân bố backend của Service.
- Nắm vững Kubernetes resource model:
requestsvslimits, đơn vị CPU (millicore), đơn vị memory (Mi/Gi). - Hiểu cơ bản về cgroups Linux (CFS quota, OOM killer) — sẽ đào sâu trong chương.
Mức độ sâu
5/5 — Chương đi tới mức từng extension point của Scheduling Framework, cơ chế ba hàng đợi nội bộ (activeQ/backoffQ/unschedulablePods), công thức tính skew của topology spread, cơ chế CFS quota throttling và OOM kill ở tầng kernel, thuật toán chọn victim khi preemption, và cách Autopilot thực thi tỷ lệ CPU:memory. Mọi tuyên bố kỹ thuật đối chiếu trực tiếp tài liệu Kubernetes và Google Cloud.
Cấu trúc chapter
1. Scheduler Architecture & Workflow — Scheduling Framework, Cycle & Queue
Bộ khung vận hành của scheduler:
- Hai pha lớn: scheduling cycle (chọn node) vs binding cycle (ghi quyết định)
- Toàn bộ extension point theo thứ tự: PreEnqueue → QueueSort → PreFilter → Filter → PostFilter → PreScore → Score → NormalizeScore → Reserve → Permit → PreBind → Bind → PostBind
- Vì sao scheduling cycle chạy tuần tự còn binding cycle chạy song song
- Optimistic locking và assume cache: scheduler "giả định" bind thành công để không chặn
- Ba hàng đợi: activeQ, backoffQ, unschedulablePods — Pod di chuyển giữa chúng thế nào
- Exponential backoff, QueueingHints, flush định kỳ
- Scheduler metrics:
pending_pods,scheduler_pending_pods,scheduling_attempt_duration_seconds, queue depth
2. Filter & Score Plugins — Lọc Node & Chấm Điểm
Bản chất thuật toán của hai pha cốt lõi:
- Filter plugins: NodeResourcesFit, NodeAffinity, TaintToleration, PodTopologySpread, VolumeBinding, NodePorts, NodeUnschedulable
- Vì sao Filter chạy song song trên các node, dừng sớm khi một plugin loại node
- Score plugins: NodeResourcesFit (LeastAllocated vs MostAllocated vs RequestedToCapacityRatio), NodeResourcesBalancedAllocation, ImageLocality, InterPodAffinity, NodeAffinity
- Spread (LeastAllocated) vs bin-packing (MostAllocated): trade-off độ bền vs chi phí
- GKE autoscaling profile
optimize-utilizationđổi scheduler thành bin-packing (gke.io/optimize-utilization-scheduler) - NormalizeScore và trọng số plugin
3. Node Affinity & Inter-Pod Affinity/Anti-Affinity
Ràng buộc "hút" Pod về đúng chỗ:
- nodeAffinity: required vs preferred, operators (In, NotIn, Exists, Gt, Lt), weight 1–100
- nodeSelectorTerms (OR) vs matchExpressions (AND)
- Inter-pod affinity: topologyKey, labelSelector, namespaceSelector
- Pod anti-affinity: use case HA, vì sao chi phí O(pods × namespaces) ở scale lớn
- IgnoredDuringExecution: vì sao label đổi sau scheduling không di dời Pod
- Anti-pattern: required anti-affinity hostname cho replica > số node
4. Pod Topology Spread Constraints — Phân Bố Theo Failure Domain
Cơ chế phân bố hiện đại thay cho anti-affinity:
- maxSkew, topologyKey, whenUnsatisfiable (DoNotSchedule vs ScheduleAnyway)
- Công thức skew = (số Pod khớp trong domain hiện tại) − (global minimum)
- minDomains: ép tối thiểu số domain, global minimum coi như 0 khi thiếu domain
- nodeAffinityPolicy, nodeTaintsPolicy, matchLabelKeys (rolling update)
- Default cluster-level constraints của Kubernetes
- So sánh trực tiếp với podAntiAffinity: vì sao topology spread linh hoạt hơn
5. Taints & Tolerations — Ràng Buộc "Đẩy" Node
Mặt đối nghịch của affinity — node tự bảo vệ:
- Ba effect: NoSchedule, PreferNoSchedule, NoExecute — khác biệt thời điểm tác động
- NoExecute + tolerationSeconds: đuổi Pod đang chạy, timing eviction
- operator Equal vs Exists
- Taint-based eviction: node condition taints (not-ready, unreachable, memory-pressure...)
- Default toleration 300s do admission controller thêm — ý nghĩa với node failure
- Taints mặc định của GKE: GPU node, Spot, ARM, dedicated node pool
6. Resource Model, QoS & Node-Pressure Eviction
Nền tảng tài nguyên định hình mọi quyết định scheduling:
- requests vs limits: scheduler chỉ nhìn requests, không nhìn limits
- CPU compressible: CFS quota throttling, chu kỳ 100ms, vì sao limit CPU thấp gây latency
- Memory incompressible: OOM kill khi vượt limit, không có throttle
- QoS classes: Guaranteed, Burstable, BestEffort — phân loại tự động
- Node-pressure eviction: signals (memory.available, nodefs.available...), soft vs hard threshold
- Thứ tự eviction theo QoS và priority, "usage above requests"
- Vì sao node-pressure eviction KHÔNG tôn trọng PodDisruptionBudget
7. Pod Priority & Preemption
Khi cluster đầy và phải hy sinh Pod:
- PriorityClass: value (≤ 1 tỷ), globalDefault, preemptionPolicy (Never vs PreemptLowerPriority)
- system-cluster-critical, system-node-critical
- Cơ chế preemption: chọn node, chọn victim, nominatedNodeName
- PodDisruptionBudget được tôn trọng "best-effort" khi preemption
- Graceful termination tạo khoảng trống giữa preempt và schedule
- Cross-node preemption không xảy ra, starvation, non-preempting pods
- ResourceQuota giới hạn tiêu thụ PriorityClass cao
8. Extended Resources & GPU Scheduling
Lập lịch phần cứng đặc biệt:
- Extended resources: vì sao phải đặt requests = limits
- GPU trên GKE:
nvidia.com/gputrong limits, node selectorcloud.google.com/gke-accelerator - Taint
nvidia.com/gpu=present:NoSchedule+ ExtendedResourceToleration admission controller - Device plugin DaemonSet, cài driver (
gpu-driver-version) - GPU sharing: time-sharing, multi-instance GPU (MIG), trade-off
- Vì sao GPU không bao giờ là Burstable
9. GKE Autopilot Scheduling, Custom ComputeClasses & Scheduler Extenders
Lớp scheduling đặc thù GKE và mở rộng:
- Autopilot: thực thi tỷ lệ CPU:memory theo compute class (general-purpose 1:1–1:6.5...)
- Tự điều chỉnh request nhỏ, từ chối workload vượt max
- Compute classes: general-purpose, Balanced, Scale-Out, Accelerator, Performance
- Custom ComputeClasses: priorities/fallback, mapping VM family, activeMigration, consolidation
- nodePoolAutoCreation, whenUnsatisfiable (DoNotScaleUp 1.33+)
- Scheduler extenders (webhook) vs scheduler plugins: vì sao plugins thắng
Cách đọc chapter theo mục tiêu công việc
Nếu bạn đang debug một Pod kẹt Pending
- Bắt đầu từ file 1 để hiểu Pod đang ở hàng đợi nào và vì sao chưa được thử lại.
- Sang file 2 để biết plugin nào loại node ở pha Filter (đọc
kubectl describe podevents đối chiếu plugin). - Nếu liên quan affinity/anti-affinity đọc file 3; topology spread file 4; taint file 5; thiếu tài nguyên thật file 6.
Nếu bạn đang thiết kế node pool & resource strategy mới
- File 6 (resource model/QoS) để khóa chuẩn requests/limits trước khi onboard workload.
- File 5 (taints) để thiết kế dedicated node pool và isolation.
- File 4 (topology spread) để thiết kế HA mặc định cho mọi Deployment.
Nếu bạn đang tối ưu chi phí/độ bền ở scale lớn
- File 2 (spread vs bin-packing) và file 9 (Autopilot/ComputeClasses) để chọn chiến lược đóng gói.
- File 7 (priority/preemption) để phân tầng workload critical vs batch.
- File 8 (GPU) nếu chạy AI/ML — đóng gói GPU sai là lãng phí lớn nhất.
Khung quyết định production (tóm tắt)
| Quyết định | Tác động chính | Rủi ro nếu làm sai | File nên đọc |
|---|---|---|---|
Chuẩn requests/limits | Đóng gói, throttling, OOM | Pending giả, latency do throttle, OOM kill | 6 |
| Spread vs bin-packing | Chi phí vs blast radius | Quá phân tán (đắt) hoặc quá gom (mất node là gãy) | 2, 9 |
| Topology spread mặc định | Độ bền khi mất zone/node | Mất zone là mất toàn bộ replica | 4 |
| Dedicated node pool qua taint | Isolation workload | Workload critical lẫn vào node sai | 5 |
| PriorityClass phân tầng | Workload critical luôn được chỗ | Batch chiếm chỗ critical, hoặc starvation | 7 |
| Autopilot vs Standard | Mức kiểm soát scheduling | Spec sai bị Autopilot từ chối/điều chỉnh | 9 |
Mapping nhanh chủ đề con với loại incident phổ biến
| Loại incident | Triệu chứng chính | File cần đọc trước |
|---|---|---|
| Pod Pending dù còn CPU/RAM | 0/N nodes available với lý do affinity/taint | 02, 03, 05 |
| Replica dồn hết vào một zone | Mất zone là mất service | 04 |
| Latency tăng đột ngột không rõ lý do | CPU throttling do limit thấp | 06 |
| Pod bị OOMKilled lặp lại | Vượt memory limit | 06 |
| Pod bị evict hàng loạt lúc node căng | Node-pressure eviction | 06 |
| Batch job đẩy service production ra | Preemption sai cấu hình priority | 07 |
| GPU node nhận Pod không cần GPU | Thiếu taint/toleration đúng | 05, 08 |
| Autopilot từ chối Deployment | Vượt max hoặc sai tỷ lệ CPU:memory | 09 |
Năm sai lầm xuyên suốt thường gặp ở cấp chương
Trước khi đi vào từng file, đây là các sai lầm lặp lại nhiều nhất, cắt ngang mọi chủ đề của chương:
- Nhầm "Pending" với "hết tài nguyên". Phần lớn Pending là do ràng buộc người dùng tạo (affinity, taint, topology spread), không phải do cluster thật sự đầy. Luôn đọc đúng dòng lý do trong events (file 1, 2).
- Đặt
requeststheo cảm tính, bỏ trốnglimitshoặc đặt limit CPU quá thấp. Requests sai làm scheduler quyết định sai; limit CPU thấp gây throttling âm thầm phá latency (file 6). - Dùng
requiredDuringSchedulinganti-affinity ở hostname cho số replica > số node. Một số replica sẽ kẹt Pending vĩnh viễn — đây là anti-pattern kinh điển; dùng topology spread thay thế (file 3, 4). - Không phân tầng priority. Khi cluster đầy, không có cách nào để service production thắng batch job, hoặc tệ hơn batch preempt nhầm production (file 7).
- Mặc định bin-packing tối đa để tiết kiệm. Gom Pod vào ít node nhất nghe rẻ, nhưng blast radius khi mất một node tăng vọt — cân bằng với topology spread (file 2, 4).
Cross-reference với Google Cloud Architecture Framework
- Reliability: phân bố Pod qua failure domain (topology spread), priority phân tầng, và tránh single point of failure ở tầng đóng gói (Reliability pillar).
- Cost optimization: bin-packing và Autopilot/ComputeClasses giảm lãng phí, nhưng phải đánh đổi với độ bền (Cost optimization).
- Performance optimization: chuẩn requests/limits đúng tránh throttling và OOM, định hình SLO latency (Architecture Framework).
References chung của chapter
- kube-scheduler
- Scheduling Framework
- Scheduler Configuration
- Assigning Pods to Nodes (affinity)
- Taints and Tolerations
- Pod Topology Spread Constraints
- Resource Management for Pods and Containers
- Node-pressure Eviction
- Pod Priority and Preemption
- GKE custom compute classes
- GKE Autopilot resource requests
- GKE GPUs
Những câu hỏi kiến trúc bắt buộc trả lời trước production
- Mọi workload đã có
requestsđặt đúng theo đo đạc thực, không phải đoán? - Service production đã có topology spread qua zone để chịu được mất một zone chưa?
- Đã phân tầng PriorityClass để critical luôn thắng batch khi cluster đầy chưa?
- Có dedicated node pool nào cần cô lập qua taint, và workload tương ứng đã có toleration?
- Chiến lược đóng gói là spread hay bin-packing, và blast radius khi mất một node có chấp nhận được?
- Nếu dùng GPU, đã hiểu vì sao GPU phải requests = limits và node có taint đúng?
- Nếu dùng Autopilot, spec đã tuân thủ tỷ lệ CPU:memory của compute class chưa?
Nếu chưa trả lời rõ các câu hỏi này, chiến lược scheduling của cluster chưa nên coi là "production ready".
Bảng thuật ngữ then chốt của chương
| Thuật ngữ | Ý nghĩa ngắn gọn |
|---|---|
| Scheduling cycle | Pha chọn node, chạy tuần tự một Pod tại một thời điểm (file 1) |
| Binding cycle | Pha ghi quyết định, chạy song song nhiều Pod (file 1) |
| activeQ / backoffQ / unschedulablePods | Ba hàng đợi nội bộ của scheduler (file 1) |
| Filter / Score | Hai pha: lọc node khả thi, chấm điểm chọn tốt nhất (file 2) |
| LeastAllocated / MostAllocated | Chiến lược chấm điểm: spread vs bin-packing (file 2) |
| nodeAffinity / podAffinity | Ràng buộc hút Pod theo label node/Pod (file 3) |
| topologySpreadConstraints | Phân bố Pod đều qua failure domain (file 4) |
| Taint / Toleration | Node đẩy Pod / Pod chấp nhận taint (file 5) |
| QoS class | Guaranteed/Burstable/BestEffort, quyết định thứ tự evict (file 6) |
| CFS quota | Cơ chế throttle CPU theo limit ở kernel (file 6) |
| Preemption | Đuổi Pod priority thấp để nhường chỗ Pod cao (file 7) |
| ComputeClass | Profile node provisioning của GKE (file 9) |
Kết luận của chapter
Scheduler là một hệ thống tất định: với cùng spec Pod, cùng trạng thái cluster, nó luôn ra cùng quyết định. Sự "bí ẩn" mà các đội gặp phải không nằm ở scheduler mà nằm ở khoảng cách giữa mô hình tinh thần của họ và thuật toán thật. Mục tiêu của chương này là xóa khoảng cách đó: khi bạn hiểu Filter loại node thế nào, Score chấm điểm ra sao, ba hàng đợi vận hành thế nào, và các ràng buộc (affinity, taint, topology spread, priority, resource) tương tác với nhau ra sao, thì mỗi Pod Pending chuyển từ "bí ẩn nhiều giờ" thành "đọc events, khớp plugin, sửa spec trong vài phút".
Cách dùng chương hiệu quả nhất là biến từng file thành một chuẩn vận hành: resource model thành policy requests/limits bắt buộc, topology spread thành template HA mặc định, taint thành chuẩn dedicated pool, priority thành phân tầng workload versioned, và Autopilot/ComputeClasses thành chiến lược đóng gói có lý do rõ ràng. Khi đó scheduler không còn là hộp đen mà là một công cụ bạn điều khiển chính xác qua spec.