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 galat | Contoh nyata | Respons yang tepat |
|---|---|---|
| Sementara (transient) | Timeout sesaat, jaringan fluktuatif | Retry dengan exponential backoff + jitter |
| Node berhenti (crash) | Proses berhenti, mesin dimatikan | Deteksi heartbeat lalu failover |
| Pengiriman duplikat | Antrian at-least-once mengirim ulang | Idempotency 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.
"""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:
[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:
- 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. - Baris 16-28 (
panggil_dengan_retry): percobaan terakhir yang gagal diteruskan sebagai exception — retry tidak boleh menyembunyikan kegagalan permanen. - 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.
- Baris 36-42 (heartbeat): setiap node mencatat ronde terakhir pengiriman heartbeat; node-gudang berhenti mengirim mulai ronde 3.
- 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.
"""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:
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.