ข้ามไปยังเนื้อหา

Node Affinity, Taint และ Toleration

Affinity ดึง Pod เข้าหา Node (หรือ Pod อื่น) บางตัว topologySpreadConstraints กระจาย Pod กลุ่มหนึ่งให้เท่ากันข้าม topology ส่วน taint ผลัก Pod ออกจาก Node เว้นแต่ Pod จะมี toleration ที่ match — กลไกที่มักถูกใช้ร่วมกัน

วิธีง่ายสุดในการจำกัดว่า Pod รันที่ไหนคือ nodeSelector — match แบบ exact กับ label ของ Node ถ้า Node ไม่มีทุก label ที่ระบุไว้ จะไม่ถูกนับเป็นตัวเลือกเลย

apiVersion: v1
kind: Pod
metadata:
name: gpu-inference
spec:
nodeSelector:
hardware: gpu
containers:
- name: inference
image: inference-server:2.1.0

Node affinity ทำงานแบบเดียวกันแต่ใช้ expression ที่ยืดหยุ่นกว่า (In, NotIn, Exists และอื่น ๆ) และที่สำคัญคือเลือกได้ว่ากฎนั้นเข้มงวดแค่ไหน

  • requiredDuringSchedulingIgnoredDuringExecution — ข้อบังคับแบบแข็ง ถ้าไม่มี Node ไหนตรงเงื่อนไข Pod จะค้างอยู่ที่ Pending
  • preferredDuringSchedulingIgnoredDuringExecution — ความชอบแบบอ่อนพร้อม weight scheduler จะเลือก Node ที่ match ก่อน แต่ถ้าจำเป็นก็ยังวาง Pod ที่อื่นได้
apiVersion: v1
kind: Pod
metadata:
name: gpu-inference-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: hardware
operator: In
values:
- gpu
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: zone
operator: In
values:
- preferred
containers:
- name: inference
image: inference-server:2.1.0

ทั้งสอง suffix ลงท้ายด้วย IgnoredDuringExecution หมายความว่าเมื่อ Pod รันอยู่แล้ว การเปลี่ยน label ของ Node ภายหลังจะไม่ทำให้ Pod ถูก evict — กฎพวกนี้มีผลแค่ตอนตัดสินใจ schedule ครั้งแรกเท่านั้น พิสูจน์ว่า preferred steers การวาง Pod จริงโดยไม่บังคับ: label worker ตัวเดียวเป็น zone=preferred แล้วสร้าง Pod ที่มีแค่ soft preference นั้น (ไม่มี nodeSelector ไม่มีข้อบังคับแบบแข็ง) มันลงที่ worker ตัวนั้นทุกครั้ง จากตัวเลือกที่เหมือนกันสามตัว

$ kubectl label node scheduling-worker2 zone=preferred
$ kubectl get pod prefers-zone -o wide
NAME READY STATUS NODE
prefers-zone 1/1 Running scheduling-worker2

Pod affinity กับ anti-affinity: schedule โดยอ้างอิง Pod อื่น พิสูจน์แบบ all-or-nothing

หัวข้อที่มีชื่อว่า “Pod affinity กับ anti-affinity: schedule โดยอ้างอิง Pod อื่น พิสูจน์แบบ all-or-nothing”

Node affinity มองที่ label ของ Node ส่วน pod affinity และ pod anti-affinity มองที่ label ของ Pod อื่นที่รันอยู่แล้ว แทน โดยใช้ topologyKey เพื่อนิยามว่า “ใกล้กัน” หมายถึงอะไร (hostname, zone และอื่น ๆ)

apiVersion: apps/v1
kind: Deployment
metadata:
name: web-anti
spec:
replicas: 4
selector:
matchLabels:
app: web-anti
template:
metadata:
labels:
app: web-anti
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["web-anti"]
topologyKey: kubernetes.io/hostname
containers:
- name: web
image: registry.k8s.io/pause:3.10

กฎนี้บอกว่า “ห้ามวาง Pod web-anti สองตัวบน host เดียวกัน” บน cluster ที่มี 3 worker replica 3 ตัวลงได้พอดี — ตัวละ Node — แต่ตัวที่ 4 ไม่เหลือที่ให้ไปแล้ว และ requiredDuringScheduling คือ all-or-nothing: มันค้างอยู่ที่ Pending จริง ๆ ไม่ใช่ “กระจายได้บ้าง”

