ความล้มเหลว (Failures) ในระบบไม่ใช่เรื่องผิดปกติอะไร มันเป็นสิ่งที่เกิดขึ้นตามธรรมชาติของระบบที่ซับซ้อน

การออกแบบที่ดี คือ การจัดการกับความล้มเหลวให้ปลอดภัย และ จำกัดผลกระทบ (blast radius) มากกว่า การพยายามป้องกันไม่ให้เกิดขึ้นเลย

บทความนี้เรามาดูกันว่า การทำระบบให้ยังอยู่รอดบนระบบที่เป็นแบบ distributed ต้องคำนึงถึงอะไรบ้าง

ตามภาพนี้ Resilience Patterns มีอยู่ด้วยกัน 8 patterns คือ

  • Circuit Breaker: ป้องกันการล้มเหลวแบบต่อเนื่อง โดยหยุดการเรียก dependency ที่ล้มเหลวชั่วคราว
  • Retry with Backoff: การ retry ต้องมี exponential backoff + jitter เพื่อหลีกเลี่ยง retry storm และต้องออกแบบ API ให้ idempotent
  • Timeouts: เป็น safety net ที่มักถูกละเลย ต้องตั้งค่าให้สอดคล้องกับ SLA ไม่ใช่ยืดเวลาจนทำให้ระบบเสียหายหนักขึ้น
  • Bulkhead Isolation: แยก resource/thread pool เพื่อไม่ให้ dependency ที่ช้าทำให้ critical path เช่น checkout ล้มไปด้วย
  • Rate Limiting: ป้องกัน traffic surge ที่ทำให้ระบบ collapse โดยใช้ token bucket, leaky bucket หรือ fixed/sliding window
  • Fallback Mechanisms: ให้ระบบตอบสนองแบบ graceful เช่นใช้ cache หรือ default response แต่ต้องมีการสังเกตและวัดผล ไม่ใช่ fallback ถาวร
  • Dead Letter Queues (DLQ): เก็บข้อความที่ process ไม่ได้ เพื่อไม่ให้ pipeline ค้าง และสามารถ replay/monitor ได้
  • Graceful Degradation: จัดลำดับความสำคัญของฟีเจอร์ (Tier 1–3) เพื่อให้ core business เช่น checkout ทำงานได้แม้ฟีเจอร์เสริมจะล้ม

เราไปลงรายละเอียดแต่ละตัวกัน


1.Circuit Breaker — การควบคุม/การจำกัดการล้มเหลว

Circuit breaker ปกป้องระบบของเราจากการเรียกใช้งาน dependency ที่ล้มเหลวซ้ำๆ

หากไม่มี circuit breaker จะเกิดเหตุการณ์นี้:

  • Service A เรียก Service B
  • Service B เริ่มทำงานช้าลง
  • Threads ใน A เริ่มรอ
  • Thread pool เต็ม
  • Requests เริ่มค้างในคิว
  • Latency พุ่งสูง
  • Service A ใช้งานไม่ได้
  • จากนั้น upstream services ก็ล้มเหลวตามไปด้วย
  • Dependency ที่ช้าเพียงตัวเดียวกลายเป็นการล่มทั้งระบบ

Circuit breaker จะตรวจสอบอัตราความล้มเหลว และ latency เมื่อเกิน threshold ที่กำหนด มันจะ “เปิด” และหยุดการเรียกใช้งานต่อไปชั่วคราวตามเวลาที่ตั้งไว้

ทำไมสิ่งนี้จึงสำคัญทางเทคนิค

  • ป้องกันการใช้ thread pool จนหมด
  • ลดแรงกดดันต่อ service ที่ไม่พร้อม (กำลัง down หรือมีปัญหา)
  • เปิดโอกาสให้ service ฟื้นตัว
  • ทำให้การล้มเหลวเกิดขึ้นเร็วแทนที่จะรอ timeout ช้า ๆ

การล้มเหลวแบบเร็ว (fail fast) มักปลอดภัยกว่าการ timeout ช้า ๆ

Engineering considerations

ข้อควรพิจารณาทางวิศวกรรม เช่น

  • กำหนด threshold ของความล้มเหลว (โดยอาจจะกำหนดจาก เปอร์เซ็นต์ หรือ จำนวนจริงก็ได้)
  • แยก circuit breaker ของ dependency แต่ละตัว
  • ติดตามสถานะ open/half-open เป็น metrics
  • ใช้ร่วมกับ timeouts (circuit breaker ที่ไม่มี timeout = ไร้ประโยชน์)

Circuit breaker ไม่ใช่ตัวเลือก แต่เป็น เครื่องมือเอาตัวรอด ในระบบที่เป็น synchronous จะได้ไม่ลากกันช้า หรือ ล่ม


2.Retry with Backoff — Controlled Persistence

การ retry คือ การเรียกใช้งานใหม่เมื่อเกิดการเรียกแล้วล้มเหลว ฟังดูเหมือนง่าย แต่การ retry แต่ถ้าเราให้มัน retry โดยไม่มีการควบคุม แทนที่มันจะเป็นตัวช่วยให้ระบบเรา recovery ตัวเองได้ มันจะกลายเป็นตัวช่วยพังระบบเราแทน

ลองนึกภาพดูว่า ถ้ามี request ล้มเหลว 1,000 ครั้ง และแต่ละอัน retry ทันที เราจะเพิ่ม traffic ไปยัง service ที่กำลังมีปัญหาเป็น 2 เท่า

ยินดีด้วย เราได้ปลดล็อค Achievement — สร้าง Retry storm สำเร็จแล้ว 😂

พฤติกรรมการ retry ที่ควรจะเป็น

  • ใช้ exponential backoff (เพิ่มเวลารอแบบทวีคูณ)
  • เพิ่ม jitter เพื่อป้องกันการ retry พร้อมกัน
  • ตั้งค่า ขีดจำกัดสูงสุด ที่รับได้
  • ใช้ร่วมกับ timeouts
  • ทำให้ operation เป็น idempotent (เรียกซ้ำแล้วผลลัพธ์ไม่เปลี่ยน)

ทำไม idempotency ถึงสำคัญ

ลองคิดดูว่า ถ้าเรา retry การเรียกชำระเงินโดยไม่มี idempotency key ที่บอกว่า request นี้เป็นการเรียกซ้ำ เราอาจเรียกเก็บเงินซ้ำ 2 ครั้งก็ได้ (หรือมากกว่านั้น)

การ retry เป็นเหมือนการเปลี่ยนรูปแบบของการเรียก API ดังนั้นเมื่อเรานำเอา retry มาใช้ เราต้องออกแบบ API ให้รองรับมันด้วย

ควรใช้ retry เมื่อไหร่

  • ปัญหาเครือข่ายชั่วคราว
  • ข้อผิดพลาด 5xx จาก dependency
  • กรณี timeout

ไม่ควรใช้ retry

  • ข้อผิดพลาดจากการตรวจสอบข้อมูล (validation errors)
  • การละเมิด Business rule (Business rule violations)
  • ความล้มเหลวที่เป็น deterministic (ความล้มเหลวแบบกำหนดได้ หรือ รู้อยู่แล้วว่ามันจะล้มเหลวแน่นอน)

3.Timeouts — เข็มขัดนิรภัยที่มักถูกละเลย

สิ่งที่ถูกละเลยที่พบมากที่สุดมากที่สุดในการทำระบบ distributed ไม่ใช่เรื่องการ scale

แต่ คือ การไม่มีการตั้งค่า timeout

เมื่อ thread รอการตอบกลับแบบไม่จำกัดเวลา มันจะบล็อก resource
ถ้าเกิดขึ้นพร้อมกันหลายร้อย request จะนำไปสู่ thread starvation

Thread Starvation

Thread Starvation คือภาวะที่เธรด (Thread) หนึ่งไม่สามารถเข้าถึงทรัพยากรที่จำเป็น (เช่น CPU, Lock, I/O) เพื่อทำงานต่อได้ เนื่องจากถูกเธรดอื่นแย่งใช้ทรัพยากรไปอย่างต่อเนื่อง หรือ มีการจัดสรรทรัพยากรที่ไม่ดี ส่งผลให้เธรดนั้นต้องรออย่างไม่มีกำหนด (waiting forever) และ ไม่สามารถทำงานจนจบได้

ให้มองว่า Timeouts คือ การกำหนดว่าเรายอมรอได้นานแค่ไหน

Engineering depth

ในทางเทคนิคอลมี timeout อยู่หลายประเภท เช่น

  • Connection timeout
  • Read timeout
  • Write timeout
  • Total request timeout

แต่ละประเภทมีหน้าที่ต่างกัน ซึ่งถ้าเราต้องทำในส่วนไหนก็ไปลงรายละเอียดในส่วนนั้นได้

ความผิดพลาดที่พบบ่อย

การตั้งค่า timeout ไว้สูงมาก เพราะ ไม่อยากให้มันล้มเหลว

วิธีนี้เป็นเพียงแค่การ เลื่อนเวลาล้มเหลวออกไป และมันมักเพิ่มความเสียหายมากกว่าเดิมด้วย

โดยทั่วไปแล้ว การกำหนด Timeout จะต้อง น้อยกว่าค่า SLA ของ upstream
ถ้า API ต้องตอบกลับภายใน 300ms การตั้ง timeout ของ dependency ไว้ 5 วินาทีมันไม่สมเหตุสมผล

Timeouts คือ การบังคับให้มีวินัยในการออกแบบระบบ

4.Bulkhead Isolation — การปกป้องเส้นทางที่สำคัญ

บนเรือรบ bulkhead ใช้เพื่อป้องกันไม่ให้น้ำท่วม จนทำให้เรือทั้งลำจมลงได้

ในระบบ distributed การทำ bulkhead ใช้เพื่อป้องกันไม่ให้ component ที่ล้มเหลวเพียงตัวเดียวใช้ resource ร่วมจนหมด

ถ้าไม่มีการแยก (isolation)

  • Dependency ที่ทำงานช้าจะใช้ threads ทั้งหมด
  • ฟีเจอร์ที่สำคัญจะไม่สามารถใช้งานได้
  • Background tasks จะต้องแข่งกับ request ของ user

Implementation techniques

  • แยก thread pool ต่อ dependency
  • ใช้ connection pool แยกเฉพาะ
  • กำหนด resource quota
  • แยก compute class ในสภาพแวดล้อม containerized

Real-world scenario

สมมติว่า recommendation engine ของเราทำงานช้า

ถ้ามันใช้ thread pool ร่วมกับ checkout ฝั่งของ user จะไม่สามารถทำการซื้อได้

ซึ่งสิ่งนี้ไม่ใช่ปัญหาด้าน performance แต่มัน คือ ปัญหาด้าน Architecture

ดังนั้น Bulkhead จะเป็นตัวบังคับให้เกิดการจัดลำดับความสำคัญ (prioritization)


5.Rate Limiting — การป้องกันการพุ่งขึ้นของ Traffic

นอกจากปัญหาภายในแล้ว ระบบยังต้องป้องกันปัญหาจากภายนอกได้ด้วย ซึ่งสิ่งที่พวกเรารู้จักกันดี คือ ปัญหาการพุ่งขึ้นของทราฟฟิก (traffic spikes)

ซึ่งปัญหานี้เกิดขึ้นได้หลายสาเหตุ เช่น

  • จำนวนผู้ใช้งานเพิ่มขึ้น
  • ระบบช้า ทำให้เกิดการเรียกใช้งานซ้ำๆ จากฝั่ง user
  • กิจกรรมทางการตลาด เช่น การจำกัดสิทธิ์/เวลา
  • โดนโจมดีด้วย DDoS

หากไม่มีการจำกัดอัตราการเรียกใช้งานไว้ (rate limiting)

  • CPU พุ่งสูง
  • ความกดดันต่อหน่วยความจำเพิ่มขึ้น
  • การเชื่อมต่อฐานข้อมูลเต็ม
  • Latency ลดลงแบบ exponential

จำไว้ว่า Rate limiting คือ การควบคุมปริมาณการรับเข้าของระบบ

Engineering decisions

  • Fixed window vs Sliding window
  • Token bucket vs Leaky bucket
  • Global vs Per-user limits
  • Gateway-level vs Service-level enforcement

Rate limiting ไม่ได้มีไว้แค่ป้องกันการใช้งานที่ผิดวัตถุประสงค์ (abuse prevention)
แต่มัน คือ การปกป้องขีดความสามารถของระบบ (capacity protection)

ให้ระบบเสถียรภายใต้โหลดบางส่วน (ที่จำกัด) ดีกว่าการปล่อยให้ระบบล้มเหลวจากโหลดจนเต็ม


6.Fallback Mechanisms — การออกแบบเพื่อความเป็นจริงที่ไม่สมบูรณ์แบบ

ในระบบของเรามีการล้มเหลวอยู่มากมาย และไม่ใช่ว่าการล้มเหลวทุกครั้งจะต้องถูกแสดงให้ผู้ใช้รับรู้

Fallbacks ช่วยให้ระบบมีพฤติกรรมทางเลือกเมื่อ dependency เกิดล้มเหลว โดยเราสามารถทำได้หลากหลายวิธี

ตัวอย่าง

  • ให้บริการข้อมูลที่ cache ไว้
  • ส่งคืนค่า configuration เริ่มต้น
  • ซ่อน component ที่ไม่จำเป็น
  • ให้ response แบบบางส่วน

คุณสมบัติที่ Fallbacks ควรมี

  • Observable (สามารถสังเกตได้)
  • Measurable (สามารถวัดได้)
  • Temporary (เป็นการชั่วคราว)

Fallbacks คือ การสร้าง ประสบการณ์ที่ราบรื่น (graceful experience) ไม่ใช่การปกปิดความล้มเหลวของระบบแบบถาวร (ซึ่งไม่มีใครทำกัน)


7.Dead Letter Queues (DLQ) — การจัดการข้อความที่ไม่สามารถประมวลผลได้

ในระบบ event-driven จะมีบางข้อความที่เกิดการล้มเหลวในการประมวลผล

ถ้าเรา retry ไปเรื่อยๆ มันจะเกิด

  • การบล็อก partitions
  • ทำให้ข้อความอื่น ๆ ล่าช้า
  • สร้างลูปที่ไม่มีที่สิ้นสุด

ดังนั้นเมื่อเรา retry ไปถึงจุดหนึ่ง เราจำเป็นต้องหยุดการทำงานตัวนี้ ซึ่งเราจำเป็นต้องจัดการกับงานที่หยุดทำตรงนี้ด้วย

ซึ่งเรานำ Dead Letter Queue มาเพื่อใช้ในการเก็บข้อความที่เกินจำนวนครั้ง retry ที่กำหนดไว้

Engineering practices

  • เก็บ metadata ของสาเหตุความล้มเหลว
  • ทำ replay mechanisms
  • ติดตามปริมาณข้อความใน DLQ
  • หลีกเลี่ยงการสะสมแบบเงียบๆ (silent accumulation)

DLQ เป็นการป้องกันไม่ให้ pipeline เป็นอัมพาต ซึ่งมันเป็นสิ่งจำเป็นในสถาปัตยกรรมที่ใช้ Distributed Streaming (อย่าง Kafka) หรือ ระบบที่ใช้ queue


8.Graceful Degradation — ปกป้อง Core Business

Graceful degradation คือ การจัดลำดับความสำคัญทางสถาปัตยกรรม ไม่ใช่ว่าฟีเจอร์ทั้งหมดจะมีความสำคัญเท่ากันหมด

ลองนึกดูว่า

  • ถ้า recommendation ล้มเหลว checkout ต้องยังทำงานได้
  • ถ้า analytics ล้มเหลว การประมวลผลคำสั่งซื้อยังต้องดำเนินต่อไป

เห็นไหมว่าแต่ละตัวความสำคัญไม่เท่ากัน บางอย่างล้มเหลวได้ บางอย่างแทบจะห้ามล้มเหลวเลย

หลักการออกแบบ (Design principle)

เราจะต้องแบ่ง Tier ของแต่ละ service ออกมา:

  • Tier 1 (critical path)
  • Tier 2 (important but optional)
  • Tier 3 (nice-to-have)

เมื่อเกิดเหตุการณ์ resource ถูกจำกัด ให้ลด Tier 3 ก่อน เพื่อให้ resource แก่ Tier 1-2 ยังสามารถทำงานได้

สิ่งที่ต้องใช้

  • Feature toggles
  • Conditional rendering
  • Independent service scaling
  • Priority-based routing

จำไว้ว่า Graceful degradation ไม่ใช่การแก้ปัญหา runtime แบบชั่วคราว แต่มัน คือ การตัดสินใจเชิงสถาปัตยกรรม (design decision)


มันทำงานร่วมกันอย่างไร

ทั้ง 8 รูปแบบนี้ มันไม่ใช่เครื่องมือที่แยกออกจากกัน จริงอยู่ว่า มันไม่ได้ผูกกันแบบทั้งทำทั้งหมด เราจะเริ่มทำอันใดอันหนึ่งก่อนก็ได้

แต่จำไว้ว่าทั้งหมดนี้มันส่งเสริมกันทั้งหมด ลองคิดดูว่า ถ้าเกิดสถานการณ์ความล้มเหลวขึ้น ทั้งหมดนี้จะทำงานสอดคล้องกัน เช่น

  • ฐานข้อมูลเกิด downstream จนทำงานช้า
  • Timeout ทำงาน
  • มีการ retry พร้อม backoff
  • Circuit breaker เปิด
  • Fallback ส่งคืนข้อมูลที่ cache ไว้
  • Bulkhead ทำให้ checkout ยังคงใช้งานได้
  • Rate limiter ป้องกันระบบจากการ surge
  • เหตุการณ์ที่ล้มเหลวถูกส่งไปยัง DLQ เพื่อการวิเคราะห์
  • ฟีเจอร์ที่ไม่สำคัญถูก degrade อย่างมีระดับ (gracefully)

เราเรียกสิ่งนี้ว่า Resilience choreography (การคืนชีพระบบ)


The Engineering Reality

ต้องเข้าใจก่อนว่า การสร้างความทนทาน (Resilience) จะทำให้ระบบของเราซับซ้อนมากขึ้น เนื่องจากเราจะต้องเพิ่มส่วนต่างๆ เข้าไป เช่น

  • การตั้งค่ามากขึ้น
  • Metrics มากขึ้น
  • Edge cases มากขึ้น
  • การเปลี่ยนสถานะ (state transitions) มากขึ้น

แต่การไม่ทำเลยมันแย่กว่ามาก

การมีกับไม่มีมันแตกต่างกันมาก

ระบบที่ไม่มี resilience patterns

  • การล้มเหลวไม่สามารถคาดเดาได้
  • สร้างการล้มเหลวต่อเนื่อง (cascading outages)
  • ยากต่อการ debug
  • ทำลายความเชื่อมั่นของผู้ใช้

ระบบที่มี resilience

  • ล้มเหลวอย่างรวดเร็ว (fail fast)
  • ฟื้นตัวได้อย่างรวดเร็ว (recover quickly)
  • แยกความเสียหายออกจากกัน (isolate damage)
  • รักษาความต่อเนื่องทางธุรกิจ (maintain business continuity)
Developer จะเริ่มออกแบบระบบจาก Success case แต่ Engineer ออกแบบระบบโดยเริ่มจาก Failure

ความผิดพลาดที่มักพบเมื่อนำ Resilience ไปใช้

แม้ทีมจะนำรูปแบบเหล่านี้ไปใช้แล้ว บ่อยครั้งพบว่า ทีมมักจะ implement ได้ไม่ดี หรือ ทำไม่ครบ

เช่น

  • Circuit breakers ที่ไม่มี metrics ที่เหมาะสม
  • Retries ที่ไม่มี jitter
  • Timeouts ที่ตั้งค่าสูงเกินไป
  • Bulkheads ที่ถูกแชร์โดยไม่ได้ตั้งใจ
  • DLQs ที่ไม่มีการ monitoring
  • Fallbacks ที่ซ่อน permanent degradation

Resilience patterns ต้องสามารถสังเกตได้ (observable) ถ้าเราไม่สามารถวัดสถานะของ breaker, จำนวนครั้ง retry, อัตรา timeout และขนาดของ DLQ ได้

เรากำลังทำงานแบบ blind (มองไม่เห็น/ตาบอด)


ระบบ distributed จะต้องล้มเหลวอย่างแน่นอน

คำถามไม่ใช่ว่ามันจะล้มเหลวหรือไม่ — แต่คือมันจะล้มเหลวอย่าง ปลอดภัยหรือไม่

Resilience patterns

  • ลด blast radius (ขอบเขตความเสียหาย)
  • ปกป้อง critical flows (เส้นทางสำคัญ)
  • ปรับปรุง mean time to recovery (MTTR)
  • รักษา ความเชื่อมั่นของลูกค้า

พวกมันไม่ได้กำจัดความซับซ้อนออกไป แต่พวกมัน จัดการกับมัน

Engineering maturity

สิ่งนี้จะเห็นได้ไม่ใช่จากพฤติกรรมของระบบในช่วงที่ทำงาน success แต่วัดจากพฤติกรรมของมันในช่วงที่เกิด ความล้มเหลวบางส่วน (partial failure)

เราจะต้องออกแบบเพื่อรองรับช่วงเวลานั้น

เพราะในระบบ distributed ช่วงเวลานั้นไม่ใช่เรื่องที่เกิดขึ้นได้ยาก

แต่มัน คือ สิ่งที่ หลีกเลี่ยงไม่ได้

สุดท้าย Resilience มันเป็น การตัดสินใจเชิงธุรกิจ เพราะในทาง technical การไม่มีมันก็ไม่ได้กระทบการทำงานในสถานการณ์ปกติ หรือ แม้แต่ตอนที่เกิดปัญหาขึ้นก็สามารถแก้ปัญหาแบบ manual ได้ในระดับหนึ่ง แต่การทำสิ่งเหล่านี้ มันช่วยรักษาความเชื่อมั่นของลูกค้า และ ลดผลกระทบจากความล้มเหลวที่หลีกเลี่ยงไม่ได้ เพราะฉนั้นมีไว้ดีกว่าไม่มี