Taints & Tolerations — Ràng Buộc "Đẩy" Node
Vì sao taint là mặt đối nghịch không thể thiếu của affinity
Affinity (file 3) là góc nhìn của Pod: "tôi muốn ở đâu". Taint là góc nhìn của node: "tôi từ chối ai". Hai cơ chế này bù trừ nhau và giải quyết hai bài toán khác nhau. Affinity không ngăn được Pod khác (không có affinity) vô tình rơi lên node bạn muốn dành riêng — chỉ taint làm được điều đó. Đây là lý do mọi chiến lược dedicated node pool, node phần cứng đặc biệt (GPU), node Spot, hay node đang gặp sự cố đều dựa vào taint.
Khác biệt cốt lõi so với affinity: một số taint không chỉ tác động lúc lập lịch mà còn đuổi Pod đang chạy (effect NoExecute). Đây là cơ chế duy nhất trong scheduling có khả năng di dời Pod đã chạy — điều affinity (IgnoredDuringExecution) không bao giờ làm. Vì vậy taint vừa là công cụ phân vùng, vừa là cơ chế phản ứng với sự cố node.
Mô hình: taint trên node + toleration trên Pod. Node taint là rào cản; toleration là "vé thông hành" cho phép Pod vượt rào. Quan trọng: toleration không kéo Pod về node taint — nó chỉ cho phép Pod ở đó nếu scheduler chọn. Muốn vừa cô lập node vừa hút đúng Pod về, phải kết hợp taint (đẩy người khác) + nodeAffinity/nodeSelector (kéo đúng Pod) — pattern "dedicated nodes" kinh điển.
Internal model: ba effect và khác biệt thời điểm
Theo tài liệu Kubernetes, taint có ba effect với hành vi rất khác nhau (Taints and Tolerations):
| Effect | Tác động lúc lập lịch | Tác động Pod đang chạy | Pha scheduler |
|---|---|---|---|
| NoSchedule | Pod không toleration sẽ không được lập lịch lên node | Không đuổi Pod đang chạy | Filter (TaintToleration) |
| PreferNoSchedule | Control plane cố tránh đặt Pod không toleration, nhưng không đảm bảo | Không đuổi | Score (TaintToleration) |
| NoExecute | Pod không toleration không được lập lịch | Đuổi Pod không toleration (ngay lập tức hoặc sau tolerationSeconds) | Filter + eviction |
Phân biệt NoSchedule vs NoExecute là điểm thi vấn đáp kinh điển và cũng là nguồn lỗi thực tế:
NoSchedulelà ràng buộc chỉ lúc lập lịch. Thêm taintNoSchedulelên node đang chạy Pod không đuổi Pod đó — chúng tiếp tục chạy cho đến khi tự chết hoặc bị xóa vì lý do khác. Dùng để "ngừng nhận Pod mới" mà không gián đoạn Pod hiện có.NoExecutetác động cả Pod đang chạy: Pod không tolerate bị đuổi. Đây là công cụ "dọn sạch node" hoặc phản ứng với node condition.
NoExecute + tolerationSeconds: timing của eviction
Với effect NoExecute, toleration có thể kèm tolerationSeconds tùy chọn, định nghĩa hành vi đuổi (Taints and Tolerations):
- Pod không tolerate taint
NoExecute→ bị đuổi ngay lập tức. - Pod tolerate không kèm
tolerationSeconds→ ở lại mãi mãi. - Pod tolerate có
tolerationSeconds: N→ ở lại N giây sau khi taint được thêm, rồi bị đuổi. Nếu taint bị gỡ trước khi hết hạn, Pod không bị đuổi.
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300 # ở lại 5 phút sau khi node not-ready, rồi mới bị di dờitolerationSeconds chính là van điều khiển "Pod chịu đựng node trục trặc bao lâu trước khi di dời" — quá ngắn gây di dời quá nhạy (node chập chờn vài giây cũng reschedule hàng loạt), quá dài làm service chịu downtime lâu khi node thật sự chết.
operator: Equal vs Exists
Equal(mặc định): khớp khikey,value,effectđều bằng. Phải khai báovalue.Exists: khớp khikeyvàeffectbằng, bất kể value. Không khai báovalue.
# Exists: tolerate mọi value của key "dedicated" với effect NoSchedule
tolerations:
- key: "dedicated"
operator: "Exists"
effect: "NoSchedule"Một toleration với operator: Exists và không key, không effect tolerate mọi taint — Pod đó lập lịch được lên bất kỳ node nào. Đây là cấu hình của các DaemonSet hệ thống cần chạy trên mọi node (CNI agent, logging, monitoring); nhưng dùng bừa bãi sẽ phá vỡ mọi cô lập bằng taint.
Quy tắc khi node có nhiều taint
Scheduler xử lý nhiều taint/toleration theo bộ lọc (Taints and Tolerations): bắt đầu với mọi taint của node, bỏ qua taint mà Pod có toleration khớp, rồi áp effect của các taint còn lại. Nếu còn taint NoSchedule → không lập lịch; nếu không còn NoSchedule nhưng còn PreferNoSchedule → cố tránh; nếu còn NoExecute → đuổi (nếu đang chạy) hoặc không lập lịch.
Taint-based eviction: node condition tự sinh taint
Đây là cơ chế ngầm khiến cluster tự phản ứng với node hỏng. Kubernetes tự động gắn taint lên node theo các điều kiện quan sát được (Taints and Tolerations):
| Node condition | Taint key | Effect | Default toleration |
|---|---|---|---|
| NotReady | node.kubernetes.io/not-ready | NoExecute | 300s |
| Unreachable | node.kubernetes.io/unreachable | NoExecute | 300s |
| MemoryPressure | node.kubernetes.io/memory-pressure | NoSchedule | — |
| DiskPressure | node.kubernetes.io/disk-pressure | NoSchedule | — |
| PIDPressure | node.kubernetes.io/pid-pressure | NoSchedule | — |
| Unschedulable | node.kubernetes.io/unschedulable | NoSchedule | — |
| NetworkUnavailable | node.kubernetes.io/network-unavailable | NoSchedule | — |
Default toleration 300s: vì sao node chết mà Pod không di dời ngay
Đây là chi tiết quyết định hành vi recovery của mọi cluster. Một admission controller tự động thêm toleration mặc định vào mọi Pod cho hai taint not-ready và unreachable, với tolerationSeconds: 300 (Taints and Tolerations):
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 300Hệ quả: khi một node mất kết nối (unreachable) hoặc not-ready, Pod trên đó không bị di dời ngay — chúng được giữ thêm 300 giây (5 phút). Lý do thiết kế: chống "di dời ồ ạt" khi node chỉ chập chờn mạng tạm thời (network blip) — nếu di dời ngay, một sự cố mạng thoáng qua sẽ gây bão reschedule khắp cluster.
Nhưng 300 giây cũng là 5 phút downtime tiềm tàng cho Pod trên node thực sự chết. Đây là trade-off mà nhiều đội không biết: "tại sao node die 2 phút rồi mà Pod chưa chuyển?" — vì default toleration 300s. Với service nhạy cảm latency, có thể giảm tolerationSeconds này (khai báo tường minh, ghi đè mặc định) để di dời nhanh hơn — đổi lại nhạy hơn với blip. Đây là quyết định độ bền phải cân nhắc theo SLO, không nên để mặc định một cách vô thức.
Taints mặc định của GKE
GKE tự gắn taint cho một số loại node, và hiểu chúng tránh được nhiều Pending khó hiểu:
| Loại node | Taint (điển hình) | Mục đích |
|---|---|---|
| GPU node | nvidia.com/gpu=present:NoSchedule | Chỉ Pod yêu cầu GPU mới lên; xem file 8 |
| Spot/Preemptible | cloud.google.com/gke-spot=true:NoSchedule (khi cấu hình) | Cô lập workload chịu được gián đoạn |
| Node mới khởi tạo | node.kubernetes.io/not-ready:NoSchedule (tạm thời) | Chặn Pod cho tới khi node sẵn sàng (CNI, kubelet) |
| ARM node | kubernetes.io/arch=arm64 (label, dùng kèm nodeSelector) | Tránh image amd64 chạy nhầm |
| Node pool tùy chỉnh | taint do người dùng đặt qua --node-taints | Dedicated workload |
GKE tự inject toleration cho GPU qua ExtendedResourceToleration admission controller — Pod yêu cầu nvidia.com/gpu tự động nhận toleration taint GPU, nên bạn không phải khai báo thủ công (GKE GPUs). Chi tiết ở file 8.
Production architecture patterns
Pattern: dedicated node pool (taint + affinity)
Để dành một node pool riêng cho workload nhạy cảm (ví dụ payment service), cần hai ràng buộc song song:
# 1. Taint node pool: đẩy mọi Pod không liên quan ra
gcloud container node-pools create payments \
--cluster=<cluster> --location=<region> \
--node-taints=dedicated=payments:NoSchedule \
--node-labels=dedicated=payments# 2. Pod payment: vừa tolerate taint, vừa nodeAffinity để bị HÚT về đúng pool
spec:
tolerations:
- key: "dedicated"
operator: "Equal"
value: "payments"
effect: "NoSchedule"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: dedicated
operator: In
values: ["payments"]Chỉ taint là chưa đủ: nó ngăn người khác vào nhưng không kéo Pod payment về — Pod payment vẫn có thể lập lịch lên node thường (vì nó tolerate taint nếu có, nhưng node thường không có taint nên không bị chặn). Phải thêm nodeAffinity để bắt buộc nó về đúng pool. Đây là sai lầm phổ biến: chỉ đặt taint, rồi ngạc nhiên khi Pod dedicated chạy trên node thường.
Pattern: node Spot với toleration có chủ đích
Workload chịu được gián đoạn (batch, stateless có replica dư) chạy trên node Spot rẻ. Taint Spot ngăn workload critical vô tình rơi vào node có thể bị thu hồi bất cứ lúc nào; chỉ Pod khai báo toleration Spot mới vào. Kết hợp với PriorityClass thấp (file 7) để khi Spot bị thu hồi, các Pod này nhường chỗ.
Real-world scenarios
Tình huống: node chết nhưng service downtime 5 phút
Một đội phàn nàn service downtime ~5 phút mỗi khi một node hardware-fail. Nguyên nhân: default toleration 300s cho unreachable — Pod không di dời cho đến hết 5 phút. Họ đặt service quan trọng có tolerationSeconds: 30 tường minh cho not-ready/unreachable, cộng với topology spread (file 4) để mất một node chỉ mất một phần nhỏ replica. Kết quả downtime giảm còn dưới 1 phút. Trade-off chấp nhận: nhạy hơn với network blip, nhưng họ đã có đủ replica để chịu reschedule lác đác.
Tình huống: GPU node bị Pod thường chiếm
Một cluster mới thêm GPU node pool nhưng quên rằng GKE chỉ tự taint khi thêm GPU pool vào cluster có sẵn non-GPU pool. Một số Pod CPU thường rơi lên GPU node đắt tiền, chiếm chỗ, khiến Pod GPU thật sự bị Pending. Bài học: luôn xác nhận GPU node mang taint nvidia.com/gpu=present:NoSchedule và chỉ Pod yêu cầu GPU (qua ExtendedResourceToleration) mới lên được (GKE GPUs).
Cơ chế sâu hơn: taint trong vòng đời node của GKE
Taint không chỉ là công cụ người dùng — nó là cơ chế GKE dùng để điều phối vòng đời node (liên hệ Chương 6). Khi một node mới join cluster, nó mang taint node.kubernetes.io/not-ready:NoSchedule cho tới khi mọi component nền (kubelet Ready, CNI agent gán được IP) sẵn sàng. Đây là lý do Pod không "nhảy" lên node ngay khi node xuất hiện trong kubectl get nodes — node tồn tại nhưng còn taint not-ready, scheduler chưa đặt Pod lên.
Tương tự, khi GKE chuẩn bị nâng cấp hoặc thay node (surge upgrade, auto-repair), nó cordon node — về bản chất là gắn taint node.kubernetes.io/unschedulable:NoSchedule — rồi drain (đuổi Pod qua API-initiated eviction, tôn trọng PDB). Hiểu chuỗi cordon→drain này giúp phân biệt ba loại "Pod rời node": cordon+drain lúc upgrade (tôn trọng PDB), preemption (best-effort PDB, file 7), và node-pressure eviction (không PDB, file 6). Cùng là "Pod rời node" nhưng ba cơ chế, ba mức bảo vệ.
Spot/Preemptible VM thêm một lớp: khi Compute Engine thu hồi node Spot, GKE nhận tín hiệu và gắn taint để bắt đầu graceful shutdown (~25–30 giây với Spot). Workload trên Spot phải xử lý được tín hiệu này — đây là lý do Spot chỉ phù hợp workload chịu gián đoạn, và nên kèm taint Spot + PriorityClass thấp để dọn dẹp có trật tự.
Cơ chế sâu hơn: tương tác taint với DaemonSet và autoscaler
DaemonSet có quan hệ đặc biệt với taint. DaemonSet controller tự thêm một số toleration để Pod hệ thống chạy được trên mọi node, kể cả node có taint not-ready/unreachable/pressure — nếu không, các agent thiết yếu (CNI, logging, monitoring) sẽ không lên được node đang khởi tạo, gây deadlock (node cần CNI để Ready, nhưng CNI Pod cần node Ready để lên). Vì vậy DaemonSet hệ thống thường tolerate rộng. Nhưng điều này cũng nghĩa là taint không cô lập được DaemonSet một cách đơn giản — nếu bạn muốn một DaemonSet không chạy trên một loại node, phải dùng nodeAffinity, không phải dựa vào taint.
Với cluster autoscaler, taint do người dùng đặt trên node pool phải được khai báo qua --node-taints lúc tạo pool, để autoscaler biết node mới của pool đó sẽ mang taint gì và mô phỏng đúng việc Pod nào lập lịch được. Nếu bạn taint thủ công node đã tồn tại bằng kubectl taint mà không cập nhật cấu hình pool, autoscaler sẽ mô phỏng sai (nghĩ node mới không có taint), dẫn tới scale-up tạo node mà Pod đích vẫn không lên được. Quy tắc: taint thuộc về cấu hình node pool, không phải thao tác thủ công trên node lẻ — trừ khi cô lập tạm thời để điều tra.
Real-world scenario bổ sung: auto-repair gây gián đoạn lặp lại
Một đội thấy service bị reschedule liên tục mỗi vài giờ. Điều tra: một node có vấn đề phần cứng chập chờn khiến node condition dao động giữa Ready và NotReady. Mỗi lần NotReady đủ lâu, taint not-ready:NoExecute kích hoạt; sau 300s (default toleration) Pod bị di dời; rồi node Ready lại, auto-repair chưa kịp thay. Vòng lặp gây gián đoạn rải rác khó truy. Bài học: kết hợp giám sát node condition với auto-repair để node lỗi bị thay hẳn thay vì dao động, và với service nhạy, tolerationSeconds ngắn hơn giúp di dời dứt khoát khỏi node bệnh thay vì bám trụ.
Khung quyết định: chọn effect và tolerationSeconds
| Mục đích | Effect | tolerationSeconds | Ghi chú |
|---|---|---|---|
| Dedicated node pool (cô lập) | NoSchedule | — | Kèm nodeAffinity để hút đúng Pod |
| Ngừng nhận Pod mới, giữ Pod hiện có | NoSchedule | — | Dùng khi rút node dần |
| Dọn sạch node (đuổi cả Pod đang chạy) | NoExecute | 0 hoặc ngắn | Drain/maintenance |
| Service nhạy latency, di dời nhanh khi node lỗi | (ghi đè default) | 30–60 | Đổi lấy nhạy hơn với blip |
| Workload chịu được node lỗi lâu | (ghi đè default) | 600+ | Giảm reschedule khi mạng chập chờn |
Common mistakes / anti-patterns
- Chỉ taint mà không nodeAffinity cho dedicated pool → Pod dedicated vẫn chạy node thường. Cần cả hai.
- Không biết default toleration 300s → ngạc nhiên vì Pod không di dời khi node chết; service downtime 5 phút.
- Đặt
tolerationSecondsquá nhỏ toàn cục → bão reschedule mỗi khi network blip vài giây. - Toleration
operator: Existskhông key/effect trên Pod thường → Pod đó tolerate mọi taint, phá vỡ mọi cô lập. Chỉ dùng cho system DaemonSet cần. - Dùng
NoSchedulerồi mong nó đuổi Pod đang chạy →NoSchedulekhông đuổi; phảiNoExecute. - Quên rằng cordon = taint
node.kubernetes.io/unschedulable:NoSchedule→ drain trước upgrade dựa trên cơ chế này (Chương 6).
GCP-native implementation guidance
Xem taint hiện có của node và lý do Pod bị chặn:
# Liệt kê taint mọi node
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
# Lý do TaintToleration loại node
kubectl describe pod <pod> | grep -i taint
# "had untolerated taint {dedicated: payments}" → thiếu tolerationThêm/gỡ taint thủ công (ví dụ cô lập node nghi lỗi để điều tra mà không đuổi Pod):
kubectl taint nodes <node> investigate=true:PreferNoSchedule # mềm, không gián đoạn
kubectl taint nodes <node> investigate=true:PreferNoSchedule- # gỡ (dấu - cuối)Tổng kết mental model
- Taint = node đẩy; toleration = vé thông hành (không kéo). Dedicated pool cần taint và affinity.
NoSchedulechỉ chặn lúc lập lịch;NoExecutecòn đuổi Pod đang chạy — cơ chế di dời duy nhất trong scheduling.tolerationSecondsđiều khiển thời gian chịu đựng node trục trặc trước khi di dời.- Default toleration 300s cho
not-ready/unreachablelà nguồn gốc "downtime 5 phút khi node chết" — biết để điều chỉnh theo SLO. - GKE tự taint GPU/Spot/node mới; ExtendedResourceToleration tự inject toleration GPU.
FAQ thực chiến
Tại sao Pod không di dời ngay khi node chết?
Vì admission controller tự thêm toleration mặc định tolerationSeconds: 300 cho not-ready và unreachable. Pod được giữ 5 phút trước khi di dời — thiết kế để chống bão reschedule khi node chỉ chập chờn mạng. Muốn di dời nhanh hơn cho service nhạy, khai tường minh tolerationSeconds ngắn hơn (đổi lấy nhạy hơn với blip).
NoSchedule hay NoExecute cho dedicated node pool?
Dùng NoSchedule. NoSchedule chặn Pod mới mà không đuổi Pod đang chạy — đủ để cô lập pool. NoExecute còn đuổi cả Pod đang chạy không tolerate, mạnh hơn cần thiết và gây gián đoạn. Chỉ dùng NoExecute khi thực sự muốn "dọn sạch" node (maintenance, drain).
Toleration có làm Pod bị hút về node taint không?
Không. Toleration chỉ gỡ rào, không tạo lực hút. Pod có toleration vẫn ưu tiên node thường (không taint) trừ khi bạn thêm nodeAffinity kéo nó về. Đây là sai lầm phổ biến nhất với taint: chỉ đặt taint + toleration rồi ngạc nhiên khi Pod dedicated chạy trên node thường.
Làm sao biết một taint đến từ GKE hay từ người dùng?
Taint GKE dùng prefix có ý nghĩa: nvidia.com/gpu, cloud.google.com/gke-*, node.kubernetes.io/* (do Kubernetes/kubelet sinh theo node condition). Taint không prefix chuẩn thường do người dùng đặt qua --node-taints hoặc kubectl taint. Dùng kubectl describe node để xem nguồn gốc và effect.