$ kubectl get pods -l app=web-anti -o wide
NAME READY STATUS NODE
web-anti-6476995565-5dwmp 1/1 Running scheduling-worker
web-anti-6476995565-glvpv 0/1 Pending <none>
web-anti-6476995565-l9tvf 1/1 Running scheduling-worker2
web-anti-6476995565-rhltf 1/1 Running scheduling-worker3
$ kubectl describe pod web-anti-6476995565-glvpv
Events:
Warning FailedScheduling 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 node(s) didn't match pod anti-affinity rules. preemption: 0/4 nodes are available: 1 Preemption is not helpful for scheduling, 3 No preemption victims found for incoming pod.

เทียบกับ topologySpreadConstraints ข้างล่างที่พูดว่า “กระจายให้เท่ากัน อนุญาตได้ถึง N ตัวต่อ Node” แทน เพดาน one-per-Node แบบเข้มงวดของ anti-affinity

topologySpreadConstraints (kubectl explain pod.spec.topologySpreadConstraints บน cluster 1.37 จริง) กระจาย Pod กลุ่มหนึ่งข้าม topology domain ให้อยู่ในค่า tolerance หนึ่งคือ maxSkew แทนที่จะเป็นกฎ all-or-nothing ของ anti-affinity

k8s/node-affinity-taints-and-tolerations/topology-spread-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
nodeTaintsPolicy: Honor
matchLabelKeys:
- pod-template-hash
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: registry.k8s.io/pause:3.10
  • maxSkew — ความต่างสูงสุดที่ยอมได้ระหว่าง domain ที่มี Pod match มากสุดกับน้อยสุด 1 ที่นี่หมายความว่าไม่มี Node ไหนมี Pod match มากกว่า Node อื่น 2 ตัวขึ้นไป
  • topologyKey: kubernetes.io/hostname — กระจายต่อ Node ใช้ topology.kubernetes.io/zone เพื่อกระจายต่อ zone แทน
  • whenUnsatisfiable — DoNotSchedule (แข็ง เหมือน required affinity) หรือ ScheduleAnyway (อ่อน best-effort)
  • matchLabelKeys: [pod-template-hash] — เช็คแล้วจริง: kubectl explain pod.spec.topologySpreadConstraints.matchLabelKeys บน API server 1.37 จริงรายงาน “This is a beta field … requires the MatchLabelKeysInPodTopologySpread feature gate (enabled by default)” — มันยังไม่ GA ยังเป็น beta ตั้งแต่ 1.27 ซึ่งต่างจากสมมติฐานว่ามันน่าจะ GA แล้วตอน 1.37 ในทางปฏิบัติมัน enable by default อยู่แล้ว จึงทำงานเหมือนมีอยู่เสมอ มันจำกัดขอบเขตการนับ skew ให้อยู่แค่ Pod ของ ReplicaSet generation เดียวกัน (ดูส่วน rollout ข้างล่าง)

พิสูจน์ การกระจาย 2-2-2 สะอาด ๆ ข้าม 3 worker ด้วย 6 replica

$ kubectl apply -f topology-spread-deploy.yaml
$ kubectl get pods -o wide
NAME READY STATUS NODE
web-b8b7dc6c7-4dpx9 1/1 Running scheduling-worker
web-b8b7dc6c7-llb6c 1/1 Running scheduling-worker
web-b8b7dc6c7-csj9b 1/1 Running scheduling-worker2
web-b8b7dc6c7-l9qrl 1/1 Running scheduling-worker2
web-b8b7dc6c7-hf4pl 1/1 Running scheduling-worker3
web-b8b7dc6c7-nqxj9 1/1 Running scheduling-worker3

manifest เดียวกันโดยไม่มี nodeTaintsPolicy: Honor ทำให้ 3 จาก 6 Pod ค้างที่ Pending ถาวรบน cluster ตัวนี้เป๊ะ ๆ

