← semua materi
05:00 Arsitektur Β· 20 Oct 2026 Β· 05:01

πŸ“š Topik: Backpressure & Queue Semantics β€” At-Least-Once vs Exactly-Once d-backpressure Β· Distributed Systems Β· depth 3


🎯 Tujuan Belajar

Setelah sesi ini, keputusan berikut jadi lebih baik:

  1. Kapan pakai queue dan kapan queue justru memindahkan masalah (dan memperburuknya).
  2. Memilih semantik delivery yang benar per hop β€” dan tahu bahwa "exactly-once" di level end-to-end itu tidak ada; yang ada effectively-once lewat idempotensi.
  3. Mendesain idempotency key dan dedup window yang tidak bocor di tepi.
  4. Mengenali metastable failure: sistem yang sudah pulih kapasitasnya tapi tidak pulih throughput-nya.

🧠 Penjelasan Inti

Dua masalah berbeda, sering dicampur

Backpressure Queue semantics
Pertanyaan "Apa yang terjadi kalau producer > consumer?" "Kalau pesan diproses 2x, apa dampaknya?"
Sifat Kontrol aliran (flow control) Jaminan pengiriman (delivery guarantee)
Kegagalan khas OOM, latency explosion, retry storm Duplikat transaksi, double charge, drift saldo

Keduanya ketemu di satu titik: queue adalah buffer, dan buffer tanpa batas = bug yang tertunda.

Kenapa backpressure harus ada

Sistem punya kapasitas layanan (ΞΌ). Kalau arrival rate Ξ» > ΞΌ, antrean tumbuh tanpa batas. Matematikanya tidak negotiable:

Little's Law: L = Ξ» Γ— W - Ξ» = 100 rps, W = 200 ms β†’ L = 20 request in-flight. Ini angka yang harus ditampung pool/thread/connection. - Balik: kalau concurrency dibatasi 20 dan latency 200 ms, throughput plafon = 100 rps. Concurrency limit adalah throughput limit.

M/M/1: L = ρ/(1-ρ), W = 1/(μ(1-ρ))

Utilisasi ρ Panjang antrean L Latency vs service time
0.5 1.0 2Γ—
0.8 4.0 5Γ—
0.9 9.0 10Γ—
0.95 19.0 20Γ—
0.99 99.0 100Γ—

Konsekuensi arsitektural: latency bukan fungsi linear dari load. Di 90% utilisasi, sedikit lonjakan = latency 2Γ—. Karena itu target utilisasi produksi biasanya 60–70%, bukan 90%. (Ini konsekuensi model antrean, bukan angka benchmark vendor β€” angka aktual tergantung distribusi arrival; arrival yang bursty/bukan Poisson lebih buruk lagi.)

Tiga strategi menghadapi overload β€” pilih salah satu, jangan diam

  1. Buffer (antrean) β€” tunda kerja. Menangani spike, kalah pada sustained overload. Buffer wajib bounded.
  2. Shed (drop/reject) β€” tolak lebih awal, murah. 429 + Retry-After, atau drop pesan non-kritis. Butuh definisi prioritas.
  3. Degrade β€” kerjakan lebih murah: cache stale, skip enrichment, sampling, jawaban parsial.

Yang tidak boleh: buffer unbounded (OOM) atau retry tanpa batas (retry storm β†’ amplifikasi beban 3–10Γ— saat downstream sudah sekarat).

Mekanisme backpressure yang nyata

Semantik delivery β€” yang sebenarnya dijamin

Semantik Arti operasional Cara dapat Risiko
At-most-once commit offset sebelum proses fire-and-forget pesan hilang saat crash
At-least-once commit offset sesudah proses ack setelah sukses duplikat saat crash/retry/rebalance
Effectively-once at-least-once + dedup/idempotensi idempotency key, upsert, transaksi butuh state dedup + window

