nodeSelector, affinity và taint/toleration
Bài trước bạn đã học requests/limits và QoS class: đó là cách nói với scheduler "pod này cần bao nhiêu tài nguyên". Bài này là mảnh ghép còn lại: nói với scheduler pod phải nằm ở đâu và node nào được phép nhận pod. Đây là dạng bài xuất hiện gần như chắc chắn trong đề CKA, và cũng là chỗ nhiều người mất điểm oan vì làm đúng cú pháp nhưng sai logic.
[!NOTE] Kiến thức cần nhớ từ bài trước: nếu pod không có node nào đủ CPU/memory theo
requests, scheduler sẽ để pod ở trạng tháiPending. Bài này thêm một lý do nữa khiến podPending: label/affinity không khớp hoặc node bị taint. Khi troubleshoot, luôn đọc dòngEventscủakubectl describe podtrước.
1. Mục tiêu bài học
Sau bài này, bạn phải làm được 4 việc trong khoảng 35 phút, không cần tra Google:
- Dùng
nodeSelectorđể ghim pod vào node có label chỉ định. - Viết
nodeAffinitydạngrequired(bắt buộc) vàpreferred(ưu tiên) bằngmatchExpressions. - Gắn và gỡ
tainttrên node, thêmtolerationvào pod. - Phân biệt được 3 effect
NoSchedule,PreferNoSchedule,NoExecutevà biết khi nào đề bài muốn cái nào.
2. Ví dụ có hướng dẫn (worked example)
Đề bài kiểu CKA: Cluster có node01 và node02. Hãy tạo pod tên ssd-pod, image nginx, chỉ chạy trên node có label disk=ssd. Node node02 phải bị chặn để pod thường không vào được, chỉ pod có toleration phù hợp mới vào.
Bước 1. Khảo sát hiện trạng trước khi sửa bất cứ thứ gì.
kubectl get nodes --show-labels
kubectl describe node node01 | grep -iA3 taints
--show-labels cho bạn thấy toàn bộ label hiện có, tránh ghi đè nhầm. Trong thi, đây là bước 10 giây nhưng cứu bạn khỏi việc tạo label trùng key.
Bước 2. Gắn label cho node.
kubectl label node node01 disk=ssd
kubectl get node node01 --show-labels
Nếu label đã tồn tại với giá trị khác, kubectl sẽ báo lỗi và yêu cầu --overwrite. Đây là chi tiết nhỏ nhưng hay xuất hiện trong đề.
Bước 3. Tạo pod bằng dry-run rồi sửa YAML (mẹo ăn điểm).
kubectl run ssd-pod --image=nginx --dry-run=client -o yaml > ssd-pod.yaml
Mở file và thêm nodeSelector:
apiVersion: v1
kind: Pod
metadata:
name: ssd-pod
spec:
nodeSelector:
disk: ssd
containers:
- name: web
image: nginx
kubectl apply -f ssd-pod.yaml
kubectl get pod ssd-pod -o wide
Cột NODE phải là node01. Nếu là node02 thì bạn gắn label sai node; nếu Pending thì đọc kubectl describe pod ssd-pod.
Bước 4. Gắn taint lên node02.
kubectl taint nodes node02 env=prod:NoSchedule
kubectl describe node node02 | grep -i taint
Kết quả mong đợi: Taints: env=prod:NoSchedule.
Bước 5. Kiểm chứng logic toleration. Tạo một pod không có toleration và một pod có toleration, sau đó xem pod nào vào được node02:
spec:
nodeSelector:
kubernetes.io/hostname: node02
tolerations:
- key: env
operator: Equal
value: prod
effect: NoSchedule
Pod không có tolerations sẽ Pending với event dạng 1 node(s) had untolerated taint {env: prod}. Pod có toleration sẽ vào được. Đây chính là điểm mấu chốt của bài: ==toleration chỉ mở cửa, không kéo pod vào trong==.
Bước 6. Nếu đề yêu cầu "ưu tiên" thay vì "bắt buộc", dùng preferred thay cho nodeSelector:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: disk
operator: In
values: ["ssd"]
Với preferred, nếu node01 hết tài nguyên, pod vẫn có thể chạy trên node02. Với nodeSelector hoặc affinity required, pod sẽ nằm Pending thay vì chọn node khác.
flowchart TD
Start["🚀 Pod được tạo"] --> NS{"Có nodeSelector?"}
NS -->|Có| Filter["🔎 Scheduler lọc node theo label"]
NS -->|Không| Aff{"Có required nodeAffinity?"}
Aff -->|Có| Filter
Aff -->|Không| Taint{"Node còn taint mà pod không tolerate?"}
Filter --> Taint
Taint -->|Có| Skip["⛔ Loại node đó"]
Taint -->|Không| Bind["✅ Bind pod vào node"]
Skip --> Pending["⏳ Hết node thì Pod Pending"]
3. Luyện tập có hướng dẫn
Chuẩn bị: cluster có ít nhất 2 node. Nếu bạn lab bằng cluster 1 node, dùng control-plane làm node đích và nhớ nó đã có taint sẵn.
Bài tập dưới đây được thiết kế đúng thứ tự khó tăng dần. Làm xong mỗi bài, ghi lại lệnh kiểm chứng bạn đã dùng.
Bài 1. nodeSelector cơ bản
Gắn label tier=frontend cho node bất kỳ có hostname node01, tạo pod web-frontend image nginx chỉ chạy trên node đó bằng nodeSelector.
Xem gợi ý
kubectl label node node01 tier=frontend
kubectl run web-frontend --image=nginx --dry-run=client -o yaml > web.yaml
Thêm vào spec:
nodeSelector:
tier: frontend
kubectl apply -f web.yaml
kubectl get pod web-frontend -o wide
Bài 2. nodeAffinity required với operator In
Lặp lại bài 1 nhưng dùng nodeAffinity thay cho nodeSelector, với requiredDuringSchedulingIgnoredDuringExecution và operator In.
Xem gợi ý
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: tier
operator: In
values: ["frontend"]
Lưu ý cấu trúc: nodeSelectorTerms là list, bên trong mỗi term là matchExpressions cũng là list. Sai một cấp lồng nhau là kubectl apply báo lỗi ngay.
Bài 3. preferred affinity
Tạo pod prefer-ssd image nginx với preferred affinity cho label disk=ssd, weight 1. Sau đó xóa label disk khỏi mọi node và quan sát: pod vẫn phải chạy được (khác với bài 2).
Xem gợi ý
kubectl label node node01 disk-
kubectl get pod prefer-ssd -o wide
Với preferred, scheduler bỏ qua tiêu chí không khớp và vẫn đặt pod. Đây là cách phân biệt nhanh required và preferred khi làm bài.
Bài 4. Taint NoSchedule và toleration
Gắn taint gpu=true:NoSchedule lên node02. Tạo pod gpu-pod image nginx có toleration tương ứng và nodeSelector ghim vào node02.
spec:
nodeSelector:
kubernetes.io/hostname: node02
tolerations:
- key: gpu
operator: Equal
value: "true"
effect: NoSchedule
Kiểm chứng: kubectl get pod gpu-pod -o wide. Sau đó thử bỏ nodeSelector và tạo lại: pod nhiều khả năng vẫn về node01, vì toleration không hề ép pod vào node bị taint.
[!TIP] Muốn "chấp nhận mọi taint" (ví dụ đề bảo pod phải chạy được trên cả control-plane), dùng toleration không có
key:tolerations: - operator: ExistsCách này chỉ nên dùng khi đề nói rõ là chấp nhận mọi node.
Bài 5. Gỡ taint và kiểm tra pod bị đuổi
Gỡ taint env=prod:NoSchedule khỏi node02. Sau đó gắn lại bằng effect NoExecute và quan sát pod đang chạy trên node02 mà không có toleration.
Xem gợi ý
kubectl taint nodes node02 env=prod:NoSchedule-
kubectl taint nodes node02 env=prod:NoExecute
kubectl get pods -o wide --field-selector spec.nodeName=node02
Với NoExecute, pod không tolerate bị đuổi khỏi node (terminated) chứ không chỉ bị chặn như NoSchedule. Đây là điểm khác biệt hay bị hỏi.
Bài 6. Bài tổng hợp kiểu đề thi
Tạo Deployment api 2 replicas image nginx thỏa đồng thời: chỉ chạy trên node có label tier=backend, và chạy được trên node bị taint dedicated=api:NoSchedule.
Xem gợi ý
kubectl create deployment api --image=nginx --replicas=2 --dry-run=client -o yaml > api.yaml
Sửa file, thêm cả nodeSelector và tolerations vào spec.template.spec, rồi:
kubectl apply -f api.yaml
kubectl get pods -l app=api -o wide
kubectl describe pod -l app=api | grep -i -A2 "Node-Selectors\|Tolerations"
describe in ra cả Node-Selectors và Tolerations, đây là cách tự chấm điểm nhanh trong phòng thi.
4. Lỗi thường gặp và troubleshooting
| Triệu chứng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
Pod Pending, event didn't match Pod's node affinity/selector |
Label node sai key hoặc sai value | kubectl get nodes --show-labels, sửa label hoặc sửa YAML |
Pod Pending, event had untolerated taint |
Thiếu tolerations hoặc sai effect |
Thêm toleration đúng key, value, effect |
| Đã thêm toleration nhưng pod vẫn không vào node bị taint | Tưởng toleration ép được pod lên node | Thêm nodeSelector hoặc nodeAffinity để ghim |
kubectl taint ... - báo lỗi |
Quên dấu - hoặc gõ sai key |
Kiểm tra lại bằng kubectl describe node | grep -i taint |
| Toleration không có tác dụng | operator: Equal nhưng thiếu value, hoặc value kiểu số thiếu dấu nháy |
Dùng operator: Exists nếu chỉ cần khớp key |
| Pod đang chạy bỗng bị terminate | Ai đó gắn taint NoExecute |
Thêm toleration, hoặc gỡ taint |
| Sửa label node nhưng pod không di chuyển | Hậu tố IgnoredDuringExecution: label chỉ xét lúc schedule |
Phải xóa và tạo lại pod nếu muốn scheduler xét lại |
Ba lỗi logic đáng nhớ nhất:
- Toleration không phải là bộ lọc chọn node. Nó là "vé vào cửa". Muốn pod nằm đúng node, luôn cần
nodeSelectorhoặcnodeAffinityđi kèm. nodeSelectorlà AND tuyệt đối. Khai báo 2 label thì node phải có đủ cả 2. Không có cú pháp OR trongnodeSelector, phải dùngnodeAffinityvới nhiềunodeSelectorTerms.nodeNamebỏ qua scheduler hoàn toàn. Nếu đề bắt buộc dùng scheduler (ví dụ phải có event scheduling), đừng dùngnodeName. Nó cũng không kiểm tra taint.
5. Góc chuyên gia
Ba câu hỏi để chọn đúng công cụ, đọc đề xong trả lời trong 5 giây:
- Đề nói "chỉ chạy trên node có label X" →
nodeSelector. Nhanh nhất, ít lỗi cú pháp nhất. - Đề nói "ưu tiên", "nếu có thể", "prefer" →
preferredDuringSchedulingIgnoredDuringExecution. - Đề nói "node bị taint, pod phải chạy được ở đó" →
tolerationscộng thêmnodeSelectorđể ghim đúng node.
Mẹo tốc độ trong phòng thi:
alias k=kubectl
export do="--dry-run=client -o yaml"
k run p1 --image=nginx $do > p1.yaml
Sau đó chỉ việc mở file, thêm 4-6 dòng YAML, apply. Không gõ tay cả manifest từ đầu.
Kiểm tra nhanh sau khi làm:
kubectl get pod <ten> -o wide # xem cột NODE
kubectl describe pod <ten> | grep -i -A3 "node-selectors\|tolerations"
kubectl get events --sort-by=.lastTimestamp | tail -20
Quy tắc ngón tay cái về effect:
NoSchedule: chặn pod mới, pod cũ ở lại. Dùng nhiều nhất trong đề.PreferNoSchedule: chỉ là gợi ý, scheduler sẽ tránh nếu còn chỗ khác.NoExecute: đuổi cả pod đang chạy. Chỉ dùng khi đề nói rõ "evict".
Nhớ taint mặc định của control plane: node-role.kubernetes.io/control-plane:NoSchedule. Nhiều bạn lab trên cluster 1 node, tạo pod bằng nodeSelector vào control-plane rồi thắc mắc tại sao Pending. Đó là taint, không phải label sai.
Đừng quên dọn dẹp: đề thi tính điểm trên trạng thái cuối cùng của cluster. Làm xong một task, xóa pod test và gỡ taint/label tạm bạn đã tạo, tránh ảnh hưởng task sau.
6. Tóm tắt và bước tiếp theo
Chốt lại bốn dòng:
nodeSelector: khớp label chính xác, đơn giản nhất, không khớp thìPending. {{c1::nodeSelector}} là cách ghim pod vào node theo label.nodeAffinity:requiredlà bắt buộc,preferredlà ưu tiên, hỗ trợ operatorIn,NotIn,Exists,DoesNotExist,Gt,Lt. Affinity bắt buộc dùng field {{c2::requiredDuringSchedulingIgnoredDuringExecution}}.taintđặt trên node với effect {{c3::NoSchedule}},PreferNoSchedulehoặc {{c4::NoExecute}}.tolerationđặt trên pod, chỉ cho phép pod vào node bị taint, không ép. Effect {{c5::NoExecute}} sẽ đuổi pod đang chạy nếu không có toleration.
Bài tiếp theo là Checkpoint: Workloads và scheduling. Đó là bài tổng hợp, đề sẽ trộn Deployment với rolling update, DaemonSet, resource requests và chính nội dung bài này. Trước khi sang bài đó, hãy tự làm lại bài 4 và bài 6 mà không nhìn gợi ý: nếu làm được dưới 3 phút mỗi bài thì bạn đã sẵn sàng.
[
{ "front": "nodeSelector khớp label theo quy tắc nào?", "back": "Khớp chính xác key=value, tất cả label khai báo phải khớp cùng lúc, không khớp thì Pod Pending." },
{ "front": "Hai loại nodeAffinity là gì?", "back": "requiredDuringSchedulingIgnoredDuringExecution (bắt buộc) và preferredDuringSchedulingIgnoredDuringExecution (ưu tiên)." },
{ "front": "Các operator dùng trong matchExpressions của nodeAffinity?", "back": "In, NotIn, Exists, DoesNotExist, Gt, Lt." },
{ "front": "Ba effect hợp lệ của taint?", "back": "NoSchedule, PreferNoSchedule, NoExecute." },
{ "front": "Toleration có ép pod lên node bị taint không?", "back": "Không. Toleration chỉ cho phép pod vào; muốn ép phải thêm nodeSelector hoặc nodeAffinity." },
{ "front": "Lệnh gắn taint env=prod:NoSchedule lên node02?", "back": "kubectl taint nodes node02 env=prod:NoSchedule" },
{ "front": "Làm sao gỡ một taint khỏi node?", "back": "Thêm dấu trừ vào cuối: kubectl taint nodes node02 env=prod:NoSchedule-" },
{ "front": "NoExecute khác NoSchedule ở điểm nào?", "back": "NoExecute đuổi cả pod đang chạy không tolerate; NoSchedule chỉ chặn pod mới được schedule." },
{ "front": "Toleration được khai báo ở field nào trong Pod spec?", "back": "spec.tolerations, gồm các field key, operator, value, effect." },
{ "front": "Operator mặc định của một toleration là gì?", "back": "Equal." },
{ "front": "Taint mặc định trên node control-plane?", "back": "node-role.kubernetes.io/control-plane:NoSchedule" },
{ "front": "Lệnh xem taint của node?", "back": "kubectl describe node node01 | grep -i taint" },
{ "front": "nodeName khác nodeSelector ra sao?", "back": "nodeName bỏ qua scheduler, gán thẳng pod vào node và không kiểm tra taint; nodeSelector do scheduler xử lý." },
{ "front": "Lệnh gắn label disk=ssd cho node01?", "back": "kubectl label node node01 disk=ssd, thêm --overwrite nếu label đã tồn tại với giá trị khác." }
]