Resilience patterns adalah teknik supaya satu bagian yang rusak gak ikut merobohkan seluruh sistem. Sistem yang baik bukan yang gak pernah gagal, tapi yang gagal dengan anggun. Bayangin listrik di rumah. Tiap ruangan punya jalur dan MCB sendiri. Korsleting di kamar mandi bikin MCB-nya turun: cuma jalur itu yang mati, kulkas di dapur tetap dingin. Setelah diperbaiki, kamu naikkan MCB-nya sekali untuk mencoba; kalau turun lagi, berarti belum beres. Jalur terpisah itu bulkhead, MCB itu circuit breaker.
🧪 Tebakan cepat: Service
bayarmacet 10 detik. Servicetiketsehat walafiat. Selama itu, berapa banyak cek kursi (dariorderketiket) yang ikut gagal: hampir nol, sekitar 40%, atau 100%? Tebak dulu, jawabannya ada di bagian "Satu dependency sakit, semua ikut sakit".
Satu dependency sakit, semua ikut sakit
Model contoh lesson ini (dan Lab-nya), 20 detik di tengah war tiket:
ordermengirim 10 pembayaran per detik kebayar(normalnya 120 ms) dan 20 cek kursi per detik ketiket(50 ms).- Untuk panggilan keluar,
orderpunya 20 koneksi (connection pool). Panggilan yang gak kebagian koneksi langsung gagal. bayarmacet dari detik 2 sampai detik 12: request diterima tapi gak pernah dijawab.ordermenyerah setelah timeout 2 detik.
Tiap pembayaran ke bayar yang macet memegang satu koneksi selama 2 detik. 10 per detik × 2 detik = 20 koneksi, artinya seluruh pool habis dipegang bayar. Cek kursi gak kebagian koneksi, padahal tiket sehat:
| Pelindung (timeout 2 detik) | Pembayaran sukses (dari 200) | Menunggu 2 detik lalu gagal | Ditolak cepat | Cek kursi gagal (dari 400) |
|---|---|---|---|---|
| cuma timeout | 100 | 100 | 0 | 168 |
| + bulkhead | 100 | 50 | 50 | 0 |
| + circuit breaker | 57 | 23 | 120 | 8 |
| breaker + bulkhead | 57 | 13 | 130 | 0 |
Jawaban tebakannya: 168 dari 400 cek kursi gagal, sekitar 42%, dan semuanya karena bayar. Ini cascading failure: satu dependency yang sakit menghabiskan sumber daya bersama, lalu bagian sehat ikut tumbang. Baris-baris lain di tabel adalah pola-pola di lesson ini.
Timeout: batas menunggu
Setiap panggilan jaringan wajib punya timeout. fetch di Node baru menyerah setelah sekitar 5 menit menunggu header (lesson Microservices Architecture di track DevOps). Panjang timeout ikut menentukan kerusakan: di model ini, timeout 1 detik membuat bayar paling banyak memegang 10 koneksi, jadi cek kursi selamat; timeout 5 detik membuat pool habis lebih lama. Pegangan awal: sedikit di atas waktu respons p99 dependency-nya (99% request selesai di bawah angka itu).
Timeout juga harus mengecil ke arah dalam. Kalau load balancer menyerah setelah 3 detik, order jangan menunggu bayar 5 detik: kerjanya sia-sia karena pengguna sudah menerima error. Sisa waktu yang dibawa turun ke tiap panggilan ini disebut deadline propagation.
Retry dengan exponential backoff dan jitter
Kegagalan sesaat (paket hilang, pod restart) layak dicoba lagi, tapi caranya penting. Exponential backoff: jeda sebelum retry berlipat dua tiap kali, misalnya 1, 2, 4, 8 detik, dibatasi maksimal (cap) 8 detik. Jitter: jedanya diacak. Rumus full jitter:
// retry dengan exponential backoff + full jitter, maksimal 5 retry
async function denganRetry(fn, { maks = 5, base = 1000, cap = 8000 } = {}) {
for (let retry = 0; ; retry++) {
try {
return await fn();
} catch (err) {
if (retry === maks) throw err;
const batas = Math.min(cap, base * 2 ** retry); // 1.000, 2.000, 4.000, 8.000, 8.000 ms
await new Promise((r) => setTimeout(r, Math.random() * batas)); // acak antara 0 dan batas
}
}
}
base * 2 ** retryadalah bagian eksponensialnya;Math.min(cap, ...)mencegahnya tumbuh tanpa batas.Math.random() * batasadalah jitter-nya: tiap client memilih jeda sendiri antara 0 dan batas itu.
Kenapa jitter penting? Model Lab: 2.000 client gagal di detik yang sama saat bayar mati, lalu bayar pulih dan sanggup 600 request per setengah detik. Tanpa jitter, semuanya retry tepat di detik 1: 2.000 sekaligus, 1.400 gagal lagi, lalu retry bersama lagi di detik 3, 7, dan 15. Itu retry storm: gelombang retry serentak yang terus menghantam service yang baru pulih. Totalnya 4.400 request dan baru beres di detik 15. Dengan full jitter, retry tersebar: puncaknya 1.145 per setengah detik, total 2.957 request, beres di detik 7,7.
Retry cuma untuk request yang aman diulang (GET, atau yang memakai Idempotency-Key, lesson Idempotency & Retry Safety), dan jangan retry di dua tempat sekaligus (lesson Service Mesh).
Circuit breaker: closed, open, half-open
Timeout membatasi lama menunggu, tapi order tetap menunggu 2 detik untuk setiap pembayaran ke bayar yang sudah jelas mati. Circuit breaker berhenti memanggil dependency yang terus gagal. Ia punya tiga state:
| State sekarang | Kejadian | State berikutnya |
|---|---|---|
| closed (normal) | panggilan sukses | closed, counter gagal kembali 0 |
| closed | gagal ke-3 berturut-turut (threshold 3) | open |
| open | ada request baru | ditolak cepat, bayar gak dipanggil |
| open | 5 detik berlalu | half-open |
| half-open | satu request percobaan (probe) sukses | closed, counter 0 |
| half-open | probe gagal | open lagi, tunggu 5 detik lagi |
Di model Lab (timeout 2 detik), bayar mati di detik 2. Kegagalan pertama baru ketahuan di detik 4,0, karena harus menunggu timeout, dan yang ketiga di detik 4,2: breaker open. Detik 9,2 half-open, probe dikirim, bayar masih mati, probe timeout di detik 11,2: open lagi. Detik 16,2 half-open, probe sukses di 16,4: closed. Selama itu bayar cuma menerima 80 request, bukan 200, dan pengguna yang ditolak cepat gak menunggu 2 detik untuk hasil yang sama. Harganya: bayar sebenarnya sudah pulih di detik 12, tapi breaker baru tahu di detik 16,4.
Breaker membungkus panggilannya:
// order: panggil bayar lewat circuit breaker, timeout 2 detik, fallback kalau gagal
async function bayarPesanan(pesanan) {
if (!breaker.bolehMencoba()) return fallbackBayar(pesanan); // open: ditolak cepat
try {
const res = await fetch("http://bayar:8080/charges", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": `ord-${pesanan.id}` },
body: JSON.stringify({ pesanan: pesanan.id, jumlah: pesanan.total }),
signal: AbortSignal.timeout(2000),
});
breaker.sukses();
return res;
} catch (err) {
breaker.gagal();
return fallbackBayar(pesanan);
}
}
breaker.bolehMencoba()menjawab "boleh" saat closed, "boleh, tapi cuma satu" saat half-open, dan "jangan" saat open.sukses()dangagal()melaporkan hasilnya, dan dari situ state-nya berpindah sesuai tabel. Library yang sudah jadi: opossum (Node.js), Resilience4j (Java), Polly (.NET).
Bulkhead: sekat per dependency
Bulkhead diambil dari sekat di lambung kapal: satu ruang bocor, kapal tetap mengapung. Di software artinya sumber daya dipisah per dependency. Pool 20 koneksi dibagi: 10 untuk bayar, 10 untuk tiket. Saat bayar macet, yang habis cuma 10 koneksi milik bayar; pembayaran ke-11 dan seterusnya langsung ditolak, dan tiket tetap punya 10 koneksinya. Hasilnya di tabel tadi: cek kursi gagal 0. Sekatnya bisa berupa connection pool, thread pool, semaphore, atau pod terpisah.
Fallback: tetap kasih sesuatu yang berguna
Saat panggilan gagal atau ditolak breaker, fallback memberi jawaban cadangan, bukan error mentah:
- Pembayaran gagal: "Pembayaran lagi gangguan. Kursi A-12 kami tahan 10 menit, coba bayar lagi sebentar."
- Cek kursi gagal: tampilkan peta kursi dari cache, ditandai "data 1 menit lalu".
- Rekomendasi konser lain gagal: sembunyikan bagian itu, sisa halaman normal.
Pengguna lebih senang melihat 80% fitur jalan daripada halaman error 500. Ini disebut graceful degradation.
⚠️ Jebakan yang sering ketemu
- Panggilan jaringan tanpa timeout. Satu dependency macet menahan koneksi dan memicu cascading failure.
- Retry tanpa jitter. Semua client retry di detik yang sama dan membanjiri service yang baru pulih.
- Retry tanpa circuit breaker. Service yang mati menerima berlipat-lipat request saat paling gak sanggup.
- Breaker terlalu sensitif (open setelah 1 kegagalan) membuat service sehat sering dianggap mati; terlalu lama open menolak traffic jauh setelah dependency pulih.
- Breaker open tanpa fallback. Pengguna tetap dapat error mentah, cuma lebih cepat.
TL;DR: Resilience patterns mencegah satu dependency yang sakit merobohkan semuanya (cascading failure: di Lab, bayar macet membuat 168 dari 400 cek kursi ke tiket yang sehat ikut gagal karena pool koneksi habis). Timeout membatasi lama menunggu, sedikit di atas p99, dan mengecil ke arah dalam. Retry memakai exponential backoff dan jitter, random(0, min(cap, base × 2^retry)), supaya client gak retry serentak dan memicu retry storm; cuma untuk request yang aman diulang. Circuit breaker punya tiga state: closed (normal, menghitung kegagalan), open (setelah kegagalan mencapai threshold; request ditolak cepat), dan half-open (setelah timeout open berlalu, satu probe dikirim untuk mengetes; sukses kembali closed, gagal kembali open). Bulkhead mengisolasi sumber daya (thread pool, connection pool) per dependency, sehingga satu service bermasalah gak menghabiskan sumber daya service lain. Fallback memberi jawaban cadangan supaya pengguna tetap dapat sesuatu yang berguna.