Data pesanan TokoKita tumbuh dari jutaan menjadi miliaran baris, dan satu server mulai kehabisan disk, memori, sekaligus daya komputasi. Distributed system menjawab masalah tersebut dengan menjalankan pekerjaan di beberapa mesin — node — yang terhubung lewat jaringan dan bekerja seolah-olah satu sistem. Solusi ini tidak gratis: begitu tersebar ke banyak node, data membawa masalah baru berupa kegagalan parsial, latensi jaringan, dan kebutuhan koordinasi antar node. Chapter ini membahas fondasi distributed system yang dipakai platform data modern seperti Spark dan Kafka, sehingga pilihan desain pada chapter berikutnya mudah diikuti.

Apa Itu Distributed System #

Distributed system adalah kumpulan node otonom yang berkomunikasi lewat pertukaran pesan (message passing) demi satu tujuan bersama. Tidak ada memori bersama: setiap node memiliki CPU, memori, dan disk sendiri, dan satu-satunya cara berkoordinasi adalah mengirim pesan lewat jaringan. Dua kata kunci pada definisi ini penting: otonom berarti satu node bisa berhenti tanpa memberi tahu node lain, dan pesan berarti setiap kerja sama menanggung risiko jaringan.

Bagi data engineering, alasan utama memakai distributed system dirangkum berikut ini:

  • Kapasitas: dataset melebihi kapasitas disk satu mesin, misalnya riwayat pesanan sepuluh tahun.
  • Performa: agregasi besar diparalelkan lintas node sehingga jauh lebih cepat selesai.
  • Ketersediaan (availability): sistem tetap melayani pembacaan saat satu node berhenti.
  • Skalabilitas biaya: menambah node biasa biasanya lebih murah daripada membeli satu mesin kelas atas.

Satu Mesin versus Banyak Mesin #

Perbedaan paling mendasar antara program biasa dan distributed system terletak pada mode kegagalannya, bukan pada kecepatannya. Tabel berikut merangkum perbedaan yang akan berulang muncul di sepanjang section ini.

AspekSatu mesinDistributed system
Batas kapasitasSpesifikasi satu serverGabungan seluruh node
Mode kegagalanProgram berhenti utuhSebagian node gagal, sisanya tetap berjalan
KoordinasiTidak dibutuhkanPertukaran pesan lewat jaringan
Latensi aksesMemori dan disk lokalJaringan: lebih tinggi dan bervariasi
Kompleksitas operasionalRendahTinggi: monitoring, rebalancing, failover

Kegagalan Parsial di Tengah Pertukaran Pesan #

Pada satu mesin, program biasanya berjalan utuh atau berhenti utuh. Distributed system mengenal kegagalan parsial (partial failure): node-B bisa berhenti di tengah transaksi sementara node-A tetap hidup, sehingga sistem berada dalam keadaan setengah selesai. Simulasi kecil berikut memperlihatkan keadaan tersebut: node-A mengirim empat event lewat antrian pesan, node-B membalas setiap event dengan ack, lalu pada skenario kedua node-B berhenti setelah dua event.

python
"""Simulasi dua node bertukar pesan lewat antrian, termasuk partial failure."""
from collections import deque


class AntrianPesan:
    """Antrian pesan antar node; berperan sebagai pengganti jaringan."""

    def __init__(self):
        self.antrian = deque()

    def kirim(self, pesan):
        self.antrian.append(pesan)

    def ambil(self):
        return self.antrian.popleft() if self.antrian else None


class NodePemroses:
    """Node yang mengambil event dari kotak masuk, memproses, lalu membalas ack."""

    def __init__(self, nama, kotak_masuk, kotak_balasan):
        self.nama = nama
        self.kotak_masuk = kotak_masuk
        self.kotak_balasan = kotak_balasan
        self.aktif = True
        self.jumlah_diproses = 0

    def langkah(self):
        """Proses satu event bila ada; node yang berhenti tidak merespons apa pun."""
        if not self.aktif:
            return
        pesan = self.kotak_masuk.ambil()
        if pesan is None:
            return
        self.jumlah_diproses += 1
        self.kotak_balasan.kirim(f"ack:{pesan}")


