บทที่ 12 · Part 3 — Scale & Reliability
Resilience Patterns
Taxonomy ของ failure, timeout/retry/circuit breaker/bulkhead/hedged request พร้อมค่าที่ควรตั้ง และ load shedding
พฤติกรรมเริ่มต้นของทุกระบบตอนโหลดเกินคือพฤติกรรมที่แย่ที่สุดเท่าที่จะเป็นได้ มันจะยุ่งที่สุดและมีประโยชน์น้อยที่สุดพร้อมกัน บทนี้คือวิธีทำให้การปฏิเสธงาน เป็นพฤติกรรมที่ออกแบบไว้ ไม่ใช่ผลข้างเคียง
จบบทนี้คุณจะ
- เรียกชื่อ failure ทั้ง 9 ชนิดได้ — โดยเฉพาะ gray failure และ metastable failure
- ตั้งค่า timeout, retry, circuit breaker, bulkhead ด้วยตัวเลขจริง ไม่ใช่ default
- รู้ว่าทำไม dependency ที่ ช้า แย่กว่าที่ ตาย
- เข้าใจ goodput collapse และแก้ด้วย bounded queue + load shedding + drop expired work
- เขียนตาราง degradation และรู้ว่าแถวไหนต้องให้คนที่มีอำนาจตัดสิน
Taxonomy ของ failure — จำชื่อมันไว้
| ชนิด | หน้าตาเป็นอย่างไร | ทำไมอันตราย | การป้องกัน |
|---|---|---|---|
| Crash / fail-stop | process หรือ node ตาย | เคสที่ง่ายที่สุด — health check จับได้ | redundancy + ตรวจจับเร็ว |
| Gray failure | ยังมีชีวิต แต่ช้า, หรือล้ม 5% ของ request, หรือเสียเฉพาะบางเส้นทาง | health check ผ่าน จึงยังส่ง traffic เข้า node ที่พังต่อไป — แย่กว่า crash | outlier detection, ติดตาม error ฝั่ง client, eject ตาม p99 |
| Partial failure | เขียน DB สำเร็จ แต่ publish event ไม่สำเร็จ | ข้อมูลไม่ตรงกันแบบเงียบ ค้นพบอีกหลายวันถัดมาโดยฝ่ายบัญชี | outbox pattern, reconciliation, replay ที่ idempotent |
| Cascading failure | การล้มของตัวหนึ่งทำให้ตัวถัดไปโหลดเกิน ต่อกันไปเรื่อยๆ | trigger เล็ก แต่ล่มทั้งระบบ; กู้ยากเพราะโหลดพุ่งตอน restart | timeout, circuit breaker, bulkhead, load shedding |
| Correlated failure | replica "อิสระ" ทั้ง 3 ตัวใช้ rack เดียวกัน, AZ เดียวกัน, config push เดียวกัน, หรือ certificate ใบเดียวกัน | เลข redundancy ของคุณเป็นนิยาย | map failure domain ออกมาให้ชัด; ทยอย rollout config และ certificate |
| Metastable failure | ระบบยังพังอยู่หลังจาก trigger หายไปแล้ว — retry และ cache ที่เย็นทำให้มันอิ่มตัวต่อไป | การกำจัดสาเหตุไม่ทำให้ service กลับมา — outage ยาวหลายชั่วโมง | load shedding, retry budget, แผน warm cache, ความสามารถปิด traffic แล้วค่อยๆ เปิด |
| Poison / data-triggered | record เสียใบเดียวหรือ config เสียชุดเดียวทำให้ consumer ทุกตัวที่อ่านมัน crash | correlated 100% ข้ามทุก replica — redundancy ไม่ช่วยอะไรเลย | validate ที่ขอบเขต, DLQ, canary config, kill switch |
| Split brain | node 2 ตัวเชื่อว่าตัวเองเป็น leader | write แยกทางกัน; reconcile ทีหลังยากจริงๆ | quorum/consensus, fencing token, ห้ามมี "แค่ promote มันเลย" แบบ manual |
| Human / procedural | คำสั่งผิด, environment ผิด, migration ที่ไม่ผ่าน review | ในเชิงสถิติเป็นสาเหตุอันดับต้นของ incident ใหญ่ | least privilege, four-eyes กับงาน destructive, โหมด dry-run, ทำ automation แทนคำสั่งเป็นเอกสาร |
Postmortem สาธารณะ 4 ฉบับที่คุ้มค่า 1 ชั่วโมงของคุณ
GitHub, ตุลาคม 2018 — network partition 43 วินาทีระหว่าง data center 2 แห่ง ทำให้ MySQL failover อัตโนมัติ หลังจากนั้น 2 ฝั่งรับ write ที่แยกทางกัน การซ่อม consistency ใช้เวลากว่า 24 ชั่วโมงในสภาพ degraded บทเรียน: automated failover ข้าม WAN เปลี่ยนสะดุด 43 วินาทีให้เป็น incident เรื่องความถูกต้องของข้อมูลนาน 1 วัน — กลไก failover เองก็เป็นความเสี่ยงที่ต้องออกแบบ (post-incident analysis)
Cloudflare, กรกฎาคม 2019 — WAF rule ที่มี regular expression ซึ่ง backtrack อย่างหายนะถูก deploy ทั่วโลกและกิน CPU ของ edge fleet ทั้งหมด บทเรียน: config และ rule data คือโค้ด ต้อง canary, ต้องมี resource limit, และต้อง revert ได้ทันที (details of the outage)
AWS Kinesis, พฤศจิกายน 2020 — การเพิ่ม capacity ให้ front-end fleet ทำให้จำนวน OS thread เกิน limit ที่ตั้งไว้ ซึ่งทำลาย membership map ภายในของ fleet บทเรียน: การ scale up เองก็เป็น trigger ได้ — รู้ limit ของคุณ (thread count, file descriptor, connection cap, ช่วงของ ID, port exhaustion) และ alarm ตอนที่กำลังใกล้ ไม่ใช่ตอนที่ชนแล้ว (service event summary)
Roblox, ตุลาคม 2021 — outage 73 ชั่วโมง ที่ฟีเจอร์ซึ่งเพิ่งเปิดในชั้น service discovery สร้างรูปแบบโหลดที่ระบบกู้คืนไม่ได้แม้จะเอา traffic ออกไปแล้ว — metastable failure ตามตำรา บทเรียน: การกู้คืนเป็นปัญหา design แยกจาก availability ให้ถามว่า "เราจะกลับขึ้นมาได้อย่างไร" และซ้อมมัน (Roblox return to service)
อ่าน postmortem เป็นนิสัย ไม่ใช่เป็นความบันเทิง — ทุกฉบับให้ถามว่า กลไกนี้เกิดขึ้นในระบบของผมได้ไหม และอะไรจะบอกผม แล้วเขียน alert ที่ขาดไป
Resilience pattern พร้อมค่าที่สำคัญ
1. Timeout — รากฐานที่ทุกอย่างวางอยู่บน
- ทุกการเรียกผ่าน network ต้องมี timeout ที่ระบุชัด default ของ library มักเป็น "ไม่จำกัด" ซึ่งแปลว่า dependency ที่ช้า 1 ตัว กิน thread ของคุณได้ทั้งหมด (Little's Law อีกครั้ง)
- ตั้ง timeout จาก p99.9 ของ dependency ตอนสุขภาพดี บวก margin ไม่ใช่จากเลขกลมๆ ที่คุณชอบ — ถ้า p99 ของมันคือ 80 ms timeout 30 วินาทีไม่ใช่ความใจกว้าง มันคือ resource leak
- จัดงบ timeout ลดหลั่นตามสาย ถ้า client รอ 2 s, gateway ควรรอ 1.8 s, service 1.5 s, database 1 s และ propagate งบที่เหลือ (deadline) ไปใน header เพื่อไม่ให้มี hop ไหนทำงานกับ request ที่ไม่มีใครรอคำตอบแล้ว
- แยก connect timeout (สั้น ~200 ms — TCP handshake สำเร็จหรือ host หายไปเลย) ออกจาก read timeout
Deadline propagation — ส่งงบที่เหลือไปด้วย
Client timeout 2000 ms -> header: X-Request-Deadline: <ts + 2000ms>
Gateway เหลือ 1850 ms -> ตั้ง timeout ของตัวเอง = min(1800, เหลือ)
Service เหลือ 1600 ms -> ตั้ง DB query timeout = min(1000, เหลือ - 200)
DB ถ้าเหลือ < 50 ms -> ไม่ต้องเรียกเลย ตอบ deadline_exceeded ทันที
--> ประหยัดงานที่ไม่มีใครรอ ซึ่งคือหัวใจของการกู้จาก overload
2. Retry — จำเป็น และเป็นวิธีที่คนทำให้ระบบล่มบ่อยที่สุด
ผิด retry ทันที 3 ครั้ง
-> โหลด 4 เท่าบน service ที่กำลังลำบาก ในวินาทีเดียวกัน
ดีขึ้น exponential backoff: 100ms, 200ms, 400ms
-> ยัง synchronized กันอยู่ client ทุกตัวที่ล้มพร้อมกัน
จะ retry พร้อมกัน เกิดเป็นคลื่น
ถูก exponential backoff + FULL JITTER
sleep = random_between(0, min(cap, base * 2^attempt))
-> กระจาย retry อย่างเรียบ มีข้อมูลพิสูจน์ว่าชนกันน้อยลง
และเสร็จเร็วกว่า exponential backoff เปล่าๆ
กฎที่สำคัญกว่าสูตร:
| กฎ | เหตุผล |
|---|---|
| Retry เฉพาะ operation ที่ idempotent หรือที่พา idempotency key ไปด้วย | ห้าม retry POST ที่ไม่ idempotent แบบมืดๆ เด็ดขาด |
Retry เฉพาะ error ที่ retryable — 429, 503, timeout, connection reset | ห้าม retry 400/401/403/422 — คำตอบไม่เปลี่ยน |
| Retry ที่ชั้นเดียว | ถ้า SDK + gateway + client retry 3 ครั้งทั้งหมด request เดียวของผู้ใช้กลายเป็น 27 การเรียก backend |
| ใช้ retry budget — จำกัด retry ที่ ~10% ของ request ทั้งหมด | เมื่อ dependency ล้มเป็นวงกว้าง การ retry มากขึ้นทำให้กู้คืนช้าลง — กฎข้อเดียวนี้ป้องกัน outage จาก retry storm |
ส่ง Retry-After เมื่อคุณเป็นคนปฏิเสธ | ให้ client รู้ว่าควรรอนานเท่าไหร่ ไม่ต้องเดา |
Retry budget คือกฎที่คนข้ามบ่อยที่สุดและมีค่าที่สุด
ถ้า dependency ล้ม 50% การ retry ทำให้โหลดที่มันได้รับเพิ่มขึ้น ตอนที่มันต้องการโหลดน้อยลงที่สุด ตั้งเพดานว่า retry รวมกันต้องไม่เกิน 10% ของ request ปกติ เกินจากนั้นให้ล้มทันทีโดยไม่ retry
อ้างอิงหลัก: Timeouts, retries, and backoff with jitter (Amazon Builders' Library) และ Exponential Backoff And Jitter — ฉบับหลังมีข้อมูล simulation ที่แสดงว่าทำไม full jitter ชนะ
3. Circuit breaker — หยุดเรียกของที่ล่ม
ตั้งค่าด้วยตัวเลขจริง ไม่ใช่ default:
| พารามิเตอร์ | ค่าที่ควรตั้ง | เหตุผล |
|---|---|---|
| ปริมาณ request ขั้นต่ำก่อนที่จะ trip ได้ | เช่น 20 request ใน 10 s | ไม่อย่างนั้น error 2 ใบตอน 03:00 จาก health probe เปิด circuit ได้ |
| Failure threshold | เช่น > 50% error ใน 20 request / 10 s | ต้องเป็นสัดส่วน ไม่ใช่จำนวนดิบ |
| Open duration | 5–30 s เป็นปกติ | นานเกินไป = degrade นานเกินจำเป็น |
| Half-open trial concurrency | 1–5 request ไม่ใช่ทั้ง fleet | ไม่อย่างนั้น half-open กลายเป็น thundering herd |
สิ่งที่สำคัญที่สุดและขาดบ่อยที่สุด
ตอน circuit เปิดอยู่ คุณจะ *ทำอะไร*
- serve ข้อมูลเก่าจาก cache (ดีที่สุด ถ้าโดเมนอนุญาต)
- คืนค่า default ที่ degrade ("ดึง limit ไม่ได้ ใช้ base limit")
- เข้าคิวไว้ทำทีหลัง (เฉพาะเมื่อ caller รอได้)
- ล้มเร็วด้วยข้อความที่ชัดเจนและซื่อสัตย์
Breaker ที่ไม่มี fallback ที่นิยามไว้ แค่เปลี่ยน error ที่ช้าให้เป็น error ที่เร็ว ซึ่งยังเป็นชัยชนะด้าน stability — แต่ต้องพูดออกมาให้ชัด เพราะประสบการณ์ของลูกค้าเหมือนกับตอนระบบล่มเป๊ะ
4. Bulkhead — แยกกันไว้เพื่อให้การล้มจุดเดียวไม่จมเรือ
- แยก connection pool และ thread pool ต่อ dependency — partner API ที่ค้างจะกิน thread ที่เส้นทาง login ต้องใช้ไม่ได้
- แยก compute ต่อชั้นของ tenant — traffic batch ของ partner รายใหญ่ที่สุด ต้องไม่อยู่ปนกับ traffic แบบ interactive ของผู้บริโภค
- แยก critical path ออกจาก nice-to-have path ในทางกายภาพ — การส่งคำสั่งจ่ายเงินกับ marketing analytics ไม่ควรใช้ database, queue หรือ autoscaling group เดียวกัน
Real case — pods ของ Shopify (cell-based architecture)
Shopify แบ่ง platform เป็น "pod" — แต่ละ pod เป็น stack ที่ครบและแยกจากกัน มีฐานข้อมูลของตัวเอง serve ร้านค้าชุดหนึ่ง การล้มใน pod หนึ่งกระทบเฉพาะร้านใน pod นั้น และมันให้หน่วยที่สะอาดสำหรับ capacity planning, การวางตาม region, และการย้ายร้านใหญ่ไปใช้ capacity เฉพาะก่อนวัน flash sale
ทำไมสำคัญกับ fintech: cell ให้ blast radius ที่มีขอบเขต และเป็นคำตอบตามธรรมชาติสำหรับคำถามของ regulator เรื่อง isolation บวกกับโครงสร้าง deployment ring ที่ปลอดภัย (deploy ที่ cell 1, ดูผล, ไปต่อ)
ต้นทุนก็จริง: operation ข้าม cell กลายเป็นเรื่องยาก และต้องมี routing กับ shard-map service — รับมาเมื่อตัวขับคือ blast radius ไม่ใช่ throughput (A pods architecture to allow Shopify to scale)
5. Hedged request — เครื่องมือสำหรับ tail latency
ถ้า p99 แย่กว่า p50 มากเพราะมี node ช้าเป็นครั้งคราว: ส่ง request ไป แล้วถ้าถึงจุด p95 ยังไม่ได้คำตอบ ให้ส่งสำเนาที่ 2 ไปที่ replica อื่น แล้วเอาคำตอบที่มาถึงก่อน
ต้นทุนคือโหลดเพิ่มไม่กี่เปอร์เซ็นต์ แลกกับ p99 ที่ดีขึ้นมาก
เงื่อนไขของ hedged request
- operation ต้อง idempotent และ read-only
- ต้องยกเลิกตัวที่แพ้ ไม่อย่างนั้นคุณเพิ่มโหลดเป็น 2 เท่าถาวร
ห้ามใช้กับ write ที่เคลื่อนย้ายเงิน — นั่นคือการสร้างรายการซ้ำ เทคนิคนี้เป็นหนึ่งในแกนของ The Tail at Scale
6. Graceful degradation — ต้องมีขั้นตอน design ของตัวเอง
สำหรับทุก dependency เขียนลงไปว่า product ทำอะไรเมื่อมันใช้ไม่ได้ ใส่ในตารางและให้ product เซ็นรับ
| Dependency ล่ม | พฤติกรรมที่ degrade | ใครตัดสิน |
|---|---|---|
| Recommendation service | แสดง list ยอดนิยมแบบ static | Product — ไม่ต้องขออนุมัติ |
| Notification service | ธุรกรรมยังสำเร็จ; notification เข้าคิวและส่งช้า | Product |
| Fraud scoring service | การตัดสินใจเชิงธุรกิจ: บล็อกทุกธุรกรรม (fail closed) หรืออนุญาตต่ำกว่า threshold แล้วรีวิวย้อนหลัง (fail open พร้อม limit) | Risk / Compliance — ต้องเป็นการตัดสินใจของคนที่มีอำนาจ และต้องมีเอกสาร |
| Ledger database | ปฏิเสธ transfer ใหม่ด้วยข้อความที่ชัดเจน — ห้ามรับการเคลื่อนย้ายเงินที่บันทึกไม่ได้ | กฎวิศวกรรมที่ไม่มีการต่อรอง |
Goodput collapse — และวิธีแก้
เมื่อโหลดมาถึงเร็วกว่าที่ serve ได้และไม่มี overload policy: request เข้าคิว → latency โต → client timeout แล้ว retry → คิวโตขึ้นอีก ระบบจึงใช้ capacity เต็มไปกับ response ที่ client เลิกรอแล้ว ขณะที่ goodput — งานที่มีประโยชน์ต่อวินาที — ลดลง
ด้านขวาของ peak ระบบยุ่งขึ้นแต่มีประโยชน์น้อยลง นี่คือ collapse ไม่ใช่ saturation และเป็นรูปแบบที่พบได้บ่อยใน outage จาก overload
เครื่องมือ 4 อย่าง ใช้พร้อมกัน:
Bounded queue ทุกที่
Unbounded queue คือระเบิดเวลาของ latency
มันเปลี่ยนภาวะ overload ให้เป็นความช้า, ซ่อนปัญหา, และทำลาย goodput ใส่ขอบเขตให้ทุก queue, thread pool และ buffer เมื่อเต็มแล้วให้ปฏิเสธเร็ว
Load shedding
เหนือ threshold ของ concurrency ให้คืน 503 พร้อม Retry-After ทันที
การปฏิเสธ 20% ของ traffic ใน 1 ms ทำให้ 80% ยังสุขภาพดี; การรับ 100% แบบแย่ๆ ไม่ทำให้ใครได้ประโยชน์เลย
Shed ตามลำดับความสำคัญ:
ลำดับการทิ้ง (ทิ้งจากบนลงล่าง)
1. batch และ analytics
2. request ที่ไม่ authenticated
3. request แบบ read ที่ไม่ critical
4. read ที่ critical
X. ห้ามทิ้ง: health check, control plane ของ operator,
และ (สำหรับ fintech) การอ่านสถานะธุรกรรมที่ค้างอยู่
หมายเหตุ: adaptive concurrency limit (วัด latency แล้วปรับเพดานอัตโนมัติ)
มักดีกว่าตัวเลขที่ตั้งมือ เพราะตัวเลขที่ตั้งมือจะล้าสมัยทันทีที่
โปรไฟล์ของ dependency เปลี่ยน — แต่ต้องมี metric ยืนยันว่ามันปรับถูกจริง
Drop expired work
ก่อนประมวลผลงานในคิว ให้เช็ค deadline ของมัน ถ้า client เลิกรอไปแล้ว 10 วินาที ให้ทิ้ง — ข้อเดียวนี้กู้ระบบที่กำลังพังทลายได้
Fairness / quota ต่อ tenant
script ที่หลุดของ partner รายเดียวต้องไม่กิน capacity ที่ใช้ร่วมกัน Rate limit ต่อ API key และต่อ tenant ไม่ใช่แค่ระดับ global — และให้แต่ละ tenant มีพื้น (floor)
อ้างอิงที่ควรอ่านก่อน capacity review ครั้งถัดไป
- Using load shedding to avoid overload — Amazon Builders' Library คำอธิบาย goodput collapse ที่ชัดที่สุด
- Addressing Cascading Failures — Google SRE Book บทที่ 22 มีหัวข้อสำคัญเรื่องวิธี กู้คืน จาก cascading failure (คำใบ้: คุณอาจต้องลด traffic ลงเป็นศูนย์แล้วค่อยๆ เพิ่ม)
- Handling Overload — Google SRE Book บทที่ 21 เรื่อง criticality level, client-side throttling และ retry budget
- Performance Under Load — Netflix adaptive concurrency limit ที่ derive มาจาก queueing theory และรันบน production
Checklist resilience
- ทุกการเรียก network มี timeout ที่ตั้งจาก p99.9 ของ dependency
- Deadline propagate ไปทุก hop และ hop ที่เหลือเวลาน้อยเกินจะตอบทันที
- Retry มี full jitter, retry เฉพาะ error ที่ retryable, retry ชั้นเดียว
- มี retry budget ~10%
- Circuit breaker มีปริมาณขั้นต่ำก่อน trip และ มี fallback ที่นิยามไว้ชัด
- Connection/thread pool แยกต่อ dependency
- Critical path ไม่แชร์ database/queue/ASG กับงาน analytics
- ทุก queue, pool, buffer มีขอบเขต
- มี load shedding ที่ shed ตามลำดับความสำคัญ และไม่ shed health check
- Worker ตรวจ deadline ก่อนทำงาน และทิ้งงานที่หมดอายุ
- Rate limit ต่อ tenant ไม่ใช่แค่ global
- มี ตาราง degradation ที่ product/risk เซ็นรับแล้ว
- มีคำตอบว่า "เราจะกลับขึ้นมาได้อย่างไร" และเคยซ้อมแล้ว
สรุปบทนี้
Gray failure แย่กว่า crash เพราะ health check ผ่าน · metastable failure ไม่หายแม้ trigger จะหายไป — ต้องปิด traffic แล้วค่อยเปิด · timeout จาก p99.9 ไม่ใช่เลขกลม · retry ต้องมี jitter, ชั้นเดียว และมี budget · circuit breaker ต้องมี fallback ไม่ใช่แค่ fail เร็วขึ้น · unbounded queue ทำลาย goodput · และการปฏิเสธงานต้องเป็นสิ่งที่ ออกแบบไว้