Skip to main content
System design langkah demi langkah · Pelajaran 5 dari 7 · 14 menit

Belajar Pola Reliability: SLO, Retry, dan Circuit Breaker

Belajar pola reliability: SLO dan error budget, redundansi dan failover, retry dengan backoff dan jitter, bulkhead, backpressure, serta circuit breaker.

Oleh Matthews WongDitinjau pada

Poin utama

Pola reliability adalah kebiasaan yang membuat sistem gagal dengan anggun: saat satu bagian rusak, dampaknya tetap kecil dan singkat. Anda menetapkan SLO di bawah 100% dan memakai selisihnya sebagai error budget, menjalankan salinan redundan di belakang health check, melakukan retry di satu lapisan dengan backoff ber-jitter, membatasi tiap dependency dengan bulkhead dan backpressure, serta memutus circuit breaker saat sebuah dependency terus gagal.

Setelah pelajaran ini, Anda bisa

  • Mengubah SLO menjadi error budget dalam menit, dan menilai apa yang benar-benar didapat dari redundansi.
  • Menjelaskan kenapa retry berlapis berubah menjadi badai, lalu memperbaikinya dengan satu lapisan retry, backoff ber-jitter, dan budget.
  • Menelusuri circuit breaker dari closed, open, sampai half-open, dan tahu kapan memakainya.
Lihat cara kerjanya

Ikuti diagramnya langkah demi langkah

Tekan Putar, atau melangkah dengan panah. Setiap langkah juga ditulis di bawah diagram.

Dari retry storm sampai circuit breaker

Sebuah payment provider mulai gagal; retry di dua lapisan berlipat menjadi 9 panggilan per klik, retry di satu lapisan dengan backoff ber-jitter menyebarkannya, lalu circuit breaker terbuka setelah kegagalan berulang, menjadi half-open setelah timeout untuk meloloskan panggilan uji, dan menutup saat panggilan itu berhasil.

Langkah 1 dari 7. Client app memanggil orders API, yang memanggil payment provider. Kedua lapisan mengulang panggilan yang gagal sampai 3 kali. Circuit breaker belum dipasang.

Tombol: ← → langkah · Home / End · K putar atau jeda

  1. Client app memanggil orders API, yang memanggil payment provider. Kedua lapisan mengulang panggilan yang gagal sampai 3 kali. Circuit breaker belum dipasang.

  2. Provider mulai menjawab 503: ia kelebihan beban. Kegagalan ini tampak sementara, jadi kedua lapisan mengulanginya, persis seperti yang tertulis di kodenya.

    Tebak langkah berikutnya Kedua lapisan retry 3 kali. Berapa panggilan sampai ke provider per klik? (3 panggilan · 6 panggilan · 9 panggilan)
  3. Retry berlipat ganda: 3 percobaan di app dikali 3 di API menjadi 9 panggilan per klik, dan semua klien menembakkannya bersamaan. Gangguan singkat berubah menjadi badai.

  4. Perbaikan pertama: hanya API yang melakukan retry, setelah jeda eksponensial berbatas dengan full jitter, yaitu jeda acak sampai 200, lalu 400 ms. Klien berhenti retry serempak.

    sleep = random(0, min(cap, base * 2 ** n))
  5. Perbaikan kedua: circuit breaker menghitung kegagalan. Saat ambangnya tercapai ia terbuka, dan panggilan langsung gagal tanpa menyentuh provider, sehingga provider punya ruang untuk pulih.

    @failure_threshold = 5 # Fowler's exampleTebak langkah berikutnya Breaker sedang open. Apa yang terjadi saat timeout-nya habis? (Menutup dan mengirim semua trafik · Beberapa panggilan uji diloloskan · Tetap open sampai di-restart)
  6. Saat timeout open habis, breaker menjadi half-open dan meloloskan sejumlah kecil panggilan uji. Jika satu saja gagal, ia langsung kembali open.

  7. Panggilan uji berhasil, jadi breaker menutup dan me-reset hitungan kegagalannya. Trafik normal mengalir lagi, dan retry ber-jitter tetap meredam gangguan kecil sesekali.