Kenapa exactly-once delivery mustahil secara umum: ack dan proses adalah dua operasi pada dua sistem berbeda tanpa commit protokol bersama. Kalau consumer crash setelah proses tapi sebelum ack, sistem tidak bisa membedakan "sudah diproses" dari "belum" β€” harus pilih salah satu: kirim ulang (duplikat) atau jangan (hilang). Ini varian Two Generals; tidak ada jumlah handshake yang menyelesaikannya. Jadi semua "exactly-once" industri adalah exactly-once processing effect, bukan exactly-once delivery.

Kafka, spesifik (fakta, bukan klaim benchmark): - Idempotent producer (enable.idempotence=true, default aktif di client modern): broker menyimpan (producerId, partition, sequenceNumber) dan membuang duplikat retry dalam sesi producer. Ini menutup duplikat akibat retry internal producer β€” bukan duplikat akibat consumer crash. - Transaksi (transactional.id + sendOffsetsToTransaction): atomicitas tulis-ke-banyak-partisi plus commit offset consumer. Offset jadi bagian transaksi β†’ hilangnya duplikat di pola read-process-write selama output juga Kafka dan consumer baca dengan isolation.level=read_committed. - Di luar batas itu (sink ke Postgres, HTTP call ke payment gateway), jaminan berhenti. Butuh idempotensi di sink.

Batas dedup yang harus eksplisit: - Dedup butuh state: store key + TTL. TTL = dedup window. Pesan yang di-retry setelah window lewat = diproses dua kali. Retry policy dan dedup window harus kompatibel (max retry delay Γ— max attempts < window). - Dedup by key tidak menyelesaikan ordering. Butuh partisi per key agar urutan terjaga (Kafka: key β†’ partition; konsekuensinya hot partition). - Natural idempotency > tabel dedup: UPDATE ... WHERE version = $expected, INSERT ... ON CONFLICT DO NOTHING, state machine dengan transisi legal (PENDING β†’ PAID idempoten, PAID β†’ PAID no-op).


βš™οΈ Cara Kerja Internal

Alur at-least-once yang benar (poll loop)

loop:
  batch = consumer.poll(max.poll.records=N, max.poll.interval.ms=M)
  try:
    result = process(batch)          # idempoten by design
    producer.send(outputs)           # atau tulis ke DB
    commit(offsets)                  # SETELAH sukses
  except Transient:
    retry with backoff (bounded)     # duplikat mungkin terjadi β†’ aman karena idempoten
  except Poison:
    send to DLQ, commit offset       # jangan block partisi

Dua timer yang sering dilupakan: - max.poll.interval.ms β€” consumer dianggap mati kalau tidak poll dalam window ini β†’ rebalance β†’ partisi pindah β†’ duplikat. Batch processing yang lambat memicu ini. Mitigasi: kurangi max.poll.records, proses async dengan batas, atau naikkan interval (trade-off: deteksi mati lebih lambat). - Rebalance = titik duplikat paling sering di Kafka. Cooperative rebalancing mengurangi stop-the-world, tidak menghapus duplikat.

Offset commit bukan ACK

Commit offset menyatakan "semua sebelum ini sudah diproses" β€” high-water mark, bukan per-pesan. Commit setelah N dari 100 pesan batch sukses = 100 pesan dianggap selesai. Kalau butuh granularitas, commit per-pesan (mahal) atau idempotensi per-pesan (murah).

Outbox pattern (menutup gap DB↔broker)

Masalah: tulis DB sukses, publish ke broker gagal β†’ event hilang. Atau publish sukses, DB rollback β†’ event hantu.

BEGIN
  INSERT INTO orders (...)
  INSERT INTO outbox (id, topic, payload, created_at)
COMMIT
-- relay terpisah (polling atau CDC/WAL) publish outbox β†’ broker, tandai sent

Satu transaksi lokal = satu sumber kebenaran. Relay at-least-once β†’ consumer wajib idempoten. Harga: latency tambahan + tabel outbox perlu di-prune.

Backpressure propagasi (rantai)

client β†’ [LB: rate limit + shed 429] β†’ [svc A: semaphore per dep] β†’ [queue bounded] β†’ [svc B: pull-based, prefetch=K]

Aturan: setiap hop punya batas, dan batas itu terlihat (metrik queue depth + in-flight + lag). Queue tanpa alarm = bug yang menunggu.


