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

Circuit Breaker

บริการของคุณเรียก dependency ในทุก ๆ request — สมมติว่าเป็น pricing service ที่ flow ของ checkout ต้องปรึกษาก่อนจะคำนวณยอดรวมได้ ส่วนใหญ่การเรียกนั้นรวดเร็วและสำเร็จ แต่ pricing service เพิ่งเริ่มล้มเหลว: อาจรับภาระเกิน อาจมีฐานข้อมูลปลายทางที่ timeout หรืออาจอยู่ระหว่าง deploy ตอนนี้การเรียกแต่ละครั้งจึงไม่ error หลังจากรอนาน ก็ค้างจนกระทั่ง timeout ของคุณทำงาน

แม้จะมี timeout ในแต่ละการเรียก (ซึ่งคุณควรมี) พฤติกรรมแบบไร้เดียงสาคือการพยายามต่อไป checkout request ที่เข้ามาทุกครั้งจะเรียก pricing service ที่พังอย่างซื่อสัตย์ รอจน timeout ล้มเหลว และจับ thread หรือ connection ไว้ตลอดเวลานั้น

การยิง request ไปหา dependency ที่รู้อยู่แล้วว่ากำลังล้มเหลวคือความสูญเปล่าล้วน ๆ และแย่กว่าสูญเปล่าคืออันตรายจริงจัง การเรียกที่ไร้ความหวังแต่ละครั้งจองไว้ทั้ง thread หนึ่งตัว connection หนึ่งตัว และหน่วยความจำอีกก้อน ตลอดช่วงเวลาจนกว่าจะ timeout พอโหลดหนัก ทรัพยากรที่ถูกจองจะกองพอกเร็วกว่าที่คืนมา แล้ว service ของคุณเองก็หมดแรงรับ ทุก request แม้แต่ request ที่ไม่ได้เกี่ยวกับ dependency ที่พังเลย ซ้ำร้าย การ retry ยังไปเพิ่มภาระให้ dependency ที่กำลังดิ้นรน ทำให้ฟื้นตัวช้าลงไปอีก

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

circuit breaker คือตัวห่อที่มีสถานะครอบการเรียก remote เอาไว้ คอยดูว่าการเรียกแต่ละครั้งผลเป็นอย่างไร พอความล้มเหลวข้ามเกณฑ์ก็จะ หยุดปล่อยให้การเรียกผ่าน ไปชั่วระยะพักตัว เหมือน breaker ไฟฟ้าที่เป็นที่มาของชื่อ คือ trip ตัดวงจรเพื่อปกป้องระบบ แล้ว reset กลับเมื่อสภาพดูปลอดภัยแล้ว

breaker มีสามสถานะ:

  • Closed — สถานะปกติ การเรียกผ่านไปหา dependency ได้ breaker คอยนับทั้งความล้มเหลวและความสำเร็จ ถ้าอัตราความล้มเหลวข้ามเกณฑ์ภายใน window ที่กำหนด ก็จะ trip ไปเป็น Open
  • Open — ถือว่า dependency พังแล้ว การเรียกจะ ไม่ ผ่านไป แต่ล้มเหลวทันทีและมักเรียก fallback แทน โดยไม่แตะ dependency เลย นี่คือ fail fast ไม่มี thread ไหนถูกจับไว้รอ พอหมดเวลาพักตัว breaker จะย้ายไปเป็น Half-Open
  • Half-Open — สถานะทดลอง ปล่อย probe call จำนวนจำกัดผ่านไป ถ้าสำเร็จ แปลว่า dependency ฟื้นแล้ว breaker กลับไปเป็น Closed ถ้าล้มเหลว ก็ trip กลับไปเป็น Open แล้วเริ่มนับเวลาพักตัวใหม่
stateDiagram-v2
  [*] --> Closed
  Closed --> Open: failure rate crosses threshold
  Open --> HalfOpen: cool-down timer expires
  HalfOpen --> Closed: probe calls succeed
  HalfOpen --> Open: probe call fails
  Closed --> Closed: success / failure below threshold
  note right of Open
    calls fail fast
    (fallback runs, no remote call)
  end note
สถานะของ circuit breaker — Closed นับความล้มเหลว, Open fail fast, Half-Open ตรวจวัดเพื่อหาการฟื้นตัว

ตัวอย่างนี้คือ breaker ที่ครอบการเรียกแบบ asynchronous หนึ่งครั้ง ตอนอยู่สถานะ Closed จะนับความล้มเหลวที่ติดต่อกัน พอข้ามเกณฑ์ก็เปลี่ยนเป็น Open แล้ว fail fast ไปจนกว่าจะหมดเวลาพักตัว จากนั้นปล่อย probe หนึ่งครั้งในสถานะ Half-Open เพื่อตัดสินว่าจะกลับไป Closed หรือไม่ แต่ละตัวอย่างรันได้ด้วยตัวเองและเขียนตามสไตล์ของภาษานั้น ๆ

