Pod Disruption กับ Cluster Autoscaling
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”PodDisruptionBudget จำกัดว่า replica ของ workload ถูก evict พร้อมกันได้กี่ตัวระหว่าง voluntary disruption — ไม่มีอำนาจเหนือ involuntary disruption เลย และพิสูจน์ข้างล่างนี้ มันไม่มีอำนาจเหนือ preemption ด้วยเช่นกัน
Voluntary กับ involuntary disruption
หัวข้อที่มีชื่อว่า “Voluntary กับ involuntary disruption”การเสีย Pod ไม่ได้เป็น event แบบเดียวกันเสมอไป และความต่างนี้สำคัญต่อสิ่งที่คุณวางแผนรับมือได้จริง
- Voluntary disruption คือสิ่งที่ operator หรือ automation ตั้งใจสั่งเอง จึงควบคุม เลื่อน หรือจำกัดอัตราได้ เช่น
kubectl drainตอน upgrade node,kubectl cordonตามด้วย rolling replacement หรือ Cluster Autoscaler ที่ลบ Node ที่ใช้งานต่ำ - Involuntary disruption คือสิ่งที่ไม่มีใครสั่ง เช่น hardware ล่ม kernel panic หรือ Node หายไปเฉย ๆ ไม่มี budget หรือ setting ไหนป้องกันเรื่องนี้ได้ — พอเกิดขึ้นแล้ว Node ก็หายไปเลย
PodDisruptionBudget มีผลแค่กับกลุ่มแรกเท่านั้น — และตามที่บทนี้พิสูจน์ ไม่ใช่ทุกกลไกในกลุ่มนั้นด้วยซ้ำ
PodDisruptionBudget: พื้นล่างของ voluntary eviction พิสูจน์ว่าบล็อก drain ได้จริง
หัวข้อที่มีชื่อว่า “PodDisruptionBudget: พื้นล่างของ voluntary eviction พิสูจน์ว่าบล็อก drain ได้จริง”PodDisruptionBudget (policy/v1) ประกาศ minAvailable หรือ maxUnavailable สำหรับกลุ่ม Pod ที่เลือกด้วย label และทุกเส้นทาง voluntary eviction — kubectl drain, Eviction API, Cluster Autoscaler ที่ scale ลด Node — ต้องเคารพ budget นี้
apiVersion: apps/v1kind: Deploymentmetadata: name: web-appspec: replicas: 3 selector: matchLabels: app: web-app template: metadata: labels: app: web-app spec: containers: - name: web-app image: registry.k8s.io/pause:3.10---apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: web-app-pdbspec: minAvailable: 3 selector: matchLabels: app: web-appด้วย minAvailable เท่ากับ replica count เต็มจำนวน ALLOWED DISRUPTIONS เป็น 0 จริง ๆ — และการ drain ที่จับเวลาจริงบน Node ที่รัน replica ทั้ง 3 ตัวจะบล็อกตลอดไป ไม่ใช่แค่ช้า
$ kubectl get pdb web-app-pdbNAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGEweb-app-pdb 3 N/A 0 1s
$ kubectl drain scheduling-worker --ignore-daemonsets --delete-emptydir-data --timeout=15sevicting pod pdb-demo/web-app-bfc88d6d-vf7wsevicting pod pdb-demo/web-app-bfc88d6d-89lv7evicting pod pdb-demo/web-app-bfc88d6d-pld6perror when evicting pods/"web-app-bfc88d6d-vf7ws" -n "pdb-demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.error when evicting pods/"web-app-bfc88d6d-89lv7" -n "pdb-demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.error when evicting pods/"web-app-bfc88d6d-pld6p" -n "pdb-demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.... (ทำซ้ำทุก 5 วินาที)error: unable to drain node "scheduling-worker" due to error: [... global timeout reached: 15s ...], continuing command...
$ kubectl get pods -o wideNAME READY STATUS NODEweb-app-bfc88d6d-89lv7 1/1 Running scheduling-workerweb-app-bfc88d6d-pld6p 1/1 Running scheduling-workerweb-app-bfc88d6d-vf7ws 1/1 Running scheduling-workerนั่นคือข้อความจริงเป๊ะ ๆ ของ eviction ที่ถูกปฏิเสธ — Cannot evict pod as it would violate the pod's disruption budget — ทำซ้ำทุกครั้งที่ retry ตลอด 15 วินาทีของ timeout และทั้ง 3 Pod ยัง Running อยู่ตอนจบ: นี่ไม่ใช่ drain ที่ช้า มันคือ drain ที่ถูกบล็อกจริง ๆ
maxUnavailable (หรือ minAvailable ที่หย่อนกว่า): ความคืบหน้าบางส่วน พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “maxUnavailable (หรือ minAvailable ที่หย่อนกว่า): ความคืบหน้าบางส่วน พิสูจน์แล้ว”ผ่อน budget เป็น minAvailable: 2 บน Deployment 3 replica ตัวเดิม แล้ว drain จะคืบหน้าเท่าที่ budget อนุญาตพอดี — evict สำเร็จหนึ่งตัว แล้วการปฏิเสธจริงตัวเดิมก็บล็อกที่เหลือ
$ kubectl patch pdb web-app-pdb --type=merge -p '{"spec":{"minAvailable":2}}'$ kubectl get pdb web-app-pdbNAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGEweb-app-pdb 2 N/A 1 30s
$ kubectl drain scheduling-worker --ignore-daemonsets --delete-emptydir-data --timeout=30sevicting pod pdb-demo/web-app-bfc88d6d-vf7wsevicting pod pdb-demo/web-app-bfc88d6d-89lv7evicting pod pdb-demo/web-app-bfc88d6d-pld6perror when evicting pods/"web-app-bfc88d6d-pld6p" -n "pdb-demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.error when evicting pods/"web-app-bfc88d6d-vf7ws" -n "pdb-demo" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.pod/web-app-bfc88d6d-89lv7 evicted... (การ evict ถัดไปยังล้มเหลวแบบเดิมต่อไป — หนึ่งตัวคือ limit ของ budget)error: unable to drain node "scheduling-worker" due to error: [... global timeout reached: 30s ...], continuing command...ALLOWED DISRUPTIONS: 1 แปลว่ามีแค่ Pod เดียวเป๊ะ ๆ (89lv7) ที่ evict ได้ พอ availability จะลดเหลือ 1 การ evict ถัดไปทุกครั้งก็โดนปฏิเสธด้วยข้อความจริงเดียวกัน maxUnavailable ทำงานแบบเดียวกัน แค่พูดกลับด้าน (จำนวนที่ลงได้ แทนที่จะเป็นจำนวนที่ต้องอยู่) — มันคือคำสะกดสองแบบของพื้นล่างเดียวกัน
Cluster Autoscaler กับ Karpenter: เพิ่มและลบ Node ทั้งตัว
หัวข้อที่มีชื่อว่า “Cluster Autoscaler กับ Karpenter: เพิ่มและลบ Node ทั้งตัว”HPA กับ VPA ทำงานระดับ Pod ส่วน Cluster Autoscaler ทำงานสูงกว่าหนึ่งชั้น คือระดับ Node
- เพิ่ม Node เมื่อ Pod ค้างอยู่ที่
Pendingเพราะไม่มี Node ไหนมี capacity ว่างพอจะ schedule Pod นั้นได้ - ลบ Node เมื่อ Node นั้นถูกใช้งานต่ำมาก และ Pod ทุกตัวบนนั้น reschedule ไปที่อื่นได้ — แต่ทำโดย drain Node นั้นก่อน โดยเคารพ PodDisruptionBudget ตลอดทาง เหมือนกับ
kubectl drainด้วยมือที่พิสูจน์ข้างบนทุกประการ
# Pods stuck here because nothing fits them is the classic trigger for Cluster Autoscaler to add a Nodekubectl get pods --field-selector=status.phase=Pendingannotation cluster-autoscaler.kubernetes.io/safe-to-evict: "false" บน Pod บอก Cluster Autoscaler ว่าอย่า drain Node ที่ Pod นั้นอยู่เด็ดขาด แม้จะใช้งานต่ำก็ตาม — มีประโยชน์กับ Pod ที่มี local state ที่ย้ายที่ไม่ได้จริง ๆ คอร์สนี้ไม่มี cloud credential ที่จะ provision หรือทำลาย Node จริง ดังนั้น Cluster Autoscaler เองเป็น docs-only ที่นี่ ไม่ได้ติดตั้งหรือรันจริง ดู Cluster Autoscaler FAQ สำหรับกลไก node-group-scaling จริง Karpenter เป็นทางเลือกใหม่ที่ควรรู้จักชื่อไว้: แทนที่จะ scale fixed node-group template ขึ้นลง มัน provision Node ที่ขนาดพอดีตรง ๆ ตอบสนอง Pod ที่ schedule ไม่ได้ — เป็น docs-only ที่นี่เช่นกัน ดู เอกสาร Karpenter สำหรับรายละเอียด
kubectl cordon/kubectl uncordon คือครึ่งที่ทำด้วยมือของสิ่งที่ทั้ง Cluster Autoscaler และ node upgrade แบบ rolling ทำอัตโนมัติ — cordon ทำเครื่องหมายว่า Node schedule ไม่ได้สำหรับ Pod ใหม่ เท่านั้น (มันไม่ evict อะไรเองเลย ต่างจาก effect NoExecute ของ taint) และ uncordon ก็กลับด้าน
$ kubectl cordon scheduling-workernode/scheduling-worker cordoned$ kubectl get nodes scheduling-workerNAME STATUS ROLESscheduling-worker Ready,SchedulingDisabled$ kubectl uncordon scheduling-workernode/scheduling-worker uncordoneddrain จริง ๆ แล้วคือ cordon + evict ทุกอย่างโดยเคารพ PDB รวมกันในคำสั่งเดียว — นี่คือเหตุผลเป๊ะ ๆ ที่ PDB แบบป้องกันเต็มที่ทำให้ drain ค้างตลอดไปได้ พิสูจน์ข้างบน ในขณะที่ cordon เปล่า ๆ ไม่เคยบล็อกอะไรเลย (มันไม่ evict)
Preemption ไม่สนใจ PodDisruptionBudget เลย พิสูจน์แล้ว
หัวข้อที่มีชื่อว่า “Preemption ไม่สนใจ PodDisruptionBudget เลย พิสูจน์แล้ว”นี่คือมุมที่คมที่สุดในบททั้งหมด: preemption (จากบทก่อนหน้า) ไม่ใช่ เส้นทาง voluntary disruption ที่เคารพ PDB — มัน evict อะไรก็ตามที่ต้องการ ไม่สนใจ budget ใด ๆ พิสูจน์: ด้วย web-app-pdb กลับไปที่ minAvailable: 3 (budget ป้องกันเต็มที่ตัวเดียวกับที่บล็อก kubectl drain ข้างบน ALLOWED DISRUPTIONS: 0) Pod priority สูงที่ request CPU มากกว่าที่ว่างบน Node ยังคงถูก schedule ได้โดย evict replica ของ web-app
$ kubectl get pdb web-app-pdbNAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGEweb-app-pdb 3 N/A 0 94s
$ kubectl get pods -o wideNAME READY STATUS NODEurgent-pod 1/1 Running scheduling-workerweb-app-7c4c89666-42wg8 0/1 Pending <none>web-app-7c4c89666-5hfk5 0/1 Pending <none>web-app-7c4c89666-65nxm 1/1 Running scheduling-worker
$ kubectl get events --sort-by=.lastTimestampNormal Killing pod/web-app-7c4c89666-khvx4 Stopping container web-appNormal Preempted pod/web-app-7c4c89666-khvx4 Preempted by pod 0337595a-c3b9-4878-b9dc-af21b99fd042 on node scheduling-workerNormal Scheduled pod/urgent-pod Successfully assigned pdb-demo/urgent-pod to scheduling-worker
$ kubectl get pdb web-app-pdbNAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGEweb-app-pdb 3 N/A 0 94sweb-app-7c4c89666-khvx4 ถูก Preempted — event จริง reason Preempted เดียวกับบท priority ในขณะที่ ALLOWED DISRUPTIONS ของ PDB เองอยู่ที่ 0 ตลอดเวลา: disruption controller ไม่เคยถูกถามเลยด้วยซ้ำ เพราะ preemption ไม่ใช่ eviction call ผ่าน API path เดียวกับที่ kubectl drain ใช้ นี่คือเหตุผลที่ docs ของ Kubernetes พูดชัดเจนว่า preemption ไม่รับประกันว่าจะเคารพ PDB — พิสูจน์ข้างบนแล้วว่ามันไม่เคารพเลย จบ เรื่องเดียวกันจริงกับ node-pressure eviction (kubelet เรียก resource คืนภายใต้ MemoryPressure/DiskPressure): ทั้งคู่เป็น involuntary จากมุมมองของ workload แม้ preemption จะถูกกระตุ้นโดยบางอย่างอย่างจงใจ (Pod priority สูงกว่าของ operator) ก็ตาม — PDB ไม่มีสิทธิ์พูดอะไรเลยในทั้งสองกรณี
flowchart LR
drain["kubectl drain /\nCluster Autoscaler scale-down"] --> pdb{"PodDisruptionBudget\nminAvailable satisfied?"}
pdb -->|yes| evict["Evict one Pod at a time"]
pdb -->|no, would violate| wait["Blocked -- Cannot evict pod\nas it would violate the\npod's disruption budget"]
evict --> empty["Node fully drained"]
empty --> remove["Node removed"]
preempt["Scheduler preemption /\nkubelet node-pressure eviction"] -->|ignores PDB entirely| force["Pod evicted regardless of budget"] Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”PodDisruptionBudget คือคำสัญญาเกี่ยวกับ ประตูเดียวเท่านั้น: voluntary eviction API ที่ kubectl drain, Eviction API และ Cluster Autoscaler เรียกผ่านทั้งหมด มันไม่ได้พูดถึงวิธีอื่นที่ Pod จะหายไปเลย — ไม่ใช่ hardware ล่ม (involuntary ไม่มีใครสั่ง) และพิสูจน์ข้างบน ไม่ใช่ preemption ด้วย (จงใจกระตุ้น แต่ถูก route ผ่านกลไกอื่นที่ไม่เคยถูกสร้างมาให้เช็ค disruption budget) “voluntary กับ involuntary” เป็นการแบ่งเบื้องต้นที่มีประโยชน์ แต่กติกาจริงแคบและคมกว่านั้น: PDB ป้องกันแค่ eviction call path ที่เฉพาะเจาะจงเท่านั้น ไม่ใช่ทุก disruption ที่จงใจ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
minAvailable เท่ากับ replica count เต็มจำนวน | ป้องกันสูงสุดต่อ voluntary disruption | drain ค้างตลอดไป (พิสูจน์ข้างบน) — node maintenance ต้องลด replica หรือ budget ก่อนด้วยมือ |
minAvailable/maxUnavailable ที่หย่อนกว่า | drain คืบหน้าได้จริง มีขอบเขต (พิสูจน์ข้างบน: evict หนึ่งตัว แล้วบล็อก) | มีช่วง capacity ลดลงบ้างระหว่าง maintenance ที่ถูกยอมรับโดยออกแบบ |
safe-to-evict: "false" บน Pod ที่มี state | Cluster Autoscaler จะไม่มีวัน drain Node ที่มันอยู่ | Node นั้นจะถูก Cluster Autoscaler scale ลงไม่ได้เลย แม้จะว่างก็ตาม |
พึ่ง PDB ป้องกัน preemption ที่ขับเคลื่อนด้วย PriorityClass | — ไม่มีเลย พิสูจน์ข้างบนว่ามันไม่ได้ผลเลย | budget ให้การป้องกันเป็นศูนย์ที่นี่ วิธีป้องกันจริงมีแค่ capacity planning กับวินัยเรื่อง priority |
| การ provision node ตรง ๆ แบบ Karpenter (docs-only ที่นี่) | ไม่ต้องมี fixed node-group template ให้กำหนดขนาดล่วงหน้า | โมเดลปฏิบัติงานและเครื่องมือต่างจาก Cluster Autoscaler แบบ node-group ดั้งเดิม |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “PodDisruptionBudget ที่
minAvailableเท่ากับ replica count คือการตั้งค่าที่ปลอดภัยและระวังเป็นพิเศษ” พิสูจน์ข้างบน: มันไม่ใช่ “ระวังพิเศษ” มันทำให้kubectl drain— และโดยส่วนขยายคือการ scale-down ของ Cluster Autoscaler บน Node นั้น — ค้างตลอดไปด้วยการปฏิเสธจริงที่ทำซ้ำ - “PodDisruptionBudget ป้องกัน preemption ได้ด้วย เพราะ preemption ก็ถูกกระตุ้นโดยบางอย่างอย่างจงใจเหมือนกัน” พิสูจน์ข้างบน: preemption evict Pod ของ
web-appได้ ในขณะที่ALLOWED DISRUPTIONSของ PDB อยู่ที่0ตลอดเวลา — preemption ไม่ผ่าน eviction API path ที่ PDB คุมอยู่เลย - “
kubectl cordonevict Pod บน Node นั้นเหมือนdrain” พิสูจน์ข้างบน:cordonเพียงอย่างเดียวแค่บล็อกการ schedule ใหม่ เท่านั้น (SchedulingDisabled) มันไม่ evict อะไรเลย นี่คือเหตุผลเป๊ะ ๆ ที่drainคือ cordon บวก eviction - “Cluster Autoscaler กับ Karpenter คือสิ่งเดียวกันคนละชื่อ” ทั้งคู่แก้ปัญหาเดียวกัน (เพิ่ม/ลบ Node ให้ตรงกับความต้องการของ Pod) ด้วยกลไกต่างกัน — Cluster Autoscaler scale node-group template ที่กำหนดไว้ล่วงหน้า Karpenter provision Node ที่ขนาดพอดีตรง ๆ ตาม เอกสาร Karpenter — คอร์สนี้ไม่ได้รันทั้งคู่จริง (ไม่มี cloud credential) แต่กลไกถูกบันทึกไว้ต่างกันด้วยเหตุผล
- “hardware ล่มกับ
kubectl drainก็แค่ ‘Pod หายไป’ เหมือนกัน ดังนั้น PDB คุมได้ทั้งคู่” พิสูจน์ข้างบนด้วย drain: PDB บล็อกและทำซ้ำการปฏิเสธจริงสำหรับ voluntary drain อย่างจริงจัง ส่วน involuntary failure ไม่มี code path ให้บล็อกอะไรเลย — Node และทุกอย่างบนนั้นหายไปเฉย ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review PodDisruptionBudget, drain script หรือ capacity plan ที่ AI สร้าง เช็คสิ่งเหล่านี้
minAvailableที่ตั้งเท่ากับ (หรือmaxUnavailable: 0คู่กับ) replica count เต็มจำนวนของ Deployment — พิสูจน์ข้างบน นี่ไม่ใช่ “ปลอดภัยสูงสุด” มันทำให้ทุก maintenance drain จริงค้างตลอดไปด้วยการปฏิเสธ API จริงที่ทำซ้ำ- comment หรือ design doc ที่อ้างว่า PodDisruptionBudget “ป้องกัน workload จาก disruption” โดยไม่ระบุว่า voluntary — พิสูจน์ข้างบน มันไม่มีผลเลยต่อ preemption หรือ node-pressure eviction ซึ่งทั้งคู่ลบ Pod ได้ไม่ว่า budget จะเป็นอะไร
maxUnavailable: 0บน rollout strategy ของ Deployment ที่ซ้อนกับ PDB ที่เข้มงวดบน Pod เดียวกัน — นี่คือพื้นล่างสองอันที่เป็นอิสระต่อกัน (rollout availability เทียบกับ disruption budget) ที่รวมกันแล้วอาจทำให้ operation ที่สมเหตุสมผล (rolling update ระหว่าง drain window) เป็นไปไม่ได้ เช็คว่าตัวไหนควรทำหน้าที่ป้องกันจริง ๆ- capacity plan ที่สมมติว่า
safe-to-evict: "false"ป้องกันทุกการ disrupt ต่อ Pod — พิสูจน์ข้างบน (โดยส่วนขยายจากผลเรื่อง preemption) มันมีผลแค่ต่อการตัดสินใจ scale-down ของ Cluster Autoscaler เท่านั้น ไม่ใช่ preemption หรือ node-pressure eviction - การพึ่ง
kubectl cordonเพียงอย่างเดียวเป็นขั้นตอน “drain” ใน runbook — พิสูจน์ข้างบน cordon ไม่ evict อะไรเลย runbook ที่ cordon แล้วรอให้ Pod ออกไปเอง “ตามธรรมชาติ” จะรอตลอดไปสำหรับ Pod ที่ไม่มีเหตุผลอื่นให้ย้าย - กลยุทธ์ capacity ที่ขับเคลื่อนด้วย PriorityClass โดยไม่รู้ว่ามันสามารถ evict Pod ที่ PDB ถูกตั้งขึ้นมาป้องกันโดยเฉพาะได้ — พิสูจน์ข้างบน นี่คือช่องว่างจริงที่เงียบระหว่างสอง feature ที่ดูเหมือนจะทำงานร่วมกันได้อย่างปลอดภัยแต่ไม่ได้เป็นแบบนั้น
🐛 หา bug ในโค้ดนี้
apiVersion: apps/v1kind: Deploymentmetadata:name: checkoutspec:replicas: 3selector: matchLabels: app: checkouttemplate: metadata: labels: app: checkout spec: containers: - name: checkout image: checkout:2.4.0---apiVersion: policy/v1kind: PodDisruptionBudgetmetadata:name: checkout-pdbspec:minAvailable: 3selector: matchLabels: app: checkoutเฉลย
นี่ดูเหมือน PodDisruptionBudget ที่ปลอดภัยที่สุดเท่าที่จะเป็นไปได้ — “อย่าให้ availability ลดต่ำกว่า replica ทั้ง 3 ตัวเลย” — แต่พิสูจน์ข้างบน minAvailable เท่ากับ replica count เต็มจำนวนแปลว่า ALLOWED DISRUPTIONS เป็น 0 ถาวร kubectl drain จริงตัวแรกสำหรับ node upgrade ตามปกติจะค้าง retry ทุก 5 วินาทีด้วย Cannot evict pod as it would violate the pod's disruption budget จนกว่า timeout จะหมด โดยไม่ได้ evict อะไรเลย ไม่มีเส้นทาง node maintenance เหลืออยู่เลยที่เคารพ budget นี้ได้ — ทางเดียวที่ไปต่อได้คือ scale Deployment ขึ้นก่อน (เพื่อให้ minAvailable: 3 ไม่ใช่ 100% ของ replica อีกต่อไป) ลด minAvailable เหลือประมาณ 2 หรือจงใจข้าม PDB ไปเลย ไม่มีอันไหน “ปลอดภัยกว่า” มันแค่เป็น trade-off ต่างกัน และมันยังให้การป้องกันเป็นศูนย์ต่อ preemption หรือ node-pressure eviction พิสูจน์ข้างบน — budget ที่เข้มงวดขนาดนี้ซื้อความติดขัดจริงต่อ maintenance ปกติ แต่ไม่ซื้อการป้องกันใด ๆ เลยต่อ disruption path ที่ไม่สนใจมัน