Kegagalan downstream pada webhook consumer sering kali memicu insiden sistemik pada sisi publisher. Ketika endpoint penerima mengembalikan respons 5xx atau mengalami timeout, worker publisher yang melakukan pengulangan (retry) secara naif akan melipatgandakan beban lalu lintas dalam waktu singkat. Pola ini dinamakan webhook retry storm.
Tanpa mekanisme pembatasan dan pengacakan jeda pengiriman, seluruh antrean pengiriman akan tersinkronisasi, menghabiskan thread pool, membebani broker antrean, dan melumpuhkan pengiriman webhook ke consumer lain yang sehat (blast radius meluas).
Anatomi Webhook Retry Storm dan Eskalasi Blast Radius
Retry storm terjadi karena akumulasi dua faktor: sinkronisasi periodik dan ketiadaan isolasi beban.
Ketika 10.000 event gagal dikirimkan pada detik yang sama dan publisher menerapkan jeda pengulangan statis (misalnya setiap 5 detik), maka 5 detik kemudian 10.000 request akan menghantam endpoint downstream secara serentak (thundering herd). Jika downstream masih belum pulih, tumpukan request baru bercampur dengan request pengulangan. Pola pengulangan eksponensial standar tanpa randomisasi juga mengalami fenomena serupa: batch request akan tetap berjalan dalam klaster waktu yang teratur (lockstep).
Dampak langsung terhadap arsitektur publisher:
- Worker Saturation: Thread atau proses worker habis terpakai untuk menunggu koneksi HTTP timeout ke endpoint yang sedang down.
- Queue Starvation: Event pengiriman baru untuk subscriber yang sehat tertahan di belakang tumpukan antrean retry subscriber yang rusak (masalah noisy neighbor).
- Cascading Outage: Database dan message broker publisher kehabisan memori atau koneksi karena antrean membesar tak terkendali.
Pola Arsitektur Pengiriman Webhook yang Resilien
Untuk mengisolasi blast radius dan mencegah retry storm, diperlukan perancangan alur kerja pengiriman pesan dengan empat pilar proteksi:
1. Isolasi Antrean per Subscriber (Tenant Partitioning)
Jangan satukan semua target webhook dalam satu antrean FIFO global. Terapkan strategi partition key berdasarkan subscriber_id atau endpoint_id. Jika Subscriber A mengalami gangguan, hanya partisi/antrean milik Subscriber A yang tertahan. Antrean Subscriber B dan C tetap diproses tanpa hambatan.
2. Exponential Backoff dengan Full Jitter
Alih-alih menunggu dengan interval tetap atau eksponensial deterministik, sebar interval pengulangan secara merata menggunakan algoritma Full Jitter:
Sleep = Uniform(0, Min(Cap, Base * 2 ** Attempt))Nilai acak seragam antara 0 dan batas atas eksponensial efektif memecah klaster request sehingga downstream menerima distribusi traffic yang rata (flat distribution) selama proses pemulihan.
3. Batas Time-To-Live (TTL) dan Retry Budget
Terapkan batas kadaluarsa pesan (TTL). Jika sebuah event finansial atau notifikasi tidak berhasil dikirim dalam rentang waktu toleransi (misalnya 24 jam) atau melampaui batas maksimal pengulangan (misalnya 5–7 percobaan), hentikan pengulangan. Selain itu, batasi alokasi retry maksimum hanya 10–20% dari total bandwidth request publisher (retry budget).
4. Dead Letter Queue (DLQ)
Event yang telah melampaui batas percobaan maksimum dialihkan langsung ke DLQ. DLQ berfungsi sebagai mekanisme isolasi akhir agar data tidak hilang (no data loss) sekaligus memutus siklus pemrosesan worker. Operator downstream dapat melakukan audit payload dan trigger manual redrive setelah sistem mereka pulih.
Implementasi: Full Jitter dan Circuit Breaker State Machine
Berikut adalah implementasi minimalis kalkulasi Full Jitter dan status Circuit Breaker berbasis Python untuk menghentikan pengiriman sementara saat consumer downstream mengalami kegagalan beruntun:
import random
import time
from enum import Enum
class CircuitState(Enum):
CLOSED = "CLOSED"
OPEN = "OPEN"
HALF_OPEN = "HALF_OPEN"
def calculate_full_jitter(attempt: int, base: float = 1.0, cap: float = 60.0) -> float:
# Full Jitter: Sleep = uniform(0, min(cap, base * 2 ** attempt))
max_sleep = min(cap, base * (2 ** attempt))
return random.uniform(0.0, max_sleep)
class WebhookCircuitBreaker:
def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 30.0):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.state = CircuitState.CLOSED
self.last_failure_time = 0.0
def allow_request(self) -> bool:
now = time.time()
if self.state == CircuitState.OPEN:
if now - self.last_failure_time >= self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
return True
return False
return True
def record_success(self):
self.failure_count = 0
self.state = CircuitState.CLOSED
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
# ponytail: in-memory state; upgrade to Redis/Key-Value store for multi-worker environments.
# Self-check assertion
if __name__ == "__main__":
cb = WebhookCircuitBreaker(failure_threshold=3, recovery_timeout=0.1)
assert cb.allow_request() is True
cb.record_failure()
cb.record_failure()
cb.record_failure()
assert cb.state == CircuitState.OPEN
assert cb.allow_request() is False
time.sleep(0.11)
assert cb.allow_request() is True # Transit ke HALF_OPEN
cb.record_success()
assert cb.state == CircuitState.CLOSED
print("All assertions passed.")
Skipped: penyimpanan state terdistribusi (Redis/Valkey). Tambahkan saat worker berjalan secara multi-instance atau autoscaling horizontal.
Strategi Observabilitas: Memantau Failure Rate dan Retry Budget
Pencegahan retry storm membutuhkan metrik visibilitas real-time untuk membedakan antara fluktuasi normal dan degradasi massal:
- Webhook Attempt Ratio: Monitor perbandingan antara request pengiriman pertama dan request retry. Jika
retries_total / (initial_deliveries_total + retries_total) > 0.20, sistem telah melampaui batas toleransi retry budget. - Queue Depth & Age per Subscriber: Pantau antrean yang menumpuk pada subscriber tertentu. Lonjakan drastis pada metrik
queue_oldest_message_age_secondsmengindikasikan subscriber mengalami kendala tanpa merusak metrik global. - DLQ Growth Rate: Siapkan alert berbasis laju pertambahan pesan pada antrean DLQ (misalnya > 100 pesan masuk dalam 5 menit) untuk memicu investigasi tim engineering publisher.
- Circuit Breaker State Changes: Kirim notifikasi event setiap kali status berubah dari
CLOSEDkeOPEN. Gunakan informasi ini untuk menonaktifkan sementara endpoint pelanggan yang bermasalah secara otomatis.
Rekomendasi Praktis: Pasang header
X-Delivery-AttemptdanX-Retry-Reasonpada setiap payload webhook HTTP. Informasi ini mempercepat proses debugging pada sisi developer downstream saat menganalisis log sistem mereka.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!