πŸ—οΈ Diagram: di mana backpressure mati

Producer ──► [queue TANPA BATAS] ──► Consumer (lambat)
                    β”‚
                    └── producer tidak pernah merasa lambat
                        queue tumbuh β†’ memory naik β†’ GC pressure β†’ latency naik
                        β†’ consumer makin lambat β†’ queue makin tumbuh  ☠

vs.

Producer ──► [queue bounded, size=N] ──► Consumer
                    β”‚
                    β”œβ”€ penuh β†’ producer block / 429 / drop-oldest
                    └─ sinyal balik ke hulu β†’ hulu melambat atau shed

Perbedaan satu properti: batas. Bounded queue mengubah "kehilangan data diam-diam + OOM" menjadi "penolakan eksplisit" β€” yang bisa diobservasi dan diberi kebijakan.


βš–οΈ Trade-off & Kapan JANGAN Dipakai

Keputusan Pakai kalau JANGAN pakai kalau
Queue async spike sementara, kerja bisa ditunda, butuh decoupling temporal butuh hasil sinkron (user menunggu), ordering ketat lintas entitas, kerja tidak boleh hilang sama sekali tanpa DLQ
Bounded queue kecil ingin latency rendah & shedding cepat burst legit besar (batch import) β€” pakai admission control di depan, bukan buffer besar
At-least-once + idempotensi default sehat untuk hampir semua sistem β€” (ini pilihan default)
Exactly-once transaksional (Kafka) read-process-write seluruhnya di Kafka sink eksternal non-transaksional; tim belum paham fencing/epoch β†’ risiko lebih besar dari manfaat
At-most-once telemetry, metrics, log sampling β€” nilai hilang < biaya duplikat apa pun yang menyentuh uang, stok, hak akses
Retry agresif error transient terverifikasi, backoff + jitter, circuit breaker downstream overload (retry = amplifikasi); error non-transient (retry = sia-sia)
Autoscaling sebagai solusi overload beban naik bertahap, cold start cepat spike instan, stateful consumer, DB sebagai bottleneck (scale app tidak menambah kapasitas DB)

Yang paling sering salah: menambah queue untuk "menyerap" overload. Queue menyerap spike, bukan defisit kapasitas. Kalau Ξ» > ΞΌ secara persisten, queue hanya mengubah kapan kegagalan terjadi, bukan apakah.

Kapan JANGAN pakai exactly-once transaksional: kalau efek sampingnya keluar dari boundary transaksi (email, payment gateway, third-party API). Transaksi Kafka tidak bisa rollback HTTP call. Di situ idempotency key di sisi penerima adalah satu-satunya jawaban jujur.


🏭 Contoh Produksi & Failure Mode

Failure mode Mekanisme Gejala Mitigasi
Retry storm 3 layer retry (client, gateway, service) Γ— 3 attempt = 27Γ— load downstream down total padahal kapasitas cukup retry hanya di satu layer, backoff + jitter, retry budget (mis. ≀10% traffic), circuit breaker
Metastable failure overload β†’ queue penuh β†’ timeout β†’ retry β†’ overload sistem tidak pulih walau load turun; butuh restart/shed manual load shedding agresif, admission control, circuit breaker, "fail fast" bukan "queue forever"
Rebalance storm consumer lambat > max.poll.interval.ms β†’ rebalance loop lag naik terus, duplikat masif, throughput jatuh kurangi batch, naikkan interval, static membership, cooperative sticky assignor
Unbounded queue OOM buffer tanpa batas, producer cepat pod OOMKilled / GC death spiral bounded queue + metrik depth + shed policy
Poison message satu pesan selalu gagal, retry selamanya partisi berhenti (head-of-line blocking), lag tak turun DLQ setelah N attempt, commit offset, alert
Duplicate side effect at-least-once ke payment gateway tanpa idempotency key double charge, tiket ganda idempotency key end-to-end + dedup di penerima
Hot partition key ordering per user β†’ 1 partisi jenuh satu consumer 100%, sisanya idle key salting / composite key / partition lebih granular β€” tapi urutan per entitas harus dipertahankan
Thundering herd saat scale-up cache miss massal / reconnect massal setelah deploy lonjakan ke DB tepat saat sistem rapuh jitter, warmup, staggered rollout, request coalescing