def tukar_pesan(skenario, daftar_event, berhenti_setelah=None):
    print(f"=== {skenario} ===")
    ke_pemroses = AntrianPesan()
    dari_pemroses = AntrianPesan()
    node_b = NodePemroses("node-B", ke_pemroses, dari_pemroses)
    terkirim = 0
    berhenti_sudah_dilaporkan = False
    for event in daftar_event:
        ke_pemroses.kirim(event)
        print(f"node-A kirim {event}")
        terkirim += 1
        ack = None
        for _ in range(3):  # node-A menunggu ack maksimal 3 ronde
            node_b.langkah()
            ack = dari_pemroses.ambil()
            if ack is not None:
                break
        if ack is None:
            print(f"  -> node-A: timeout, ack untuk {event} tidak pernah datang")
        else:
            print(f"  -> node-A menerima {ack}")
        if (berhenti_setelah is not None and not berhenti_sudah_dilaporkan
                and node_b.jumlah_diproses >= berhenti_setelah):
            node_b.aktif = False
            berhenti_sudah_dilaporkan = True
            print(f"  (node-B berhenti merespons setelah {node_b.jumlah_diproses} event)")
    print(f"ringkasan: {terkirim} event terkirim, {node_b.jumlah_diproses} diproses\n")


tukar_pesan("Skenario 1: kedua node sehat", ["event-1", "event-2", "event-3", "event-4"])
tukar_pesan("Skenario 2: node-B berhenti setelah 2 event (partial failure)",
            ["event-1", "event-2", "event-3", "event-4"], berhenti_setelah=2)

Output yang Dihasilkan:

text
=== Skenario 1: kedua node sehat ===
node-A kirim event-1
  -> node-A menerima ack:event-1
node-A kirim event-2
  -> node-A menerima ack:event-2
node-A kirim event-3
  -> node-A menerima ack:event-3
node-A kirim event-4
  -> node-A menerima ack:event-4
ringkasan: 4 event terkirim, 4 diproses

=== Skenario 2: node-B berhenti setelah 2 event (partial failure) ===
node-A kirim event-1
  -> node-A menerima ack:event-1
node-A kirim event-2
  -> node-A menerima ack:event-2
  (node-B berhenti merespons setelah 2 event)
node-A kirim event-3
  -> node-A: timeout, ack untuk event-3 tidak pernah datang
node-A kirim event-4
  -> node-A: timeout, ack untuk event-4 tidak pernah datang
ringkasan: 4 event terkirim, 2 diproses

Mari kita telaah baris-baris kode di atas:

  1. Baris 5-15 (AntrianPesan): kelas ini menggantikan jaringan; pesan masuk lewat kirim() dan keluar lewat ambil(), dan antrian kosong mengembalikan None — meniru kotak masuk yang belum terisi.
  2. Baris 28-36 (langkah): node memproses paling banyak satu event per ronde. Pemeriksaan if not self.aktif di baris 30 adalah titik kegagalan parsial: node yang sudah berhenti membiarkan pesan menumpuk tanpa balasan.
  3. Baris 50-55 (menunggu ack): node-A memberi node-B maksimal tiga ronde untuk membalas; loop berhenti begitu ack diterima. Inilah bentuk paling sederhana dari mekanisme timeout.
  4. Baris 56-59 (cabang hasil): ack yang tidak pernah datang membuat node-A melaporkan timeout, sementara program tetap hidup dan melanjutkan pengiriman event berikutnya.
  5. Baris 60-64 (pemicu kegagalan): setelah jumlah_diproses mencapai berhenti_setelah, atribut aktif diubah menjadi False sehingga pemrosesan berhenti di tengah jalan — persis keadaan sistem saat node berhenti mendadak.

Skenario 2 membuktikan kegagalan parsial: node-A tetap hidup dan terus mengirim, tetapi dua event terakhir tidak pernah mendapat ack. Sistem diam-diam berada dalam keadaan setengah selesai — dua event diproses, dua event tidak jelas nasibnya. Node-A harus memutuskan sendiri kapan menyerah menunggu dan apa yang dilakukan terhadap event yang menggantung: diulang, ditandai, atau diabaikan.

Asumsi yang Sering Keliru tentang Jaringan #

Pemrogram yang terbiasa memanggil fungsi lokal kerap membawa asumsi yang tidak berlaku lagi begitu panggilan menyeberangi jaringan. Daftar asumsi keliru tersebut dikenal sebagai fallacies of distributed computing, dan tiga yang paling sering menimbulkan masalah pada pipeline data adalah:

  • Jaringan itu andal: pesan bisa gagal sampai tujuan; ack yang tidak datang tidak serta-merta berarti pesan tidak diproses.
  • Latensi itu nol: setiap pertukaran pesan menambah waktu tempuh; pipeline yang terlalu banyak bolak-balik kecil melambat meski bandwidth besar.
  • Topologi tidak berubah: node ditambah, diperbarui, dan diganti sepanjang waktu; kode tidak boleh berasumsi daftar node bersifat tetap.

Fondasi sudah terpasang: banyak node, pertukaran pesan, dan kegagalan parsial. Langkah desain pertama setelah sadar satu mesin tidak cukup adalah memilih arah penambahan kapasitas — memperbesar mesin atau menambah mesin — yang dibahas pada Chapter Horizontal vs Vertical Scaling.