$ kubectl get pods -o wide
NAME READY STATUS NODE
web-57c75d6bbb-7m652 0/1 Pending <none>
web-57c75d6bbb-gn7s7 0/1 Pending <none>
web-57c75d6bbb-k67n4 1/1 Running scheduling-worker3
web-57c75d6bbb-nwc64 1/1 Running scheduling-worker
web-57c75d6bbb-xjl2m 0/1 Pending <none>
web-57c75d6bbb-xz9lt 1/1 Running scheduling-worker2
$ kubectl describe pod web-57c75d6bbb-7m652
Events:
Warning FailedScheduling 0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 node(s) didn't match pod topology spread constraints.

kubectl explain pod.spec.topologySpreadConstraints.nodeTaintsPolicy บอกเหตุผลชัด ๆ: “If this value is nil, the behavior is equivalent to the Ignore policy” — หมายความว่า Node control-plane ที่มี taint และ schedule ไม่ได้ยังถูกนับเป็น topology domain ที่ถูกต้องด้วย Pod match 0 ตัว โดย default พอมี Pod หนึ่งตัวอยู่แล้วบนแต่ละ worker ทั้ง 3 ตัว (1-1-1) และ control-plane ถูกนับเป็น domain ที่ 4 อยู่ที่ 0 การวาง Pod ตัวที่ 4 ที่ไหนก็ตามจะทำให้ skew เป็น 2 - 0 = 2 เกิน limit maxSkew: 1 — ดังนั้นไม่มีอะไรหลังจาก 3 Pod แรก schedule ได้เลย ทั่วทั้ง cluster ตลอดกาล การตั้ง nodeTaintsPolicy: Honor ตัด Node ที่ Pod ที่เข้ามาไม่ tolerate ออกจากการนับ domain ซึ่งเป็นสิ่งที่แก้ปัญหาข้างบนได้

trigger rolling update (maxUnavailable: 0, maxSurge: 1) บน Deployment ที่ spread โดยไม่มี matchLabelKeys แล้วดู surge Pod ลงบน Node ที่มี Pod ของ generation ก่อนหน้า อยู่แล้ว 2 ตัว เพราะ constraint นับทุก generation รวมกัน

$ kubectl set image deploy/rollout-demo web=nginx:1.27.2-alpine
t+8s: rollout-demo-579bddcff9-2cpvt Running scheduling-worker (old gen 579bddcff9)
t+8s: rollout-demo-579bddcff9-hmj2l Running scheduling-worker (old gen 579bddcff9)
t+8s: rollout-demo-676f6cdd75-2bdwb Running scheduling-worker (NEW gen 676f6cdd75)

scheduling-worker ตอนนี้มี 3 Pod จริง ๆ (old 2 + new 1) อยู่ครู่หนึ่ง — ยังอยู่ใน maxSkew: 1 โดยรวม (3 เทียบ 2 เทียบ 2) แต่การกระจายของ generation ใหม่เองไม่สม่ำเสมอเพียงเพราะบังเอิญว่า Pod เก่าอยู่ตรงไหน เพิ่ม matchLabelKeys: [pod-template-hash] แล้วทำ rollout เดิมซ้ำ: ตอนนี้ constraint นับแต่ละ generation แยกกัน ดังนั้น surge Pod ตัวแรกของ generation ใหม่เอี่ยมจะเริ่มนับ domain ของตัวเองที่ 0 ทุกที่เสมอ — ไม่ว่า Pod ของ generation ที่กำลังจะออกจะอยู่ตรงไหนก็ตาม

$ kubectl patch deploy rollout-demo --type=json -p='[{"op":"add","path":"/spec/template/spec/topologySpreadConstraints/0/matchLabelKeys","value":["pod-template-hash"]}]'
$ kubectl set image deploy/rollout-demo web=nginx:1.27.3-alpine
t+1s: rollout-demo-548b8ff4cb-djfk9 ContainerCreating scheduling-worker (NEW gen 548b8ff4cb, 1st of its generation)
t+1s: rollout-demo-676f6cdd75-2bdwb Running scheduling-worker (old gen 676f6cdd75)
t+1s: rollout-demo-7b5cbfbdb7-bdd57 Running scheduling-worker (old gen 7b5cbfbdb7)