Seberapa andal sistem harus berjalan, dan apa itu error budget?

Mulailah dari angka, bukan perasaan. Service level indicator (SLI) adalah rasio event yang baik terhadap event yang valid, misalnya request yang berhasil dibagi semua request. Service level objective (SLO) adalah target untuk rasio itu dalam satu jendela waktu, dan Google SRE Workbook terang-terangan menyebut 100% sebagai target yang salah: pengguna tidak bisa merasakan bedanya, dan mengejarnya memperlambat setiap rilis. SLA adalah kontrak yang punya konsekuensi, dan sebaiknya lebih longgar dari SLO supaya alarm Anda sendiri berbunyi lebih dulu.

Baca selengkapnya (1)

Error budget adalah 100% dikurangi SLO, dan konsep ini mengubah reliability menjadi sesuatu yang bisa dibelanjakan. Jendela 30 hari berisi 43.200 menit, jadi 99,9% mengizinkan 43,2 menit kegagalan dan 99,99% hanya 4,32 menit, yang bisa habis oleh satu deploy buruk yang lambat ditangani. Tulis kebijakannya sebelum gangguan pertama: saat budget habis, bekukan perubahan berisiko dan dahulukan perbaikan reliability. Saat budget nyaris utuh, rilis lebih cepat, karena reliability yang tidak dibutuhkan siapa pun sama dengan kecepatan yang terbuang.

Apa yang sebenarnya didapat dari redundansi dan failover?

Bagian-bagian dalam jalur request tersusun seri, jadi availability-nya dikalikan: tiga tier yang masing-masing 99,9% menghasilkan 0,999 pangkat tiga, sekitar 99,7%. Salinan redundan dari satu tier tersusun paralel, jadi tier itu baru mati jika semua salinannya mati: dua salinan independen 99,9% menghasilkan 1 dikurangi 0,001 kuadrat, 99,9999% di atas kertas. Frasa di atas kertas itu penting. Rumusnya mengasumsikan salinan gagal secara independen dan failover terjadi seketika, padahal dua replika di satu host, satu push konfigurasi, atau satu pipeline deploy akan gagal bersamaan.

Baca selengkapnya (1)

Failover punya tiga bagian, mendeteksi, memutuskan, dan mengalihkan, dan deteksi biasanya yang paling lama. Health check hanyalah deteksi: ia mengubah sebuah status sampai ada yang bertindak atasnya. Tier stateless mudah di-failover, karena salinan mana pun bisa melayani request apa pun begitu session dan upload disimpan di tempat lain. Database adalah bagian tersulit, karena salinannya harus sepakat pada setiap write, dan mempromosikan replika yang tertinggal akan menghilangkan commit terbaru.

Bagaimana melakukan retry tanpa memicu retry storm?

Retry adalah taruhan bahwa percobaan berikutnya akan berhasil, sekaligus beban tambahan yang diarahkan ke sesuatu yang sedang kesulitan. Ulangi hanya kegagalan sementara, seperti timeout, koneksi terputus, 429, dan 503, dan hanya operasi yang aman diulang. RFC 9110 menyebut method idempotent jika mengirimnya dua kali punya efek yang sama dengan mengirimnya sekali. POST tidak termasuk, jadi pembayaran yang di-retry butuh idempotency key yang dibuat sebelum percobaan pertama dan dipakai ulang di setiap retry.

Baca selengkapnya (1)

