Bulkhead Mengisolasi Concurrency Antar-Dependency
Sebuah service dapat memiliki CPU yang masih longgar tetapi tetap tidak tersedia karena satu dependency berhenti menyelesaikan pekerjaan. Request yang menunggu database, remote API, atau storage service yang lambat tetap menahan slot eksekusi, connection, memory, dan posisi queue. Jika operasi yang tidak berkaitan memakai finite pool yang sama, satu jalur yang jenuh dapat menghabiskan kapasitas yang dibutuhkan jalur sehat.
Isolasi bulkhead membagi concurrency bersama itu menjadi budget yang eksplisit. Panggilan ke satu dependency atau kelas workload memakai bounded pool yang tidak dapat dihabiskan kelas lain. Pola ini tidak memperbaiki dependency yang gagal. Fungsinya membatasi kapasitas lokal yang dapat ditempati kegagalan tersebut.
Shared pool menghubungkan failure domain yang tidak berkaitan
Bayangkan service dengan 200 worker slot. Sebagian besar request memanggil backend katalog yang cepat, sedangkan kelompok lebih kecil memanggil backend laporan. Jika keduanya memakai pool yang sama dan panggilan laporan mulai memakan 30 detik, traffic laporan yang cukup besar dapat menempati seluruh 200 slot. Request katalog kemudian ikut menunggu meski backend katalog tetap sehat.
Keterkaitan itu berasal dari kepemilikan resource lokal, bukan hubungan langsung antara kedua backend. Executor, connection pool, semaphore, atau queue tunggal menciptakan batas saturasi bersama.
Memisahkan laporan ke 30 slot dan katalog ke 170 slot mengubah batas tersebut. Laporan dapat menghabiskan jatahnya sendiri, tetapi tidak dapat langsung mengambil alokasi katalog. Angka tepatnya merupakan keputusan kapasitas, bukan konstanta universal.
Bulkhead memerlukan resource yang dibatasi
Isolasi hanya efektif jika setiap kelas memiliki limit yang dapat ditegakkan saat admission. Mekanisme yang umum meliputi thread pool terpisah, semaphore concurrency asynchronous, connection pool, partisi queue, atau kelompok worker.
Semaphore di sekitar remote call asynchronous dapat memadai ketika thread bukan resource yang langka:
catalog_limit = 170
report_limit = 30
if request.kind == "catalog":
acquire(catalog_limit)
call_catalog()
release(catalog_limit)
else:
acquire(report_limit)
call_reporting()
release(report_limit)Kode nyata harus melepas permit saat sukses, error, timeout, maupun cancellation. Permit yang bocor diam-diam mengecilkan kapasitas efektif dan pada akhirnya dapat menghentikan kelas yang diisolasi.
Resource yang dilindungi harus sesuai dengan bottleneck. Membatasi task menjadi 30 sementara semua task masih dapat memakai database connection pool bersama tanpa batas tetap menyisakan jalur interferensi lain.
Batas queue perlu berdampingan dengan batas concurrency
Concurrency limit tanpa queue limit dapat sekadar memindahkan saturasi. Setelah semua permit terpakai, pekerjaan baru dapat menumpuk di memory. Latency naik sementara caller tetap menahan socket, request body, tracing state, dan resource lain.
Bounded queue memberi bulkhead footprint yang terbatas. Saat kapasitas aktif dan kapasitas tunggu sama-sama penuh, service memerlukan hasil admission yang eksplisit: menolak, melakukan load shedding, atau memakai fallback terdokumentasi jika fallback tersebut valid secara semantik.
Panjang queue juga mengubah profil latency. Queue besar dapat menghasilkan response yang berhasil lama setelah nilainya tidak lagi berguna. Karena itu, deadline perlu tetap berlaku saat pekerjaan menunggu admission, bukan hanya setelah eksekusi dimulai.
Partisi mengikuti perilaku kegagalan, bukan jumlah endpoint
Membuat satu pool untuk setiap endpoint dapat menghasilkan fragmentasi berlebihan. Kapasitas menganggur di satu partisi sementara partisi lain menolak pekerjaan yang masih berguna. Unit partisi yang lebih tepat biasanya berupa kumpulan operasi dengan perilaku kegagalan, biaya resource, prioritas, atau risiko dependency yang serupa.
Panggilan ke backend yang sama dapat memerlukan budget berbeda ketika satu operasi murah dan operasi lain menjalankan scan mahal. Sebaliknya, beberapa endpoint dapat berbagi bulkhead jika semuanya bergantung pada constrained resource yang sama dan memiliki service objective serupa.
Model partisi sebaiknya tetap cukup kecil untuk dioperasikan. Setiap pool tambahan membawa limit, metric, keputusan tuning, dan edge case saat pola traffic berubah.
Reserved capacity dan borrowing adalah policy berbeda
Partisi ketat mencadangkan kapasitas dengan tidak mengizinkan satu kelas memakai slot idle milik kelas lain. Pendekatan ini memberi jaminan isolasi yang jelas, tetapi dapat menurunkan utilisasi.
Sebagian sistem mengizinkan borrowing secara terkendali. Satu kelas dapat memakai kapasitas kosong dari partisi lain sambil mempertahankan reserve minimum untuk pemiliknya. Utilisasi dapat membaik, tetapi implementasi harus menghentikan admission pinjaman baru saat pemilik mulai sibuk. Tanpa itu, reserve hanya ada di konfigurasi.
Borrowing juga mengubah cara membaca kondisi operasional. Dashboard yang menunjukkan sebuah kelas melewati alokasi nominalnya dapat tetap sehat jika slot tersebut merupakan pinjaman, sehingga metric perlu membedakan kapasitas reserved, owned, dan borrowed.
Timeout dan circuit breaker menangani masalah yang berdekatan
Timeout membatasi durasi satu operasi. Circuit breaker dapat menghentikan panggilan berulang setelah dependency menunjukkan kegagalan berkelanjutan. Bulkhead membatasi jumlah kapasitas lokal yang dapat ditempati panggilan tersebut secara bersamaan.
Kontrol ini saling memperkuat tetapi tidak saling menggantikan. Timeout 30 detik masih memungkinkan ribuan panggilan menumpuk jika admission tidak dibatasi. Bulkhead berukuran 30 menahan concurrency tersebut bahkan sebelum breaker terbuka. Breaker kemudian dapat mengurangi attempt yang sia-sia selama dependency tetap tidak sehat.
Retry policy juga perlu mematuhi budget yang sama. Mengirim retry melalui executor tanpa batas akan melewati batas isolasi dan dapat memperbesar tekanan saat terjadi kegagalan.
Limit memerlukan pengukuran yang terkait dengan saturasi
Telemetry yang berguna mencakup active permit, queue depth, queue wait time, admission yang ditolak, operation latency, timeout rate, cancellation rate, dan utilisasi per partisi. Metric dari sisi dependency seperti penggunaan connection dan response latency melengkapi gambaran tersebut.
Partisi yang terus berada pada concurrency limit tidak otomatis berarti salah konfigurasi. Dependency dapat benar-benar jenuh, dan menaikkan limit lokal justru dapat memperburuk kondisinya. Perubahan kapasitas perlu mempertimbangkan limit downstream, biaya request, target latency, dan completion rate yang teramati.
Utilisasi yang sangat rendah juga memberi sinyal. Kondisi itu dapat menunjukkan reservation berlebih atau partisi yang tidak lagi sesuai dengan perilaku traffic.
Isolasi perlu mempertahankan bagian service yang tetap berguna
Bulkhead paling bernilai ketika sebuah service memiliki operasi dengan jalur dependency atau tingkat kepentingan yang berbeda. Pertanyaan desain utamanya adalah pekerjaan mana yang harus tetap dapat berjalan ketika kelas lain macet.
Jawaban tersebut menentukan partisi, kapasitas minimum, batas queue, dan perilaku penolakan. Hasilnya adalah failure boundary yang disengaja: dependency lambat masih dapat merusak fitur yang membutuhkannya, tetapi klaimnya atas resource yang diperlukan bagian service lain tetap terbatas.