Bulkhead Mengisolasi Concurrency Antar-Failure Domain
Sebuah service dapat tetap dapat diakses ketika kapasitas bergunanya sudah habis. Dependency yang lambat menahan request, request tersebut memakai worker atau connection slot, lalu traffic lain menunggu di belakang pekerjaan yang tidak dapat segera selesai. Fault bermula pada satu jalur, tetapi resource pool bersama membuatnya menghabiskan kapasitas yang dibutuhkan semua jalur.
Isolasi bulkhead membagi kapasitas terbatas tersebut. Call yang terkait dengan satu failure domain memperoleh bagian yang dibatasi, bukan bersaing tanpa pemisahan untuk seluruh pool. Ketika satu partisi penuh, admission gagal atau menunggu di dalam partisi itu sementara kapasitas untuk pekerjaan lain tetap tersedia.
Nama pola ini berasal dari kompartemen kedap air pada kapal, tetapi nilai rekayasanya konkret: kepemilikan resource mengikuti batas failure.
Kapasitas bersama menghubungkan jalur yang tidak terkait
Pertimbangkan service dengan 16 worker slot dan dua downstream dependency. Jika semua request memakai pool yang sama, 16 call yang tersendat ke dependency A dapat memakai seluruh worker.
16 shared slot
A A A A A A A A A A A A A A A A
request B -> menungguDependency B mungkin sehat, tetapi request yang membutuhkan B tidak dapat berjalan. Worker pool telah mengubah kesehatan downstream yang independen menjadi satu availability boundary bersama.
Bulkhead dapat menyediakan budget concurrency terpisah:
dependency A -> 10 slot
dependency B -> 6 slotJika A memenuhi 10 slot, B masih memiliki enam slot. Service kehilangan kapasitas untuk A tanpa otomatis kehilangan seluruh kapasitas untuk B.
Pemisahan ini tidak menciptakan kapasitas baru. Ia mengubah workload mana yang diizinkan memakai kapasitas yang ada.
Partisi perlu mengikuti failure domain
Boundary bulkhead yang berguna mengikuti resource dengan failure yang cenderung berkorelasi. Scope yang umum mencakup downstream service, shard, kelas tenant, kelas operasi, atau prioritas workload.
Partisi yang terlalu luas tetap mempertahankan coupling. Satu downstream pool global masih memungkinkan shard yang gagal mendesak shard sehat. Partisi yang terlalu sempit dapat membuat kapasitas menganggur dan konfigurasi berlebihan.
Karena itu, boundary berasal dari failure model, bukan dari preferensi untuk menambah jumlah pool.
Untuk dependency berbasis shard, isolasi per shard dapat sesuai:
shard-1 -> 4 slot
shard-2 -> 4 slot
shard-3 -> 4 slotUntuk service dengan beberapa endpoint yang memakai database terbatas yang sama, partisi per endpoint mungkin hanya memberi sedikit isolasi karena bottleneck sebenarnya tetap dibagi di bawahnya.
Concurrency limit menjadi mekanisme inti
Bulkhead memerlukan admission limit yang dapat ditegakkan. Implementasinya dapat memakai semaphore, dedicated worker pool, connection pool terpisah, bounded executor, atau primitive lain yang membatasi pekerjaan concurrent.
Boundary berbasis semaphore secara konsep cukup kecil:
if slot_available(partition):
acquire_slot()
call_dependency()
release_slot()
else:
reject_or_wait_with_bound()Jalur release harus berjalan saat success, failure, cancellation, maupun timeout. Permit yang bocor perlahan mengurangi kapasitas yang dapat dipakai dan dapat terlihat seperti outage pada downstream.
Dedicated pool memberi pemisahan scheduling yang lebih kuat, tetapi memakai lebih banyak resource. Partisi semaphore lebih ringan ketika pekerjaan masih berjalan pada scheduler bersama, meski tidak mengisolasi setiap resource di bawah scheduler tersebut.
Waktu tunggu juga memerlukan batas
Concurrency cap tanpa queue policy dapat memindahkan overload dari pekerjaan aktif ke pekerjaan yang menunggu. Queue tanpa batas tetap memakai memory, mempertahankan state request, dan meningkatkan latency.
Setiap partisi karena itu memerlukan keputusan eksplisit untuk pekerjaan berlebih: reject segera, tunggu selama durasi terbatas, atau masuk ke bounded queue.
request
|
v
[partition limit] -- penuh --> reject
|
admitted
v
remote callWaktu tunggu singkat yang dibatasi dapat menyerap variasi scheduling kecil. Waktu tunggu panjang berisiko ketika caller sudah membawa deadline. Request yang menghabiskan sebagian besar budget untuk menunggu slot mungkin hanya memiliki sedikit waktu tersisa untuk remote work yang berguna.
Admission perlu memperhitungkan deadline yang tersisa, bukan menganggap queue time tanpa biaya.
Alokasi kapasitas merupakan pilihan policy
Partisi statis membuat isolasi dapat diprediksi, tetapi kapasitas yang dicadangkan dapat menganggur. Jika A memakai dua dari sepuluh slot sementara B mengalami saturasi, enam slot milik A mungkin tetap kosong meski B dapat memakainya.
Inefisiensi tersebut dapat disengaja. Kapasitas cadangan adalah biaya untuk mempertahankan satu jalur service saat jalur lain mengalami saturasi.
Sebagian sistem mengizinkan borrowing secara terkontrol. Partisi dapat memakai kapasitas kosong dari shared reserve sambil mempertahankan minimum yang dijamin untuk partisi lain. Borrowing meningkatkan utilisasi, tetapi aturan reclaim harus mencegah pekerjaan pinjaman menghalangi guaranteed share ketika demand kembali.
Invariant yang relevan bukan utilisasi sempurna. Overload pada satu domain tidak boleh menghabiskan kapasitas yang dijanjikan kepada domain lain.
Bulkhead dan rate limit membatasi dimensi berbeda
Rate limit membatasi arrival sepanjang waktu. Concurrency limit membatasi pekerjaan yang aktif pada saat yang sama. Keduanya terkait tetapi tidak dapat saling menggantikan.
Dependency yang biasanya selesai dalam 20 milidetik dapat menangani request rate tinggi dengan concurrency rendah. Jika latency naik menjadi dua detik, arrival rate yang sama dapat menghasilkan jauh lebih banyak pekerjaan in-flight.
Perkiraan concurrency mengikuti arrival rate dikalikan service time:
concurrency ~= arrival_rate * service_timeRate limit saja karena itu dapat membiarkan concurrency meningkat tajam ketika latency memburuk. Bulkhead langsung membatasi exposure in-flight tersebut.
Timeout mencegah slot tertahan permanen
Bulkhead bergantung pada durasi call yang dibatasi. Slot yang dipegang request tanpa timeout efektif dapat tetap terpakai tanpa batas.
Timeout dan cancellation menyediakan jalur keluar:
admit -> call -> success
-> failure
-> timeout
-> cancellation
|
v
release slotTimeout perlu mencakup operasi yang dilindungi dan menghormati deadline caller yang lebih awal. Cancellation juga perlu mencapai operasi dasar ketika client library dan protocol mendukungnya; melepas permit lokal sementara remote work yang ditinggalkan tetap berjalan dapat mengurangi tekanan lokal tanpa mengurangi load downstream.
Retry harus kembali melewati admission control
Retry adalah attempt baru dan secara umum perlu melewati bulkhead yang sama. Membiarkan retry melewati partisi akan merusak capacity bound tepat ketika failure meningkatkan volume retry.
Retry langsung juga dapat memonopoli slot yang langka. Retry budget, backoff, jitter, dan pemeriksaan deadline perlu berada di sekitar admission boundary yang sama.
Jika request kehilangan slot setelah attempt gagal, attempt berikutnya perlu bersaing kembali berdasarkan policy yang ditetapkan, bukan mempertahankan akses istimewa ke kapasitas.
Circuit breaker dan bulkhead menangani tahap failure berbeda
Circuit breaker menekan call setelah outcome terbaru menunjukkan dependency tidak sehat. Bulkhead membatasi exposure bahkan sebelum tersedia cukup bukti failure untuk membuka breaker.
Kedua kontrol dapat disusun:
request
|
bulkhead admission
|
circuit breaker
|
timeout-bounded callUrutan dapat berbeda mengikuti detail implementasi, terutama terkait apakah call yang ditolak breaker secara lokal perlu memakai bulkhead slot. Properti pentingnya adalah tidak ada mekanisme yang diam-diam melewati resource bound milik mekanisme lain.
Breaker membawa state lintas request. Bulkhead menegakkan capacity boundary pada pekerjaan saat ini. Keduanya tidak saling menggantikan.
Metrics perlu memperlihatkan tekanan tiap partisi
Utilisasi service secara agregat dapat menyembunyikan partisi yang mengalami saturasi. Telemetry perlu mempertahankan identity partisi.
Signal yang berguna mencakup active slot, configured limit, queue depth, queue wait, jumlah admission rejection, call duration, jumlah timeout, dan jumlah cancellation. Untuk alokasi dinamis, borrowed capacity dan guaranteed capacity juga perlu terlihat terpisah.
Partisi yang terus berada dekat limit dapat menandakan utilisasi tinggi yang normal, dependency lambat, budget terlalu kecil, atau perubahan traffic. Mengorelasikan occupancy dengan downstream latency dan rejection rate membedakan kondisi tersebut lebih baik daripada satu persentase utilisasi.
Isolasi merupakan kontrak resource
Bulkhead paling efektif ketika resource yang dilindungi dinyatakan secara eksplisit. Worker semaphore tidak dapat mengisolasi shared database connection pool yang lebih dulu mengalami saturasi. HTTP pool terpisah tidak mengisolasi CPU jika pemrosesan response yang mahal mendominasi execution time.
Desain perlu mengidentifikasi resource yang exhaustion-nya menyebarkan failure, lalu menempatkan aturan kepemilikan terbatas pada resource tersebut atau cukup dekat dengannya.
Aturan itu membentuk kontrak resource: satu failure domain boleh menghabiskan alokasinya, tetapi tidak dapat otomatis mengambil setiap unit yang dibutuhkan pekerjaan lain. Service kemudian dapat mengalami degradasi per partisi alih-alih runtuh sebagai satu shared pool.