ข้ามไปยังเนื้อหา

Bulkhead

บริการของคุณติดต่อกับ dependency หลายตัว ตัวอย่างเช่น หน้า product page อาจเรียก catalog service, reviews service และ recommendations service เพื่อประกอบเป็น response เดียว การเรียกทั้งสามใช้ทรัพยากรชุดเดียวกันที่มีจำกัดภายใน process ของคุณ: thread pool เดียวกันที่จัดการ request ที่เข้ามา, connection pool ของฐานข้อมูลเดียวกัน, จำนวน HTTP connection ที่กำลังทำงานคงที่จำนวนเดียวกัน

การแชร์นั้นมองไม่เห็นและสะดวก — จนกระทั่ง dependency ตัวหนึ่งมีพฤติกรรมผิดปกติ

สมมติว่า recommendations service ซึ่งสำคัญน้อยที่สุดในสามตัว เริ่มทำงานช้า ทุก request ที่เรียกไปจะจอด thread ทิ้งไว้จนกว่าจะ timeout และเพราะทุก dependency ดึง thread จาก pool ที่แชร์กันชุดเดียว thread ที่ถูกจอดไว้จึงไม่ใช่ “thread ของ recommendations” แต่เป็น thread ชุดเดียวกัน กับที่การเรียก catalog และ reviews ต้องใช้ พอ recommendations request สะสมมากขึ้น ก็กิน pool จนหมด ทีนี้ request ของ catalog ที่ยังแข็งแรงดีก็ขอ thread ไม่ได้เช่นกัน สรุปคือความล้มเหลวของ dependency ที่สำคัญน้อยที่สุด ล้มตัวที่สำคัญที่สุดลงไปด้วย

นี่คือปัญหาการแย่งทรัพยากรผ่าน pool ที่แชร์กัน dependency ตัวเดียวที่ตะกละหรือติดขัดทำให้ตัวอื่นอดอยากทั้งหมด และ circuit breaker เพียงอย่างเดียวก็แก้ไม่ครบ เพราะมีช่วงเวลาก่อน breaker จะ trip ที่ pool ที่แชร์กันถูกดูดจนแห้งไปแล้ว

แรงที่ขัดกันจึงเป็นแบบนี้ คุณมีทรัพยากรจำกัด มี dependency หลายตัวแย่งกันใช้ และคุณต้องการจำกัดโควตาของ dependency ที่ช้า ไม่ให้กินส่วนแบ่งที่งานอื่นซึ่งยังแข็งแรงดีต้องใช้

bulkhead แบ่งกั้นทรัพยากรเพื่อให้ dependency แต่ละตัวดึงจาก pool ที่มีขอบเขตของตัวเองแทนที่จะเป็น pool ที่แชร์กันชุดเดียว ชื่อนี้มาจากการต่อเรือ: ตัวเรือถูกแบ่งออกเป็นช่องกันน้ำด้วย bulkhead เพื่อให้รอยรั่วในช่องหนึ่งท่วมเฉพาะช่องนั้นและเรือยังคงลอยอยู่ ในซอฟต์แวร์ คุณมอบ thread pool, connection pool หรือ — อย่างง่ายที่สุด — bounded semaphore ให้ dependency แต่ละตัว ซึ่งจำกัดจำนวนการเรียกแบบ concurrent ที่ไปยัง dependency นั้นที่กำลังทำงานพร้อมกันได้

พอ recommendations dependency อิ่มตัว สิ่งที่ยึดไปได้มากที่สุดคือ permit ในช่อง ของตัวเอง เท่านั้น เมื่อ permit หมด การเรียก recommendation ครั้งถัดไปจะโดนปฏิเสธทันที แล้ว fall back ไปเป็น “ไม่มี recommendations” ได้ ส่วนช่องของ catalog และ reviews ไม่ถูกแตะเลย ความเสียหายถูกจำกัดวงอยู่แค่นั้น

flowchart TB
  R[Incoming requests] --> C{Per-dependency<br/>bulkheads}
  C --> CAT
  C --> REV
  C --> REC
  subgraph CAT[Catalog compartment]
    CP[8 permits] --> CS[Catalog service]
  end
  subgraph REV[Reviews compartment]
    VP[4 permits] --> VS[Reviews service]
  end
  subgraph REC[Recommendations compartment]
    RP[2 permits — saturated] --> RS[Recommendations service]
  end
  RP -. "extra calls rejected fast" .-> FB[Fallback: no recs]
dependency แต่ละตัวได้ช่องที่มีขอบเขตของตัวเอง ช่องที่อิ่มตัวจะปฏิเสธอย่างรวดเร็วโดยไม่แตะตัวอื่น

นี่คือ bulkhead ที่ implement เป็น bounded semaphore: การเรียกไปยัง dependency หนึ่งจะรันพร้อมกันได้มากที่สุด N ครั้ง และผู้เรียกที่ไม่สามารถได้ permit ทันทีจะถูกปฏิเสธแทนที่จะถูกปล่อยให้กองพะเนิน แต่ละตัวอย่างเป็นอิสระในตัวเองและเขียนตามแบบฉบับของแต่ละภาษา

