Có một nhầm lẫn logic mà ai cũng mắc ít nhất một lần: lẫn được phép với bị buộc. Tấm vé vào phòng VIP cho phép bạn bước vào, nhưng nó không lôi bạn vào — bạn hoàn toàn có thể cầm vé rồi ngồi lì ngoài sảnh. Muốn chắc chắn ngồi trong phòng VIP, bạn cần hai thứ: tấm vé (được phép) và một ghế được chỉ định (bị đưa vào). toleration của Kubernetes là tấm vé đó, và rất nhiều người khai nó rồi ngạc nhiên vì pod vẫn ngồi ngoài sảnh. Bốn cơ chế cùng điều khiển pod chạy ở đâu, và chúng dễ lẫn vào nhau — bài này đo từng cái trên cụm thật.
Ba loại vết
sc-worker = NoSchedule sc-worker2 = PreferNoSchedule
sc-worker3 = không vết
Tạo 6 pod không khai toleration nào:
6 pod đều lên sc-worker3
Rồi đổi sc-worker3 thành NoExecute:
mọi pod bị đuổi khỏi sc-worker3 sau 1,3 giây
| Tác dụng | |
|---|---|
NoSchedule |
Chặn pod mới |
PreferNoSchedule |
Cố gắng tránh, vẫn nhận nếu cần |
NoExecute |
Chặn pod mới và đuổi pod đang chạy |
1,3 giây cho NoExecute — nhanh hơn nhiều so với mọi cơ chế đuổi khác trong Kubernetes. So với phần 42: node chết thì mất gần sáu phút; node bị đánh vết thì 1,3 giây. Khác biệt là ở chỗ Kubernetes biết chắc: bạn vừa nói với nó rằng node này không được chạy pod. Không cần suy đoán gì.
Đây cũng là cơ chế đằng sau việc đuổi pod khi node NotReady — Kubernetes tự thêm vết node.kubernetes.io/unreachable:NoExecute, và tolerationSeconds mặc định 300 giây là con số tạo ra sáu phút ở phần 42.
toleration cho phép, không ép
Thêm toleration:
tolerations:
- {key: vet, operator: Exists}
đủ 6 pod chạy lại sau 2,1 giây
2 sc-worker 3 sc-worker2 2 sc-worker3
Pod rải ra cả ba node — nhưng toleration không ép pod lên node có vết. Nó chỉ cho phép. Đây là điểm hay nhầm nhất: khai toleration cho node GPU không khiến pod chạy trên node GPU. Nó chỉ khiến pod được phép chạy ở đó. Muốn ép thì cần nodeSelector hoặc nodeAffinity.
Cặp đúng cho node chuyên dụng là cả hai:
nodeSelector: {phan-cung: gpu} # ép pod lên node GPU
tolerations: # cho phép vượt qua vết
- {key: gpu, operator: Exists, effect: NoSchedule}
Vết giữ pod khác ra khỏi node GPU; nodeSelector đưa pod của bạn vào.
podAntiAffinity: cứng chặn, mềm chỉ ưu tiên
5 bản, topologyKey: kubernetes.io/hostname, cụm có 3 worker.
Cứng (requiredDuringSchedulingIgnoredDuringExecution):
3 Running (một mỗi node) 2 Pending
0/4 nodes are available: 3 node(s) didn't match pod anti-affinity rules
Mềm (preferredDuringSchedulingIgnoredDuringExecution):
5 Running
1 sc-worker 2 sc-worker2 2 sc-worker3
Cứng bảo đảm mỗi node đúng một bản, đổi lại số bản bị trần bởi số node. Mềm rải đều hết mức có thể, nhưng không bao giờ để pod Pending.
Với hầu hết ứng dụng, mềm là lựa chọn đúng: bạn muốn rải đều để một node chết không mất hết, nhưng không muốn một sự cố node biến thành pod không chạy được. Cứng chỉ đúng khi hai bản trên cùng node là sai về nghĩa — ví dụ mỗi bản cần một cổng hostPort riêng.
topologySpreadConstraints: cách hiện đại hơn
podAntiAffinity chỉ nói "không cùng chỗ". topologySpreadConstraints nói mức lệch cho phép:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector: {matchLabels: {app: web}}
maxSkew: 1 nghĩa là chênh lệch giữa vùng nhiều nhất và ít nhất không quá 1. Đó là thứ bạn thật sự muốn — không phải "tối đa một bản mỗi nơi". whenUnsatisfiable có hai giá trị, tương ứng với cứng và mềm ở trên: DoNotSchedule và ScheduleAnyway. Với cụm nhiều vùng, đây là công cụ đúng. podAntiAffinity cứng trên zone sẽ giới hạn bạn ở đúng ba bản.
IgnoredDuringExecution nghĩa là gì
Cả hai dạng affinity đều kết thúc bằng IgnoredDuringExecution, và nó có nghĩa đen: luật chỉ áp lúc xếp chỗ, không áp sau đó. Pod đã chạy sẽ ở nguyên chỗ dù luật bị vi phạm về sau — ví dụ ai đó gỡ nhãn khỏi node, hoặc thêm pod khiến anti-affinity không còn thoả. Đây là khác biệt cơ bản với vết NoExecute, thứ có áp lúc chạy và đuổi pod ngay.
Bốn cơ chế, một bảng
| Ai điều khiển | Áp lúc nào | |
|---|---|---|
taint + toleration |
node đẩy pod ra | xếp chỗ, và cả lúc chạy với NoExecute |
nodeSelector |
pod chọn node | chỉ xếp chỗ |
nodeAffinity |
pod chọn node, có mức mềm | chỉ xếp chỗ |
podAffinity / podAntiAffinity |
pod chọn theo pod khác | chỉ xếp chỗ |
Hàng đầu là cơ chế từ chối; ba hàng sau là cơ chế thu hút. Node chuyên dụng cần cả hai loại.
Mẫu số chung: "cho phép" không phải "ép", và ràng buộc cứng đánh đổi khả dụng lấy bảo đảm
Bài này gói hai bài học đúng ở mọi hệ có luật đặt chỗ. Thứ nhất, một nhầm lẫn logic cốt lõi: "được phép" (may) không phải "bị buộc" (must), và "giữ kẻ khác ra" là một việc tách biệt với "đưa mình vào". toleration chỉ cấp vé, nodeSelector mới chỉ ghế; node chuyên dụng cần cả đẩy (vết) lẫn kéo (selector). Cùng cái bẫy khắp nơi: một luật firewall cho phép gói tin đi qua nhưng không định tuyến nó tới đâu; một header CORS cho phép một origin nhưng không gửi request thay bạn; một GRANT cho phép truy cập chứ không gây ra truy cập; một phụ thuộc tuỳ chọn có sẵn không có nghĩa nó đang được dùng. Và vế thứ hai — loại-trừ so với thu-nạp là hai phép toán khác nhau — cũng lặp lại: một ràng buộc UNIQUE giữ trùng lặp ra ngoài nhưng không bỏ giá trị nào vào, một mutex chặn kẻ khác nhưng bạn vẫn phải tự giành lấy. Đọc một cơ chế, luôn hỏi: nó cho phép hay bắt buộc, nó đẩy ra hay kéo vào — và bạn có cần cả hai không.
Thứ hai: một ràng buộc cứng đánh đổi khả dụng lấy một bảo đảm, và cái giá đó thường đắt hơn bạn tưởng — hãy mặc định chọn mềm (xuống cấp êm) trừ khi việc vi phạm là sai về nghĩa. Anti-affinity cứng khoá số bản bằng số node và để 2 pod Pending; mềm rải đều mà không bao giờ kẹt. Cùng lằn ranh với required so với preferred, với một khoá ngoại bắt buộc so với một tham chiếu cho phép rỗng, với một schema từ chối so với một schema chỉ cảnh báo, với việc ép chặt một trình tối ưu SQL bằng hint tới mức nó không tìm nổi kế hoạch. Bảo đảm cứng chỉ đáng khi vi phạm nó là hỏng thật, không phải chỉ không đẹp.
Mẫu số chung: phân biệt cho-phép với bắt-buộc và đẩy-ra với kéo-vào trước khi tin một cơ chế làm điều bạn nghĩ; và chỉ dùng ràng buộc cứng khi vi phạm nó là sai về bản chất — còn lại, ưu tiên mềm để một sự cố không biến thành bế tắc.
Muốn tự soi, xem vết nào đang tồn tại trên cụm của bạn:
kubectl get nodes -o custom-columns=\
TEN:.metadata.name,VET:.spec.taints[*].key,HIEU_LUC:.spec.taints[*].effect
Và pod nào có anti-affinity cứng — tức có thể kẹt Pending khi thiếu node:
kubectl get pods -A -o json | python3 -c '
import sys,json
for p in json.load(sys.stdin)["items"]:
a=(p["spec"].get("affinity") or {}).get("podAntiAffinity",{})
if a.get("requiredDuringSchedulingIgnoredDuringExecution"):
print(p["metadata"]["namespace"], p["metadata"]["name"])
'
Mỗi dòng là một ứng dụng mà số bản bị trần bởi số node — và điều đó chỉ lộ ra khi bạn thử mở rộng.
Phần sau đo PodDisruptionBudget: một dòng cấu hình sai khiến node không bao giờ rút ra bảo trì được.