Level 5 · Hidup-Mati Pod di OpenShift
Sesi 35: Probe yang Membunuh Pod Sehat: Liveness, Readiness, & Startup Probe
Bagaimana jika pod yang membunuh dirinya sendiri di tengah load test sebenarnya tidak sakit sama sekali, dan yang menarik pelatuknya justru health check yang kita tulis sendiri?
1. Bayangkan Jika
Sebuah rumah sakit punya perawat jaga yang setiap 20 detik menepuk bahu pasien dan bertanya, "Bapak masih sadar?" Aturannya sederhana: kalau pasien tidak menjawab dalam 5 detik sebanyak tiga kali berturut-turut, perawat langsung memanggil tim resusitasi dan pasien dibius ulang dari nol.
Masalahnya, pasien baru saja keluar dari operasi besar. Ia butuh satu menit penuh untuk sadar dari anestesi. Perawat mulai bertanya di detik ke-15, lalu detik ke-35, lalu detik ke-55. Tiga kali tidak dijawab. Tim resusitasi datang, pasien dibius lagi, dan siklus terulang. Pasien itu sebenarnya sehat. Ia hanya tidak diberi waktu.
Lebih buruk lagi: suatu hari perawat diganti dengan aturan baru, "Tanyakan juga apakah lift rumah sakit berfungsi." Saat lift macet, semua pasien di seluruh lantai dianggap tidak sadar dan dibius ulang bersamaan. Lift tidak jadi lebih cepat. Rumah sakit justru lumpuh.
Itulah probe di OpenShift: alat penyelamat yang, kalau salah dikonfigurasi, menjadi mesin pembunuh pod sehat.
2. Apa yang Sebenarnya Terjadi
Di Sesi 15 kita sudah mengenal liveness dan readiness probe secara singkat. Sekarang kita bedah mesinnya. Probe dijalankan oleh kubelet di node tempat pod berjalan, bukan oleh control plane. Kubelet memanggil endpoint container secara berkala dan memutuskan nasib pod berdasarkan hasilnya.
Tiga jenis probe, tiga konsekuensi berbeda:
| Probe | Pertanyaan | Jika gagal |
|---|---|---|
livenessProbe |
"Apakah proses ini macet/deadlock?" | Container di-restart oleh kubelet |
readinessProbe |
"Apakah pod ini siap menerima trafik?" | Pod dikeluarkan dari Service endpoints, tidak di-restart |
startupProbe |
"Apakah aplikasi sudah selesai booting?" | Liveness & readiness ditahan sampai sukses; jika budget habis, container di-restart |
Mekanisme pengecekan (handler):
- httpGet: HTTP request ke path/port; status 200-399 dianggap sukses.
- tcpSocket: cukup bisa membuka koneksi TCP.
- exec: menjalankan perintah di dalam container; exit code 0 = sukses. Setiap cek berarti fork proses baru, jadi mahal kalau period-nya pendek dan pod-nya banyak.
- grpc: memanggil gRPC Health Checking Protocol.
Parameter yang menentukan waktu:
| Parameter | Arti |
|---|---|
initialDelaySeconds |
Jeda setelah container start sebelum probe pertama |
periodSeconds |
Interval antar probe |
timeoutSeconds |
Batas waktu satu probe menunggu jawaban; lewat = gagal |
failureThreshold |
Jumlah kegagalan berturut-turut sebelum dianggap gagal |
successThreshold |
Jumlah sukses berturut-turut untuk dianggap pulih; untuk liveness dan startup wajib 1 |
Studi kasus. Ini konfigurasi nyata yang sering kita temukan di Deployment:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
Sekilas wajar. Sekarang hitung matematikanya.
Timeline liveness saat aplikasi belum pernah menjawab (startup lambat):
| Waktu | Kejadian |
|---|---|
| t=0s | Container start, JVM mulai booting |
| t=15s | Liveness probe #1 → gagal (1/3) |
| t=35s | Liveness probe #2 → gagal (2/3) |
| t=55s | Liveness probe #3 → gagal (3/3) → kubelet kill container |
| t=55s+ | Restart #1; dalam skenario terburuk (jitter, timeout 5s per probe) baru terjadi menjelang ~75s |
Artinya: jika aplikasi butuh lebih dari ~55 detik untuk siap, ia akan dibunuh sebelum sempat hidup. Di laptop developer Spring Boot start dalam 25 detik. Di node yang sibuk, dengan CPU limit 500m dan CFS throttling, startup yang sama bisa 70-90 detik. Hasilnya: kill, restart, kill lagi. Kubelet memberi jeda yang makin panjang di antara restart (exponential backoff: 10s, 20s, 40s, 80s, ... dibatasi 5 menit). Status pod berubah menjadi CrashLoopBackOff, padahal aplikasinya tidak punya bug.
Liveness di tengah umur pod. Kalau aplikasi yang sedang berjalan tiba-tiba deadlock, deteksinya memakan waktu antara (failureThreshold-1) × periodSeconds dan failureThreshold × periodSeconds + timeoutSeconds, yaitu sekitar 40-65 detik. Selama itu pod zombie masih dianggap hidup.
Readiness di tengah umur pod. Saat /ready mulai gagal, pod baru dikeluarkan dari endpoints setelah 3 kegagalan × 10 detik, yaitu sekitar 20-30 detik (plus timeout 3 detik). Sepanjang jendela itu, Service tetap mengirim trafik ke pod yang sudah tidak sanggup. Di hasil load test, ini terlihat sebagai gumpalan error atau latency spike yang durasinya kurang lebih sama dengan jendela tersebut.
3. Bagaimana Jika Kita Coba...
"Gampang, endpoint /healthz kita bikin lengkap: cek Redis, cek database, cek payment gateway. Kalau ada yang mati, pod dianggap tidak sehat."
Kedengarannya teliti. Inilah cara tercepat mengubah gangguan kecil menjadi outage total.
- Redis melambat, misalnya karena satu perintah
KEYS *atau failover replica. Latency naik dari 1ms ke 6 detik. /healthzdi semua pod menunggu Redis, melewatitimeoutSeconds: 5. Semua pod gagal liveness pada waktu yang hampir sama.- Setelah 3 kegagalan, kubelet me-restart semua pod. Kapasitas aplikasi jatuh ke nol.
- Restart tidak memperbaiki Redis sedikit pun. Masalahnya ada di luar pod.
- Saat Redis pulih, puluhan pod booting bersamaan dan membuka koneksi baru secara serentak: connection storm (lihat Sesi 16). Redis kembali tertekan, probe gagal lagi. Ini pola cascading failure yang kita bahas di Sesi 25.
"Oke, kalau begitu cek dependency kita pindah ke /ready saja."
Lebih baik, tapi masih berbahaya. Jika semua pod menjadi unready karena satu dependency bersama mati, Service punya nol endpoints. Router OpenShift langsung mengembalikan 503 untuk semua request, termasuk endpoint yang sama sekali tidak butuh Redis. Lebih masuk akal: /ready mengecek hal yang pod-local (thread pool tersedia, warm-up cache selesai, koneksi pool milik pod ini sendiri), dan aplikasi melakukan degradasi anggun (fallback, circuit breaker) saat dependency bermasalah.
"Kalau startup lambat, naikkan saja initialDelaySeconds jadi 120."
Ini menambal gejala. Setiap kali pod start, termasuk saat restart karena deadlock sungguhan, liveness tidak bekerja selama 2 menit. Dan saat node sedang sangat sibuk sehingga startup butuh 130 detik, masalahnya kembali lagi.
Jebakan tambahan: timeout 5 detik saat load test. Di puncak beban, container mentok di CPU limit dan kena CFS throttling. Thread HTTP untuk /healthz antre di belakang request bisnis. Aplikasinya sehat, hanya lambat, tapi /healthz menjawab dalam 6 detik. Liveness gagal, pod di-restart tepat di saat puncak. Kapasitas turun, pod yang tersisa menanggung beban lebih berat, ikut throttling, ikut gagal probe. Ini death spiral yang dipicu oleh health check.
4. Nama Resminya
- Liveness Probe: Deteksi proses macet; gagal = container di-restart.
- Readiness Probe: Gerbang trafik; gagal = pod keluar dari Service endpoints.
- Startup Probe: Pelindung masa booting; menahan liveness & readiness sampai aplikasi selesai start.
- CrashLoopBackOff: Status saat container berulang kali mati dan kubelet menunda restart dengan exponential backoff (maks 5 menit).
- CFS Throttling: Pembatasan CPU oleh Linux Completely Fair Scheduler saat container menghabiskan kuota
limits.cpu. - Cascading Failure / Correlated Restart: Banyak pod gagal bersamaan karena satu penyebab bersama.
- Endpoints / EndpointSlice: Daftar IP pod yang Ready, yang menjadi target Service.
Konfigurasi revisi yang lebih aman:
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
failureThreshold: 30 # budget startup hingga 300 detik
livenessProbe:
httpGet:
path: /healthz # cek lokal saja: event loop/thread hidup, tanpa Redis/DB
port: 8080
periodSeconds: 10
timeoutSeconds: 5 # beri ruang untuk jitter saat CPU throttling
failureThreshold: 6 # ~60 detik gagal terus baru restart
readinessProbe:
httpGet:
path: /ready # cek kesiapan pod-local; dependency ditangani graceful
port: 8080
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3 # keluar dari endpoints dalam ~10-15 detik
Kenapa ini lebih baik:
- startupProbe memberi budget 300 detik untuk boot, tapi berhenti begitu sukses. Startup cepat tidak dibuat menunggu, startup lambat tidak dibunuh.
- initialDelaySeconds tidak perlu lagi; liveness baru aktif setelah startup probe sukses.
- Liveness lebih sabar (period 10 × threshold 6) sehingga satu-dua probe lambat karena throttling tidak memicu restart, tapi deadlock sungguhan tetap tertangkap dalam sekitar satu menit.
- Readiness lebih responsif (period 5) sehingga pod yang kewalahan cepat dikeluarkan dari rotasi, dan cepat masuk kembali saat pulih.
5. Di Dunia Kita
- Java/Spring Boot di node padat: Startup 30 detik di staging menjadi 80 detik di production saat 20 pod di-rollout bersamaan dan semua berebut CPU. Tanpa startup probe, rolling update macet di
CrashLoopBackOff. - Spring Boot Actuator:
/actuator/health/livenessdan/actuator/health/readinessmemisahkan health group. Jebakan umum: health indicator Redis/DB ikut masuk group liveness secara default di konfigurasi lama. - Pengecekan di OpenShift:
oc get pods→ kolomRESTARTSyang terus naik.oc describe pod <nama>→ bagian Events:Liveness probe failed: Get "http://10.128.2.14:8080/healthz": context deadline exceeded.oc get events --field-selector reason=Unhealthy→ semua kegagalan probe di namespace.oc get endpoints <service>→ berapa pod yang benar-benar menerima trafik saat ini.oc logs <pod> --previous→ log container sebelum dibunuh.- Rolling update: Readiness probe yang lolos terlalu cepat (misal hanya cek port terbuka) membuat pod baru menerima trafik sebelum cache dan connection pool siap, menghasilkan lonjakan latency di setiap deploy.
6. Lensa Performance Tester
Probe adalah bagian dari sistem yang diuji, bukan sekadar konfigurasi infrastruktur. Probe yang salah bisa menjadi penyebab utama error di hasil load test.
Metrik yang harus dipantau selama test:
| Metrik | Sumber | Sinyal bahaya |
|---|---|---|
| Restart count | oc get pods (kolom RESTARTS) |
Naik selama load test tanpa OOMKilled |
| Event probe | oc get events (Liveness probe failed, Readiness probe failed) |
Muncul berkelompok di puncak beban |
| Time-to-ready | Selisih waktu container start → Ready | Jauh lebih lama di node sibuk dibanding idle |
| Endpoint count | oc get endpoints / metrik kube_endpoint_address_available |
Turun naik (flapping) atau jatuh ke 0 |
| Error rate per jendela waktu | Hasil load test tool | Gumpalan error berdurasi ~20-30 detik selaras dengan event readiness |
Skenario uji yang wajib:
1. Load test di CPU limit: Dorong beban sampai pod mentok di limits.cpu. Amati apakah ada restart yang dipicu liveness. Restart tanpa OOMKilled di puncak beban hampir pasti berarti probe terlalu ketat.
2. Chaos pada dependency: Perlambat Redis (latency injection) atau matikan sementara. Verifikasi bahwa pod tidak di-restart; yang boleh terjadi hanya degradasi atau error terkontrol.
3. Cold start di node sibuk: Scale dari 2 ke 20 pod saat beban sedang tinggi. Ukur time-to-ready dan pastikan tidak ada pod yang masuk CrashLoopBackOff.
4. Deadlock sungguhan: Simulasikan thread macet. Ukur berapa detik sampai liveness me-restart pod, dan berapa error yang terjadi di jendela itu.
5. Korelasi waktu: Tempelkan timestamp event probe ke grafik error rate. Kalau polanya cocok, sumber error-nya ada di probe, bukan di kode.
Pertanyaan yang perlu dibawa ke tim saat review: "Apa yang dicek /healthz kita? Kalau Redis lambat 10 detik, apakah pod kita di-restart?"
7. Pertanyaan untuk Ronde Berikutnya
Probe sudah kita jinakkan: pod tidak lagi dibunuh tanpa alasan. Tapi pod tetap akan mati, entah karena rolling update, scale down, atau node drain. Saat OpenShift memutuskan sebuah pod harus berhenti, apa yang terjadi pada request yang sedang diproses di dalamnya? Dan kenapa load test kita selalu menunjukkan sederet error 502 tepat di setiap deploy, padahal semua probe hijau?
Jawabannya ada di Sesi 36: Detik-Detik Terakhir Sebuah Pod, tentang SIGTERM, terminationGracePeriodSeconds, dan preStop hook.