Skip to content

Pod Priority & Preemption

Vì sao priority là quyết định kiến trúc, không phải tinh chỉnh

Khi cluster còn dư tài nguyên, priority gần như vô hình. Nhưng cluster production sẽ có lúc đầy — lúc tải đỉnh, lúc một zone mất capacity, lúc autoscaler chưa kịp thêm node. Đúng khoảnh khắc đó, priority quyết định ai được sống: service thanh toán hay batch job phân tích log? Nếu bạn chưa phân tầng priority, scheduler không có cơ sở để chọn, và kết quả thường tệ — một batch job có thể chiếm chỗ khiến service production không scale được, hoặc tệ hơn, một cấu hình priority sai khiến batch preempt (đuổi) chính service production.

Preemption là cơ chế mạnh và nguy hiểm: nó cố ý giết Pod đang chạy để nhường chỗ. Hiểu sai nó dẫn tới hai thái cực đều tệ: hoặc workload critical không bao giờ thắng (không dùng priority), hoặc preemption gây gián đoạn dây chuyền ngoài ý muốn (lạm dụng priority cao). File này mổ xẻ cơ chế để bạn thiết kế phân tầng priority an toàn.

Internal model: PriorityClass

PriorityClass là object không gắn namespace, ánh xạ một tên sang một giá trị số nguyên (Pod Priority and Preemption):

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: production-critical
value: 1000000                         # số càng lớn càng ưu tiên (≤ 1 tỷ cho user)
globalDefault: false
preemptionPolicy: PreemptLowerPriority # hoặc Never
description: "Dùng cho service production tier-1."

Các trường và ràng buộc then chốt:

  • value: khoảng từ −2.147.483.648 đến 1.000.000.000 (1 tỷ) cho PriorityClass do người dùng tạo. Số lớn = ưu tiên cao. Pod không có priorityClassName mặc định priority 0 (trừ khi có globalDefault).
  • Tên dành riêng: system-cluster-criticalsystem-node-critical là hai PriorityClass tích hợp cho component hệ thống, có giá trị cao hơn mọi priority người dùng. Tên người dùng không được bắt đầu bằng system-.
  • globalDefault: khi true, áp cho Pod không khai priorityClassName. Chỉ một PriorityClass được globalDefault: true. Đặt globalDefault không làm đổi priority các Pod đã tồn tại.
  • preemptionPolicy: PreemptLowerPriority (mặc định) — Pod này có thể đuổi Pod priority thấp hơn; Never — Pod này priority cao nhưng không đuổi ai, chỉ chờ trong hàng đợi tới khi tài nguyên tự rảnh.

preemptionPolicy: Never là công cụ bị đánh giá thấp

preemptionPolicy: Never tạo ra một Pod ưu tiên cao nhưng hiền lành: nó được xếp trước trong hàng đợi (QueueSort theo priority, file 1) nên được scheduler xét sớm và chọn node tốt trước Pod thấp hơn, nhưng không giết Pod nào để có chỗ. Đây là lựa chọn lý tưởng cho workload "quan trọng nhưng không khẩn cấp" — ví dụ data pipeline ưu tiên cao nên được chỗ trước batch thường, nhưng không đáng để gián đoạn các Pod đang chạy. Nó cho phép phân tầng thứ tự phục vụ mà không gây tác dụng phụ preemption.

Internal model: cơ chế preemption

Preemption là một plugin PostFilter — nó chỉ chạy sau khi Filter xác nhận không node nào khả thi cho Pod đang chờ (Scheduling Framework). Luồng (Pod Priority and Preemption):

  1. Pod P (priority cao) không lập lịch được trên node nào.
  2. Logic preemption tìm một node N mà nếu xóa một hoặc nhiều Pod priority thấp hơn, P sẽ lập lịch được.
  3. Nếu tìm thấy, các Pod priority thấp đó (victim) bị đuổi.
  4. P được lập lịch lên N.

nominatedNodeName: đặt chỗ tạm

