Chapter pertama section ini menutup dengan dilema node yang berhenti di tengah pertukaran pesan, dan Chapter Replication memperlihatkan failover yang menyelamatkan akses. Fault tolerance merangkum keduanya menjadi satu tujuan: sistem tetap menjalankan fungsinya meski komponen di dalamnya gagal. Kuncinya membedakan fault (komponen menyimpang dari perilaku semestinya) dari failure (layanan berhenti total), lalu merancang respons yang menjaga fault tidak menjalar menjadi failure. Chapter ini mensimulasikan tiga respons paling mendasar: retry dengan jeda cerdas, deteksi node berhenti, dan penulisan yang aman diulang.

Galat, Kegagalan, dan Tiga Respons Dasar #

Galat sementara — jaringan lambat sekejap, disk penuh sesaat — adalah kejadian rutin di cluster, bukan kejadian luar biasa. Menanggapi tiap jenis galat butuh alat yang berbeda; salah memilih alat justru memperberat situasi, misalnya me-retry permintaan yang sudah pasti ditolak karena validasi. Membedakan fault dari failure membantu menetapkan sasaran: fault pada satu node harus diserap mekanisme otomatis — retry, failover, dedup — sebelum menjalar menjadi failure yang dirasakan pengguna. Redundansi dari Chapter Replication menyediakan bahannya berupa salinan data di node lain; tabel berikut memetakan jenis galat ke respons yang tepat.

Jenis galatContoh nyataRespons yang tepat
Sementara (transient)Timeout sesaat, jaringan fluktuatifRetry dengan exponential backoff + jitter
Node berhenti (crash)Proses berhenti, mesin dimatikanDeteksi heartbeat lalu failover
Pengiriman duplikatAntrian at-least-once mengirim ulangIdempotency key untuk dedup

Ketiganya saling melengkapi: retry menyembuhkan galat sementara, heartbeat menemukan node yang harus diambil alih, dan idempotency membuat pengulangan apa pun aman terhadap data. Alur retry yang sehat berbentuk berikut ini.

Simulasi: Backoff, Jitter, dan Heartbeat #

Simulasi pertama memakai dua mekanisme. Bagian pertama memanggil layanan yang gagal acak — sengaja diberi seed agar hasilnya dapat direproduksi — dan mencoba ulang dengan jeda yang menggandakan diri setiap percobaan. Bagian kedua menghidupkan tiga node yang mengirim heartbeat tiap ronde; satu node berhenti mengirim di tengah, dan monitor menandainya setelah melewati batas waktu tunggu.

python
"""Dua mekanisme fault tolerance: retry dengan exponential backoff + jitter,
dan deteksi node berhenti lewat heartbeat."""
import random
import time

rng = random.Random(7)  # seed agar hasil dapat direproduksi


def layanan_tidak_stabil(nomor):
    """Layanan yang gagal acak; berhasil bila angka acak < 0.30."""
    if rng.random() < 0.30:
        return f"OK-{nomor}"
    raise ConnectionError("timeout koneksi")


def panggil_dengan_retry(maks_percobaan=5):
    """Coba layanan hingga berhasil; jeda antar percobaan naik eksponensial."""
    for percobaan in range(1, maks_percobaan + 1):
        try:
            hasil = layanan_tidak_stabil(percobaan)
            return percobaan, hasil
        except ConnectionError as galat:
            if percobaan == maks_percobaan:
                raise
            jeda = min(0.4, 0.05 * 2 ** (percobaan - 1))  # backoff eksponensial
            jeda += rng.uniform(0, 0.05)                  # jitter acak
            print(f"  percobaan {percobaan} gagal ({galat}), jeda {jeda:.3f} detik")
            time.sleep(jeda)


print("[1] Retry dengan exponential backoff + jitter")
percobaan, hasil = panggil_dengan_retry()
print(f"  sukses pada percobaan ke-{percobaan}: {hasil}")

print("[2] Deteksi node berhenti lewat heartbeat (timeout 2 ronde)")
detak_terakhir = {"node-web": 0, "node-etl": 0, "node-gudang": 0}
status = {node: "hidup" for node in detak_terakhir}
for ronde in range(1, 9):
    for node in detak_terakhir:
        if node == "node-gudang" and ronde >= 3:
            continue  # node-gudang berhenti mengirim heartbeat mulai ronde 3
        detak_terakhir[node] = ronde
    for node, terakhir in detak_terakhir.items():
        if status[node] == "hidup" and ronde - terakhir > 2:
            status[node] = "tidak tersedia"
            print(f"  ronde {ronde}: monitor menandai {node} sebagai tidak tersedia")
print(f"  status akhir: {status}")

Output yang Dihasilkan:

text
[1] Retry dengan exponential backoff + jitter
  percobaan 1 gagal (timeout koneksi), jeda 0.058 detik
  percobaan 2 gagal (timeout koneksi), jeda 0.104 detik
  percobaan 3 gagal (timeout koneksi), jeda 0.218 detik
  sukses pada percobaan ke-4: OK-4