Pola yang terbukti: backpressure yang benar terlihat di metrik, bukan di kode. Kalau kamu tidak bisa menjawab "berapa queue depth sekarang, berapa lag, berapa in-flight per dependency, berapa yang di-shed" β€” kamu belum punya backpressure, kamu punya harapan.


⚠️ Miskonsepsi Umum

  1. "Kafka exactly-once, jadi sistem saya exactly-once." Jaminan berhenti di boundary Kafka. Sink non-Kafka = at-least-once + idempotensi milikmu.
  2. "Idempotent producer = consumer aman dari duplikat." Tidak. Itu menutup duplikat retry producer. Duplikat consumer (crash setelah proses, sebelum commit) tetap terjadi.
  3. "Tambah queue = tambah resilience." Queue tanpa batas = penundaan kegagalan. Queue tanpa consumer yang di-scale = akumulasi utang.
  4. "At-least-once itu jelek, kita butuh exactly-once." At-least-once + idempotensi = effectively-once, lebih murah, lebih sederhana, dan bekerja lintas sistem. Exactly-once transaksional mahal dan rapuh di tepi.
  5. "Retry bikin sistem lebih tahan gagal." Retry tanpa batas + tanpa jitter = amplifier kegagalan. Retry adalah kredit, bukan hak.
  6. "Autoscaling menyelesaikan overload." Kalau bottleneck di DB atau di dependency bersama, menambah app instance hanya menambah tekanan.
  7. "Latency tinggi berarti butuh lebih banyak worker." Kalau Ξ» sudah > ΞΌ, lebih banyak worker membantu; kalau Ξ» < ΞΌ tapi latency tinggi, masalahnya di antrean/blocking/contention β€” tambah worker bisa memperburuk (context switch, lock contention, connection pool exhaustion).

βœ… Cek Pemahaman

  1. Consumer Kafka memproses 500 pesan per batch, commit setelah batch selesai. Di pesan ke-300 prosesnya lambat (60s), max.poll.interval.ms=300000. Crash terjadi di pesan ke-450. Berapa pesan yang diproses ulang, dan perubahan konfigurasi mana yang mengurangi jumlah duplikat tanpa mengorbankan throughput secara berlebihan?
  2. Sistem punya target 200 rps, latency p95 50 ms. Berapa concurrency minimum yang dibutuhkan (Little's Law)? Kalau utilisasi naik ke 90%, apa yang terjadi pada p95 dan berapa concurrency efektif yang perlu disiapkan?
  3. Kamu menulis event ke Postgres lalu publish ke Kafka. Publish gagal setelah commit DB. Sebutkan dua solusi dan trade-off masing-masing β€” dan jelaskan mengapa "publish dulu, lalu commit DB" bukan solusi.

πŸ”‘ Poin Kunci


➑️ Eksplorasi Lanjut

  1. Outbox + CDC β€” bandingkan polling relay vs Debezium/WAL tailing: latency, beban DB, kompleksitas operasional.
  2. Saga vs 2PC β€” kenapa saga menukar atomicity dengan kompensasi, dan kapan kompensasi tidak mungkin (efek eksternal ireversibel).
  3. Idempotency key di HTTP API β€” desain header, scope key, retry semantics (Idempotency-Key + cached response), dan interaksi dengan 409/422.
  4. CoDel & adaptive concurrency β€” load shedding berbasis sojourn time, bukan queue length; kenapa lebih tahan terhadap bufferbloat.
  5. Backpressure di runtime β€” Go channel buffer + errgroup.SetLimit, Node stream highWaterMark/pipeline, Rust tokio bounded mpsc, actor mailbox bounded. Bandingkan default-nya: mana yang default-nya unbounded dan kenapa itu jebakan.
  6. Ordering vs parallelism β€” partitioning per key, hot partition, dan kapan harus menyerah pada global ordering.