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

ความน่าเชื่อถือใน Microservices

ใน monolith ฟังก์ชันหนึ่งเรียกอีกฟังก์ชันหนึ่ง ผลลัพธ์มีแค่ return หรือ throw และกลับมาในระดับนาโนวินาที ไม่มีเครือข่ายคั่น ไม่มี process แยกที่อาจกำลัง restart ไม่มีคิวที่อาจเต็ม พอแยกระบบออกเป็นหลาย service ทุกการเรียกภายใน process ที่ข้ามเส้นแบ่ง service จะกลายเป็นการเรียกแบบ remote ต้องวิ่งผ่าน socket ผ่าน load balancer ข้าม availability zone dependency ที่คุณเคยเชื่อใจได้เต็มร้อย ตอนนี้กลายเป็นโปรแกรมคนละตัวที่อาจช้า อาจรับภาระเกิน หรืออาจหายไปดื้อ ๆ

ข้อเท็จจริงที่นิยามระบบแบบกระจายคือ partial failure ทุกช่วงเวลาจะมีบางส่วนของระบบที่พัง ขณะที่ส่วนที่เหลือยังทำงานอยู่ dependency ไม่ได้มีแค่สถานะขึ้นหรือลง แต่ ช้า ได้ด้วย ซึ่งแย่กว่า เพราะ dependency ที่ช้าจะจับทรัพยากรของคุณไว้เป็นตัวประกัน ลองนึกภาพ order service ที่เรียก payment service ทุก request แล้ววันหนึ่ง payment service เริ่มตอบในแปดวินาทีแทนที่จะเป็นแปดสิบมิลลิวินาที ทีนี้ order request แต่ละครั้งก็จอด thread ทิ้งไว้รอ payment thread กองพะเนิน connection pool แห้ง แล้วภายในไม่กี่วินาที order service ก็หยุดตอบ health check ทั้งที่ ตัว order service เองไม่ได้พังอะไรเลยสักนิด ความล้มเหลวแพร่ย้อนขึ้นมาถึงต้นทางแล้ว

นี่คือ cascading failure component ที่กำลังดิ้นรนลากผู้เรียกลงไปด้วย ผู้เรียกก็ลากผู้เรียกของตัวเองลงไปอีกทอด จนสุดท้าย dependency ที่ช้าเพียงตัวเดียวล้มทั้งสาย service ที่แข็งแรงดีอยู่

flowchart LR
  U[Users] --> GW[API Gateway]
  GW --> A[Order Service]
  A --> B[Payment Service]
  B --> X[(Slow / failing dependency)]
  X -. "8s latency" .-> B
  B -. "threads blocked" .-> A
  A -. "pool exhausted" .-> GW
  GW -. "timeouts surface to users" .-> U
Cascading failure — dependency ที่ช้าทำให้ทรัพยากรต้นทางหมดไปทีละ hop

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

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

  • Circuit Breaker — พอการเรียกไปยัง dependency ข้ามเกณฑ์ error ก็หยุดเรียกไปสักพัก เลือก fail fast แทนที่จะปล่อยให้ request ไร้ความหวังกองพะเนิน แล้วส่ง probe ไปตรวจเป็นระยะเพื่อดูว่าฟื้นหรือยัง
  • Retry + Timeout — ใส่เพดานเวลาที่เข้มงวดให้ทุกการเรียก remote เพื่อไม่ให้ค้างได้ตลอดกาล แล้ว retry เฉพาะความล้มเหลวแบบ transient ด้วย exponential backoff และ jitter โดยทำเฉพาะกับ idempotent operation เท่านั้น
  • Bulkhead — แยกทรัพยากร (thread pool, connection pool, semaphore) ที่ใช้สำหรับแต่ละ dependency เพื่อให้ dependency หนึ่งที่อิ่มตัวไม่สามารถระบายทรัพยากรที่ส่วนที่เหลือของระบบต้องการได้
  • Rate Limiting / Throttling — จำกัดอัตราของงานที่เข้ามา ไม่ให้ traffic ที่พุ่งสูงหรือ client ที่ทำตัวผิดปกติดัน service เกินกำลังที่รับไหวจริง ด้วยการทิ้งหรือเข้าคิวส่วนที่เกิน

pattern เหล่านี้เสริมซึ่งกันและกัน timeout ทำให้การนับ error ของ circuit breaker มีความหมาย circuit breaker กันไม่ให้ retry กลายเป็น retry storm bulkhead ควบคุมสิ่งที่สองอย่างแรกพลาดไป และ rate limiting ป้องกันไม่ให้บริการถูกถาโถมตั้งแต่แรก

เราเริ่มต้นด้วย circuit breaker ที่เป็น pattern ที่เปลี่ยน dependency ที่ค้างให้กลายเป็นความล้มเหลวที่รวดเร็วและคาดเดาได้ จากนั้นเราเพิ่ม retry และ timeout ที่เป็นวินัยระดับการเรียกแต่ละครั้งที่จำกัด latency และฟื้นตัวจากการสะดุดชั่วครู่ ต่อมาคือ bulkhead ซึ่งควบคุมความเสียหายด้วยการแบ่งกั้นทรัพยากร เราปิดท้ายด้วย rate limiting ซึ่งปกป้องกำลังของบริการจากการรับภาระเกิน แต่ละบทเรียนมีรูปแบบเดียวกัน: สถานการณ์ที่นำไปสู่ pattern แรงที่ตึงเครียดกัน สิ่งที่ pattern ทำ ไดอะแกรม code ตัวอย่างที่รันได้ในสี่ภาษา และการมองอย่างตรงไปตรงมาว่า pattern นั้นมีต้นทุนอะไรกับคุณ

  • Circuit Breaker — เริ่มจากตัวนี้ เป็น pattern ที่กันไม่ให้อาการค้างลุกลามเป็นการล่ม
  • Retry and Timeout — วินัยระดับการเรียกแต่ละครั้งที่ breaker สร้างขึ้นอยู่บนนั้น
"partial failure" หมายความว่าอย่างไรในระบบแบบกระจาย?
ทำไม dependency ที่ช้าจึงมักอันตรายกว่า dependency ที่ล่มไปเลย?
cascading failure คืออะไร?