Khi P preempt các Pod trên node N, trường status.nominatedNodeName của P được đặt thành N. Nó "đặt chỗ" tài nguyên và báo cho người dùng biết đã có preemption (Pod Priority and Preemption). Lưu ý quan trọng: P không được đảm bảo lập lịch lên đúng node nominated — scheduler luôn thử node đó trước, nhưng nếu một Pod priority cao hơn nữa xuất hiện, nominatedNodeName có thể bị xóa và gán lại. Đây là lý do nominatedNodeName đôi khi "nhảy" và không nên dựa vào nó như quyết định cuối.

Graceful termination tạo khoảng trống

Victim được nhận graceful termination period (mặc định 30 giây). Điều này tạo một khoảng trống giữa thời điểm preempt và thời điểm P thực sự lập lịch được — node N phải chờ victim tắt xong mới đủ chỗ. Tài liệu gợi ý đặt terminationGracePeriodSeconds: 0 trên Pod priority thấp để giảm khoảng trống này nếu cần preempt nhanh (Pod Priority and Preemption). Hệ quả: preemption không tức thời — đừng kỳ vọng Pod critical lên ngay khi preempt; có độ trễ bằng graceful period của victim.

PodDisruptionBudget được tôn trọng "best-effort"

Khi chọn victim, scheduler cố chọn các Pod mà việc đuổi không vi phạm PDB của chúng. Nhưng tài liệu nói rõ đây là best-effort, không đảm bảo: "Kubernetes tôn trọng PDB khi preemption, nhưng không đảm bảo. Scheduler cố tìm victim mà PDB không bị vi phạm; nếu không có, preemption vẫn tiến hành, vi phạm PDB" (Pod Priority and Preemption).

So sánh với file 6: node-pressure eviction hoàn toàn không tôn trọng PDB; preemption tôn trọng best-effort; API-initiated eviction (kubectl drain) tôn trọng đầy đủ. Ba cơ chế đuổi Pod với ba mức tôn trọng PDB khác nhau — nhầm lẫn dẫn tới kỳ vọng sai về độ bền.

Cross-node preemption không xảy ra

Preemption chỉ diễn ra trên một node: scheduler không đuổi Pod ở các node khác để gom đủ chỗ cho một Pod chờ (Pod Priority and Preemption). Tức là nếu không một node nào có thể, bằng cách xóa Pod thấp hơn trên chính nó, chứa được P, thì preemption thất bại — kể cả khi tổng tài nguyên rải rác trên nhiều node là đủ. Đây là giới hạn quan trọng: preemption không "dồn" tài nguyên xuyên node.

Starvation và non-preempting

Pod priority cao với preemptionPolicy: Never có thể bị starvation nếu tài nguyên không bao giờ tự rảnh; Pod priority thấp cũng có thể starvation nếu liên tục bị Pod cao hơn chiếm chỗ (Pod Priority and Preemption). Phân tầng priority phải tính tới điều này — quá nhiều tầng cao tạo nguy cơ tầng thấp không bao giờ chạy.

Bảo vệ: ResourceQuota giới hạn priority cao

Vì priority cao là tài nguyên "quyền lực", người dùng không tin cậy có thể lạm dụng để bỏ đói Pod khác. Kubernetes cho phép dùng ResourceQuota giới hạn lượng Pod được tạo với một PriorityClass cụ thể trong namespace (Pod Priority and Preemption). Trong môi trường đa tenant, đây là kiểm soát bắt buộc — nếu không, một team có thể gán mọi Pod của mình priority cao nhất và độc chiếm cluster.

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: limit-critical
  namespace: team-a
spec:
  hard:
    pods: "10"
  scopeSelector:
    matchExpressions:
    - operator: In
      scopeName: PriorityClass
      values: ["production-critical"]

Production architecture patterns

Pattern: phân tầng priority chuẩn

Một phân tầng thực tế, đủ tầng nhưng không quá nhiều:

Tầngvalue gợi ýpreemptionPolicyDùng cho
system-node-critical(tích hợp)kube-system thiết yếu node
system-cluster-critical(tích hợp)CoreDNS, metrics-server...
production-critical1000000PreemptLowerPriorityService tier-1 (payment, auth)
production-default100000PreemptLowerPriorityService production thường
batch-high10000NeverBatch ưu tiên (không gây gián đoạn)
batch-default0 (default)NeverBatch thường, best-effort