class BulkheadFullError extends Error {}
class Bulkhead {
private inFlight = 0;
constructor(private readonly limit: number) {}
async run<T>(fn: () => Promise<T>): Promise<T> {
if (this.inFlight >= this.limit) {
throw new BulkheadFullError('bulkhead full');
}
this.inFlight += 1;
try {
return await fn();
} finally {
this.inFlight -= 1;
}
}
}
// One compartment per dependency — sized independently.
const catalog = new Bulkhead(8);
const reviews = new Bulkhead(4);
const recs = new Bulkhead(2);
const recommendations = await recs
.run(() => recsClient.fetch(productId))
.catch(() => []); // recommendations saturated → degrade, do not block catalog

สิ่งที่คุณได้รับ:

  • fault isolation dependency ที่ช้าหรือล้มเหลวกินได้มากสุดแค่ทรัพยากรในช่องของตัวเอง ส่วน dependency ที่ยังแข็งแรงก็รักษาโควตาของตัวเองไว้ได้ ความล้มเหลวจุดเดียวจึงไม่ลากทั้ง service ลงไปอีกต่อไป
  • การใช้ทรัพยากรที่คาดเดาได้ ความ concurrency สูงสุดของ dependency แต่ละตัวถูกระบุชัดเจนและมีขอบเขต ซึ่งทำให้การวางแผนกำลังและการ load testing จัดการได้ง่ายขึ้นมาก — คุณรู้กรณีเลวร้ายที่สุดของแต่ละช่อง
  • การเสื่อมประสิทธิภาพอย่างนุ่มนวล (graceful degradation) เมื่อช่องเต็ม การเรียกจะถูกปฏิเสธอย่างรวดเร็วและสามารถ fall back ได้ ดังนั้นผู้ใช้จึงได้หน้า product page ที่ไม่มี recommendations แทนที่จะไม่ได้หน้าเลย

สิ่งที่คุณต้องจ่าย:

  • ต้องกำหนดขนาดของแต่ละช่อง ตอนนี้คุณมีหลาย pool ให้จูนแทนที่จะมีแค่อันเดียว ตั้งช่องเล็กไปก็ปฏิเสธการเรียกที่ dependency ซึ่งยังแข็งแรงรับไหว ตั้งใหญ่ไปก็เลิกเป็นเส้นแบ่งที่ได้ผล
  • peak utilization ลดลง การกันโควตาไว้ให้แต่ละ dependency แปลว่า permit บางส่วนจะนั่งว่างตอนที่ dependency นั้นไม่มีงาน เท่ากับคุณแลก throughput ดิบไปนิดหน่อยเพื่อได้ isolation ปกติแล้วคุ้ม แต่ก็ยังเป็นการแลกอยู่ดี
  • ชิ้นส่วนที่ขยับมากขึ้น pool แยกตาม dependency เพิ่มพื้นผิวของการ config และการ monitoring แต่ละช่องคุ้มค่ากับ metric หนึ่งตัวเพื่อให้คุณเห็นว่าตัวไหนกำลังอิ่มตัว
  • Circuit Breaker — bulkhead ควบคุมความเสียหาย จากนั้น breaker จะหยุดเรียก dependency ที่พังโดยสิ้นเชิง
  • Retry and Timeout — กำหนดขอบเขตว่าการเรียกแต่ละครั้งจะถือ permit ได้นานแค่ไหน ซึ่งกันไม่ให้ช่องเต็มอยู่ตลอดกาล
  • Rate Limiting — จำกัด อัตรา ของงาน ส่วน bulkhead จำกัด ความ concurrency ของงาน
ข้อดีข้อแลกเปลี่ยน
ป้องกัน cascade failure — resource pool หนึ่งหมดไม่กระทบ pool อื่นเพิ่ม resource ที่ต้องจัดสรรล่วงหน้า — อาจ underutilize บางส่วน
isolate critical path จาก non-critical — ให้ priority ได้configuration ซับซ้อน — ต้องรู้จัก workload pattern
ระบบยังทำงานได้บางส่วนแม้ dependency ล้มจำนวน pool มากเกินทำให้ manage ยาก
ลด blast radius เมื่อ dependency ช้าหรือล้มต้องทดสอบ failure scenario — hard ถ้าไม่มี chaos engineering

Bulkhead ที่ Partition ผิด — แบ่ง pool ตาม technical boundary ไม่ใช่ failure boundary อาการ:

  • แบ่ง pool ตาม thread type แต่ไม่แบ่งตาม dependency ที่อาจล้ม
  • failure ยังลาม เพราะ partition ไม่ตรงกับแหล่ง failure จริง

Pool ขนาดเดียวสำหรับทุกอย่าง — ไม่ปรับ pool size ตาม traffic pattern อาการ:

  • critical payment flow ได้ thread เท่ากับ batch report
  • payment ช้าเพราะแย่งกับ non-critical workload

💡 ตัวอย่างจากของจริง

Netflix:

  • ใช้ Hystrix (thread pool isolation) สำหรับทุก external call
  • แต่ละ dependency มี thread pool ของตัวเอง
  • recommendation service ล้ม → thread pool ของตัวเองเต็ม → ไม่กระทบ payment thread pool

Resilience4j (successor ของ Hystrix):

  • semaphore-based bulkhead สำหรับ reactive systems
  • ใช้แพร่หลายในระบบที่ใช้ Spring Boot + reactive programming
bulkhead pattern แยกอะไร?
bulkhead pattern ได้ชื่อมาจากไหน?
bulkhead ป้องกัน dependency ที่ช้าตัวเดียวไม่ให้ล้มทั้งบริการได้อย่างไร?
bulkhead ต่างจาก rate limiter อย่างไร?