Retry berlipat ganda antar lapisan. Jika sebuah app mencoba 3 kali dan API di belakangnya juga mencoba 3 kali, satu panggilan yang gagal sampai ke dependency sebanyak 9 kali. Buku Google SRE menyebutnya combinatorial explosion dan menyarankan retry hanya di lapisan tepat di atas lapisan yang menolak request, dengan budget per klien yang hanya mengizinkan retry selama jumlahnya di bawah 10% dari request. Lalu sebarkan: backoff eksponensial berbatas dengan full jitter menunggu waktu acak antara nol dan jeda berbatas yang berlipat dua setiap percobaan. Analisis Marc Brooker di AWS menyebut backoff tanpa jitter sebagai pecundang yang jelas, karena semua klien tetap terbangun di detik yang sama.

Bagaimana bulkhead, backpressure, dan circuit breaker menahan sebuah kegagalan?

Bulkhead memberi setiap dependency jatah tetapnya sendiri atas resource yang terbatas, entah connection pool, slot konkurensi, atau memori sebuah container, sehingga dependency yang lambat hanya bisa menghabiskan jatahnya sendiri. Azure menamai pola ini dari sekat-sekat lambung kapal. Hitung tiap jatah dengan Little's law: jumlah panggilan bersamaan sama dengan laju kedatangan dikali latency, jadi 40 request per detik yang masing-masing ditahan 0,05 detik butuh 2 koneksi. Backpressure menerapkan ide yang sama antara producer dan consumer: batasi setiap antrean dan tentukan apa yang terjadi saat penuh, lalu di tepi sistem jawab 429 atau 503 dengan header Retry-After yang menyebut berapa lama harus menunggu.

Baca selengkapnya (2)

Circuit breaker berhenti memanggil dependency yang terus gagal. Ia membungkus panggilan dan menghitung kegagalan terbaru; setelah melewati ambang, ia terbuka (open), dan panggilan langsung gagal tanpa menyentuh dependency. Setelah timeout ia menjadi half-open dan meloloskan sejumlah kecil panggilan uji. Jika berhasil, ia menutup dan me-reset hitungannya; jika satu saja gagal, ia terbuka lagi dan memulai ulang timer-nya. Begitulah deskripsi Azure Architecture Center, dan contoh dari Fowler terbuka setelah 5 kegagalan. Resilience4j, library yang umum dipakai, secara default terbuka pada tingkat kegagalan 50%, tetap open selama 60 detik, dan mengizinkan 10 panggilan uji.

Pola-pola ini menjawab pertanyaan yang berbeda dan saling melengkapi. Bulkhead bertanya seberapa besar kapasitas saya yang boleh dipakai dependency ini; breaker bertanya apakah saya perlu memanggilnya sekarang; retry bertanya apakah satu panggilan ini layak diberi kesempatan kedua. Saran Azure untuk menggabungkannya: lakukan retry melalui breaker, dan hentikan retry saat breaker melaporkan bahwa kegagalannya bukan sementara.

Cara situs ini melakukannya

Konfigurasi nyata di balik pelajaran ini

Kutipan dari repositori yang membangun dan men-deploy situs ini, apa adanya.

docker-compose.yml:27-42
    mem_limit: 512m
    cpus: "1.0"
    healthcheck:
      test:
        [
          "CMD",
          "wget",
          "--no-verbose",
          "--tries=1",
          "--spider",
          "http://127.0.0.1:3000/robots.txt",
        ]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

Situs ini menjalankan satu container, jadi angka redundansi yang jujur adalah nol: jika server atau container-nya mati, situsnya mati. Dua ide lain dari pelajaran ini ada di sini. mem_limit dan cpus adalah bulkhead di level container: aplikasi paling banyak memakai 512 MB dan satu CPU, jadi render yang lepas kendali tidak bisa membuat container lain di VPS yang sama kelaparan. Health check hanyalah deteksi: tiga kali gagal dengan jeda 30 detik menandai container unhealthy, tetapi policy restart: unless-stopped di baris 10 bereaksi saat proses berhenti, bukan pada label itu.