Nguyên tắc: chỉ tầng production mới có PreemptLowerPriority; mọi tầng batch dùng Never để batch không bao giờ đuổi nhau hay đuổi production một cách bất ngờ. Khoảng cách value đủ lớn giữa các tầng để chèn tầng mới sau này.

Pattern: balloon / overprovisioning pods

Để có capacity "đệm" cho scale-up nhanh, dùng các "balloon Pod" priority âm (thấp hơn 0) chỉ giữ chỗ. Khi Pod production cần chỗ, chúng preempt balloon Pod ngay (graceful 0s), cho phép Pod production lên trước khi autoscaler kịp thêm node. Đây là pattern phổ biến để giảm độ trễ scale-up trên GKE (kết hợp với cluster autoscaler, Chương 9).

Real-world scenarios

Tình huống: batch job preempt nhầm service production

Một đội đặt batch job value: 1000000 (cùng tầng production) với PreemptLowerPriority, nghĩ "cao để được chạy sớm". Khi cluster đầy, batch preempt service production thường (priority thấp hơn), gây gián đoạn. Sửa: batch luôn dùng preemptionPolicy: Never và value thấp — ưu tiên cao chỉ nên cho thứ thực sự khẩn cấp, và batch hiếm khi khẩn cấp.

Tình huống: Pod critical không lên dù đã preempt

Service critical preempt thành công nhưng vẫn Pending thêm ~30s. Nguyên nhân: victim có terminationGracePeriodSeconds: 30, tạo khoảng trống. Với workload cần lên thật nhanh, họ đặt graceful period nhỏ trên các Pod batch (victim tiềm năng), rút ngắn khoảng trống. Hiểu rằng preemption không tức thời là chìa khóa.

Tình huống: đa tenant lạm dụng priority

Trên nền tảng nội bộ, vài team gán mọi Pod production-critical. Khi cluster đầy, không còn ý nghĩa phân tầng vì "ai cũng cao". Sửa: ResourceQuota giới hạn số Pod priority cao mỗi namespace + review PriorityClass ở cấp platform. Priority cao trở thành tài nguyên được cấp phát có kiểm soát.

Cơ chế sâu hơn: priority ảnh hưởng hàng đợi, không chỉ preemption

Một hiểu lầm phổ biến là nghĩ priority chỉ liên quan preemption. Thực ra priority tác động sớm hơn nhiều — ở QueueSort (file 1). activeQ là một priority heap sắp theo spec.priority; scheduler luôn lấy Pod priority cao nhất ra xử lý trước. Nghĩa là kể cả khi không có preemption, Pod priority cao vẫn được scheduler ngó tới trước, nên có nhiều khả năng "giành" được node tốt trước khi Pod thấp kịp xét. Đây là vì sao preemptionPolicy: Never vẫn hữu ích: nó không giết ai nhưng vẫn được phục vụ trước.

Hệ quả tinh tế: trên cluster đông Pod Pending, một Deployment priority thấp có thể bị "đẩy xuống cuối hàng" liên tục mỗi khi có Pod priority cao mới — đây là dạng starvation ở tầng hàng đợi, khác với starvation do thiếu tài nguyên. Phân tầng priority quá nhiều tầng cao làm trầm trọng vấn đề này.

Cơ chế sâu hơn: tương tác preemption với cluster autoscaler

Preemption và cluster autoscaler là hai cách khác nhau để giải quyết Pod Pending, và chúng tương tác. Khi Pod priority cao Pending vì cluster đầy, hai điều có thể xảy ra song song: (1) scheduler preempt Pod thấp trên một node có sẵn; (2) autoscaler thêm node mới. Thường preemption nhanh hơn (không cần provision VM), nên Pod cao lên ngay bằng cách đuổi Pod thấp; Pod thấp bị đuổi lại trở thành Pending và trigger autoscaler thêm node. Kết quả ròng: Pod cao lên nhanh, Pod thấp chuyển sang node mới — đúng ý đồ.

