Pod Topology Spread Constraints — Phân Bố Theo Failure Domain
Vì sao topology spread là công cụ HA mặc định đúng
Mọi service production đều có một yêu cầu ngầm: mất một node, một zone, hay một region không được làm gãy toàn bộ service. Trong Kubernetes, công cụ trực tiếp nhất để diễn đạt yêu cầu này là Pod Topology Spread Constraints — kiểm soát cách Pod được rải qua các failure domain như region, zone, node, hoặc miền do người dùng tự định nghĩa (Pod Topology Spread Constraints).
Trước khi có topology spread, người ta dùng pod anti-affinity để phân tán replica — nhưng như đã phân tích ở file 3, anti-affinity required dễ gây Pending vĩnh viễn và đắt ở scale lớn, còn anti-affinity preferred thì không kiểm soát được mức độ phân bố. Topology spread giải quyết cả hai: nó cho phép khai báo "lệch tối đa bao nhiêu là chấp nhận được" (maxSkew) thay vì "tuyệt đối không chung domain", và scheduler có cơ chế tối ưu riêng nên rẻ hơn anti-affinity. Tài liệu Kubernetes mô tả nó là "cách tiếp cận khai báo linh hoạt hơn so với podAffinity/podAntiAffinity" để kiểm soát phân bố Pod trong khi vẫn đảm bảo HA và sử dụng tài nguyên hiệu quả (Pod Topology Spread Constraints).
Internal model: cấu trúc constraint
spec:
topologySpreadConstraints:
- maxSkew: <integer> # bắt buộc, > 0
minDomains: <integer> # tùy chọn, > 0
topologyKey: <string> # bắt buộc
whenUnsatisfiable: DoNotSchedule | ScheduleAnyway
labelSelector: <object> # chọn Pod để đếm
matchLabelKeys: <list> # tùy chọn (beta 1.27+)
nodeAffinityPolicy: Honor | Ignore # tùy chọn (GA 1.33)
nodeTaintsPolicy: Honor | Ignore # tùy chọn (beta 1.26+)Công thức skew — trái tim của cơ chế
Đây là phần phải nắm chính xác, vì mọi hành vi đều suy ra từ nó. Theo tài liệu:
skew = (số Pod khớp trong topology hiện tại) − (global minimum số Pod khớp qua các topology)Trong đó global minimum là số Pod khớp nhỏ nhất trong một domain đủ điều kiện (eligible domain), hoặc bằng 0 nếu số domain đủ điều kiện nhỏ hơn minDomains (Pod Topology Spread Constraints).
Ví dụ cụ thể với topologyKey: topology.kubernetes.io/zone và 3 zone:
- Zone A: 2 Pod, Zone B: 1 Pod, Zone C: 1 Pod → global min = 1.
- Skew của A = 2 − 1 = 1; skew của B = 0; skew của C = 0.
- Nếu
maxSkew: 1: đặt thêm Pod vào A sẽ làm skew A = 3 − 1 = 2 > 1 → vi phạm. Scheduler chỉ được đặt Pod mới vào B hoặc C (skew sau khi đặt = 1, hợp lệ).
maxSkew chính là "độ lệch tối đa cho phép giữa domain đông nhất và domain ít nhất". maxSkew: 1 nghĩa là phân bố gần như đều tuyệt đối; maxSkew lớn hơn nới lỏng dần.
whenUnsatisfiable: ý nghĩa thay đổi theo giá trị
Đây là trường quyết định constraint là cứng hay mềm, và nó còn đổi cả ngữ nghĩa của maxSkew (Pod Topology Spread Constraints):
DoNotSchedule(mặc định): ràng buộc cứng, đánh giá ở pha Filter.maxSkewđịnh nghĩa chênh lệch tối đa cho phép giữa domain đích và global minimum. Nếu đặt Pod vào mọi domain đều vi phạmmaxSkew, Pod Pending.ScheduleAnyway: ràng buộc mềm, đánh giá ở pha Score. Scheduler ưu tiên domain làm giảm skew, nhưng vẫn lập lịch Pod kể cả khi không domain nào thỏa hoàn hảo.
Quy tắc thực chiến: DoNotSchedule cho yêu cầu HA cứng (ví dụ "mỗi zone tối đa lệch 1 replica"); ScheduleAnyway khi muốn phân bố tốt nhưng không hy sinh khả năng lập lịch (ví dụ một zone cạn capacity vẫn cho phép dồn tạm).
minDomains: chống "gom hết vào một domain hiện có"
Một cạm bẫy tinh tế: nếu chỉ có Pod ở một zone, "global minimum" tính trên các domain đang có Pod có thể khiến scheduler không thấy lý do phân tán. minDomains giải quyết: khi số domain đủ điều kiện nhỏ hơn minDomains, global minimum bị coi như 0, ép skew của domain đang đông trở nên lớn, buộc scheduler tìm domain mới (Pod Topology Spread Constraints).
Ràng buộc: minDomains chỉ dùng được với whenUnsatisfiable: DoNotSchedule, phải > 0; không khai báo thì coi như bằng 1. Đây là công cụ ép service trải tối thiểu N zone — quan trọng cho yêu cầu "luôn ít nhất 3 zone" của hệ thống chịu lỗi cao.
nodeAffinityPolicy & nodeTaintsPolicy: domain nào được đếm
Hai trường này quyết định node nào được tính vào phép tính skew (Pod Topology Spread Constraints):
nodeAffinityPolicy(mặc địnhHonor, GA từ 1.33):Honorchỉ tính các node khớp nodeAffinity/nodeSelector của Pod;Ignoretính mọi node.nodeTaintsPolicy(mặc địnhIgnore):Honorchỉ tính node không taint hoặc node có taint mà Pod tolerate;Ignorebỏ qua taint, tính mọi node.
Tại sao quan trọng: nếu Pod có nodeAffinity giới hạn vào 2 trong 5 zone, mà policy là Ignore, scheduler tính skew trên cả 5 zone — phép tính vô nghĩa vì 3 zone kia Pod không bao giờ vào được. Honor (mặc định cho nodeAffinity) làm phép tính skew khớp với tập node Pod thực sự đặt được. Với nodeTaintsPolicy, đặc biệt lưu ý: mặc định Ignore nghĩa là node bị taint vẫn được đếm vào domain — có thể khiến skew tính sai nếu một zone toàn node taint mà Pod không tolerate.
matchLabelKeys: cứu rolling update
Vấn đề: trong một rolling update, Pod cũ (revision A) và Pod mới (revision B) cùng khớp labelSelector (cùng app). Scheduler đếm cả hai vào skew, khiến phép tính bị "ô nhiễm" bởi Pod sắp bị xóa — Pod mới có thể bị từ chối oan. matchLabelKeys giải quyết bằng cách thêm key label (thường pod-template-hash) vào phép tính: apiserver gộp các key này với labelSelector, nên skew chỉ tính trong cùng một revision (Pod Topology Spread Constraints).
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: foo
matchLabelKeys:
- pod-template-hash # tách skew theo từng revision, tránh kẹt rolling updateNếu thiếu matchLabelKeys, một DoNotSchedule maxSkew: 1 chặt có thể làm rolling update kẹt giữa chừng — đây là nguyên nhân ẩn của nhiều sự cố deploy.
Ràng buộc về tổ hợp constraint
Chỉ được có một topologySpreadConstraint cho mỗi cặp (topologyKey, whenUnsatisfiable). Bạn có thể dùng cùng topologyKey với hai whenUnsatisfiable khác nhau, nhưng không trùng cặp (Pod Topology Spread Constraints). Nhiều constraint được AND với nhau — Pod phải thỏa mọi constraint.
So sánh trực tiếp: topology spread vs podAntiAffinity
| Tiêu chí | Topology Spread | Pod Anti-Affinity |
|---|---|---|
| Diễn đạt mức độ phân bố | Có (maxSkew) | Không (chỉ "chung domain hay không") |
| Required dễ gây Pending | Ít hơn (điều chỉnh maxSkew) | Cao (replica ≥ node → kẹt vĩnh viễn) |
| Chi phí scheduler ở scale | Thấp (tối ưu riêng) | Cao (quét Pod nhiều namespace) |
| Ép tối thiểu N domain | Có (minDomains) | Không |
| Xử lý rolling update | matchLabelKeys | Khó |
| Co-location (hút) | Không hỗ trợ | podAffinity hỗ trợ |
Kết luận: để phân tán (spread), dùng topology spread; để hút (co-location), dùng podAffinity. Anti-affinity required chỉ nên dành cho ràng buộc cứng phạm vi hẹp với số replica nhỏ (file 3).
Production architecture patterns
Pattern hai tầng: zone cứng, node mềm
Pattern HA phổ biến nhất cho service production trên GKE regional cluster:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # cứng: bắt buộc trải đều qua zone
labelSelector:
matchLabels:
app: web
matchLabelKeys: [pod-template-hash]
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway # mềm: cố trải qua node nhưng không kẹt
labelSelector:
matchLabels:
app: web
matchLabelKeys: [pod-template-hash]Tầng zone DoNotSchedule đảm bảo mất một zone chỉ mất ~1/3 capacity; tầng node ScheduleAnyway cố tránh dồn Pod lên một node nhưng không hy sinh khả năng lập lịch khi node căng. Đây là cân bằng tốt giữa độ bền và tính khả thi.
Default cluster-level constraints
Kubernetes có thể cấu hình constraint mặc định ở cấp cluster (qua PodTopologySpread plugin args), áp cho Pod không tự khai báo. GKE Autopilot và một số cấu hình mặc định đã áp spread theo zone/hostname ở mức hợp lý, nhưng không nên ỷ lại: với service quan trọng, hãy khai báo tường minh để kiểm soát maxSkew và whenUnsatisfiable đúng yêu cầu thay vì phụ thuộc mặc định có thể đổi.
Real-world scenarios
Tình huống: service web trên regional cluster 3 zone
Một service web 9 replica trên cluster 3 zone. Không topology spread, scheduler (LeastAllocated) rải theo tài nguyên — có thể ra 5/3/1 hoặc tệ hơn nếu một zone có node lớn hơn. Mất zone đông nhất = mất hơn nửa capacity. Thêm maxSkew: 1 zone DoNotSchedule ép phân bố 3/3/3; mất một zone chỉ mất 1/3. Chi phí gần như bằng 0, lợi ích độ bền lớn — đây là lý do topology spread nên là mặc định cho mọi Deployment production.
Tình huống: batch job tối ưu chi phí
Một batch job 100 Pod không cần HA chặt. Dùng ScheduleAnyway ở zone giúp phân tán nhẹ để tránh hotspot nhưng không chặn lập lịch khi cần dồn vào zone rẻ/có Spot capacity. DoNotSchedule ở đây sẽ phản tác dụng — làm job kẹt khi capacity lệch giữa các zone.
Cơ chế sâu hơn: nhiều constraint kết hợp và tương tác giữa các tầng
Khi một Pod khai nhiều topologySpreadConstraints, chúng được AND với nhau — Pod chỉ lập lịch được lên node thỏa mọi constraint. Điều này tạo ra một tương tác tinh tế giữa tầng zone và tầng node mà nhiều người không lường:
Giả sử cluster 3 zone, mỗi zone 2 node (tổng 6 node), Deployment 6 replica với hai constraint DoNotSchedule maxSkew:1 — một ở zone, một ở hostname. Phân bố hợp lệ duy nhất là 2 Pod mỗi zone, mỗi Pod một node — tức mỗi node đúng 1 Pod. Bây giờ scale lên 7 replica: tầng zone cho phép (3/2/2, skew 1), nhưng tầng hostname không — vì một zone phải có node chứa 2 Pod, vi phạm maxSkew:1 hostname. Replica thứ 7 Pending. Đây là cách hai constraint cứng giao nhau thành ràng buộc chặt hơn tổng từng phần.
Bài học thiết kế: khi xếp chồng constraint cứng ở nhiều tầng, hãy tính phân bố hợp lệ ở cả số replica hiện tại và số replica đỉnh. Nếu không chắc, để tầng chi tiết hơn (hostname) ở ScheduleAnyway và chỉ giữ tầng failure-domain quan trọng (zone) ở DoNotSchedule — đó chính là pattern hai tầng đã nêu, và nó được thiết kế đúng để tránh chính cạm bẫy này.
Cơ chế sâu hơn: skew được tính lại mỗi lần schedule, không phải một lần
Một hiểu lầm phổ biến: nghĩ rằng topology spread "đặt sẵn" phân bố. Thực tế, skew được tính tại thời điểm lập lịch từng Pod, dựa trên các Pod đang tồn tại khớp selector. Nghĩa là phân bố cuối cùng phụ thuộc thứ tự Pod được lập lịch và trạng thái cluster tại mỗi thời điểm. Trong một rolling update hay sau sự cố node, thứ tự này thay đổi, và phân bố có thể lệch khỏi lý tưởng nếu constraint là ScheduleAnyway (mềm).
Hệ quả: topology spread mềm không tự "sửa" phân bố theo thời gian — nếu một zone mất rồi phục hồi, các Pod đã dồn sang zone khác không tự di về. Để phân bố luôn đúng, cần DoNotSchedule (cứng, từ chối vi phạm) cộng với cơ chế tái tạo Pod có kiểm soát (rolling restart định kỳ hoặc descheduler). Đây là giới hạn cố hữu của mọi cơ chế scheduling IgnoredDuringExecution.
Real-world scenario bổ sung: zonal capacity lệch trên regional cluster
Một regional cluster 3 zone, nhưng một zone (vd -c) thường khan hiếm loại máy c3 hơn hai zone kia. Deployment với DoNotSchedule maxSkew:1 zone trên c3 sẽ kẹt mỗi khi zone-c không có capacity c3 — vì scheduler không thể đặt Pod vào zone-c để giữ skew, mà cũng không được dồn quá vào -a/-b (vi phạm maxSkew). Kết quả: Pod Pending dù -a/-b còn chỗ.
Lời giải: hoặc dùng nodeTaintsPolicy/nodeAffinityPolicy: Honor để skew chỉ tính trên các zone thực sự có node c3 khả dụng (khi đó zone-c không đủ điều kiện sẽ không bị tính vào global min sai lệch), hoặc nới maxSkew, hoặc chuyển sang ComputeClass đa machine-family (file 9) để zone-c có thể dùng n4 thay c3. Đây là minh họa vì sao nodeAffinityPolicy: Honor (mặc định) quan trọng — nó làm phép tính skew khớp với thực tế capacity từng domain.
Khung quyết định: chọn tham số topology spread
| Yêu cầu | topologyKey | whenUnsatisfiable | maxSkew | minDomains |
|---|---|---|---|---|
| HA cứng qua zone (chuẩn production) | topology.kubernetes.io/zone | DoNotSchedule | 1 | số zone mong muốn |
| Tránh hotspot node (mềm) | kubernetes.io/hostname | ScheduleAnyway | 1 | — |
| Batch phân tán nhẹ, ưu tiên lập lịch | zone | ScheduleAnyway | 2–3 | — |
| Service buộc trải ≥3 zone | zone | DoNotSchedule | 1 | 3 |
Mọi cấu hình production nên kèm matchLabelKeys: [pod-template-hash] để rolling update không kẹt — đây không phải tùy chọn mà là yêu cầu thực tế.
Common mistakes / anti-patterns
DoNotSchedulemaxSkew: 1mà không cómatchLabelKeys→ rolling update kẹt vì Pod cũ/mới cùng được đếm. Luôn thêmpod-template-hash.- Quên
minDomainskhi yêu cầu "luôn ≥ N zone" → service có thể gom hết vào ít zone hơn mong muốn. - Hiểu sai
nodeTaintsPolicymặc địnhIgnore→ node taint vẫn được đếm vào domain, skew tính sai. Cân nhắcHonorkhi có node pool taint. - Dùng topology spread cho co-location → nó chỉ phân tán; muốn hút phải dùng podAffinity.
maxSkewquá nhỏ trên cluster tự co giãn → có thể kẹt khi số node trong một zone thay đổi nhanh; cân nhắc nớimaxSkewhoặc dùngScheduleAnyway.
GCP-native implementation guidance
Kiểm tra phân bố thực tế qua zone:
kubectl get pods -l app=web -o wide \
--sort-by='.spec.nodeName' \
-o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName
# Đối chiếu node với zone:
kubectl get nodes -L topology.kubernetes.io/zoneĐọc lý do Filter khi DoNotSchedule chặn:
kubectl describe pod <pod> | grep -i skew
# "node(s) didn't match pod topology spread constraints" → PodTopologySpread FilterTổng kết mental model
- skew = (Pod trong domain hiện tại) − (global minimum);
maxSkewlà độ lệch tối đa cho phép. DoNotSchedule= Filter (cứng, có thể Pending);ScheduleAnyway= Score (mềm).minDomainsép tối thiểu số domain (global min coi như 0 khi thiếu domain).matchLabelKeys: [pod-template-hash]là bắt buộc thực tế để rolling update không kẹt.nodeAffinityPolicy: Honor(mặc định) và cân nhắcnodeTaintsPolicy: Honorđể skew tính đúng tập node khả dụng.- Topology spread là công cụ phân tán mặc định đúng; anti-affinity chỉ cho ràng buộc cứng phạm vi hẹp.
FAQ thực chiến
maxSkew nên đặt bao nhiêu?
maxSkew: 1 cho phân bố đều nhất, phù hợp service production muốn HA tối đa. Nhưng maxSkew: 1 cứng (DoNotSchedule) dễ kẹt trên cluster tự co giãn khi số node mỗi domain thay đổi nhanh. Quy tắc: dùng maxSkew: 1 với topologyKey: zone (số zone ổn định) và whenUnsatisfiable: DoNotSchedule; dùng maxSkew lớn hơn hoặc ScheduleAnyway với topologyKey: hostname (số node biến động). Không có giá trị "đúng" tuyệt đối — nó phản ánh mức độ lệch bạn chấp nhận.
Topology spread có thay thế hoàn toàn được pod anti-affinity không?
Cho mục tiêu phân tán, gần như có. Topology spread linh hoạt hơn (diễn đạt mức độ qua maxSkew), rẻ hơn (scheduler tối ưu riêng), và an toàn hơn (ít gây Pending vĩnh viễn). Pod anti-affinity chỉ còn cần cho ràng buộc tuyệt đối cứng phạm vi hẹp với số replica nhỏ (vd 3 primary database không bao giờ chung host). Anti-affinity không diễn đạt được "lệch tối đa 1" — đó là khác biệt cốt lõi.
Vì sao rolling update của tôi kẹt khi bật topology spread cứng?
Gần như chắc chắn vì thiếu matchLabelKeys: [pod-template-hash]. Không có nó, Pod revision cũ và mới cùng khớp labelSelector, nên skew bị tính lẫn cả Pod sắp bị xóa — Pod mới bị DoNotSchedule từ chối. Thêm matchLabelKeys để skew tính riêng từng revision.
GKE có áp topology spread mặc định không?
GKE Autopilot và một số cấu hình mặc định áp spread ở mức hợp lý qua cluster-level default constraints, nhưng đừng ỷ lại. Với service quan trọng, luôn khai tường minh để kiểm soát maxSkew/whenUnsatisfiable theo đúng yêu cầu — mặc định có thể đổi giữa các phiên bản và không khớp ý đồ HA cụ thể của bạn.
Topology spread có tự sửa phân bố sau sự cố node không?
Không, với constraint mềm (ScheduleAnyway). Skew chỉ được đánh giá lúc lập lịch mỗi Pod; Pod đã chạy không tự di dời khi phân bố lệch (tính chất IgnoredDuringExecution). Sau khi một zone mất rồi phục hồi, các Pod đã dồn sang zone khác sẽ không tự quay lại để cân bằng. Muốn phân bố luôn đúng, kết hợp DoNotSchedule (từ chối mọi lần lập lịch mới vi phạm) với tái tạo Pod có kiểm soát (rolling restart định kỳ hoặc descheduler). Đừng kỳ vọng topology spread "tự chữa lành" phân bố theo thời gian.
Nên đặt topology spread ở mức Deployment hay dùng default cluster-level?
Với service production quan trọng, đặt tường minh ở Deployment để kiểm soát chính xác. Default cluster-level hữu ích như lưới an toàn cho các workload không tự khai (tránh trường hợp quên hoàn toàn), nhưng nó áp một cấu hình chung cho mọi Pod — không phù hợp khi các service có yêu cầu HA khác nhau. Pattern trưởng thành: default cluster-level "mềm" làm sàn, cộng khai báo tường minh "cứng" cho service quan trọng.