matchLabelKeys merge pod-template-hash: 548b8ff4cb เข้ากับ labelSelector ที่มีผลจริงสำหรับ admission ของ surge Pod ตัวนั้น ดังนั้นการคำนวณ skew ของมันจะเห็นแค่ Pod ของ hash เดียวกัน — ไม่มีวันถูกกันหรือถูกกระจุกตัวโดยไม่ตั้งใจจาก Node ที่ generation เก่าบังเอิญอยู่

Affinity คือ Pod เป็นฝ่ายตัดสินใจว่าอยากไปไหน taint กลับด้านตรงข้าม — คือถูกตั้งบน Node และผลัก Pod ทุกตัวที่ไม่มี toleration ที่ match ออกไป

Terminal window
# Taint a node so it repels ordinary Pods
kubectl taint nodes gpu-node-1 hardware=gpu:NoSchedule

taint effect มีสามระดับ เรียงจากอ่อนไปแรง

  • NoSchedule — Pod ใหม่ที่ไม่มี toleration ที่ match จะไม่ถูก schedule ที่นี่ Pod ที่รันอยู่แล้วไม่ถูกแตะ
  • PreferNoSchedule — เวอร์ชันอ่อน scheduler จะพยายามเลี่ยง Node นี้ แต่ถ้าจำเป็นก็ยังใช้ได้
  • NoExecute — Pod ใหม่ถูกกันไว้ และ Pod ที่รันอยู่บน Node นั้นที่ไม่มี toleration ที่ match จะถูก evict ด้วย

Pod เข้าร่วมได้ด้วยการใส่ tolerations ที่ match key, value และ effect ของ taint

apiVersion: v1
kind: Pod
metadata:
name: gpu-inference-toleration
spec:
tolerations:
- key: hardware
operator: Equal
value: gpu
effect: NoSchedule
nodeSelector:
hardware: gpu
containers:
- name: inference
image: inference-server:2.1.0

toleration แค่หักล้าง taint เท่านั้น ไม่ได้ดึง Pod ไปที่นั่น นี่คือเหตุผลที่ node pool เฉพาะสำหรับ GPU มักสร้างจากสองกลไกพร้อมกัน: Node ถูก taint ไว้ให้ Pod ทั่วไปอยู่ห่าง ๆ ส่วน Pod ของ GPU workload ทั้ง tolerate taint นั้นและใช้ nodeSelector (หรือ node affinity) เพื่อขอ schedule ไปที่นั่น พร้อมกับ request GPU resource ด้วย

tolerationSeconds ให้ Pod tolerate taint NoExecute ได้ แต่แค่ในช่วงเวลาผ่อนผันที่จำกัดก่อนจะถูก evict อยู่ดี — มีประโยชน์สำหรับ “ทนปัญหา Node สั้น ๆ ได้ แต่ถ้ายืดเยื้อก็ออก”

apiVersion: v1
kind: Pod
metadata:
name: tolerates-briefly
spec:
nodeSelector:
kubernetes.io/hostname: scheduling-worker3
tolerations:
- key: dedicated
operator: Equal
value: batch
effect: NoExecute
tolerationSeconds: 10
containers:
- name: c
image: registry.k8s.io/pause:3.10
$ kubectl taint nodes scheduling-worker3 dedicated=batch:NoExecute
$ kubectl apply -f toleration-seconds-pod.yaml # created 16:43:07, taint already present
16:43:07 Running
16:43:16 Running
16:43:18 Succeeded (deletionTimestamp set)
16:43:19 Error from server (NotFound): pods "tolerates-briefly" not found

Pod รันอยู่ประมาณ 10 วินาทีหลัง taint ถูกใส่ แล้วก็ถูก evict ตรงตามกำหนด — tolerationSeconds คือนาฬิกาจริงที่บังคับใช้จริง ไม่ใช่แค่คำแนะนำ

Kubernetes ใส่ taint บางตัวให้อัตโนมัติ โดยไม่ต้อง operator ทำอะไรเลย — Node control-plane ของ kind มีตัวหนึ่งติดมาตั้งแต่สร้าง

$ kubectl describe node scheduling-control-plane | grep Taints
Taints: node-role.kubernetes.io/control-plane:NoSchedule