Đây là nền tảng của pattern balloon/overprovisioning pod đã nêu: balloon Pod priority âm giữ chỗ sẵn; khi Pod thật cần, preempt balloon ngay (lên nhanh); balloon bị đuổi trigger autoscaler chuẩn bị node cho lần sau. Pattern này "mua" độ trễ scale-up bằng chi phí giữ một ít capacity rỗng — đáng giá cho service nhạy thời gian scale.

Cơ chế sâu hơn: ba cơ chế đuổi Pod và mức tôn trọng PDB

Khép lại bức tranh "Pod rời node" đã rải qua nhiều file, đây là bảng so sánh dứt điểm:

Cơ chếAi thực hiệnTôn trọng PDBTôn trọng graceful periodKhi nào
PreemptionschedulerBest-effortCó (graceful của victim)Pod cao cần chỗ, cluster đầy
Node-pressure evictionkubeletKhôngKhông (hard) / một phần (soft)Node cạn RAM/đĩa/PID (file 6)
API-initiated eviction (drain)người dùng/GKEUpgrade, maintenance (file 5)

Nhầm lẫn ba cơ chế này là nguồn gốc kỳ vọng sai về độ bền. PDB chỉ bảo vệ chắc chắn khỏi drain (upgrade) — không bảo vệ khỏi node-pressure, và chỉ best-effort với preemption. Độ bền thật sự đến từ: requests/limits đúng (tránh node-pressure), priority phân tầng đúng (kiểm soát preemption), và topology spread (giảm tác động mọi loại đuổi).

Real-world scenario bổ sung: preemption dây chuyền (cascading)

Một cluster phân tầng: tier-1 (1M), tier-2 (100K), tier-3 (10K). Cluster đầy. Một burst Pod tier-1 mới đến, preempt Pod tier-2. Pod tier-2 bị đuổi thành Pending, preempt Pod tier-3. Pod tier-3 Pending, không còn gì thấp hơn để preempt → Pending thật. Kết quả: một burst tier-1 gây gián đoạn dây chuyền xuống tận tier-3 — "cascading eviction". Nếu tier-2 và tier-3 có graceful period dài, độ trễ cộng dồn còn làm tier-1 lên chậm.

Phòng vệ: (1) giữ headroom capacity (overprovisioning balloon) để burst tier-1 không phải preempt; (2) giới hạn quy mô burst tier-1 bằng ResourceQuota; (3) graceful period ngắn cho tier thấp; (4) chấp nhận tier-3 là "bỏ được" và thiết kế nó idempotent/retry-able. Cascading là hệ quả tự nhiên của phân tầng sâu — phải thiết kế tier thấp để chịu được việc bị đuổi.

Common mistakes / anti-patterns

  • Không phân tầng priority → khi cluster đầy, không có cách để critical thắng batch.
  • Gán batch priority cao + PreemptLowerPriority → batch đuổi production. Batch luôn Never.
  • Kỳ vọng preemption tức thời → có độ trễ bằng graceful period của victim.
  • Tin PDB chặn được preemption → chỉ best-effort; node-pressure eviction thì không tôn trọng gì.
  • Quên cross-node preemption không xảy ra → tài nguyên rải rác nhiều node không cứu được Pod chờ.
  • Không giới hạn priority cao trong đa tenant → một team độc chiếm cluster.
  • Quá nhiều tầng priority → tầng thấp starvation, khó suy luận.

GCP-native implementation guidance

Tạo PriorityClass và gán cho workload:

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: production-critical
value: 1000000
preemptionPolicy: PreemptLowerPriority
globalDefault: false
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      priorityClassName: production-critical
      terminationGracePeriodSeconds: 10   # nếu Pod này là victim, rút ngắn khoảng trống

Quan sát preemption qua events và metrics:

bash
# Event preemption trên Pod chờ
kubectl describe pod <pod> | grep -i preempt
# "Preempted by <ns>/<pod> on node <node>" → Pod này là victim

# Metric (Chương 1): scheduler_preemption_attempts_total tăng → cluster thường xuyên thiếu chỗ

