Animated Architecture Patterns

Circuit Breaker

เลิกเรียก dependency ที่กำลังพัง ให้ fail fast แล้วใช้ fallback แทน จากนั้นคอยลองเรียกดูจนกว่ามันจะกลับมาปกติ

Service A caller serves fallback circuit breaker CLOSED OPEN HALF-OPEN failures cooldown Service B dependency ● healthy ● failing ● recovering trial failures ≥ 3 cooldown ends trial succeeds trial fails CLOSED OPEN HALF-OPEN CLOSED OPEN HALF-OPEN

ขั้นที่ 1 · สถานะ Closed ตอนปกติ breaker อยู่ในสถานะ closed การเรียกทุกครั้งผ่านไปถึง Service B ได้หมด ตัว breaker คั่นอยู่กลางสาย และคอยจดผลของการเรียกแต่ละครั้งไว้

ขั้นที่ 2 · Error เริ่มสะสม Service B เริ่มตอบ error กลับมา ทุกครั้งที่ fail ตัวนับของ breaker ก็ขยับขึ้นหนึ่ง พอถึง threshold (ในตัวอย่างนี้คือ 3 ครั้ง แต่งานจริงมักดูเป็น อัตรา การ fail ใน sliding window) breaker ก็ ตัดวงจรเป็น open

ขั้นที่ 3 · Open: fail fast ตอนที่ open อยู่ breaker จะปฏิเสธการเรียกทันที ไม่ต้องรอจน timeout ส่วนฝั่ง Service A ก็ตอบด้วย fallback แทน (ข้อมูลจาก cache, ค่า default หรือข้อความ error ที่เป็นมิตรกับผู้ใช้) ส่วน Service B ก็ได้มีเวลาหายใจและฟื้นตัว

ขั้นที่ 4 · Half-open: ลองเรียกดู พอหมด cooldown ตัว breaker ก็เปลี่ยนเป็น half-open แล้วปล่อยให้ลองเรียกผ่านไปแค่ครั้งเดียว ครั้งนี้สำเร็จ breaker เลยกลับไปเป็น closed และล้างตัวนับเป็นศูนย์ ถ้าครั้งนั้น fail ตัว breaker ก็จะกลับไปเป็น open ทันที แล้วรอ cooldown อีกรอบ

ปัญหา

ถ้า downstream service ช้าหรือล่ม ฝั่งที่เรียกมันก็ยังส่ง request ไปเรื่อย ๆ แล้วนั่งรอจนกว่าจะ timeout ทำให้ thread และ connection กองพะเนินอยู่หลังการเรียกที่ช้า latency พุ่งขึ้น แล้วความเสียหายก็ลามขึ้นไปข้างบนกลายเป็น cascading failure ในขณะเดียวกัน service ที่กำลังแย่อยู่ก็โดน traffic (และ retry) ถล่มใส่ ในจังหวะที่มันต้องการพักที่สุดพอดี

ทำงานยัง⁠ไง

circuit breaker ห่อการเรียก dependency ไว้แล้วคอยดูผลของการเรียกแต่ละครั้ง มันเป็น state machine เล็ก ๆ:

  • Closed: การเรียกผ่านไปได้ตามปกติ ตัว breaker นับจำนวนครั้งที่ fail หรือคำนวณอัตราการ fail ใน sliding window
  • Open: พอเกิน threshold แล้ว การเรียกจะถูกปฏิเสธทันทีโดยไม่แตะ network เลย แล้วฝั่งที่เรียกก็ไปใช้ fallback
  • Half-open: หลังผ่าน cooldown ไปแล้ว จะปล่อยให้ลองเรียกผ่านไปได้จำนวนจำกัด ถ้าสำเร็จ breaker ก็ปิด ถ้า fail ก็เปิดอีกรอบแล้วรอ cooldown ใหม่

ใช้ตอนไหนดี

  • การเรียก remote service หรือ shared resource ที่ fail หรือช้าลงได้: HTTP API, database, SaaS ของบุคคลที่สาม
  • ตอนที่คำตอบแบบลดคุณภาพแต่เร็ว ดีกว่าการ fail ช้า ๆ: ข้อมูลจาก cache, ค่า default, "ลองใหม่อีกครั้งภายหลัง"
  • ไม่ใช่สำหรับการเรียกภายใน process ล้วน ๆ และไม่ใช่ของที่ใช้แทนการแก้ dependency ที่ไม่น่าเชื่อถือ

ได้อะไร เสียอะไร

  • การ tune สำคัญ ถ้าไวเกินไปก็ตัดเพราะ noise แต่ถ้าหละหลวมเกินไปก็ตัดช้าเกินไป ใช้อัตราการ fail ที่มีจำนวนการเรียกขั้นต่ำ และมี threshold สำหรับการเรียกที่ช้า ดีกว่าใช้จำนวนครั้งดิบ ๆ
  • ต้องมี fallback จริง ๆ ถ้าไม่มี fallback ตัว breaker ที่ open ก็แค่ทำให้คุณ fail เร็วขึ้นเท่านั้น
  • แต่ละ instance เรียนรู้เอง breaker ที่แยกต่อ instance เห็นแค่ traffic ของตัวเอง replica แต่ละตัวเลยตัดวงจรแยกกัน
  • ใช้คู่กับ timeout และ retry ที่ระวัง การเรียกที่ค้างจะไม่ถูกนับเป็น fail เลยถ้าไม่มี timeout และ retry ควรมอง "breaker open" ว่าเป็นกรณีที่ไม่ควร retry จะได้ไม่ไปถล่มวงจรซ้ำ

ข้อควรรู้ตอนลงมือทำ

  • Library: Resilience4j (Java), Polly (.NET), opossum (Node.js), pybreaker (Python), gobreaker (Go)
  • Infrastructure: service mesh และ proxy ให้แนวคิดเดียวกันโดยไม่ต้องแก้โค้ด ตัวอย่างเช่น outlier detection กับ circuit-breaking threshold ของ Envoy และ outlierDetection ใน DestinationRule ของ Istio
  • คอยสังเกตมัน: ปล่อย metric หรือ event ทุกครั้งที่ state เปลี่ยน breaker ที่ open คือ alert ที่ชัดที่สุดตัวหนึ่งที่คุณจะได้