Node Affinity & Inter-Pod Affinity/Anti-Affinity
Vì sao affinity vừa mạnh vừa nguy hiểm
Affinity là tập đòn bẩy biểu cảm nhất mà người dùng có để điều khiển scheduler — và cũng là nguồn gốc của nhiều Pod Pending vĩnh viễn nhất. Khác với requests (scheduler tự suy ra node phù hợp) hay taint (node tự bảo vệ), affinity là ý định tường minh của người vận hành: "đặt Pod này gần/xa thứ kia". Khi ý định đó bất khả thi với trạng thái cluster thật, scheduler không "cố hết sức" — với ràng buộc required, nó đơn giản từ chối, và Pod kẹt Pending cho đến khi cluster thay đổi hoặc spec được sửa.
Có hai họ affinity hoàn toàn khác nhau về cơ chế và chi phí:
- Node affinity: ràng buộc theo label của node (zone, machine type, GPU, nhãn tùy chỉnh). Rẻ, đánh giá nhanh.
- Inter-pod affinity/anti-affinity: ràng buộc theo label của các Pod khác đang chạy. Đắt, đặc biệt anti-affinity ở scale lớn.
Trộn lẫn hai họ này là sai lầm phổ biến. Chương này tách bạch chúng và nêu rõ chi phí thực của từng loại.
Internal model: Node Affinity
Node affinity là phiên bản biểu cảm hơn của nodeSelector, với hai biến thể (Assigning Pods to Nodes):
requiredDuringSchedulingIgnoredDuringExecution: scheduler không thể đặt Pod trừ khi rule thỏa. Đây là ràng buộc cứng, đánh giá ở pha Filter.preferredDuringSchedulingIgnoredDuringExecution: scheduler cố tìm node khớp; nếu không có, vẫn đặt Pod. Đây là ràng buộc mềm, đánh giá ở pha Score vớiweighttừ 1 đến 100.
Hậu tố IgnoredDuringExecution mang ý nghĩa sống còn: nếu label node đổi sau khi Pod đã được lập lịch, Pod vẫn tiếp tục chạy — affinity chỉ tác động lúc scheduling, không di dời Pod đang chạy. Kubernetes không có biến thể RequiredDuringExecution cho node affinity; nếu cần đuổi Pod khi điều kiện node thay đổi, công cụ là taint NoExecute (file 5), không phải affinity.
Operators và logic kết hợp
Trường operator hỗ trợ: In, NotIn, Exists, DoesNotExist, Gt, Lt (Assigning Pods to Nodes). Gt/Lt so sánh theo chuỗi số nguyên — hữu ích cho nhãn kiểu phiên bản hay dung lượng.
Logic kết hợp dễ nhầm và phải nắm chính xác:
- Nhiều
nodeSelectorTerms: quan hệ OR — Pod lập lịch được nếu bất kỳ term nào thỏa. - Nhiều
matchExpressionstrong cùng một term: quan hệ AND — mọi expression phải thỏa.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions: # term 1 (mọi expr trong term AND nhau)
- key: topology.kubernetes.io/zone
operator: In
values: ["asia-southeast1-a", "asia-southeast1-b"]
- key: cloud.google.com/gke-spot
operator: NotIn
values: ["true"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80 # mềm: cộng 80 vào điểm node khớp
preference:
matchExpressions:
- key: cloud.google.com/machine-family
operator: In
values: ["c3"]Với preferred, scheduler cộng dồn weight của mọi preference thỏa vào điểm node; node tổng điểm cao nhất được ưu tiên. Nhiều preference với weight khác nhau cho phép biểu đạt thứ tự ưu tiên tinh vi (ví dụ "ưu tiên c3, nếu không thì n4, nếu không thì gì cũng được").
Lưu ý: nếu khai báo cả nodeSelector và nodeAffinity, cả hai phải thỏa (Assigning Pods to Nodes).
Node affinity trên GKE: các label sẵn có
GKE tự gắn nhiều label hệ thống lên node, dùng trực tiếp trong node affinity:
| Label | Ý nghĩa |
|---|---|
topology.kubernetes.io/zone | Zone của node |
topology.kubernetes.io/region | Region |
cloud.google.com/gke-nodepool | Tên node pool |
cloud.google.com/machine-family | Họ máy (n4, c3, e2...) |
cloud.google.com/gke-spot | Node Spot hay không |
cloud.google.com/gke-accelerator | Loại GPU (xem file 8) |
kubernetes.io/arch | amd64 / arm64 |
Ghim Pod vào một node pool cụ thể qua cloud.google.com/gke-nodepool là pattern phổ biến nhưng cần cân nhắc: nó loại bỏ tính linh hoạt mà cluster autoscaler cần. Thường tốt hơn là ghim theo thuộc tính (machine-family, arch) thay vì theo tên pool.
Internal model: Inter-Pod Affinity & Anti-Affinity
Inter-pod affinity ràng buộc Pod dựa trên label của các Pod khác đang chạy trên node, thay vì label node (Assigning Pods to Nodes). Các trường then chốt:
topologyKey(bắt buộc): key label node định nghĩa "miền topology". Hai node có cùng giá trị label này thuộc cùng một miền. Không được rỗng với required anti-affinity.labelSelector: chọn các Pod đích theo label của chúng.namespaceSelector/namespaces: chọn namespace chứa Pod đích (mặc định là namespace của Pod đang lập lịch).
Cùng có required (Filter) và preferred (Score, kèm weight).
topologyKey: nơi sức mạnh và chi phí gặp nhau
topologyKey quyết định "đơn vị co-location/spread":
kubernetes.io/hostname: miền = một node. Anti-affinity hostname nghĩa là "không hai Pod khớp selector trên cùng node".topology.kubernetes.io/zone: miền = một zone. Anti-affinity zone nghĩa là "không hai Pod cùng zone".
Đây là điểm sức mạnh: pod affinity cho phép diễn đạt "đặt cache gần web cùng zone để giảm latency cross-zone" hay "đảm bảo replica database không cùng node". Nhưng cũng là điểm chi phí — phần dưới.
Pod affinity vs anti-affinity — use case
Pod affinity (hút):
- Co-location giảm latency: đặt Pod đọc/ghi gần nhau trong cùng zone.
- Gom workload chia sẻ cache cục bộ hoặc volume.
Pod anti-affinity (đẩy):
- HA: phân tán replica để mất một node/zone không làm gãy service.
- Tránh "noisy neighbor": không đặt hai workload ngốn tài nguyên trên cùng node.
Failure modes & chi phí ở scale lớn
Cảnh báo hiệu năng chính thức
Tài liệu Kubernetes cảnh báo trực tiếp: inter-pod affinity, đặc biệt là podAffinity, có thể gây chậm đáng kể (significant slowdown) trong cluster lớn, vì scheduler phải kiểm tra label trên nhiều Pod ở nhiều namespace (Assigning Pods to Nodes). Lý do: với mỗi node ứng viên, scheduler phải duyệt các Pod liên quan để xác định miền topology có thỏa không — độ phức tạp tăng theo số Pod × số namespace cần xét. Trên cluster hàng nghìn Pod, điều này đẩy scheduling_attempt_duration_seconds (file 1) lên cao.
Khuyến nghị thực chiến: giới hạn phạm vi namespaceSelector và tránh dùng inter-pod affinity rộng (selector khớp quá nhiều Pod). Nếu mục tiêu chỉ là phân bố replica của chính Deployment đó, dùng topology spread constraints (file 4) — rẻ hơn nhiều vì scheduler có cơ chế tối ưu riêng cho spread.
Anti-pattern kinh điển: required anti-affinity hostname với replica > node
Đây là sai lầm gặp ở gần như mọi tổ chức ít nhất một lần. Cấu hình:
# ANTI-PATTERN
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: my-service
topologyKey: kubernetes.io/hostnameRàng buộc này nói "không hai Pod my-service trên cùng node". Nếu Deployment có 10 replica nhưng cluster chỉ 6 node, thì 4 Pod sẽ kẹt Pending vĩnh viễn — không node nào còn "trống" theo định nghĩa anti-affinity, và scheduler đúng đắn từ chối. Tệ hơn, nó còn chặn cả cluster autoscaler trong một số trường hợp vì node mới cũng chỉ chứa được một replica.
Vì sao nó xảy ra: người ta dùng required anti-affinity để "đảm bảo HA tuyệt đối", không lường trước ràng buộc cứng với số node.
Hệ quả ở scale: rolling update kẹt (Pod mới không có chỗ vì Pod cũ chưa chết, mà mỗi node chỉ một Pod), scale-up bị giới hạn bởi số node thay vì tài nguyên.
Cách phòng tránh:
- Đổi sang
preferredDuringSchedulingnếu chỉ cần phân tán tốt nhất có thể. - Tốt hơn: dùng
topologySpreadConstraintsvớimaxSkewvàwhenUnsatisfiable: ScheduleAnywayđể phân tán mềm, hoặcDoNotSchedulenhưng vớimaxSkewđủ lớn để không kẹt (file 4).
Bẫy IgnoredDuringExecution
Vì affinity chỉ tác động lúc schedule, một cluster có thể trôi khỏi ý định ban đầu: bạn đặt anti-affinity để phân tán replica, nhưng sau một sự cố node + recovery, các replica có thể tái lập lịch lệch khỏi phân bố mong muốn nếu ràng buộc là preferred. Affinity không "tự sửa" phân bố theo thời gian. Nếu cần đảm bảo phân bố liên tục, topology spread (đánh giá lại mỗi lần schedule) cộng với việc tái tạo Pod theo lô là cách thực tế hơn.
Production architecture patterns
Pattern: co-location giảm chi phí cross-zone egress
Lưu lượng cross-zone trên GCP tốn phí. Đặt Pod gọi nhau nhiều (ví dụ app ↔ cache) cùng zone bằng podAffinity preferred với topologyKey: topology.kubernetes.io/zone giảm cả latency lẫn chi phí egress. Dùng preferred (không required) để không hy sinh khả năng lập lịch khi một zone cạn capacity.
Pattern: tách workload theo thuộc tính node, không theo tên pool
Thay vì nodeAffinity ghim cloud.google.com/gke-nodepool: pool-x (cứng nhắc, cản autoscaler), ghim theo thuộc tính như cloud.google.com/machine-family: c3 hoặc kubernetes.io/arch: arm64. Cách này cho phép GKE tự tạo/scale node pool phù hợp (kết hợp ComputeClasses ở file 9) mà vẫn đảm bảo Pod chạy đúng loại phần cứng.
Pattern: HA bằng topology spread, không bằng anti-affinity
Với phần lớn nhu cầu "phân tán replica để HA", topology spread là lựa chọn mặc định đúng đắn, để dành anti-affinity cho các ràng buộc thực sự cứng và phạm vi hẹp (ví dụ "không hai Pod của StatefulSet database nhạy cảm trên cùng node", với số replica nhỏ và đảm bảo đủ node).
Real-world scenarios
Tình huống: nền tảng nội bộ với hàng trăm namespace
Một internal platform chạy hàng trăm team, mỗi team một namespace. Một team thêm podAffinity với namespaceSelector: {} (mọi namespace) để "gom gần Pod loại X". Hệ quả: mỗi lần schedule Pod đó, scheduler quét label Pod khắp toàn cluster — scheduling_attempt_duration_seconds tăng vọt, ảnh hưởng mọi team. Bài học: inter-pod affinity là tài nguyên dùng chung của scheduler; phải giới hạn namespaceSelector và review ở cấp platform.
Tình huống: fintech với database StatefulSet 3 replica
Ba replica của primary database tuyệt đối không được cùng node và không cùng zone. Ở đây required anti-affinity ở topologyKey: kubernetes.io/hostname là lựa chọn đúng — vì số replica (3) nhỏ hơn nhiều số node, không gây kẹt; và yêu cầu là cứng (không chấp nhận hai primary chung node). Đây là minh họa: required anti-affinity không xấu, nó chỉ xấu khi replica > node hoặc phạm vi quá rộng.
Cơ chế sâu hơn: affinity tương tác với cluster autoscaler
Một khía cạnh ít được nói tới nhưng gây nhiều sự cố production là cách affinity tương tác với cluster autoscaler (Chương 9). Khi một Pod Pending vì nodeAffinity required, autoscaler phải mô phỏng xem node mới (loại nào, ở pool nào) có thỏa affinity đó không trước khi quyết định scale-up. Với affinity theo thuộc tính chuẩn (zone, machine-family, arch), autoscaler suy ra được vì các node-pool template mang sẵn label tương ứng. Nhưng với label tùy chỉnh không có trên template node pool, autoscaler không biết node mới sẽ có label đó — nên nó không scale-up, và Pod kẹt Pending mãi mãi dù bạn nghĩ "autoscaler sẽ lo".
Quy tắc thực chiến: nếu dùng nodeAffinity required theo label tùy chỉnh, label đó phải được khai trên node pool (qua --node-labels) để autoscaler nhận diện. Đây là lý do nữa để ưu tiên ghim theo thuộc tính chuẩn (GKE tự gắn và autoscaler hiểu) thay vì label tự chế.
Với inter-pod affinity, vấn đề còn nặng hơn: autoscaler khó mô phỏng vì nó phụ thuộc các Pod khác đang chạy, không chỉ thuộc tính node. Tài liệu autoscaler nêu rõ inter-pod affinity là một trong các trường hợp autoscaler xử lý hạn chế. Đây là một lý do kỹ thuật nữa để tránh inter-pod affinity rộng trên cluster tự co giãn — nó làm autoscaler ra quyết định kém chính xác.
Cơ chế sâu hơn: thứ tự đánh giá và tương tác giữa các ràng buộc
Khi một Pod khai nhiều loại ràng buộc cùng lúc (nodeAffinity + podAntiAffinity + topology spread + taint toleration), tất cả được đánh giá ở pha Filter (với phần required) và phải thỏa đồng thời — quan hệ AND. Một node chỉ khả thi nếu vượt qua mọi Filter. Hệ quả: càng nhiều ràng buộc required, tập node khả thi càng co lại nhanh, và rủi ro Pending tăng theo cấp số nhân.
Sai lầm thường gặp là chồng nhiều ràng buộc cứng "cho chắc" mà không nhận ra chúng giao nhau thành tập rỗng. Ví dụ: nodeAffinity ép zone-a + podAntiAffinity hostname + topology spread DoNotSchedule maxSkew:1 — nếu zone-a chỉ có 2 node mà cần 3 replica không chung host, ba ràng buộc này không thể thỏa cùng lúc. Khi thiết kế, hãy nghĩ theo "tập node còn lại sau mỗi Filter" và kiểm tra tập đó không rỗng ở đỉnh tải.
Một heuristic hữu ích: với mỗi ràng buộc required bạn thêm, tự hỏi "ràng buộc này loại đi bao nhiêu % node, và phần còn lại có đủ cho số replica ở đỉnh tải + headroom upgrade không?". Nếu không trả lời được bằng số, ràng buộc đó là rủi ro.
Real-world scenario bổ sung: migration sang ARM tiết kiệm chi phí
Nhiều đội chuyển một phần workload stateless sang node ARM (Axion/Tau T2A) để tiết kiệm. Cạm bẫy scheduling: image amd64 chạy nhầm trên node ARM sẽ crash (exec format error). Lời giải đúng là kết hợp nodeAffinity theo kubernetes.io/arch với image multi-arch:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["arm64"]Nhưng nếu image không multi-arch, ràng buộc này đẩy Pod lên ARM rồi crash — tệ hơn Pending. Bài học: affinity chỉ đảm bảo vị trí, không đảm bảo tương thích runtime. Phải kết hợp affinity với image phù hợp, và lý tưởng là dùng ComputeClass (file 9) để GKE quản lý việc khớp arch một cách nhất quán.
Common mistakes / anti-patterns
- Required anti-affinity hostname với replica ≥ số node → Pending vĩnh viễn. Dùng topology spread.
namespaceSelectorrỗng (toàn cluster) trên inter-pod affinity → quét toàn cluster, chậm scheduler cho mọi người.- Ghim
nodeAffinitytheo tên pool → cản autoscaler; ghim theo thuộc tính node. - Dùng affinity để mong "di dời Pod khi điều kiện đổi" → affinity là
IgnoredDuringExecution, không di dời. Dùng taintNoExecute. - Trộn nhiều
nodeSelectorTermsmà nghĩ là AND → thực ra là OR; kiểm tra kỹ logic kết hợp.
GCP-native implementation guidance
Phân tán mềm replica qua zone (thay anti-affinity required), kết hợp ghim phần cứng theo thuộc tính:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution: # mềm, không gây Pending
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: my-service
topologyKey: kubernetes.io/hostnameĐọc lý do Filter loại node do affinity:
kubectl describe pod <pod> | grep -A3 Events
# "node(s) didn't match pod affinity rules" → InterPodAffinity Filter
# "node(s) didn't match Pod's node affinity/selector" → NodeAffinity FilterTổng kết mental model
- Node affinity (label node) rẻ; inter-pod affinity (label Pod khác) đắt, anti-affinity ở scale lớn rất đắt.
required= Filter (gây Pending nếu bất khả thi);preferred= Score (chỉ ưu tiên).IgnoredDuringExecution: affinity không di dời Pod đang chạy.- Anti-pattern số một: required anti-affinity hostname với replica ≥ số node → Pending vĩnh viễn.
- Cho nhu cầu phân tán HA, mặc định dùng topology spread; để dành anti-affinity cho ràng buộc cứng, phạm vi hẹp.
FAQ thực chiến
Khi nào dùng nodeSelector thay vì nodeAffinity?
nodeSelector là cú pháp đơn giản cho ràng buộc cứng theo label (AND tất cả). Dùng nó khi chỉ cần "node phải có label X=Y" và không cần biểu thức phức tạp hay ràng buộc mềm. Khi cần operator (In/NotIn/Gt/Lt), nhiều term OR, hay phần preferred, mới cần nodeAffinity. Về Filter, cả hai do cùng plugin NodeAffinity xử lý — nodeSelector chỉ là dạng rút gọn.
Affinity có ảnh hưởng tới Pod đang chạy không?
Không. Mọi affinity hiện tại đều là IgnoredDuringExecution — chỉ tác động lúc lập lịch. Label node/Pod đổi sau đó không di dời Pod. Muốn phản ứng với thay đổi điều kiện node lúc runtime, công cụ là taint NoExecute (file 5), không phải affinity.
Vì sao Pod của tôi vẫn không vào đúng node dù đã có toleration cho node đó?
Vì toleration cho phép nhưng không kéo. Nếu node có taint dành riêng, toleration giúp Pod được phép vào, nhưng scheduler vẫn có thể đặt nó nơi khác. Để bắt buộc vào đúng node/pool, cần thêm nodeAffinity/nodeSelector theo label của node đó — đây là cặp taint+affinity của pattern dedicated nodes.
Inter-pod affinity preferred có "an toàn" để dùng rộng rãi không?
An toàn hơn required (không gây Pending), nhưng vẫn tốn chi phí scheduler vì phải đánh giá ở pha Score trên nhiều node và nhiều Pod. Trên cluster lớn với selector rộng, ngay cả preferred cũng đẩy scheduling_attempt_duration_seconds lên. Giới hạn namespaceSelector và selector hẹp vẫn là quy tắc, kể cả với preferred.
Có nên ghim Pod vào một node pool cụ thể bằng tên không?
Hạn chế tối đa. Ghim cloud.google.com/gke-nodepool: <tên> khóa Pod vào đúng pool đó, làm cluster autoscaler mất linh hoạt (không thể chọn pool khác có capacity) và gây Pending khi pool đó cạn dù pool khác còn chỗ. Tốt hơn là ghim theo thuộc tính phần cứng (machine-family, arch, GPU type) — vừa diễn đạt đúng ý định "loại máy nào", vừa cho GKE/autoscaler tự do chọn pool phù hợp. Chỉ ghim theo tên pool khi thực sự cần cô lập một pool đặc biệt và đã chấp nhận đánh đổi linh hoạt.
Affinity và topology spread dùng cùng lúc được không?
Được, và thường nên. nodeAffinity giới hạn tập node hợp lệ (vd chỉ amd64, chỉ một số zone), còn topologySpreadConstraints phân bố Pod trong tập đó. Khi kết hợp, đặt nodeAffinityPolicy: Honor (mặc định) để phép tính skew chỉ tính các node mà affinity cho phép — nếu không, skew tính trên cả node Pod không bao giờ vào được, ra kết quả sai (file 4).