Hình dung bộ lập lịch như một nhân viên xếp bàn nhà hàng — nhưng là kiểu nhà hàng chỉ nhìn vào sổ đặt chỗ chứ không nhìn xem khách có đến hay không. Bàn bốn ghi "đặt 10 người" thì nó coi bàn đó kín, dù thực tế chỉ một người ngồi. Toàn bộ câu chuyện về kube-scheduler nằm ở khoảng cách giữa con số bạn khai và mức bạn dùng thật — và gần như mọi hoá đơn Kubernetes phình to đều sinh ra từ đúng khoảng cách đó.

kube-scheduler làm đúng một việc: chọn node cho pod. Phần 43 đo được rằng tắt nó đi thì pod mới nằm Pending mãi. Bài này đo cách nó chọn.

Nhanh, và rải đều

9 pod trên cụm 3 worker:

3 sc-worker      3 sc-worker2      3 sc-worker3

Đều tuyệt đối. Bộ lập lịch chấm điểm node theo mức đã dùng, nên node trống được ưu tiên.

Thời gian từ lúc tạo pod tới lúc spec.nodeName có giá trị:

81 ms    75 ms    75 ms

Đã gồm 24 ms binary kubectl khởi động (phần 46), nên bộ lập lịch quyết định trong khoảng 50 mili giây.

Với cụm nhỏ thì việc xếp chỗ không bao giờ là nút thắt. Trên cụm hàng nghìn node nó tốn hơn, và Kubernetes có percentageOfNodesToScore để chỉ chấm điểm một phần node thay vì tất cả.

requests quyết định, không phải mức dùng thật

Đây là điều quan trọng nhất về bộ lập lịch, và cũng là chỗ hay bị hiểu sai nhất. Nếu bạn chỉ nhớ một ý từ cả bài, nhớ ý này.

3 worker × 16 CPU cấp phát được = 48 CPU. Sáu pod nginx, mỗi pod requests: cpu: "10" → cần 60 CPU:

3 Running   (một pod mỗi worker)
3 Pending

0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu

nginx thật sự dùng vài phần nghìn CPU. Bộ lập lịch không quan tâm — nó chỉ cộng requests và so với allocatable. Đặt chỗ theo sổ, không theo khách đến.

Hai hệ quả đi ngược nhau, và trớ trêu là cả hai cùng tai hại:

Khai requests quá cao → node trông đầy trong khi thật ra rỗng, và bạn trả tiền cho máy không dùng tới. Đây là dạng lãng phí phổ biến nhất trên cụm Kubernetes.

Khai requests quá thấp hoặc không khai → node bị nhồi quá nhiều pod, và khi tất cả cùng bận thì chúng tranh nhau CPU và bộ nhớ. Pod không khai requests bị xếp vào lớp BestEffort — cái đầu tiên bị giết khi node hết RAM.

Con số đúng đến từ đo mức dùng thật (kubectl top pods, hoặc VPA ở chế độ khuyến nghị), không đến từ đoán.

Chú ý thêm dòng lỗi: 1 node(s) had untolerated taint(s) là node control plane. Nó bị loại vì có vết, không phải vì thiếu tài nguyên — và thông báo nói rõ từng lý do cho từng nhóm node, điều rất đáng quý khi gỡ lỗi.

nodeSelector: ép cứng

Gắn nhãn lên một worker rồi ép pod lên đó:

kubectl label node sc-worker2 dia=ssd
# nodeSelector: {dia: ssd}
4 sc-worker2

Cả bốn pod lên một node, hai worker khác để trống.

nodeSelector là ràng buộc cứng: không node nào có nhãn thì pod nằm Pending vô thời hạn. Với ràng buộc mềm — "ưu tiên node có nhãn, nhưng không có cũng được" — phải dùng nodeAffinity với preferredDuringSchedulingIgnoredDuringExecution.

Cụm thật dùng nhãn cho ba việc: loại phần cứng (gpu=true, disk=ssd), vùng khả dụng (topology.kubernetes.io/zone), và tách môi trường.

Ưu tiên: pod quan trọng giết pod đang chạy

Đây là hành vi bất ngờ nhất với người mới — lần đầu thấy nó tôi cũng giật mình.

Trạng thái ban đầu — sáu pod thường, mỗi pod xin 10 CPU:

3 Running   3 Pending

Thêm một pod với priorityClassName: cao (value: 1000000), cũng xin 10 CPU:

pod ưu tiên cao:  Running
pod thường:       2 Running   4 Pending

event:  Preempted by pod ... on node sc-worker2

Một pod đang chạy bị giết để nhường chỗ.

Đây là hành vi mặc định khi có PriorityClass, và nó đúng — pod quan trọng phải chạy được kể cả khi cụm đầy. Nhưng nó có nghĩa là:

  • Pod của bạn có thể bị giết bất cứ lúc nào bởi một pod ưu tiên cao hơn.
  • Việc đó không hiện ra như một lỗi của ứng dụng; nó là một event Preempted.
  • Ứng dụng phải chịu được bị giết đột ngột — cùng yêu cầu như khi node chết.