taint built-in ที่สำคัญด้าน operation ที่สุดคือ node.kubernetes.io/not-ready กับ node.kubernetes.io/unreachable (ทั้งคู่ NoExecute ถูกใส่โดย node controller ทันทีที่ condition Ready ของ Node เป็น false หรือ node controller ขาดการติดต่อกับมัน) และ node.kubernetes.io/unschedulable (NoSchedule สะท้อน kubectl cordon) ทุก Pod tolerate not-ready/unreachable เป็นเวลา 300s โดย default (ผ่าน tolerationSeconds default ที่ API server ใส่ให้อัตโนมัติ) — นี่คือกลไกเบื้องหลังพอดีของ “Pod รอดจาก node ที่สะดุดสั้น ๆ แต่ถูก evict ถ้า Node ล่มค้างไว้”

flowchart LR
  node["Node: gpu-node-1\n(tainted hardware=gpu:NoSchedule)"]
  podA["Pod A: no toleration"] -- blocked --> node
  podB["Pod B: tolerates hardware=gpu"] -- scheduled --> node
  node2["Node: tainted NoExecute + tolerationSeconds: 10"] --> podC["Pod C: tolerates for 10s, then evicted"]
Taint ผลัก Pod ธรรมดาออก ในขณะที่ Pod ที่มี toleration ตรงกันผ่านเข้าไปได้ ภายใต้เงื่อนไข tolerationSeconds

จริง ๆ แล้วมีแค่สองทิศทางในบทนี้ และทุกอย่างเป็นหนึ่งในสองแบบ ดึง: nodeSelector, node affinity, pod affinity และครึ่ง preferred ของ topologySpreadConstraints ล้วนอธิบายสิ่งที่ Pod ต้องการ — Node ที่มี label, ความใกล้กับ Pod อื่น, การกระจายที่เท่ากัน — และ Pod ที่ไม่ได้สิ่งที่ต้องการก็จะค้างที่ Pending (required) หรือยอมรับตัวเลือกรองลงมา (preferred) ผลัก: taint อธิบายสิ่งที่ Node ปฏิเสธ ไม่ขึ้นกับความต้องการของ Pod เลย และมีแค่ toleration ที่ match เท่านั้นที่หักล้างการผลักนั้นได้ — มันไม่เคยกลายเป็นการดึง topologySpreadConstraints อยู่ตรงกลาง: มันนับ Pod ข้าม topology และ nodeTaintsPolicy ของมันเองตัดสินว่า taint (การผลัก) จะทำให้ Node หลุดออกจากการถูกนับเป็นเป้าหมายการกระจายด้วยหรือเปล่า — พิสูจน์ข้างบน การตั้งค่า field นี้ผิดทำให้ “กระจายเท่ากัน” กลายเป็น “ค้างตลอดกาล” แบบเงียบ ๆ