type State = 'closed' | 'open' | 'half-open';
class OpenCircuitError extends Error {}
class CircuitBreaker {
private state: State = 'closed';
private failures = 0;
private openedAt = 0;
constructor(
private readonly threshold = 5,
private readonly cooldownMs = 10_000,
) {}
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.openedAt < this.cooldownMs) {
throw new OpenCircuitError('circuit is open');
}
this.state = 'half-open';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess(): void {
this.failures = 0;
this.state = 'closed';
}
private onFailure(): void {
this.failures += 1;
if (this.state === 'half-open' || this.failures >= this.threshold) {
this.state = 'open';
this.openedAt = Date.now();
}
}
}
const breaker = new CircuitBreaker();
const price = await breaker
.call(() => pricingClient.quote(cart))
.catch(() => fallbackPrice(cart)); // degrade gracefully when open

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

  • ตัด cascading failure ได้ ระหว่างที่ breaker อยู่สถานะ Open การเรียกที่ไร้ความหวังจะไปไม่ถึง dependency ที่พัง จึงจับ thread และ connection ของคุณไว้ไม่ได้ dependency ที่ล้มเหลวหมดสิทธิ์ดูดทรัพยากรของผู้เรียกจนแห้ง
  • ความล้มเหลวที่รวดเร็วและคาดเดาได้ แทนที่ทุก request จะต้องรอจน timeout เต็มจำนวน request จะล้มเหลวทันทีและสามารถรัน fallback ได้ — ค่าที่ cache ไว้ ค่าเริ่มต้น หรือ error ที่ชัดเจน — ทำให้ latency มีขอบเขต
  • การฟื้นตัวอัตโนมัติ probe ของ Half-Open หมายความว่าระบบรักษาตัวเองได้: ไม่ต้องมีคนมาสังเกตการฟื้นตัวแล้วเปิด traffic ใหม่

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

  • การจูนเป็นงานจริง ทั้ง threshold, window ที่ใช้นับความล้มเหลว, ระยะเวลาพักตัว และจำนวน probe ของ half-open ต้องกำหนดค่าเองทั้งหมด ตั้งไวไป breaker ก็กระพริบเพราะการสะดุดปกติ ตั้งหย่อนไปก็ไม่เคยปกป้องอะไรเลย
  • คุณต้องวางแผน fallback เอง breaker ทำแค่ fail fast ส่วนจะ return อะไร ตอนอยู่สถานะ Open เป็นการตัดสินใจของคุณ ค่าจาก cache ที่เก่าหน่อย ค่าเริ่มต้นที่สมเหตุสมผล หรือ response บางส่วน ช่วยให้ประสบการณ์ผู้ใช้ยังไปต่อได้ ส่วน error ที่ไม่ได้จัดการก็แค่ย้ายความล้มเหลวไปโผล่ที่อื่น
  • สถานะไม่ได้แชร์ข้าม instance แต่ละ instance ของ service มักมี breaker ของตัวเอง dependency ตัวเดียวจึงอาจ open บน instance หนึ่งแต่ closed บนอีก instance หนึ่ง ระบบส่วนใหญ่รับได้ แต่ต้องจำไว้ว่ามุมมองเรื่องสุขภาพเป็นแบบต่อ instance เว้นแต่คุณจะทำให้แชร์กัน
  • Retry and Timeout — timeout คือสิ่งที่เปลี่ยนการค้างให้กลายเป็นความล้มเหลวที่นับได้ซึ่ง breaker สามารถนำไปทำงานต่อได้
  • Bulkhead — ควบคุมทรัพยากรที่ dependency สามารถกินได้ตั้งแต่ก่อนที่ breaker จะ trip ด้วยซ้ำ
  • Rate Limiting — ปกป้อง dependency ที่อยู่ปลายทางที่รับการเรียกซึ่ง breaker ควบคุมอยู่
ข้อดีข้อแลกเปลี่ยน
ป้องกัน cascade failure — หยุดส่ง request ไป dependency ที่ล้มแล้วfallback ที่ไม่ดีทำให้ user experience แย่กว่า error ตรง ๆ
fail fast — ไม่เสียเวลารอ timeout ทุก requestthreshold ที่ผิดทำให้ circuit เปิดโดยไม่จำเป็น
ให้เวลา dependency ฟื้นตัว — ไม่กระหน่ำระบบที่กำลัง recoverHalf-Open probe ที่ผิด config ทำให้ recovery ช้า
metrics ที่ circuit เก็บช่วย observabilitycomplexity เพิ่มในการ test — ต้อง simulate failure

Circuit Breaker ที่ Threshold ต่ำเกิน — เปิด circuit เพราะ error rate 1% อาการ:

  • circuit เปิดบ่อยจาก transient error ธรรมดา
  • service ปฏิเสธ request โดยไม่จำเป็น
  • ตั้ง threshold ตาม normal error baseline ไม่ใช่ absolute low value

ไม่มี Fallback — circuit เปิดแล้ว throw error ธรรมดา อาการ:

  • user เห็น error 500 แทนที่จะเห็น degraded experience
  • ควรมี fallback: cached data, default response, หรือ “try again later” message

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

Netflix (Hystrix):

  • ใช้ Circuit Breaker สำหรับทุก external call — รวมถึงระหว่าง internal service
  • Hystrix dashboard แสดง circuit state ของทุก dependency แบบ real-time
  • เมื่อ recommendation service ล้ม → fallback คือ popular content แทน personalized

Resilience4j:

  • มาแทน Hystrix ใน Netflix OSS
  • lightweight, functional style, รองรับ reactive programming
circuit breaker มีสามสถานะอะไรบ้าง?
เกิดอะไรขึ้นกับการเรียกขณะที่ breaker อยู่ในสถานะ Open?
สถานะ Half-Open มีจุดประสงค์อะไร?
ทำไม circuit breaker จึงต้องจับคู่กับ timeout ต่อการเรียก?