Bulkhead Mencegah Satu Dependency yang Jenuh Menghabiskan Semua Worker
Sebuah service dapat memiliki CPU yang sehat, memory yang masih tersedia, dan internal code yang responsif tetapi tetap menjadi unavailable. Satu downstream dependency cukup untuk menghabiskan seluruh concurrency budget jika call ke sana melambat dan setiap request dibiarkan menunggu.
Dampaknya tidak berhenti pada dependency yang lambat. Worker pool, connection pool, semaphore, queue, dan request slot yang dipakai bersama dapat mengubah saturasi lokal menjadi outage di seluruh service. Bulkhead isolation membatasi blast radius tersebut dengan menyediakan kapasitas terpisah untuk workload atau dependency yang berbeda.
Concurrency bersama mengikat traffic yang tidak berkaitan
Bayangkan sebuah API dengan 100 worker slot. Request account data memanggil dependency A, sedangkan request catalog data memanggil dependency B. Kedua kelas request memakai worker pool yang sama.
Dalam kondisi normal, worker hanya menunggu sebentar pada kedua dependency. Jika A mulai membutuhkan 20 detik per call, request menuju A akan menumpuk:
shared pool: 100 slots
A waits: [A][A][A][A][A][A] ... [A]
B work: queued behind occupied capacityB tidak harus gagal. B dapat tetap cepat, tetapi caller-nya mengalami timeout karena request menuju A menempati seluruh execution slot bersama.
Ini adalah masalah capacity coupling. Timeout dapat membatasi durasi setiap call yang tertahan, tetapi arrival rate yang cukup tinggi dapat terus mengganti call yang timeout dengan call baru. Pool tetap jenuh walaupun setiap waktu tunggu memiliki batas.
Bulkhead membuat batas kapasitas menjadi eksplisit
Bulkhead memberikan concurrency limit yang independen. Service yang sama dapat mengizinkan maksimal 60 call concurrent ke A dan 30 ke B, lalu menyisakan kapasitas lain untuk local work atau kelas tambahan.
A pool: [A][A][A][A][A][A] limit 60
B pool: [B][B][ ][ ][ ][ ] limit 30
A reaches its limit
B retains its own slotsKetika A mencapai limit, work tambahan untuk A ditolak, dimasukkan ke bounded queue sesuai policy, atau ditangani melalui fallback. Work tersebut tidak dapat memakai kapasitas yang disediakan untuk B.
Properti utamanya adalah isolation, bukan primitive implementasi tertentu. Thread pool terpisah adalah salah satu pilihan. Service async dapat memakai semaphore atau admission controller yang independen. Message consumer dapat memakai worker group terpisah. Database access dapat memakai connection pool berbeda ketika workload membutuhkan failure boundary yang independen.
Partisi sebaiknya mengikuti failure boundary
Membagi kapasitas per endpoint tidak otomatis memberi hasil yang baik. Beberapa endpoint yang bergantung pada database yang sama masih dapat berbagi bottleneck yang sama. Sebaliknya, satu endpoint dapat menyentuh beberapa resource dengan karakter latency dan saturasi yang sangat berbeda.
Partisi yang berguna biasanya mengikuti resource atau workload yang kegagalannya tidak boleh menghabiskan kapasitas milik work lain. Contohnya remote payment provider, report generator yang mahal, background export, interactive request, atau traffic dari tenant yang dapat menghasilkan concurrency sangat tinggi.
Partisi yang terlalu sedikit mempertahankan coupling yang tidak diinginkan. Partisi yang terlalu banyak menghasilkan kapasitas terfragmentasi yang menganggur ketika partisi lain menolak work. Desain ini merupakan keputusan alokasi resource, bukan sekadar resilience toggle.
Queue tidak menciptakan kapasitas
Respons yang umum terhadap saturasi adalah menaruh lebih banyak work di queue. Queue dapat menyerap burst singkat, tetapi queue tanpa batas mengubah overload langsung menjadi overload tertunda.
Jika sebuah partisi dapat menyelesaikan 200 request per detik sementara 350 request per detik terus masuk, backlog bertambah sekitar 150 request per detik. Menunggu lebih lama tidak menutup selisih kapasitas tersebut.
Karena itu, bulkhead memerlukan policy untuk work yang datang setelah active capacity penuh. Bounded queue dapat sesuai untuk burst singkat yang dapat pulih. Jalur yang sensitif terhadap latency dapat langsung menolak. Background work dapat menerima queue yang lebih dalam jika deadline-nya masih memungkinkan.
Queue limit, concurrency limit, dan request deadline sebaiknya membentuk satu budget yang konsisten. Request yang menghabiskan seluruh deadline untuk menunggu slot tidak lagi memiliki execution budget yang berguna.
Isolation memerlukan admission control pada boundary
Semaphore di sekitar downstream call membatasi jumlah call concurrent, tetapi masih dapat membiarkan banyak upstream request menunggu untuk memperoleh semaphore. Request yang menunggu tersebut tetap memakai memory, socket, request context, dan mungkin upstream concurrency.
Karena itu, admission control sering kali perlu ditempatkan sebelum work yang mahal dimulai. Service dapat menolak work berlebih ketika sebuah partisi tidak memiliki kapasitas, alih-alih membiarkan pressure berpindah ke shared resource lain.
Untuk HTTP API, penolakan dapat direpresentasikan dengan response yang sesuai kontrak, sering berupa status yang dapat dicoba kembali ketika kondisi bersifat sementara. Retry harus dibatasi dan memakai jitter; retry agresif dapat menambah pressure pada partisi yang sama.
Bulkhead dan circuit breaker menangani masalah berbeda
Circuit breaker bereaksi terhadap failure atau outcome buruk yang teramati dan dapat menghentikan call ke dependency selama suatu periode. Bulkhead membatasi seberapa banyak kapasitas yang boleh dipakai call tersebut, terlepas dari apakah dependency sudah diklasifikasikan gagal.
Dependency dapat cukup lambat untuk menghabiskan concurrency sambil tetap mengembalikan response sukses. Dalam kondisi itu, bulkhead memberi perlindungan sebelum failure-rate threshold pada circuit breaker harus tercapai.
Keduanya dapat dipakai bersama. Bulkhead membatasi concurrent exposure. Timeout membatasi waktu tunggu tiap call. Circuit breaker dapat menekan call ketika outcome terbaru menunjukkan bahwa percobaan lanjutan kecil kemungkinannya memberi hasil berguna. Retry policy mengatur percobaan tambahan. Setiap mekanisme melindungi boundary yang berbeda.
Limit statis menukar utilization dengan isolation
Kapasitas yang dicadangkan memiliki biaya. Jika A idle sementara B sibuk, partisi yang ketat dapat membiarkan slot A tidak terpakai ketika B menolak request. Inefisiensi tersebut adalah biaya untuk menjamin bahwa B tidak dapat memakai alokasi A, begitu pula sebaliknya.
Sebagian sistem memakai shared pool dengan maximum per kelas, atau mengizinkan peminjaman kapasitas idle secara terkendali. Peminjaman meningkatkan utilization, tetapi harus memiliki reclaim rule. Jika kapasitas pinjaman tidak dapat dikembalikan saat pemilik membutuhkannya, isolation guarantee hilang ketika terjadi contention.
Limit sebaiknya ditentukan dari service time yang terukur, downstream capacity, connection limit, request deadline, dan queueing delay yang dapat diterima. Angka concurrency yang disalin dari service lain tidak banyak berarti tanpa constraint tersebut.
Metric perlu memperlihatkan setiap partisi secara terpisah
Global utilization metric dapat menyembunyikan bulkhead yang bermasalah. Operator memerlukan visibility terhadap active slot, queue depth, queue wait, rejection count, execution latency, timeout count, dan downstream outcome untuk setiap partisi.
Pemisahan antara queue wait dan execution time sangat berguna. Queue wait yang meningkat saat downstream latency stabil mengarah pada admission pressure lokal. Execution time yang meningkat menunjukkan dependency atau operation yang dilindungi menempati slot lebih lama.
Rejection juga tidak otomatis berarti implementation defect. Saat overload, penolakan yang disengaja dapat menjadi tanda bahwa isolation boundary bekerja. Pertanyaan operasionalnya adalah apakah kapasitas dan traffic policy yang dikonfigurasi sesuai dengan service objective.
Isolation mengubah saturasi menjadi failure yang terbatas
Tanpa concurrency boundary, satu jalur lambat dapat menempati semua shared slot dan membuat jalur lain terlihat tidak sehat. Bulkhead memberi jalur tersebut klaim yang terbatas atas kapasitas service.
Bulkhead tidak memperbaiki downstream dependency dan tidak menambah throughput. Mekanisme ini mengubah bentuk failure. Work berlebih untuk partisi yang jenuh ditolak atau ditunda sesuai policy eksplisit, sementara kapasitas yang disediakan untuk partisi lain tetap tersedia. Pada service yang harus mengalami degradasi secara selektif alih-alih runtuh secara seragam, boundary tersebut merupakan bagian inti dari concurrency design.