Skip to content

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ới weight từ 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 matchExpressions trong cùng một term: quan hệ ANDmọi expression phải thỏa.
yaml
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 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/zoneZone của node
topology.kubernetes.io/regionRegion
cloud.google.com/gke-nodepoolTên node pool
cloud.google.com/machine-familyHọ máy (n4, c3, e2...)
cloud.google.com/gke-spotNode Spot hay không
cloud.google.com/gke-acceleratorLoại GPU (xem file 8)
kubernetes.io/archamd64 / 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:

yaml
# ANTI-PATTERN
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: my-service
      topologyKey: kubernetes.io/hostname

Rà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 preferredDuringScheduling nếu chỉ cần phân tán tốt nhất có thể.
  • Tốt hơn: dùng topologySpreadConstraints với maxSkewwhenUnsatisfiable: ScheduleAnyway để phân tán mềm, hoặc DoNotSchedule nhưng với maxSkew đủ 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ứngphạ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 không cùng zone. Ở đây required anti-affinity ở topologyKey: kubernetes.io/hostname 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:

yaml
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.
  • namespaceSelector rỗng (toàn cluster) trên inter-pod affinity → quét toàn cluster, chậm scheduler cho mọi người.
  • Ghim nodeAffinity theo 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 taint NoExecute.
  • Trộn nhiều nodeSelectorTerms mà 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:

yaml
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:

bash
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 Filter

Tổ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).

References