Tắt bằng preemptionPolicy: Never trên PriorityClass: pod đó vẫn được xếp trước trong hàng đợi nhưng không đuổi ai.

Và PodDisruptionBudget giới hạn số pod bị đuổi cùng lúc:

apiVersion: policy/v1
kind: PodDisruptionBudget
spec:
  minAvailable: 2
  selector: {matchLabels: {app: quan-trong}}

Hai giai đoạn

Bộ lập lịch chạy hai bước cho mỗi pod:

Lọc — node nào chạy được pod này. Đủ tài nguyên chưa, có vết không chịu được không, nodeSelector có khớp không, volume có gắn được không.

Chấm điểm — trong số node còn lại, node nào tốt nhất. Ít tải nhất, đã có sẵn ảnh, phù hợp với podAntiAffinity.

Thông báo FailedScheduling nói rõ từng nhóm node trượt ở bước lọc vì sao. Đó là dòng hữu ích nhất khi pod kẹt ở Pending:

kubectl get events --field-selector reason=FailedScheduling -o custom-columns=MSG:.message

Ba lý do Pending thường gặp

Insufficient cpu / Insufficient memory — như đo ở trên. Kiểm requests trước khi kiểm cụm.

had untolerated taint — node có vết mà pod không khai toleration. Control plane luôn có vết; node đang bị drain cũng vậy.

node(s) didn't match Pod's node affinity/selector — nhãn không khớp. Thường chỉ là gõ sai tên nhãn.

Không có lý do nào trong ba cái này là "cụm hỏng". Cả ba đều là cấu hình, và cả ba đều nằm gọn trong một dòng event.

Cùng một lằn ranh "đặt chỗ vs dùng thật", đổi ngăn xếp

Chuyện "hệ thống phân bổ theo con số bạn khai trước, không theo mức bạn dùng thật" không riêng gì Kubernetes — nó là cách gần như mọi bộ cấp phát tài nguyên làm việc, và cái bẫy đi kèm cũng lặp lại y hệt.

Trong thế giới Java, -Xmx4g là một tờ đặt chỗ: JVM giữ quyền dùng tới 4 GB heap và node phải tính đủ chừng đó, dù live set thật chỉ 300 MB. Thread pool corePoolSize cũng vậy — khai 200 luồng là 200 suất đếm vào ngân sách dù phần lớn đang ngủ. Con số bạn khai, không phải con số bạn chạm tới, mới là con số bị booking.

Trong Go, GOMAXPROCS và dung lượng dựng sẵn của channel/slice (make(chan T, 1000)) là cùng kiểu đặt chỗ: bộ nhớ được cấp ngay theo sức chứa khai báo, không theo số phần tử thật nằm trong đó.

Còn ở cơ sở dữ liệu, max_connections của PostgreSQL là tờ đặt chỗ kinh điển — mỗi suất kết nối giữ sẵn một lượng bộ nhớ work_mem dù phiên đó đang rảnh; và cơ chế ưu tiên cũng có bản sao: khi deadlock, engine chọn một nạn nhân để giết (thường là giao dịch rẻ nhất để quay lui) hệt như pod ưu tiên thấp bị Preempted.

Mẫu số chung: bộ cấp phát chia tài nguyên theo lượng được khai báo giữ chỗ, không theo lượng thực sự tiêu thụ — nên khai thừa thì lãng phí im lặng (trả tiền cho chỗ trống), khai thiếu thì vỡ trận khi mọi bên cùng đòi, và khi hết chỗ thật, một bên ưu tiên cao có thể cưỡng chế lấy lại chỗ từ một bên yếu hơn. Vì thế con số requests không phải thứ để "đoán cho an toàn" rồi để đó: nó là một lời hứa booking mà bạn trả tiền suốt vòng đời pod. Nên câu hỏi đúng khi chỉnh requests không phải "đặt bao nhiêu để chắc chắn đủ" mà là "mức dùng thật đo được là bao nhiêu, và tôi đang trả tiền cho bao nhiêu chỗ không ai ngồi".

Muốn tự soi cụm của bạn còn bao nhiêu chỗ theo cách bộ lập lịch nhìn, chạy thử:

kubectl describe nodes | grep -A5 "Allocated resources"

So Requests với Allocatable. Nếu Requests gần 100% mà kubectl top nodes cho thấy mức dùng thật dưới 20%, bạn đang trả tiền cho tài nguyên bị requests giữ chỗ mà không ai dùng — và trên hầu hết cụm, đó là khoản tiết kiệm lớn nhất đang nằm chờ.

Phần sau đo kubelet: từ lúc bộ lập lịch ghi tên node tới lúc container thật sự chạy.