Skip to content

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

PluginLoại node khiFile liên quan
NodeResourcesFitNode không đủ tài nguyên requestable (CPU, memory, ephemeral-storage, extended resources) so với requests của Pod06, 08
NodeAffinityNode không khớp nodeSelector/nodeAffinity required03
TaintTolerationNode có taint NoSchedule/NoExecute mà Pod không toleration05
PodTopologySpreadĐặt Pod lên node sẽ vi phạm constraint DoNotSchedule04
InterPodAffinityVi phạm pod affinity/anti-affinity required03
VolumeBindingKhông thể bind PVC (zone của volume không khớp node, vượt giới hạn volume)
NodePortshostPort Pod yêu cầu đã bị chiếm trên node
NodeUnschedulableNode có .spec.unschedulable=true (đang cordon)06
NodeNamespec.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: 500mlimits.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 nodeTác dụng
NodeResourcesFitTùy scoringStrategyLeastAllocated (mặc định) ưu tiên node ít dùng; MostAllocated ưu tiên node nhiều dùng
NodeResourcesBalancedAllocationNode có CPU/memory cân đối sau khi đặt PodTránh node lệch (nhiều CPU rảnh nhưng hết RAM)
ImageLocalityNode đã có sẵn image của PodGiảm thời gian pull image, tăng tốc khởi động
InterPodAffinityNode thỏa pod affinity/anti-affinity preferredCo-location/spread mềm
NodeAffinityNode thỏa nodeAffinity preferred (weight 1–100)Ưu tiên mềm theo label node
TaintTolerationNode ít taint PreferNoSchedule không toleratedTránh node "không khuyến khích"
PodTopologySpreadNode giảm skewPhâ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 profile balanced.
  • 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 nodeNhỏ (Pod phân tán)Lớn (nhiều Pod trên một node)
Hiệu quả scale-down của autoscalerKém (node nào cũng có vài Pod, khó dọn)Tốt (node rỗng dễ xóa)
Headroom chịu burstNhiề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:

bash
# 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):

bash
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 threshold

Cơ 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 workloadChiến lược đề xuấtCơ chế GKE
Stateful, critical, cần độ bền caoSpread + topology spread DoNotScheduleprofile balanced + constraint cứng
Stateless co giãn nhanh, chịu mất PodBin-packingprofile optimize-utilization
Batch/job ngắn, tối ưu chi phí tuyệt đốiBin-packing + Spotoptimize-utilization + ComputeClass Spot (file 9)
Đa tenant cần cân bằng chi phí/độ bềnBin-packing + spread mềmoptimize-utilization + topology spread ScheduleAnyway
GPU/accelerator đắtBin-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:

  1. 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).
  2. Phân loại lý do:
    • Insufficient cpu/Insufficient memory/Insufficient nvidia.com/gpuNodeResourcesFit. Kiểm tra requests quá lớn hay node thật sự đầy (đối chiếu Allocated resources trong describe node).
    • had untolerated taint {…}TaintToleration (file 5).
    • node(s) didn't match Pod's node affinity/selectorNodeAffinity (file 3).
    • node(s) didn't match pod affinity rulesInterPodAffinity (file 3).
    • node(s) didn't match pod topology spread constraintsPodTopologySpread (file 4).
    • node(s) had volume node affinity conflictVolumeBinding (zone của PV không khớp node).
    • node(s) were unschedulable → node đang cordon (NodeUnschedulable).
  3. 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ỹ.
  4. 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 requests chứ không limits, và dựa trên allocatable chứ không capacity — đâ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 balanced vs optimize-utilization (scheduler gke.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.

References