Tổng kết mental model

  • PriorityClass (value ≤ 1 tỷ) quyết định thứ tự xét và quyền preempt. system-* cao hơn mọi user priority.
  • preemptionPolicy: Never = ưu tiên cao nhưng không giết ai — lý tưởng cho batch ưu tiên.
  • Preemption là PostFilter (chỉ khi không node khả thi), chọn victim trên một node, tôn trọng PDB best-effort, có độ trễ bằng graceful period.
  • Cross-node preemption không xảy ra; tài nguyên rải rác không cứu được Pod chờ.
  • Đa tenant: giới hạn priority cao bằng ResourceQuota.
  • Phân tầng: chỉ production dùng PreemptLowerPriority, batch luôn Never.

FAQ thực chiến

Đặt priority cao có làm Pod chạy nhanh hơn không?

Không nhanh hơn về thực thi — priority không cấp thêm CPU/memory. Nó chỉ ảnh hưởng thứ tự lập lịch (được xét trước trong activeQ) và quyền preempt (đuổi Pod thấp khi cluster đầy). Khi cluster còn dư chỗ, priority gần như vô hình. Đừng dùng priority như "đòn bẩy hiệu năng" — nó là đòn bẩy giành chỗ khi khan hiếm.

preemptionPolicy: Never khác gì priority thấp?

Khác ở thứ tự hàng đợi. Never với value cao vẫn được scheduler xét trước các Pod value thấp (QueueSort theo priority), nên có lợi thế giành node tốt — chỉ là không giết ai để có chỗ. Priority thấp thì vừa bị xét sau, vừa là nạn nhân preemption. Never value cao là "ưu tiên phục vụ, không gây hại" — lý tưởng cho batch quan trọng.

Preemption có giải phóng tài nguyên ngay không?

Không tức thời. Victim được graceful termination period (mặc định 30s) để tắt nhẹ nhàng, tạo khoảng trống trước khi Pod ưu tiên lên. Muốn preempt nhanh, đặt terminationGracePeriodSeconds ngắn trên các Pod là victim tiềm năng (tier thấp).

Tại sao Pod priority cao của tôi vẫn Pending dù có Pod thấp đang chạy?

Có thể vì cross-node preemption không xảy ra — không một node nào giải phóng đủ chỗ bằng cách đuổi Pod thấp trên chính nó. Hoặc Pod thấp được PDB bảo vệ và scheduler tìm được cách khác. Hoặc preemptionPolicy: Never trên Pod cao. Đọc events kubectl describe pod để biết chính xác.

Bao nhiêu tầng priority là hợp lý?

Đủ để phân biệt các mức quan trọng thực sự khác nhau, thường 4–6 tầng (gồm hai tầng system-* tích hợp). Quá ít thì không phân biệt được critical vs batch; quá nhiều thì tầng thấp dễ starvation ở cả hàng đợi lẫn tài nguyên, và khó suy luận khi incident. Để khoảng cách value lớn giữa các tầng (vd 0, 10K, 100K, 1M) để còn chèn tầng mới sau này mà không phải đánh số lại.

PriorityClass bị xóa thì Pod đang dùng nó ra sao?

Pod đang chạy với PriorityClass đã xóa vẫn tiếp tục chạy và giữ nguyên priority đã gán (priority được "đóng băng" vào Pod lúc tạo). Nhưng bạn không tạo được Pod mới tham chiếu tên PriorityClass đã xóa — admission sẽ từ chối. Vì vậy xóa PriorityClass đang dùng là thao tác rủi ro: Deployment scale-up hay rolling update sau đó sẽ thất bại tạo Pod. Luôn kiểm tra không còn workload tham chiếu trước khi xóa.

Đặt globalDefault có đổi priority Pod đang chạy không?

Không. globalDefault chỉ áp cho Pod mới tạo không khai priorityClassName; Pod đã tồn tại giữ nguyên priority (thường là 0). Vì vậy thêm globalDefault không "nâng cấp" cluster hiện có một cách hồi tố — cần tái tạo Pod để chúng nhận default mới.

References