Level 5 · Hidup-Mati Pod di OpenShift
Sesi 38: Semua Telur di Satu Keranjang: topologySpreadConstraints, Anti-Affinity, & PodDisruptionBudget
Bagaimana jika kita sudah menjalankan 6 replika pod, HPA aktif, probe rapi, graceful shutdown sempurna — tapi 5 dari 6 pod itu ternyata tidur di worker node yang sama, dan node itu baru saja mati?
1. Bayangkan Jika
Seorang peternak punya 60 telur dan 3 keranjang. Ia bangga: "Telurku banyak, kalau satu pecah masih ada 59." Tapi karena keranjang pertama paling dekat dengan pintu dan paling lega, setiap kali ia menaruh telur baru, tangannya otomatis masuk ke keranjang itu. Akhirnya 50 telur ada di keranjang pertama, 7 di keranjang kedua, 3 di keranjang ketiga.
Suatu pagi keranjang pertama jatuh dari meja. Peternak itu tidak kehilangan satu telur. Ia kehilangan 83% telurnya dalam satu detik.
Jumlah telur tidak pernah menjadi masalah. Masalahnya adalah di mana telur itu diletakkan. Di OpenShift, telur adalah pod, keranjang adalah worker node atau zone, dan tangan yang otomatis memilih keranjang paling lega adalah kube-scheduler.
2. Apa yang Sebenarnya Terjadi
Di Sesi 13 kita belajar bahwa redundansi palsu adalah SPOF yang menyamar. Di Sesi 14 kita naik satu lantai ke level gedung dan data center. Sesi ini menurunkannya lagi ke tempat yang paling sering dilupakan: penempatan pod di dalam satu cluster.
-
Scheduler tidak menjanjikan sebaran: - Default scoring kube-scheduler memang punya kecenderungan menyebar (plugin seperti
PodTopologySpreaddefault danNodeResourcesBalancedAllocation), tapi itu hanya preferensi skor, bukan garansi. Node dengan resource bebas paling besar sering menang berulang kali. - Hasilnya, 6 replika bisa mendarat 5 di satu node dan 1 di node lain. Secara deklarasireplicas: 6terpenuhi, secara ketahanan sistem kita hampir tidak punya redundansi. -
Scheduler tidak pernah memindahkan pod yang sudah jalan: - Bayangkan worker-2 di-drain untuk maintenance. Semua pod-nya pindah ke worker-1 dan worker-3. Satu jam kemudian worker-2 kembali
Ready, tapi kosong. Pod tidak otomatis kembali ke sana. - Scheduler hanya bekerja saat pod baru dibuat. Untuk menyeimbangkan ulang pod yang sudah berjalan dibutuhkan descheduler (di OpenShift: Kube Descheduler Operator) atau rollout ulang.
SEBELUM DRAIN SETELAH worker-2 KEMBALI
worker-1 [P][P] worker-1 [P][P][P]
worker-2 [P][P] --> worker-2 [ ][ ][ ] <- kosong selamanya
worker-3 [P][P] worker-3 [P][P][P]
-
topologySpreadConstraints: aturan sebaran yang eksplisit:
yaml apiVersion: apps/v1 kind: Deployment metadata: name: checkout-api spec: replicas: 6 selector: matchLabels: app: checkout-api template: metadata: labels: app: checkout-api spec: topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule # hard: antar zone wajib seimbang labelSelector: matchLabels: app: checkout-api matchLabelKeys: - pod-template-hash # hitung per ReplicaSet saat rolling update - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: ScheduleAnyway # soft: antar node diusahakan seimbang labelSelector: matchLabels: app: checkout-api-maxSkew: Selisih maksimum jumlah pod yang cocok antara domain topologi paling padat dan paling sepi. -topologyKey: Label node yang mendefinisikan "keranjang".kubernetes.io/hostnameberarti per node,topology.kubernetes.io/zoneberarti per zone. -whenUnsatisfiable:DoNotSchedule= aturan keras, pod tetapPendingjika tidak bisa dipenuhi.ScheduleAnyway= aturan lunak, hanya menjadi preferensi skor. -labelSelector: Harus cocok dengan label pod itu sendiri. Salah ketik satu huruf, constraint menghitung nol pod dan tidak berpengaruh apa pun tanpa error. - Opsional:minDomains(jumlah minimum domain yang dianggap ada),matchLabelKeys(misalnyapod-template-hashagar sebaran dihitung per ReplicaSet saat rolling update, bukan campuran pod lama dan baru),nodeAffinityPolicy/nodeTaintsPolicy(apakah node yang tidak lolos affinity/taint ikut dihitung sebagai domain). -
Hitungan maxSkew, contoh nyata (3 zone):
| Replika | Sebaran | maxSkew 1 boleh? | Alasan |
|---|---|---|---|
| 6 | 2 / 2 / 2 | Ya | selisih 0 |
| 7 | 3 / 2 / 2 | Ya | selisih 1 |
| 7 | 4 / 2 / 1 | Tidak | selisih 3 |
| 6 | 4 / 1 / 1 | Tidak | selisih 3 |
Penting: skew hanya dievaluasi saat penjadwalan. Jika setelah itu sebuah node mati dan sebarannya jadi timpang, constraint tidak memperbaikinya.
-
podAntiAffinity: "paling banyak satu":
yaml affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: checkout-api topologyKey: kubernetes.io/hostname- Versirequiredberarti maksimal 1 pod per node. Jika replika lebih banyak dari node, sisanyaPending. - VersipreferredDuringSchedulingIgnoredDuringExecutionadalah versi lunak. - Bedanya dengan spread: anti-affinity berkata "jangan berdua", spread berkata "seimbang". Anti-affinity juga mahal secara komputasi untuk scheduler di cluster besar dengan ribuan pod. -
PodDisruptionBudget (PDB): rem untuk gangguan yang disengaja:
yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: checkout-api-pdb spec: maxUnavailable: 1 # atau minAvailable: 5 selector: matchLabels: app: checkout-api- Melindungi dari voluntary disruption:oc adm drain, cluster upgrade, dan MachineConfig rollout di OpenShift yang me-reboot node satu per satu. - Tidak melindungi dari node crash, kernel panic, atau zone padam. PDB hanya didengar oleh proses yang meminta izin lewat Eviction API.
3. Bagaimana Jika Kita Coba...
"Naikkan saja replika jadi 10, pasti aman!"
Tanpa aturan sebaran, 10 replika bisa saja seluruhnya mendarat di 2 node yang kebetulan paling lega. Kita membayar 10 pod tapi hanya punya 2 keranjang. Satu node mati, separuh kapasitas hilang sekaligus.
"Oke, pasang maxSkew: 1 dengan DoNotSchedule di semua tempat!"
Sekarang masalahnya terbalik. Saat flash sale, HPA ingin naik dari 6 ke 9 pod. Zone C ternyata sudah penuh (resource requests habis). Zone A dan B masing-masing hanya boleh menerima satu pod tambahan (3/3/2). Pod berikutnya untuk A atau B akan membuat skew jadi 2, jadi ikut ditahan, padahal A dan B masih punya ruang. Hasilnya:
zone-a [P][P][P] zone-b [P][P][P] zone-c [P][P] FULL
pod baru: Pending
Events: 0/9 nodes are available: 3 node(s) didn't match pod topology
spread constraints, 3 Insufficient cpu ...
HPA melaporkan desired replicas 9, dashboard bisnis melihat latency naik, dan tidak ada yang sadar bahwa scale-up gagal diam-diam karena pod Pending. Aturan yang terlalu kaku mengubah ketahanan menjadi batas kapasitas.
Karena itu kombinasi yang umum adalah zone hard + hostname soft (cocok jika kapasitas tiap zone longgar), atau zone soft + hostname hard (jika jumlah zone sedikit tapi node banyak). Pilihannya ditentukan kapasitas, bukan selera.
4. Nama Resminya
- Pod Topology Spread Constraints: Aturan sebaran pod lintas domain topologi (
maxSkew,topologyKey,whenUnsatisfiable,labelSelector). - Topology Domain: Satu nilai label topologi, misalnya satu node atau satu zone.
- Inter-Pod Anti-Affinity: Aturan agar pod tertentu tidak ditempatkan bersama dalam satu domain.
- PodDisruptionBudget (PDB): Batas jumlah pod yang boleh tidak tersedia akibat voluntary disruption.
- Voluntary vs Involuntary Disruption: Gangguan yang disengaja (drain, upgrade) vs tidak disengaja (crash, power loss).
- Descheduler: Komponen yang mengusir pod agar dijadwalkan ulang ke sebaran yang lebih sehat.
- N+1 Capacity: Kapasitas cukup untuk menahan beban puncak saat satu domain hilang.
5. Di Dunia Kita
- Matematika kegagalan: 6 pod, 4 di node A. Node A mati, 67% kapasitas hilang sekaligus. 2 pod sisa menanggung 3x beban, latency melonjak, readiness probe gagal, lalu liveness probe (Sesi 35) mulai membunuh pod yang sebenarnya hanya kelelahan. Pod yang di-restart kembali ke beban 3x, mati lagi. Itulah cascading failure.
| Sebaran | Domain hilang | Kapasitas hilang | Beban per pod sisa |
|---|---|---|---|
| 4 / 1 / 1 (per node) | node A | 67% | 3x |
| 2 / 2 / 2 (per zone) | zone A | 33% | 1.5x |
- N+1 per zone: Dengan 3 zone, saat satu zone hilang, dua zone sisa harus menahan 100% puncak. Artinya setiap zone harus sanggup menahan 50% beban puncak, bukan 33%. Sebaran yang rapi tanpa headroom tetap tumbang.
- Data juga butuh keranjang terpisah: Redis primary dan replica (Sesi 37) atau database primary dan standby yang tidur di node yang sama adalah SPOF klasik Sesi 13 dalam bentuk baru. StatefulSet butuh aturan sebaran yang sama ketatnya.
- PDB yang terlalu ketat:
maxUnavailable: 0atauminAvailablesama dengan jumlah replika membuatoc adm drainmacet selamanya. Cluster upgrade OpenShift berhenti di tengah jalan, MachineConfigPool tertahanUpdating, dan tim platform harus menghapus PDB secara manual. - Biaya lintas zone: Menyebar pod ke banyak zone berarti sebagian request melompati zone, menambah latency jaringan dan, di cloud, biaya transfer data. Topology-aware routing (Topology Aware Hints) bisa menjaga trafik tetap di zone yang sama selama kapasitas lokal cukup.
6. Lensa Performance Tester
Sebelum dan saat pengujian:
1. Cek penempatan sebelum setiap tes: Jangan percaya replicas: 6. Lihat di mana pod benar-benar tinggal.
bash
oc get pods -l app=checkout-api -o wide --no-headers | awk '{print $7}' | sort | uniq -c
oc get nodes -L topology.kubernetes.io/zone
2. Node/Zone Loss Test: Saat load test berjalan di beban stabil, oc adm drain satu node (atau cordon dan matikan semua node di satu zone). Ukur kapasitas yang hilang, lonjakan p95/p99, error rate, dan recovery time sampai throughput kembali normal.
3. Scale-up Under Constraint: Picu HPA scale-up saat puncak. Hitung pod Pending dan cari event didn't match pod topology spread constraints atau didn't match pod anti-affinity rules.
bash
oc get events --field-selector reason=FailedScheduling
4. PDB Drain Test: Pastikan drain tetap bisa jalan di tengah beban tanpa menembus SLO, dan pastikan PDB tidak memblokir drain selamanya.
5. Validasi N+1: Uji beban puncak dengan satu zone sengaja dimatikan. Jika dua zone sisa tidak tahan, sebaran yang rapi hanyalah kosmetik.
6. Rebalance Check: Setelah node kembali Ready, cek ulang sebaran. Jika timpang, catat sebagai risiko sampai ada descheduler atau rollout.
7. Pertanyaan untuk Ronde Berikutnya
Level 5 sudah mengikuti seluruh hidup sebuah pod: ditempatkan di mana (Sesi 38), dinyatakan hidup dan siap (Sesi 35), memikul koneksi ke Redis dan database (Sesi 37), dan mati dengan anggun (Sesi 36). Sekarang pertanyaannya: di sistem yang sedang kamu uji, tahap mana dari empat itu yang paling mungkin patah lebih dulu saat satu node hilang di tengah beban puncak, dan bagaimana kamu akan membuktikannya, bukan menebaknya?
Ini sesi terakhir seri SYSTEM UNDER PRESSURE. Ronde berikutnya bukan video baru, melainkan sistemmu sendiri. Kembali ke Sesi 31 untuk membaca design doc dan menandai setiap keranjang tunggal, lalu ke Sesi 29 untuk merancang load test dan chaos test yang mematikan keranjang itu dengan sengaja, sebelum produksi yang melakukannya untukmu.