ทางเลือกได้อะไรเสียอะไร
podAntiAffinity requiredDuringSchedulingรับประกันแน่นอน: ไม่มีทาง Pod match สองตัวอยู่ domain เดียวกัน (พิสูจน์ข้างบน)all-or-nothing — replica ตัวที่ N+1 ค้างที่ Pending ไม่มีคะแนนบางส่วน พิสูจน์ข้างบน
topologySpreadConstraints พร้อม maxSkewปรับได้: “กระจายเท่ากัน อนุญาตความต่างได้ถึง N” แทน all-or-nothingต้องมี nodeTaintsPolicy: Honor บน cluster ที่มี Node ถูก taint/ยังไม่ Ready ไม่งั้นค้างเงียบ ๆ ได้ (พิสูจน์ข้างบน)
matchLabelKeys: [pod-template-hash] บน spread constraintแต่ละ generation ของ rollout ถูกกระจายแยกกัน ไม่ถูกกันหรือ skew โดย generation ที่กำลังออก (พิสูจน์ข้างบน)ยังเป็น beta field อยู่ (เช็คแล้ว: 1.37, enabled by default) ยังไม่ใช่การรับประกันแบบ stable ที่บันทึกไว้
preferredDuringScheduling (node หรือ pod affinity)ไม่มีวันบล็อกการ schedule Pod ลงที่ไหนสักที่เสมอไม่มีการรับประกันเลยเมื่อมีการแย่งชิง — พิสูจน์ข้างบน soft preference ถูกข้ามได้แบบเงียบ ๆ
tolerationSeconds บน toleration ของ NoExecuteทนปัญหา Node สั้น ๆ ได้แทนที่จะ evict ทันทีถ้าตั้งนานเกินไป Node ที่แย่จริง ๆ จะเก็บ Pod ไว้เกินเวลาที่ควรย้าย (พิสูจน์ข้างบนด้วย eviction ที่จับเวลาจริง)
  • “topologySpreadConstraints กับ podAntiAffinity คือ syntax สองแบบของการรับประกันเดียวกัน” พิสูจน์ข้างบน: requiredDuringScheduling ของ anti-affinity คือหนึ่งต่อ domain เข้มงวด (all-or-nothing) ส่วน maxSkew ของ spread อนุญาตความต่างที่ปรับได้และลดระดับได้อย่างนุ่มนวลด้วย ScheduleAnyway
  • “Node control-plane ที่ถูก taint หรือยังไม่ Ready จะถูกละเว้นโดย scheduling feature อัตโนมัติ” พิสูจน์ข้างบน: ถ้าไม่มี nodeTaintsPolicy: Honor Node ที่ถูก taint ยังถูกนับเป็น topology domain ที่มี 0 Pod ซึ่งอาจทำให้ maxSkew เป็นไปไม่ได้ที่จะพอใจที่ไหนเลย ทั่วทั้ง cluster
  • “preferredDuringSchedulingIgnoredDuringExecution จะ evict Pod ถ้า preference ไม่จริงอีกต่อไปในภายหลัง” ทั้ง required และ preferred ลงท้ายด้วย IgnoredDuringExecution — พิสูจน์ใน API docs และจากข้อเท็จจริงว่า affinity ทั้งสองแบบไม่เคย trigger การ evict เองเลยเมื่อ Pod รันอยู่แล้ว
  • “toleration ดึง Pod เข้าหา Node ที่ถูก taint เหมือนที่ affinity ทำ” พิสูจน์ข้างบน: toleration แค่หักล้างการผลักของ taint เท่านั้น — Pod ยังต้องมี nodeSelector หรือกฎ affinity เพื่อถูกดึงไปที่นั่นจริง ๆ
  • “tolerationSeconds เป็นแค่คำแนะนำ เหมือน soft preference” พิสูจน์ข้างบนด้วย timestamp จริง: Pod ถูก evict ตรงตามกำหนดทันทีที่ window หมด ไม่ใช่ “สักวัน” หรือ “ถ้าสะดวก”

ตอน review affinity, spread หรือ toleration block ที่ AI สร้าง เช็คสิ่งเหล่านี้

  • topologySpreadConstraints ที่ไม่ได้ตั้ง nodeTaintsPolicy เลย บน cluster ที่มี Node control-plane หรือ Node ที่ cordon ไว้ — พิสูจน์ข้างบน อาจทำให้ scheduling ค้างเงียบ ๆ หลังจาก N Pod แรก (N = จำนวน Node ที่ไม่ถูก taint) โดยไม่มี error ที่ชี้ไปที่สาเหตุจริงเลย
  • podAntiAffinity requiredDuringScheduling ที่ใช้ทั้งที่เจตนาจริงคือ “กระจายเท่ากัน แต่อย่าบล็อกเพราะมัน” — พิสูจน์ข้างบน required anti-affinity คือ all-or-nothing topologySpreadConstraints พร้อม whenUnsatisfiable: ScheduleAnyway คือเครื่องมือที่อ่อนกว่าสำหรับเจตนานั้น
  • Deployment ที่ทำ rolling update พร้อม topologySpreadConstraints แต่ไม่มี matchLabelKeys: [pod-template-hash] — พิสูจน์ข้างบน ถ้าไม่มีมัน spread constraint จะผสม generation เข้าด้วยกันระหว่าง rollout ซึ่งไม่ใช่เจตนาส่วนใหญ่
  • nodeSelector หรือ tolerations ที่อ้างถึง label/taint key ที่ไม่มี Node ไหนใน cluster มีจริง — นี่คือ no-op แบบเงียบ ๆ: toleration ไม่ match อะไรเลย และ Pod ก็ schedule ได้ (หรือไม่ได้) เหมือนไม่มีมันอยู่เลย ซึ่งเข้าใจผิดได้ง่ายว่า “มันกำลังป้องกัน taint นี้อยู่” ทั้งที่มันไม่ได้ป้องกันอะไรเลย
  • tolerationSeconds ที่ตั้งบน taint ที่ Pod ไม่ได้เจตนาจะทนจริง ๆ (เช่น copy มาจากตัวอย่างที่ไม่เกี่ยวข้อง) — พิสูจน์ข้างบน มันคือ timer จริงที่บังคับใช้ ค่าที่ผิดจะทำให้ evict เร็วเกินไปหรือปล่อยให้ Pod อยู่บน Node ที่แย่นานเกินตั้งใจ
  • toleration ของ Node เฉพาะทาง GPU/dedicated ที่ไม่มี nodeSelector/affinity คู่กัน — Pod แค่หยุดถูกผลักออก มันไม่ได้ถูกดึงไปที่ Node ที่ถูก taint จึงลงที่ไหนใน cluster ก็ได้พอ ๆ กัน

