UNDER PRESSURE

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?

Sesi 35 / 389 menit baca

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.

  1. Redis melambat, misalnya karena satu perintah KEYS * atau failover replica. Latency naik dari 1ms ke 6 detik.
  2. /healthz di semua pod menunggu Redis, melewati timeoutSeconds: 5. Semua pod gagal liveness pada waktu yang hampir sama.
  3. Setelah 3 kegagalan, kubelet me-restart semua pod. Kapasitas aplikasi jatuh ke nol.
  4. Restart tidak memperbaiki Redis sedikit pun. Masalahnya ada di luar pod.
  5. 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/liveness dan /actuator/health/readiness memisahkan health group. Jebakan umum: health indicator Redis/DB ikut masuk group liveness secara default di konfigurasi lama.
  • Pengecekan di OpenShift:
  • oc get pods → kolom RESTARTS yang 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.