[2] Deteksi node berhenti lewat heartbeat (timeout 2 ronde)
  ronde 5: monitor menandai node-gudang sebagai tidak tersedia
  status akhir: {'node-web': 'hidup', 'node-etl': 'hidup', 'node-gudang': 'tidak tersedia'}

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-13 (layanan_tidak_stabil): layanan berhasil dengan peluang 30 persen; angka acak berasal dari generator ber-seed sehingga urutan gagal-berhasil selalu sama antar eksekusi.
  2. Baris 16-28 (panggil_dengan_retry): percobaan terakhir yang gagal diteruskan sebagai exception — retry tidak boleh menyembunyikan kegagalan permanen.
  3. Baris 25-26 (jeda): jeda dasar berlipat dua tiap percobaan (0.05, 0.10, 0.20 detik, dibatasi 0.4) lalu ditambah jitter acak; tanpa jitter, ribuan klien yang gagal bersamaan akan mencoba ulang pada saat yang sama persis.
  4. Baris 36-42 (heartbeat): setiap node mencatat ronde terakhir pengiriman heartbeat; node-gudang berhenti mengirim mulai ronde 3.
  5. Baris 43-46 (deteksi): monitor menunggu lebih dari dua ronde tanpa heartbeat sebelum menandai node tidak tersedia — jeda ini mencegah satu paket hilang langsung memicu failover.

Batas waktu deteksi adalah kompromi. Timeout dua ronde membuat node-gudang baru ditandai pada ronde 5, cukup lama setelah detak terakhirnya, sehingga keputusan failover tidak terburu-buru. Timeout yang terlalu pendek memicu failover palsu setiap satu paket heartbeat hilang, sedangkan timeout yang terlalu panjang membiarkan sistem berjalan dengan kapasitas berkurang tanpa diketahui. Praktik umum menyetel batas waktu beberapa kali periode heartbeat.

Simulasi: Tulisan yang Aman Diulang #

Retry dan failover selalu berisiko memproses permintaan yang sama dua kali — persis peringatan pada Chapter Dasar Distributed Systems. Antrian pesan bertipe at-least-once memang bisa mengirim ulang, sehingga penerima wajib mengenali duplikat. Caranya dengan idempotency key: setiap permintaan membawa pengenal unik, dan penerima hanya menerapkan permintaan yang pengenalnya belum pernah diterapkan.

python
"""Idempotency key: pengiriman ulang pesan yang sama hanya diterapkan sekali."""

# antrian pengiriman menyerahkan pesan at-least-once: duplikat dimungkinkan
pesan_masuk = [
    ("req-101", "[email protected]", 3),
    ("req-102", "[email protected]", 1),
    ("req-101", "[email protected]", 3),   # duplikat req-101 (pengiriman ulang)
    ("req-103", "[email protected]", 2),
    ("req-102", "[email protected]", 1),   # duplikat req-102
]

sudah_diterapkan = set()  # penyimpan idempotency key
tabel_pesanan = {}


def terapkan(req_id, pelanggan, jumlah):
    """Terapkan pesan hanya bila req_id belum pernah diterapkan (dedup)."""
    if req_id in sudah_diterapkan:
        return f"{req_id}: duplikat, dilewati"
    tabel_pesanan[pelanggan] = jumlah
    sudah_diterapkan.add(req_id)
    return f"{req_id}: diterapkan ({pelanggan} = {jumlah})"


print("5 pesan diterima (2 di antaranya duplikat):")
for req_id, pelanggan, jumlah in pesan_masuk:
    print(f"  {terapkan(req_id, pelanggan, jumlah)}")
print(f"hasil akhir: {tabel_pesanan}")
print(f"jumlah tulisan nyata: {len(sudah_diterapkan)} "
      f"(bukan {len(pesan_masuk)} kali)")

Output yang Dihasilkan:

text
5 pesan diterima (2 di antaranya duplikat):
  req-101: diterapkan ([email protected] = 3)
  req-102: diterapkan ([email protected] = 1)
  req-101: duplikat, dilewati
  req-103: diterapkan ([email protected] = 2)
  req-102: duplikat, dilewati
hasil akhir: {'[email protected]': 3, '[email protected]': 1, '[email protected]': 2}
jumlah tulisan nyata: 3 (bukan 5 kali)

Lima pesan masuk tetapi hanya tiga tulisan yang benar-benar diterapkan: kedua duplikat dikenali lewat pengenal req-101 dan req-102 yang sudah tercatat di sudah_diterapkan. Kombinasi at-least-once di sisi pengirim dan dedup di sisi penerima menghasilkan efek effectively-once — hasil akhir seolah setiap pesan diproses tepat sekali, tanpa membutuhkan jaringan yang mustahil hilang pesan.

Tiga teknik ini menjaga satu layanan tetap hidup, tetapi cluster juga perlu keputusan bersama: node mana yang dipercaya memimpin setelah leader lama tidak terdeteksi? Pertanyaan tersebut masuk wilayah konsensus dan dibahas pada Chapter Distributed Coordination.