🐛 หา bug ในโค้ดนี้

k8s/node-affinity-taints-and-tolerations/web-spread.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: registry.k8s.io/pause:3.10
เฉลย

ดูเหมือนถูกต้อง — spread maxSkew: 1 มาตรฐานข้าม hostname — แต่มันขาด nodeTaintsPolicy: Honor พิสูจน์ข้างบนบน cluster 4 Node จริง (1 control-plane + 3 worker): kubectl explain ยืนยันว่า default เทียบเท่ากับ Ignore ดังนั้น Node control-plane ที่ถูก taint และ schedule ไม่ได้ยังถูกนับเป็น topology domain ที่ถูกต้องอยู่ที่ 0 Pod match พอแต่ละ worker ทั้ง 3 ตัวมี 1 Pod แล้ว Pod เพิ่มเติมใด ๆ จะทำให้ skew เป็น 2 - 0 = 2 เกิน maxSkew: 1 — replica ครึ่งหนึ่งพอดี (3 จาก 6) เลยค้างที่ Pending ถาวรด้วย didn't match pod topology spread constraints ทั่วทั้ง cluster โดยไม่มีปัญหา quota หรือ resource ขาดแคลนที่ไหนเลย ทางแก้คือเพิ่มบรรทัดเดียว: nodeTaintsPolicy: Honor ซึ่งตัด Node ที่ Pod นี้ tolerate ไม่ได้ออกจากการนับ domain คืนการกระจาย 2-2-2 ที่ตั้งใจไว้กลับมา

requiredDuringSchedulingIgnoredDuringExecution กับ preferredDuringSchedulingIgnoredDuringExecution ของ node affinity ต่างกันอย่างไร
Node มี taint effect เป็น NoExecute จะเกิดอะไรกับ Pod ที่รันอยู่บน Node นั้นแล้วโดยไม่มี toleration ที่ match
ความต่างด้านทิศทางหลักระหว่าง affinity กับ taint/toleration คืออะไร
นี่คือ replica ตัวที่ 4 ของ Deployment ที่ใช้ podAntiAffinity requiredDuringScheduling บน cluster 3-worker ทำไมมันถึงค้างที่ Pending และอะไรจะแก้ได้
$ kubectl describe pod web-anti-6476995565-glvpv
Warning  FailedScheduling  0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 node(s) didn't match pod anti-affinity rules.
บน cluster ที่มี Node control-plane ถูก taint นี่กระจาย 6 replica ข้าม 3 worker แต่ครึ่งหนึ่งค้างที่ Pending ขาดอะไรไป
topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels: { app: web }
ทำไมการเพิ่ม matchLabelKeys: [pod-template-hash] ให้ topologySpreadConstraints ถึงสำคัญระหว่าง rolling update
Pod มี tolerationSeconds: 10 บน toleration ของ NoExecute และ taint ที่ match ถูกใส่บน Node ของมัน จะเกิดอะไรขึ้นจริง