app/api/chat/route.ts:392-397
        // Groq's rate limit (org-wide tokens/requests per minute) throws a 429
        // before streaming starts. Surface it as its own code so the client can
        // say "busy, try again shortly" instead of "connection interrupted".
        // Duck-typed on status so it doesn't depend on the SDK's error class.
        const status = (error as { status?: unknown } | null)?.status;
        const code = status === 429 ? "rate_limited" : "stream_failed";

Route chat tidak menambahkan loop retry sendiri saat model provider menjawab 429: ia melaporkan sibuk dan membiarkan pengunjung memutuskan, sesuai saran buku SRE agar error karena overload diteruskan ke atas. Satu lapisan di bawahnya ada retry yang tidak terlihat di route ini. Client dibuat dengan new Groq({ apiKey }), dan groq-sdk secara default melakukan retry 2 kali untuk error 408, 409, 429, dan 5xx dengan backoff eksponensial singkat. Jadi request yang terkena rate limit sampai ke provider hingga 3 kali sebelum cabang ini berjalan, padahal kuotanya dipakai bersama oleh semua pengunjung.

lib/api/errors.ts:125-133
export function rateLimited(limit: number, resetIn: number): Response {
  return apiError({
    status: 429,
    code: "rate_limited",
    message: "Too many requests. Please try again later.",
    hint: `Wait ${resetIn} seconds before retrying; the limit is ${limit} requests per minute per client.`,
    retryAfter: resetIn,
    headers: {
      "Retry-After": String(resetIn),

Load shedding di tepi situs ini. Saat sebuah klien melewati batas per menitnya, API langsung menjawab 429 dengan header Retry-After, alih-alih mengantrekan pekerjaan yang tidak sanggup ditanggungnya. Hitungannya disimpan di memori satu container yang sedang berjalan (lib/rate-limit.ts). Itu hanya berhasil karena salinannya cuma satu: replika kedua akan memberi setiap klien jatah kedua, itulah sebabnya state bersama harus dipindahkan keluar sebelum sebuah tier bisa dibuat redundan.

Yang sering rusak

Kesalahan yang benar-benar membuang waktu

  • Retry di setiap lapisan

    HTTP client, SDK, consumer antrean, dan gateway masing-masing bisa melakukan retry secara default, dan tidak ada yang melihat hasil perkaliannya sampai dependency tumbang. Daftar setiap lapisan antara pengguna dan dependency, tentukan satu lapisan yang melakukan retry, dan matikan sisanya.

  • Backoff tanpa jitter

    Menggandakan jeda memang menjarangkan retry seiring waktu, tetapi jika semua klien gagal bersamaan, semuanya menunggu 200, 400, dan 800 ms yang sama lalu menghantam dependency bergelombang. Acak setiap jeda dengan full jitter, dan tetap pasang batas atasnya, karena jitter tidak membatasi lamanya jeda.

  • Retry POST tanpa idempotency key

    Setelah timeout, Anda tidak bisa tahu apakah server sudah mengerjakan permintaannya. Retry pembayaran lalu menagih dua kali, kecuali klien membuat satu idempotency key sebelum percobaan pertama dan server mengembalikan hasil tersimpan untuk key yang sudah pernah dilihatnya.

  • Redundansi yang gagal bersamaan

    Dua replika di belakang satu load balancer, di satu host, dan diisi oleh satu pipeline deploy, sebenarnya satu titik kegagalan dengan dua nama. Cari bagian yang dipakai bersama, entah proxy, host, atau push konfigurasi, sebelum menghitung jumlah salinannya.

  • Breaker yang disetel untuk waktu pulih yang keliru

    Azure memperingatkan kedua ujungnya: breaker yang terlalu lama open tetap menggagalkan panggilan setelah masalahnya beres, dan breaker yang terlalu cepat half-open akan bolak-balik dan memperlambat semua pemanggil. Sesuaikan timeout open dengan cara dependency itu benar-benar pulih, dan kirim alert saat breaker terbuka.

  • Yang akan saya ubah di sini: lapisan retry yang tak terlihat

    Route chat situs ini membuat client Groq tanpa menyetel maxRetries, jadi default SDK berupa 2 kali retry berlaku untuk setiap request yang terkena rate limit. Menuliskan maxRetries secara eksplisit, 0 agar langsung gagal atau 2 dengan komentar, akan membuat keputusan retry terlihat di kode, bukan tersembunyi di dalam dependency.

Retry, circuit breaker, atau bulkhead: pertanyaan yang dijawab masing-masing
PertanyaanLayakkah panggilan ini dicoba lagi?Perlukah dependency ini dipanggil sekarang?Berapa kapasitas yang boleh dipakainya?
PemicuError sementara pada satu panggilanKegagalan melewati ambangBatas tetap pada slot atau memori
State yang disimpanJumlah percobaan per panggilanClosed, open, atau half-openPanggilan yang sedang berjalan
Salah jikaSemua lapisan melakukannya, serempakTimeout mengabaikan waktu pulih yang nyataTotal jatah melebihi batas yang sebenarnya
Coba sendiri

Lakukan di komputer Anda sendiri

Anda hanya butuh Node 22 atau lebih baru. Sebuah server kecil gagal selama 5 detik pertamanya; tiga klien memanggilnya dengan cara berbeda, dan server mencetak berapa request yang diterimanya setiap detik.

  1. Simpan server yang tidak stabil ini sebagai server.mjs dan jalankan di terminal tersendiri. Restart sebelum setiap langkah berikutnya, supaya gangguan 5 detiknya dimulai lagi.

    // server.mjs: answers 503 for the first 5 seconds, then 200, and counts requests
    import http from "node:http";
    const start = Date.now();
    let hits = 0;
    http
      .createServer((req, res) => {
        hits += 1;
        const down = Date.now() - start < 5000;
        res.writeHead(down ? 503 : 200).end(down ? "busy\n" : "ok\n");
      })
      .listen(8080, () => console.log("listening on :8080"));
    setInterval(() => console.log("requests so far:", hits), 1000);
  2. Picu retry storm: 50 klien, dua lapisan yang masing-masing mencoba 3 kali tanpa jeda. Hitungan di server melonjak ke 450 dalam satu detik, 50 klien dikali 9.

    // storm.mjs: 50 clients, two layers that each try 3 times with no wait
    const call = () => fetch("http://127.0.0.1:8080/").then((res) => res.ok);
    async function api() {
      for (let i = 0; i < 3; i++) if (await call()) return true;
      return false;
    }
    async function app() {
      for (let i = 0; i < 3; i++) if (await api()) return true;
      return false;
    }
    await Promise.all(Array.from({ length: 50 }, app));
    console.log("done");
  3. Lakukan retry di satu lapisan dengan backoff eksponensial berbatas dan full jitter. Hitungannya kini naik perlahan selama beberapa detik alih-alih melonjak, dan jumlah klien yang berhasil bergantung pada apakah jeda acaknya melewati masa gangguan.

    // jitter.mjs: one retry layer, capped exponential backoff with full jitter
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const call = () => fetch("http://127.0.0.1:8080/").then((res) => res.ok);
    async function withRetry(attempts = 6, base = 200, cap = 4000) {
      for (let n = 0; n < attempts; n++) {
        if (await call()) return true;
        if (n < attempts - 1) await sleep(Math.random() * Math.min(cap, base * 2 ** n));
      }
      return false;
    }
    const results = await Promise.all(Array.from({ length: 50 }, () => withRetry()));
    console.log("succeeded:", results.filter(Boolean).length, "of 50");
  4. Pasang circuit breaker di depannya. Breaker terbuka setelah 5 kegagalan, langsung gagal selama open, menjadi half-open setelah 2 detik, dan menutup begitu panggilan uji berhasil. Perhatikan perpindahan state-nya, dan betapa sedikit request yang diterima server selama gangguan.

    // breaker.mjs: closed, open after 5 failures, half-open after 2 s, closed again
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    class Breaker {
      state = "closed";
      failures = 0;
      openedAt = 0;
      constructor(threshold = 5, openMs = 2000) {
        this.threshold = threshold;
        this.openMs = openMs;
      }
      async call(fn) {
        if (this.state === "open") {
          if (Date.now() - this.openedAt < this.openMs) return "failed fast";
          this.move("half-open");
        }
        const ok = await fn();
        if (ok) {
          this.failures = 0;
          if (this.state !== "closed") this.move("closed");
          return "ok";
        }
        this.failures += 1;
        if (this.state === "half-open" || this.failures >= this.threshold) {
          this.openedAt = Date.now();
          this.move("open");
        }
        return "failed";
      }
      move(next) {
        console.log(this.state, "->", next);
        this.state = next;
      }
    }
    const breaker = new Breaker();
    const call = () => fetch("http://127.0.0.1:8080/").then((res) => res.ok);
    for (let i = 0; i < 40; i++) {
      console.log(i, await breaker.call(call));
      await sleep(250);
    }
Kuis singkat

Tiga pertanyaan sebelum lanjut

Pilih jawaban untuk melihat kenapa benar atau salah. Tidak ada yang disimpan atau dinilai, jadi tebak saja dengan bebas.

  1. 1.Seribu klien gagal pada saat yang sama dan semuanya retry setelah tepat 200, 400, dan 800 ms. Apa yang dilihat provider yang sedang pulih?
  2. 2.Setelah deploy, health check container gagal tiga kali berturut-turut. Service-nya memakai restart: unless-stopped. Apa yang dilakukan Docker?
  3. 3.Circuit breaker yang half-open mengirim dua panggilan uji. Yang pertama berhasil dan yang kedua gagal. Apa yang dilakukan breaker berikutnya?
Pikirkan dulu

Jawab dalam hati, lalu cek

  1. 1.SLO Anda 99,9% dalam 30 hari. Berapa banyak kegagalan yang boleh terjadi, dan apa yang harus dilakukan saat jatah itu habis?Lihat jawabannya

    Jawaban:0,1% dari 43.200 menit, yaitu 43,2 menit. Saat habis, kebijakan error budget berlaku: bekukan perubahan berisiko dan dahulukan pekerjaan reliability di atas fitur sampai service kembali memenuhi SLO-nya.

  2. 2.Kenapa sepasang server 99,9% tidak benar-benar menghasilkan 99,9999%?Lihat jawabannya

    Jawaban:Rumusnya mengasumsikan keduanya gagal secara independen dan failover terjadi seketika. Host, proxy, konfigurasi, atau pipeline deploy yang dipakai bersama membuat keduanya gagal bersamaan, dan waktu untuk mendeteksi kegagalan juga terhitung sebagai downtime.

  3. 3.App Anda dan API di belakangnya sama-sama retry 3 kali. Di mana retry sebaiknya diletakkan?Lihat jawabannya

    Jawaban:Hanya di satu lapisan, yaitu lapisan tepat di atas dependency yang gagal, dengan backoff ber-jitter dan retry budget. Matikan retry di lapisan lainnya, atau setiap klik yang gagal menjadi 9 panggilan.

  4. 4.Kapan circuit breaker lebih membantu daripada retry?Lihat jawabannya

    Jawaban:Saat kegagalannya bukan sementara. Retry berasumsi percobaan berikutnya kemungkinan berhasil; breaker menyadari bahwa panggilan terus gagal, berhenti mengirim panggilan agar dependency bisa pulih, lalu mengujinya lagi dengan beberapa panggilan uji.

Baca modulnya

Semua artikel di modul ini, berurutan

Pelajaran di atas adalah petanya. Setiap artikel membahas satu bagiannya secara mendalam, lengkap dengan contoh.