Scheduler Architecture & Workflow — Scheduling Framework, Cycle & Queue
Vì sao phải hiểu guồng máy trước khi hiểu từng plugin
Hầu hết tài liệu về scheduling bắt đầu từ affinity và taint — tức là từ các đòn bẩy người dùng cầm trong tay. Cách tiếp cận đó tạo ra một lỗ hổng nguy hiểm: bạn biết viết nodeAffinity nhưng không biết nó được đánh giá ở pha nào, vì sao một Pod kẹt Pending lại không được thử lại dù bạn vừa thêm node, hay vì sao scheduler vẫn "chậm" dù mỗi quyết định scheduling chỉ tốn vài mili giây. Tất cả những câu hỏi đó chỉ trả lời được khi bạn nắm guồng máy: vòng đời một quyết định scheduling, kiến trúc plugin, và đặc biệt là ba hàng đợi nội bộ quyết định khi nào một Pod được scheduler ngó tới.
Trên GKE, kube-scheduler là control plane component do Google vận hành. Bạn không sửa được cấu hình của nó trên cluster Standard thông thường, và trên Autopilot bạn không thấy node lẫn scheduler. Vì mọi can thiệp đều gián tiếp (qua spec Pod, qua node pool), hiểu sâu cơ chế nội tại là điều kiện duy nhất để dự đoán hành vi. Theo tài liệu Kubernetes, kube-scheduler "theo dõi các Pod mới tạo chưa được gán Node và tìm Node tối ưu để đặt chúng" (kube-scheduler).
Internal model: một quyết định scheduling là gì
Hai pha lớn: scheduling cycle và binding cycle
Theo tài liệu Scheduling Framework, "mỗi lần thử lập lịch một Pod được chia thành hai pha: scheduling cycle và binding cycle" (Scheduling Framework). Đây là phân biệt nền tảng nhất của toàn bộ scheduler:
- Scheduling cycle: chọn một node cho Pod. Đây là phần "ra quyết định" — lọc node khả thi, chấm điểm, chọn node thắng. Quan trọng: các scheduling cycle chạy tuần tự (serially), mỗi lần một Pod.
- Binding cycle: áp quyết định đó lên cluster — gọi API
Bindđể ghispec.nodeName, đồng thời thực hiện các tác vụ chuẩn bị (gắn volume, v.v.). Các binding cycle có thể chạy song song (concurrently).
Vì sao thiết kế bất đối xứng này? Vì scheduling cycle phải đọc trạng thái nhất quán của toàn cluster (node nào còn bao nhiêu tài nguyên, Pod nào đang ở đâu) để ra quyết định đúng. Nếu hai scheduling cycle chạy song song, chúng có thể cùng "nhắm" một node còn đúng 1 CPU và cùng quyết định đặt Pod lên đó — xung đột. Tuần tự hóa scheduling cycle loại bỏ class lỗi này một cách triệt để.
Trong khi đó, binding cycle thường chứa thao tác I/O chậm (gọi API server, provision volume mạng) mà không cần độc quyền trạng thái cluster — node đã được chọn rồi. Cho phép binding chạy song song giúp scheduler không bị một volume mount chậm chặn toàn bộ throughput.
Optimistic locking: scheduler "giả định" thành công
Đây là chi tiết quyết định throughput của scheduler và cũng là nguồn gốc của nhiều hiểu lầm. Khi scheduling cycle chọn xong node, scheduler không chờ binding cycle hoàn tất trước khi chuyển sang Pod tiếp theo. Thay vào đó, nó ghi giả định "Pod này đã ở trên node đó" vào một bộ nhớ nội bộ (assume cache / scheduler cache), rồi lập tức bắt đầu scheduling cycle cho Pod kế tiếp.
Pha Reserve trong Scheduling Framework tồn tại chính để phục vụ cơ chế này: "Pha Reserve xảy ra trước khi scheduler thực sự bind Pod vào node được chỉ định. Nó tồn tại để ngăn race condition trong lúc scheduler chờ bind thành công" (Scheduling Framework). Nếu binding sau đó thất bại, plugin gọi Unreserve để hoàn tác giả định, và Pod quay lại hàng đợi.
Hệ quả thực chiến: nếu bạn nhìn vào scheduler cache, một node có thể "đã đầy" theo góc nhìn scheduler trong khi API server chưa kịp ghi nodeName. Điều này hoàn toàn bình thường — đó là optimistic locking. Nó cũng giải thích vì sao scheduler có thể đạt throughput hàng trăm Pod/giây dù mỗi bind tốn hàng chục mili giây.
Scheduling Framework: toàn bộ extension point theo thứ tự
Scheduling Framework là "kiến trúc pluggable cho scheduler, gồm một tập API 'plugin' biên dịch thẳng vào scheduler" (Scheduling Framework). Mọi tính năng scheduling — kể cả những thứ trông như "built-in" như affinity, taint, resource fit — đều là plugin đăng ký vào các extension point. Hiểu thứ tự các extension point là hiểu chính xác khi nào mỗi ràng buộc được đánh giá.
Thứ tự thực thi trong một lần thử lập lịch:
| # | Extension point | Pha | Vai trò |
|---|---|---|---|
| 1 | PreEnqueue | trước hàng đợi | Quyết định Pod có được vào activeQ không. Chỉ khi mọi PreEnqueue trả về Success, Pod mới vào hàng đợi active |
| 2 | QueueSort | hàng đợi | Cung cấp hàm Less(Pod1, Pod2) sắp xếp Pod trong hàng đợi. Chỉ một plugin được bật |
| 3 | PreFilter | scheduling | Tiền xử lý thông tin Pod, kiểm tra điều kiện chung. Lỗi ở đây hủy cả scheduling cycle |
| 4 | Filter | scheduling | Loại các node không chạy được Pod. Gọi cho từng node; một plugin loại node thì các plugin sau không chạy cho node đó |
| 5 | PostFilter | scheduling | Chỉ chạy khi không có node khả thi. Preemption sống ở đây |
| 6 | PreScore | scheduling | Tạo state dùng chung cho Score. Lỗi hủy scheduling cycle |
| 7 | Score | scheduling | Chấm điểm các node đã qua Filter, trong một khoảng số nguyên xác định |
| 8 | NormalizeScore | scheduling | Chuẩn hóa điểm trước khi tính xếp hạng cuối. Gọi một lần mỗi plugin mỗi cycle |
| 9 | Reserve | scheduling | Reserve/Unreserve — giữ chỗ giả định, chống race condition |
| 10 | Permit | scheduling | Cuối scheduling cycle: approve / deny / wait (có timeout) việc bind |
| 11 | PreBind | binding | Tác vụ trước bind (ví dụ provision & mount volume mạng). Lỗi → Pod bị từ chối, quay lại hàng đợi |
| 12 | Bind | binding | Bind Pod vào Node. Plugin đầu tiên xử lý Pod thì các plugin Bind còn lại bị bỏ qua |
| 13 | PostBind | binding | Thông tin: dọn dẹp tài nguyên sau khi bind thành công. Kết thúc binding cycle |
Sơ đồ extension point chính thức của Scheduling Framework được mô tả chi tiết trong tài liệu Kubernetes (Scheduling Framework) — bảng trên đã liệt kê đầy đủ thứ tự và vai trò từng điểm.
Những điểm dễ hiểu sai về thứ tự
Filter chạy song song trên các node, không tuần tự. Tài liệu nói rõ "các node có thể được đánh giá đồng thời" ở pha Filter (Scheduling Framework). Tuần tự là giữa các Pod (scheduling cycle), không phải giữa các node trong một cycle. Trên cluster lớn, scheduler còn áp dụng percentageOfNodesToScore để chỉ chấm điểm một phần node thay vì toàn bộ, đổi độ chính xác lấy độ trễ.
PostFilter chỉ chạy khi Filter loại sạch. Preemption — phần được phân tích sâu ở file 7 — là một plugin PostFilter. Nghĩa là scheduler chỉ cân nhắc đuổi Pod khác sau khi xác nhận không node nào khả thi cho Pod hiện tại. Đây là lý do preemption không phải "tính năng luôn bật" mà là lối thoát cuối cùng.
Permit cho phép gang scheduling. Khả năng "wait" của Permit là nền tảng cho co-scheduling/gang scheduling (lập lịch cả nhóm Pod cùng lúc hoặc không lập lịch ai). Workload AI/ML phân tán cần tính chất này — sẽ nhắc lại ở file 8.
Ba hàng đợi: trái tim của vấn đề "Pod không được thử lại"
Đây là phần ít được hiểu nhất nhưng giải thích nhiều incident nhất. Scheduler không chỉ có "một danh sách Pod chờ". Nó có ba cấu trúc dữ liệu mà Pod di chuyển qua lại, và việc Pod đang ở đâu quyết định bao giờ scheduler thử lại nó.
activeQ — hàng đợi đang xét
activeQ là một priority heap: các Pod chưa lập lịch được sắp theo spec.priority (qua plugin QueueSort mặc định PrioritySort), và scheduler luôn lấy Pod ở đỉnh heap ra để xử lý. Đây là hàng đợi duy nhất mà scheduler thực sự "pop" để bắt đầu một scheduling cycle. Pod ở các hàng đợi khác không được scheduler ngó tới cho đến khi chúng quay về activeQ.
unschedulablePods — nơi Pod "thất bại" nằm chờ
Khi một Pod đi qua scheduling cycle nhưng không có node khả thi (Filter loại sạch và PostFilter cũng không cứu được), nó không quay ngay về activeQ. Nó bị đẩy vào unschedulablePods — một map giữ các Pod đã thử và hiện được xác định là không lập lịch được.
Điều mấu chốt: Pod ở unschedulablePods chỉ quay lại khi có sự kiện cluster liên quan xảy ra — ví dụ một node mới được thêm, một Pod khác bị xóa giải phóng tài nguyên, một PVC được bind. Nếu không có sự kiện nào khiến trạng thái của Pod đó có thể thay đổi, scheduler không lãng phí chu kỳ thử lại một Pod chắc chắn vẫn thất bại.
backoffQ — vùng đệm exponential backoff
Khi Pod ở unschedulablePods được xác định cần thử lại (do sự kiện liên quan), nó không nhảy thẳng về activeQ mà đi qua backoffQ nếu vẫn trong khoảng backoff. backoffQ áp exponential backoff: lần retry đầu chờ ~1s, lần hai ~2s, lần ba ~4s, lần bốn ~8s, với trần khoảng 10s. Hết thời gian backoff, Pod mới được chuyển về activeQ để thử lại.
Cơ chế backoff bảo vệ scheduler khỏi "busy loop" trên các Pod liên tục thất bại — một Pod cấu hình sai (ví dụ requests lớn hơn mọi node) sẽ bị giãn dần khoảng thử lại thay vì ngốn CPU scheduler mỗi vài mili giây.
QueueingHints: giảm retry vô ích
Trước đây, một sự kiện cluster (ví dụ "có Pod bị xóa") khiến toàn bộ Pod trong unschedulablePods được đánh thức để thử lại, kể cả những Pod mà sự kiện đó không liên quan. Trên cluster lớn với hàng nghìn Pod Pending, đây là nguồn lãng phí lớn. Tính năng QueueingHints cho phép mỗi plugin khai báo "sự kiện loại X có khả năng làm Pod tôi đang chặn trở nên lập lịch được không" — nếu không, Pod không bị đánh thức. Điều này cắt giảm đáng kể số lần retry vô ích và là một trong những cải tiến throughput quan trọng nhất của scheduler hiện đại (Kubernetes blog: QueueingHints).
Ngoài ra, scheduler còn có cơ chế flush định kỳ (flushUnschedulablePodsLeftover): các Pod nằm quá lâu trong unschedulablePods (mặc định ~5 phút) sẽ được đẩy về activeQ/backoffQ dù không có sự kiện rõ ràng, như một lưới an toàn chống việc Pod bị "quên" do bỏ lỡ sự kiện.
Hệ quả thực chiến của mô hình ba hàng đợi
Đây là nơi lý thuyết biến thành kỹ năng debug. Khi một Pod kẹt Pending:
- Bạn vừa thêm node nhưng Pod vẫn Pending vài giây. Bình thường — Pod đang ở backoffQ chờ hết exponential backoff, hoặc QueueingHint chưa đánh thức nó. Không phải scheduler hỏng.
- Pod Pending rất lâu mà bạn không thấy event mới. Có thể nó nằm yên trong
unschedulablePodschờ sự kiện liên quan; nếu nguyên nhân là cấu hình sai (requests quá lớn, affinity bất khả thi) thì sẽ không có sự kiện nào cứu được — phải sửa spec. - Hàng nghìn Pod Pending làm scheduler "chậm". Throughput scheduler tỉ lệ nghịch với số Pod phải retry; QueueingHints và backoff chính là cơ chế bảo vệ, nhưng cấu hình sai hàng loạt (ví dụ một Deployment 5000 replica với anti-affinity bất khả thi) vẫn có thể gây áp lực.
Scheduler metrics: quan sát guồng máy
Bạn không sửa được scheduler trên GKE, nhưng bạn quan sát được nó qua metrics Prometheus mà scheduler phát ra (trên GKE, các control plane metrics chọn lọc có thể bật qua Cloud Monitoring). Những metric quan trọng nhất:
| Metric | Ý nghĩa | Dùng để phát hiện |
|---|---|---|
scheduler_pending_pods (theo queue) | Số Pod đang chờ, tách theo active/backoff/unschedulable | Tồn đọng scheduling, phân biệt "đang backoff" vs "không lập lịch được" |
scheduler_scheduling_attempt_duration_seconds | Histogram thời gian một lần thử lập lịch | Scheduler chậm, cluster quá lớn cho percentageOfNodesToScore mặc định |
scheduler_schedule_attempts_total (theo result) | Đếm lần thử theo kết quả: scheduled/unschedulable/error | Tỷ lệ thất bại tăng |
scheduler_queue_incoming_pods_total (theo event) | Pod vào hàng đợi theo loại sự kiện | Hiểu Pod được đánh thức bởi sự kiện nào |
scheduler_pod_scheduling_duration_seconds | Tổng thời gian từ lúc Pod được scheduler thấy đến lúc scheduled (gồm cả thời gian chờ trong hàng đợi) | SLO scheduling end-to-end |
scheduler_preemption_attempts_total | Số lần thử preemption | Cluster thường xuyên phải đuổi Pod → thiếu capacity hoặc priority cấu hình sai |
Chỉ báo cảnh giác sớm hữu ích nhất là scheduler_pending_pods{queue="unschedulable"} tăng và không giảm: đó là dấu hiệu có Pod cấu hình sai không cách nào lập lịch được, khác hẳn với queue="backoff" cao (đang thử lại bình thường) hay queue="active" cao (tồn đọng do thiếu capacity tạm thời).
Production architecture patterns
Một scheduler hay nhiều profile?
Scheduler hỗ trợ nhiều scheduling profile, mỗi profile có schedulerName và bộ plugin riêng, nhưng tất cả dùng chung một hàng đợi pending (Scheduler Configuration). Pod chọn profile qua spec.schedulerName; nếu không khai báo, apiserver gán default-scheduler. Trên GKE Standard, bạn không sửa cấu hình scheduler mặc định, nhưng có thể chạy thêm một scheduler thứ hai dưới dạng Deployment trong cluster cho các workload đặc thù (ví dụ batch scheduler như Kueue/Volcano). Đây là pattern phổ biến cho nền tảng chạy lẫn online service và batch job.
GKE còn cung cấp scheduler thứ hai có sẵn: khi bật autoscaling profile optimize-utilization, GKE đặt schedulerName: gke.io/optimize-utilization-scheduler để gom Pod vào node đã dùng nhiều — chi tiết ở file 2 (About cluster autoscaling).
Ràng buộc bất biến giữa các profile
Tài liệu cấu hình scheduler nêu một ràng buộc quan trọng: "Mọi profile phải dùng cùng plugin ở extension point queueSort với cùng tham số, vì scheduler chỉ có một hàng đợi pending duy nhất" (Scheduler Configuration). Điều này nghĩa là bạn không thể có hai profile sắp xếp hàng đợi theo hai logic khác nhau — quyết định thứ tự xét Pod là toàn cục.
Common mistakes / anti-patterns
Nhầm "Pod ở unschedulablePods" với "scheduler hỏng". Đây là sai lầm số một. Khi Pod Pending lâu, nhiều người restart scheduler (không làm được trên GKE) hoặc xóa Pod để "ép thử lại". Thực ra Pod đang chờ đúng cơ chế: hoặc backoff, hoặc chờ sự kiện liên quan. Cách đúng là kubectl describe pod đọc lý do thất bại và sửa nguyên nhân gốc, không "ép" scheduler.
Cho rằng thêm node sẽ lập tức giải phóng Pod Pending. Vì backoff và QueueingHints, có độ trễ giữa "node mới Ready" và "Pod được thử lại". Nếu Pod thất bại do lý do không liên quan đến capacity node (ví dụ thiếu volume zone, affinity sai), thêm node hoàn toàn vô ích — nhưng người vận hành thường thêm node trước rồi mới nhận ra.
Tạo Deployment khổng lồ với ràng buộc bất khả thi rồi đổ lỗi scheduler chậm. Ví dụ kinh điển: 5000 replica với requiredDuringScheduling pod anti-affinity ở kubernetes.io/hostname trên cluster 100 node. 4900 Pod sẽ vào unschedulablePods và liên tục gây áp lực retry. Đây là lỗi spec, không phải lỗi scheduler — sửa bằng topology spread (file 4).
Bỏ qua percentageOfNodesToScore trên cluster cực lớn. Trên cluster hàng nghìn node, chấm điểm toàn bộ node mỗi cycle làm scheduling_attempt_duration_seconds tăng. Scheduler tự giảm tỷ lệ node được chấm điểm theo kích thước cluster, nhưng đây là trade-off độ chính xác (có thể bỏ sót node tốt nhất) lấy độ trễ — cần biết khi cluster lớn.
GCP-native implementation guidance
Kiểm tra một Pod kẹt Pending và đọc đúng nguyên nhân từ scheduler:
# Xem lý do scheduler từ chối, theo từng plugin/node
kubectl describe pod <pod> | sed -n '/Events/,$p'
# Ví dụ output điển hình:
# 0/12 nodes are available: 3 node(s) had untolerated taint {dedicated: gpu},
# 5 Insufficient cpu, 4 node(s) didn't match pod affinity rules.Mỗi cụm như Insufficient cpu hay had untolerated taint chính là tên plugin Filter đã loại node đó. Đối chiếu chúng với các file sau của chương để biết sửa ở đâu.
Trên GKE, bật control plane metrics của scheduler để quan sát hàng đợi:
# Bật system & control plane metrics (Managed Service for Prometheus)
gcloud container clusters update <cluster> \
--location=<region> \
--monitoring=SYSTEM,SCHEDULER,CONTROLLER_MANAGER,API_SERVERSau đó trong Cloud Monitoring, truy vấn scheduler_pending_pods tách theo nhãn queue để phân biệt tồn đọng active (thiếu capacity) với unschedulable (cấu hình sai).
Tổng kết mental model
- Một quyết định scheduling = scheduling cycle (chọn node, tuần tự) + binding cycle (ghi quyết định, song song).
- Optimistic locking + assume cache cho phép throughput cao; Reserve/Unreserve chống race.
- Mọi tính năng scheduling là plugin trên các extension point; thứ tự PreFilter → Filter → PostFilter → PreScore → Score → ... quyết định khi nào mỗi ràng buộc được đánh giá.
- Ba hàng đợi (activeQ/backoffQ/unschedulablePods) quyết định khi nào Pod được thử lại — hiểu sai chỗ này là nguồn gốc đa số hiểu lầm "scheduler hỏng".
- Quan sát qua metrics:
scheduler_pending_pods(tách theo queue) là tín hiệu quan trọng nhất.