Filter & Score Plugins — Lọc Node & Chấm Điểm
Vì sao đây là phần định hình toàn bộ hành vi scheduler
Mọi quyết định "Pod này lên node nào" cuối cùng đều quy về hai câu hỏi mà Filter và Score trả lời: node nào chạy được Pod? (Filter) và trong số đó node nào tốt nhất? (Score). Theo tài liệu Kubernetes, scheduler chọn node qua thao tác hai bước — filtering tìm tập "feasible nodes", rồi scoring xếp hạng để chọn node phù hợp nhất; nếu nhiều node điểm bằng nhau, scheduler chọn ngẫu nhiên một node (kube-scheduler).
Phân biệt Filter và Score cực kỳ quan trọng trong debug: một node bị Filter loại sẽ xuất hiện trong thông báo 0/N nodes are available với lý do cụ thể (ví dụ Insufficient cpu, had untolerated taint); còn Score không bao giờ làm Pod Pending — nó chỉ ảnh hưởng node nào trong số khả thi được chọn. Nói cách khác: Filter quyết định được hay không, Score quyết định ở đâu. Nhầm hai pha này dẫn tới debug sai hướng — ví dụ chỉnh affinity weight (Score) trong khi vấn đề là một taint (Filter).
Internal model: Filter plugins
Pha Filter gọi từng Filter plugin cho mỗi node theo thứ tự cấu hình; nếu bất kỳ plugin nào đánh dấu node là không khả thi, các plugin còn lại không chạy cho node đó, và "các node có thể được đánh giá đồng thời" (Scheduling Framework). Tức là Filter song song giữa các node, dừng sớm trong một node.
Các Filter plugin mặc định quan trọng nhất (Scheduler Configuration):
| Plugin | Loại node khi | File liên quan |
|---|---|---|
| NodeResourcesFit | Node không đủ tài nguyên requestable (CPU, memory, ephemeral-storage, extended resources) so với requests của Pod | 06, 08 |
| NodeAffinity | Node không khớp nodeSelector/nodeAffinity required | 03 |
| TaintToleration | Node có taint NoSchedule/NoExecute mà Pod không toleration | 05 |
| PodTopologySpread | Đặt Pod lên node sẽ vi phạm constraint DoNotSchedule | 04 |
| InterPodAffinity | Vi phạm pod affinity/anti-affinity required | 03 |
| VolumeBinding | Không thể bind PVC (zone của volume không khớp node, vượt giới hạn volume) | — |
| NodePorts | hostPort Pod yêu cầu đã bị chiếm trên node | — |
| NodeUnschedulable | Node có .spec.unschedulable=true (đang cordon) | 06 |
| NodeName | spec.nodeName được đặt nhưng không khớp node này | — |
NodeResourcesFit: hiểu đúng "đủ tài nguyên"
Đây là Filter gây Pending nhiều nhất và cũng bị hiểu sai nhiều nhất. Tài liệu nói rõ: scheduler dùng requests, không phải limits, để quyết định đặt Pod. Một Pod với requests.cpu: 500m và limits.cpu: 2000m chỉ "đặt chỗ" 500m trên node — scheduler không tính tới 2000m khi xét node nào chứa được Pod (Resource Management).
Hệ quả là khái niệm node "đầy" theo góc nhìn scheduler hoàn toàn dựa trên tổng requests của các Pod đã đặt, không liên quan đến mức sử dụng thực tế. Một node với CPU thực rảnh 90% vẫn có thể bị NodeResourcesFit loại vì tổng requests của các Pod trên đó đã chạm allocatable — đây là nguồn gốc của nghịch lý "Pod Pending nhưng cluster nhìn rảnh" mà mọi đội GKE đều gặp. Cách sửa không phải thêm node mù quáng mà là chỉnh requests về sát thực tế (xem file 6).
Lưu ý "allocatable" khác "capacity": GKE dành một phần CPU/memory của node cho kubelet, OS, và các system DaemonSet (kube-reserved, system-reserved, eviction threshold). Pod chỉ tranh nhau phần allocatable, luôn nhỏ hơn capacity vật lý — chi tiết ở Chương 6.
Internal model: Score plugins
Sau Filter, các node khả thi đi vào Score. Mỗi Score plugin chấm mỗi node một điểm trong khoảng số nguyên xác định; NormalizeScore chuẩn hóa; rồi scheduler nhân với trọng số (weight) của từng plugin và cộng lại để ra điểm cuối. Node điểm cao nhất thắng; bằng điểm thì random (Scheduling Framework).
Các Score plugin mặc định (Scheduler Configuration):
| Plugin | Ưu tiên node | Tác dụng |
|---|---|---|
| NodeResourcesFit | Tùy scoringStrategy | LeastAllocated (mặc định) ưu tiên node ít dùng; MostAllocated ưu tiên node nhiều dùng |
| NodeResourcesBalancedAllocation | Node có CPU/memory cân đối sau khi đặt Pod | Tránh node lệch (nhiều CPU rảnh nhưng hết RAM) |
| ImageLocality | Node đã có sẵn image của Pod | Giảm thời gian pull image, tăng tốc khởi động |
| InterPodAffinity | Node thỏa pod affinity/anti-affinity preferred | Co-location/spread mềm |
| NodeAffinity | Node thỏa nodeAffinity preferred (weight 1–100) | Ưu tiên mềm theo label node |
| TaintToleration | Node ít taint PreferNoSchedule không tolerated | Tránh node "không khuyến khích" |
| PodTopologySpread | Node giảm skew | Phân bố đều theo domain |
LeastAllocated vs MostAllocated: spread hay bin-packing
Đây là quyết định kiến trúc quan trọng nhất của pha Score. NodeResourcesFit hỗ trợ ba chiến lược chấm điểm (Scheduler Configuration):
LeastAllocated(mặc định): ưu tiên node ít tài nguyên đã cấp phát. Hệ quả là Pod được rải đều ra nhiều node. Đây là hành vi mặc định của kube-scheduler và của GKE với autoscaling profilebalanced.MostAllocated: ưu tiên node nhiều tài nguyên đã cấp phát. Hệ quả là Pod được gom vào ít node nhất — bin-packing. Tốt cho tiết kiệm chi phí vì cho phép cluster autoscaler thu nhỏ số node.RequestedToCapacityRatio: chấm theo tỷ lệ requested/capacity với hàm hình dạng tùy chỉnh — công cụ tinh chỉnh nâng cao, ít dùng trong thực tế GKE.
Trade-off cốt lõi:
| Tiêu chí | LeastAllocated (spread) | MostAllocated (bin-packing) |
|---|---|---|
| Chi phí | Cao hơn (nhiều node ít dùng) | Thấp hơn (ít node, dùng kỹ) |
| Blast radius khi mất một node | Nhỏ (Pod phân tán) | Lớn (nhiều Pod trên một node) |
| Hiệu quả scale-down của autoscaler | Kém (node nào cũng có vài Pod, khó dọn) | Tốt (node rỗng dễ xóa) |
| Headroom chịu burst | Nhiều (node có chỗ trống) | Ít (node đã chật) |
Không có lựa chọn "đúng tuyệt đối": spread tối ưu cho độ bền và burst, bin-packing tối ưu cho chi phí. Quyết định nên dựa trên loại workload — service production stateful nên nghiêng spread + topology spread (file 4); batch/stateless co giãn nhanh có thể nghiêng bin-packing.
GKE optimize-utilization: bin-packing có chủ đích
GKE không cho bạn sửa scoringStrategy của scheduler mặc định, nhưng cung cấp đòn bẩy tương đương qua autoscaling profile. Với profile optimize-utilization, "GKE ưu tiên lập lịch Pod vào các node đã có mức cấp phát CPU, memory hoặc GPU cao... để đạt điều này, GKE đặt scheduler name trong Pod spec thành gke.io/optimize-utilization-scheduler" (About cluster autoscaling).
Tài liệu nêu rõ trade-off: profile này "gom Pod vào node dùng nhiều nhất và làm Cluster Autoscaler quyết liệt hơn khi scale down... kết quả là đóng gói workload tốt hơn vào ít node hơn, nhưng làm tăng blast radius nếu một node hỏng" (About cluster autoscaling). Đây chính xác là trade-off LeastAllocated vs MostAllocated ở trên, đóng gói thành một switch cấp cluster.
Profile balanced (mặc định trên Standard) scale down ít quyết liệt hơn, giữ headroom — phù hợp khi độ bền quan trọng hơn chi phí. Việc đào sâu cluster autoscaler thuộc Chương 9; ở đây chỉ cần nắm: profile autoscaling là cách GKE phơi bày lựa chọn spread vs bin-packing.
ImageLocality: đòn bẩy thầm lặng cho thời gian khởi động
ImageLocality ưu tiên node đã có sẵn image container của Pod, giảm thời gian pull. Với image vài GB (phổ biến trong AI/ML hoặc app Java nặng), điều này tạo khác biệt lớn cho thời gian Pod sẵn sàng. Tuy nhiên nó có thể kéo nhiều replica của cùng một image về cùng vài node — đối nghịch với mục tiêu spread. Khi độ bền quan trọng, topology spread (Filter DoNotSchedule) sẽ ghi đè được xu hướng gom của ImageLocality vì Filter mạnh hơn Score.
Production architecture patterns
Phân tầng workload theo chiến lược đóng gói
Một pattern trưởng thành là không áp một chiến lược đóng gói duy nhất cho cả cluster, mà tách node pool theo mục đích:
- Node pool cho stateful/critical service: profile
balanced(spread), bắt buộc topology spread qua zone. Mất một node ảnh hưởng tối thiểu. - Node pool cho batch/stateless: có thể nghiêng bin-packing để tiết kiệm, vì Pod mất đi được tạo lại dễ dàng.
Tách biệt bằng taint + toleration (file 5) hoặc bằng nodeAffinity (file 3), kết hợp với compute class (file 9).
Cân bằng requests để Score hoạt động đúng
NodeResourcesBalancedAllocation chỉ phát huy tác dụng khi requests của Pod phản ánh đúng tỷ lệ CPU:memory thực dùng. Nếu mọi Pod đều khai báo CPU mà bỏ trống memory request, scheduler không có cơ sở để cân bằng — node sẽ lệch (cạn CPU trong khi RAM còn nhiều hoặc ngược lại). Đây là một lý do nữa để chuẩn hóa requests cho cả hai chiều (file 6).
Real-world scenarios
Tình huống: SaaS đa tenant tối ưu chi phí
Một nền tảng SaaS chạy hàng nghìn Pod nhỏ (mỗi tenant một vài Pod nhẹ). Với profile balanced mặc định, Pod rải đều khiến cluster phải giữ nhiều node, mỗi node dùng 30–40% — hóa đơn cao. Chuyển sang optimize-utilization, scheduler gom Pod vào ít node hơn, autoscaler thu nhỏ cluster, chi phí giảm rõ rệt. Đánh đổi: mất một node ảnh hưởng nhiều tenant cùng lúc. Lời giải cân bằng: bật bin-packing nhưng thêm topologySpreadConstraints với whenUnsatisfiable: ScheduleAnyway để vẫn rải mềm qua zone, giữ blast radius trong tầm kiểm soát.
Tình huống: Fintech ưu tiên độ bền
Một hệ thống thanh toán không chấp nhận việc nhiều replica của cùng service nằm chung node. Ở đây bin-packing là sai lầm. Giữ profile balanced, ép topologySpreadConstraints DoNotSchedule ở mức node và zone, chấp nhận chi phí cao hơn để đảm bảo mất một node hay một zone không bao giờ làm gãy service. Score (spread mềm) không đủ — phải dùng Filter (topology spread cứng) để có đảm bảo.
Common mistakes / anti-patterns
Chỉnh Score để sửa Pending. Pod Pending luôn là vấn đề Filter (không node nào khả thi), không bao giờ là Score. Tăng weight nodeAffinity preferred không cứu được Pod Pending — phải tìm Filter nào loại sạch node. Đọc kubectl describe pod events: mỗi lý do là một Filter.
Bật bin-packing toàn cục mà không thêm spread. optimize-utilization mà không kèm topology spread biến mọi service thành rủi ro single-node. Tài liệu GKE cảnh báo trực tiếp về blast radius (About cluster autoscaling). Luôn ghép bin-packing với spread mềm.
Tưởng node "rảnh CPU thực" thì còn chỗ. Scheduler chỉ nhìn tổng requests. Node dùng 10% CPU thực nhưng tổng requests chạm allocatable vẫn bị NodeResourcesFit loại. Cách sửa là chỉnh requests, không phải thêm node.
Quên allocatable < capacity. Tính capacity node theo thông số VM mà quên GKE reserve cho system → ước lượng sai số Pod chứa được, dẫn tới sizing node pool lệch. Luôn dùng kubectl describe node đọc Allocatable, không dùng spec VM.
GCP-native implementation guidance
Đặt autoscaling profile khi tạo/cập nhật cluster Standard:
# Bin-packing quyết liệt để tối ưu chi phí (kèm trade-off blast radius)
gcloud container clusters update <cluster> \
--location=<region> \
--autoscaling-profile=optimize-utilization
# Quay về spread, giữ headroom (mặc định)
gcloud container clusters update <cluster> \
--location=<region> \
--autoscaling-profile=balancedĐọc allocatable thật của node để sizing đúng (Filter NodeResourcesFit dựa trên đây):
kubectl get node <node> -o jsonpath='{.status.allocatable}{"\n"}{.status.capacity}{"\n"}'
# allocatable luôn nhỏ hơn capacity do kube-reserved/system-reserved/eviction thresholdCơ chế sâu hơn: percentageOfNodesToScore và đánh đổi độ chính xác
Trên cluster nhỏ, scheduler chấm điểm mọi node khả thi rồi chọn node tốt nhất. Nhưng trên cluster hàng nghìn node, việc này tốn kém, nên scheduler dừng sớm sau khi đã tìm đủ một tỷ lệ node khả thi do percentageOfNodesToScore quy định. Mặc định, scheduler tự suy ra tỷ lệ này theo kích thước cluster (giảm dần khi cluster lớn lên), với một sàn tối thiểu để vẫn có lựa chọn (Scheduler Performance Tuning).
Đây là một đánh đổi quan trọng và ít người biết: scheduler không đảm bảo chọn node tốt nhất tuyệt đối trên cluster lớn — nó chọn node tốt nhất trong tập con đã xét. Hệ quả thực tế: trên cluster rất lớn, hành vi spread/bin-packing trở nên "gần đúng" thay vì chính xác. Để phân bố chính xác qua failure domain, không nên dựa vào Score (vốn xấp xỉ) mà phải dùng Filter cứng của topology spread DoNotSchedule (file 4) — đây là lý do kiến trúc vì sao HA cứng phải đặt ở Filter chứ không phải Score.
Để duyệt node công bằng, scheduler còn duy trì một con trỏ vòng (round-robin) qua danh sách node giữa các scheduling cycle, đảm bảo không phải lúc nào cũng xét cùng một nhóm node đầu danh sách — tránh "bỏ quên" node ở cuối danh sách trên cluster lớn.
Cơ chế sâu hơn: NodeResourcesBalancedAllocation
NodeResourcesBalancedAllocation là Score plugin dễ bị bỏ qua nhưng quan trọng cho hiệu quả đóng gói. Nó ưu tiên node mà sau khi đặt Pod, tỷ lệ sử dụng CPU và memory cân đối với nhau. Mục tiêu: tránh tình trạng node bị "lệch" — ví dụ một node cạn CPU (mọi core đã được request) trong khi RAM còn rảnh 70%, khiến phần RAM đó không bao giờ dùng được vì không còn CPU để chạy thêm Pod.
Plugin này hoạt động tốt nhất khi requests của Pod phản ánh đúng tỷ lệ CPU:memory thực dùng. Nếu mọi Pod khai CPU mà bỏ trống memory, plugin không có dữ liệu để cân bằng và node sẽ lệch dần — đây là một lý do nữa, ngoài QoS (file 6), để luôn khai cả hai chiều requests. Trên GKE, sự lệch CPU/memory ở tầng node cũng là nguồn gốc của "stranded capacity" — tài nguyên đã trả tiền nhưng không dùng được, một dạng lãng phí khó thấy hơn node rỗng.
Khung quyết định: chọn chiến lược đóng gói
| Đặc tính workload | Chiến lược đề xuất | Cơ chế GKE |
|---|---|---|
| Stateful, critical, cần độ bền cao | Spread + topology spread DoNotSchedule | profile balanced + constraint cứng |
| Stateless co giãn nhanh, chịu mất Pod | Bin-packing | profile optimize-utilization |
| Batch/job ngắn, tối ưu chi phí tuyệt đối | Bin-packing + Spot | optimize-utilization + ComputeClass Spot (file 9) |
| Đa tenant cần cân bằng chi phí/độ bền | Bin-packing + spread mềm | optimize-utilization + topology spread ScheduleAnyway |
| GPU/accelerator đắt | Bin-packing GPU (nhồi đầy node GPU) | gpuConsolidationThreshold (file 9) |
Nguyên tắc xuyên suốt: chi phí và độ bền là hai đầu của một trục, và Score (spread vs bin-packing) là núm điều chỉnh. Không có cấu hình "đúng cho mọi workload" — phải phân tầng theo loại workload, thường bằng cách tách node pool và áp profile/ComputeClass khác nhau.
Runbook: đọc một Pod Pending qua lăng kính Filter
Quy trình chuẩn khi gặp 0/N nodes are available:
- Chạy
kubectl describe pod <pod>và đọc dòng events — nó liệt kê từng nhóm node bị loại và lý do (tên Filter ngầm). - Phân loại lý do:
Insufficient cpu/Insufficient memory/Insufficient nvidia.com/gpu→ NodeResourcesFit. Kiểm trarequestsquá lớn hay node thật sự đầy (đối chiếuAllocated resourcestrongdescribe node).had untolerated taint {…}→ TaintToleration (file 5).node(s) didn't match Pod's node affinity/selector→ NodeAffinity (file 3).node(s) didn't match pod affinity rules→ InterPodAffinity (file 3).node(s) didn't match pod topology spread constraints→ PodTopologySpread (file 4).node(s) had volume node affinity conflict→ VolumeBinding (zone của PV không khớp node).node(s) were unschedulable→ node đang cordon (NodeUnschedulable).
- Tổng các con số phải bằng N (tổng số node). Nếu không khớp, có node bị loại bởi nhiều lý do — đọc kỹ.
- Sửa nguyên nhân gốc, không xóa Pod để "ép thử lại" hay thêm node mù quáng.
Lưu ý: nếu lý do là 100% Insufficient mà cluster có autoscaler, Pod Pending có thể trigger scale-up — chờ node mới (Chương 9). Nếu lý do là affinity/taint/topology, thêm node không giúp gì.
Tổng kết mental model
- Filter quyết định được hay không (gây Pending); Score quyết định ở đâu (không bao giờ gây Pending).
- NodeResourcesFit dùng
requestschứ khônglimits, và dựa trênallocatablechứ khôngcapacity— đây là gốc của nghịch lý "Pending nhưng nhìn rảnh". - LeastAllocated (spread) vs MostAllocated (bin-packing) là trade-off chi phí vs blast radius/độ bền.
- GKE phơi bày lựa chọn này qua autoscaling profile
balancedvsoptimize-utilization(schedulergke.io/optimize-utilization-scheduler). - Luôn ghép bin-packing với topology spread để không biến tiết kiệm thành rủi ro single-node.