Resilience Patterns — System Design

Sistem production yang baik bukan yang tidak pernah gagal — melainkan yang gagal dengan anggun. Resilience patterns adalah teknik untuk mencegah satu komponen…

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 bayar macet 10 detik. Service tiket sehat walafiat. Selama itu, berapa banyak cek kursi (dari order ke tiket) 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:

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
    }
  }
}

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);
  }
}

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:

Pengguna lebih senang melihat 80% fitur jalan daripada halaman error 500. Ini disebut graceful degradation.

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari