ความล้มเหลว (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 ได้ในระดับหนึ่ง แต่การทำสิ่งเหล่านี้ มันช่วยรักษาความเชื่อมั่นของลูกค้า และ ลดผลกระทบจากความล้มเหลวที่หลีกเลี่ยงไม่ได้ เพราะฉนั้นมีไว้ดีกว่าไม่มี