บทที่ 6 · Part 2 — Building Blocks
Edge & Compute
DNS, CDN, load balancer L4/L7, API gateway และ service tier ที่ stateless — พร้อม failure mode ของแต่ละตัว
กล่องเครื่องมือของ system design ทั้งหมดมีประมาณ 15 ชิ้น ทุกระบบขนาดใหญ่คือการจัดเรียง 15 ชิ้นนี้ในรูปแบบเฉพาะ เรียนแต่ละชิ้นตาม 3 คำถามแล้วคุณออกแบบอะไรก็ได้: แก้ปัญหาอะไร, ต้นทุนเท่าไหร่, มันล้มแบบไหน และระบบหน้าตาเป็นอย่างไรตอนที่มันกำลังล้ม
จบบทนี้คุณจะ
- รู้ว่าทำไม DNS เป็น failover mechanism ที่แย่
- แยก L4 กับ L7 load balancer ได้ และรู้ว่า health check ที่ผิดทำให้ทั้ง fleet ตายได้อย่างไร
- รู้ว่าอะไรควรอยู่ใน API gateway และอะไรห้ามอยู่
- audit โค้ดตัวเองหาสิ่งที่ทำให้ service กลายเป็น stateful แบบเงียบๆ ได้
- เลือกหน่วย deployment (monolith / few services / microservices / serverless) อย่างซื่อสัตย์
[!CAUTION] ทุก component ที่เพิ่มคือหนี้สิน component ใหม่ 1 ตัวเพิ่ม failure mode 1 อย่าง, deployment 1 ชุด, ช่องว่างของ monitoring, เส้นทาง upgrade, พื้นที่โจมตี, license, และ pager ของใครคนหนึ่ง
ค่า default ที่ถูกคือไม่เพิ่ม เกณฑ์ที่ต้องผ่าน: "requirement ข้อไหนที่เขียนไว้ในขั้นที่ 1 ที่ component นี้ตอบ — และอะไรคือสิ่งที่ง่ายที่สุดที่ตอบข้อนั้นได้เหมือนกัน"
modular monolith + Postgres + cache 1 ตัว รองรับธุรกิจจริงได้มากกว่าที่ conference talk ทำให้คุณเชื่อ
DNS — ของที่คุณลืมจนกระทั่งมันทำให้ระบบล่ม
DNS map ชื่อไปเป็น address ปัญหาคือ client และ OS cache อย่างดุเดือด และมัก ignore TTL ของคุณ เพราะฉะนั้น DNS เป็น failover mechanism ที่แย่ — ให้คาดหวังว่าจะมี client หางยาวที่ยังใช้ address เดิมอีกหลายนาทีถึงหลายชั่วโมง
| ฟีเจอร์ที่มีประโยชน์ | ใช้ทำอะไร |
|---|---|
| Health-checked failover record | ตัด endpoint ที่ตายออกโดยอัตโนมัติ (ยังติดข้อจำกัดเรื่อง cache) |
| Weighted routing | ย้ายทีละน้อย 5% → 25% → 100% |
| Latency / geo routing | ส่งผู้ใช้ไทยไปสิงคโปร์ ไม่ใช่ไอร์แลนด์ |
Failure mode: zone/registrar config ผิด (ล่มทั้งระบบ และแก้เร็วไม่ได้เพราะ cache), โดเมนหมดอายุ, DNS provider เจ้าเดียวล่ม, DNSSEC config ผิด
กฎที่ใช้ได้จริง
- ลด TTL เป็น 60 s ก่อน migration ที่วางแผนไว้ ไม่ใช่ตอนเกิด incident
- ใช้ DNS provider 2 เจ้าสำหรับอะไรที่ critical
- ใส่วันหมดอายุโดเมนไว้ในปฏิทิน โดยมีเจ้าของ 2 คน
CDN — ย้าย byte ให้ใกล้ผู้ใช้ และซับพีค
CDN cache static content ที่ edge แต่ประโยชน์ที่คนมักมองข้ามคือ มัน terminate TLS ใกล้ผู้ใช้ ซึ่งตัด round trip ของ handshake ไป 1–2 ครั้ง บน mobile link ที่ช้า — บ่อยครั้งได้ประโยชน์มากกว่าตัว cache เอง
ความถูกต้องของ cache ทั้งหมดอยู่ที่ cache key — Set-Cookie 1 ตัว,
Vary: User-Agent ที่ไม่จำเป็น, หรือ query string สุ่มอย่าง ?utm_source=...
ทำให้ hit rate เหลือใกล้ศูนย์ได้ normalize key อย่างตั้งใจ
cache key ที่แย่:
https://api.example.com/v1/promotions?utm_source=fb&utm_campaign=aug&t=1723
--> ทุกคนได้ key ไม่ซ้ำกัน hit rate ~0%
cache key ที่ดี (normalize แล้ว):
path + query ที่ whitelist ไว้ (region, lang) + Accept-Encoding
--> ตัด utm_*, t, fbclid ออกที่ edge ก่อนคิด key
ใช้งานสมัยนี้ไม่ได้จำกัดที่รูป: cache response ของ API ที่เป็น public read-only, rate limiting ที่ edge, bot management, ซับ DDoS, และ edge compute สำหรับ redirect กับการ assign A/B group
Failure mode: content เก่าค้างหลัง deploy (แก้ด้วย filename ที่มี content hash แล้ว cache แบบ immutable — ห้ามแก้ด้วยการ "purge ทั้ง cache"), cache poisoning ผ่าน header ที่ไม่ validate, และ origin โดนถล่มทันทีที่ edge cache ถูก purge
"Purge ทั้ง cache" คือการยิงเท้าตัวเอง
การ purge cache ทั้งหมดตอนพีคทำให้ traffic 100% ตกลง origin พร้อมกัน ซึ่งคือ thundering herd ที่คุณสร้างขึ้นเอง ทางที่ถูกคือ immutable asset + content hash (ไม่ต้อง purge เลย) หรือถ้าจำเป็นต้อง purge ให้ purge เป็น path ที่แคบที่สุด
Load balancer — L4 กับ L7
| L4 (TCP/UDP) | L7 (HTTP) | |
|---|---|---|
| เห็นอะไร | IP + port เท่านั้น | path, header, method, body |
| ทำอะไรได้ | throughput สูงมาก, latency ต่ำ, ไม่ผูกกับ protocol | path routing, canary ตาม header, retry, sticky session, TLS termination, rate limit ต่อ route |
| ใช้กับ | ฐานข้อมูล, gRPC pass-through, protocol ที่ไม่ใช่ HTTP, throughput สุดขั้ว | traffic ของ application แทบทั้งหมด |
| Algorithm | round robin, least connections, consistent hash ตาม source | เหมือนกัน + least outstanding requests (จุดเริ่มที่ดีเมื่อเวลาประมวลผลต่อ request ไม่เท่ากัน) และ power-of-two-choices |
Health check คือทั้งเกม
- Shallow check ที่ตื้นเกินไป (
GET /pingตอบ 200 ขณะที่ connection pool หมด) = ส่ง traffic เข้า node ที่พังต่อไป - Deep check ที่ลึกเกินไป (ตรวจ DB ทุกครั้ง) = ตอน DB กระตุก 1 วินาที fleet ทั้งหมดถูกตัดออกพร้อมกัน ซึ่งเปลี่ยน blip ให้เป็น outage
- ทางประนีประนอมที่ใช้กันจริง: shallow สำหรับ routing, deep สำหรับ alert, และมีสัญญาณ readiness แยกที่ node ใช้ถอนตัวเองออกได้
Deep health check คือ correlated failure ที่คุณสร้างเอง
ถ้า node ทุกตัวตรวจ DB เดียวกัน แล้วตัดตัวเองออกเมื่อ DB ตอบช้า DB ที่กระตุก 2 วินาทีจะทำให้ระบบล่ม 100% แทนที่จะช้าลง — นี่คือ failure mode ที่เกิดจริงบ่อยมาก
gRPC / HTTP2 gotcha
Connection แบบ long-lived ที่ multiplex หลาย stream ทำให้ L4 balancer ปักหมุด client ไว้กับ backend ตัวเดียวตลอดไป backend ตัวใหม่ที่เพิ่งขึ้นมาจะไม่ได้ traffic เลย
ทางแก้: ใช้ L7-aware balancing, client-side load balancing,
หรือ recycle connection เป็นระยะ (max_connection_age)
Sticky session คือกลิ่นไม่ดี
มันทำให้ service ของคุณ เป็น stateful โดยพฤตินัย, ทำให้โหลดกระจายไม่เท่ากัน, และเปลี่ยนการตายของ node 1 ตัวให้เป็น error ที่ผู้ใช้เห็น ทางที่ดีกว่า: shared session store หรือ signed token
API gateway — ประตูเดียว นโยบายเดียว
| ควรอยู่ใน gateway | ห้ามอยู่ใน gateway |
|---|---|
| TLS termination | business logic |
| Authentication | การแปลงข้อมูลระหว่าง service |
| Coarse authorization | อะไรที่ต้อง deploy ต่อ feature |
| Rate limiting | การรวม response จากหลาย service ตาม use case |
| Request ID injection | |
| Request size limit | |
| Schema validation | |
| WAF rule | |
| Audit logging |
หลักคือ ใส่เฉพาะสิ่งที่ต้องเหมือนกันสำหรับทุกคน gateway ที่มี business rule จะกลายเป็นคอขวดร่วมที่ทุกทีมต้องเข้าคิวเพื่อแก้
Real case — เส้นทางของ gateway ที่ Netflix
Zuul ของ Netflix ตั้งอยู่หน้า instance หลายพันตัว มีบทเรียน 2 ข้อที่เผยแพร่ออกมา: (1) พวกเขาย้ายจาก gateway แบบ blocking ไปเป็น non-blocking (บน Netty) เพราะ thread-per-connection รับจำนวน connection ที่ idle และ long-lived ไม่ไหว (2) พวกเขาสร้าง adaptive concurrency limit ไว้ใน gateway ให้มัน shed load อัตโนมัติ แทนที่จะเข้าคิวไปเรื่อยๆ จนทุกอย่าง timeout
บทเรียนที่เอาไปใช้ได้: งานที่ยากที่สุดของ gateway ไม่ใช่การ route แต่คือการทำตัวให้ถูกต้องเมื่อของที่อยู่ข้างหลังมันช้า — Zuul 2 · Performance Under Load
Compute — stateless คือกุญแจของทุกอย่าง
"Stateless" หมายถึง instance ตัวไหนก็ serve request ไหนก็ได้ เพราะ state ทั้งหมดอยู่ใน datastore, cache หรือใน token เมื่อข้อนี้เป็นจริงคุณจะได้ horizontal scaling, rolling deploy ที่ง่าย, blue/green, autoscaling และการตายของ node ที่ผู้ใช้ไม่รู้สึก — ทุกอย่างในบทที่ 11–13 พึ่งข้อนี้
สิ่งที่ทำให้ service เป็น stateful แบบเงียบๆ — audit โค้ดหาของพวกนี้
| อาการ | ทำไมมันพัง |
|---|---|
| Session หรือ rate-limit counter เก็บใน memory | ถูกบน 1 node ผิดบน 10 node |
| เขียนไฟล์ลง local disk แล้ว request อื่นคาดว่าจะอ่านได้ | request ถัดไปไปลง node อื่น |
| Scheduled job รันอยู่ในทุก replica | ตอนนี้มันรัน N ครั้ง — bug คลาสสิกของการตัดเงินซ้ำ |
| In-process cache ที่ไม่มีเพดานความเก่า | ผู้ใช้ 2 คนได้คำตอบต่างกันตาม routing |
| WebSocket connection ที่อยู่นาน | stateful จริงๆ — แก้ด้วย connection registry + pub/sub fan-out (บทที่ 19) |
Scheduled job ในทุก replica = การตัดเงินซ้ำ
ถ้ามี cron อยู่ใน pod ทั้ง 6 ตัว งาน "ตัดค่าบริการรายเดือน" จะรัน 6 ครั้ง ถ้างานนั้นไม่ idempotent ลูกค้าถูกเรียกเก็บ 6 ครั้ง ต้องมี leader election หรือ distributed lock และงานต้อง idempotent อยู่แล้ว — รายละเอียดอยู่ในบทที่ 9
เลือกหน่วย deployment อย่างซื่อสัตย์
| ทางเลือก | เลือกเมื่อ | ต้นทุนจริง |
|---|---|---|
| Modular monolith | engineer < ~30 คน, product เดียว, ความถูกต้องสำคัญกว่าการ deploy แยก — นี่คือ default ที่ถูก | ต้องมีวินัยรักษาขอบเขต module; scale ได้แบบทั้งก้อนหรือไม่ scale เลย |
| แยกไม่กี่ service ตามรอยต่อของ domain | profile การ scale ต่างกัน หรือโซน compliance ต่างกัน (เช่นแยกขอบเขตข้อมูลบัตรออกจากส่วนอื่น) | distributed transaction กลายเป็น saga; debug ต้องมี tracing |
| Microservices จำนวนมาก | มีหลายทีมที่ต้องปล่อยของด้วยจังหวะของตัวเอง และคุณมี platform, CI/CD, tracing, วุฒิภาวะ on-call อยู่แล้ว | ภาษี operational มหาศาล คุณเป็นเจ้าของ distributed system ไปแล้วไม่ว่าอยากหรือไม่ |
| Serverless function | workload พีคเป็นก้อน, event-driven, baseline ต่ำ; glue code; scheduled job | cold start, เพดานเวลาทำงาน, connection pool กับ relational DB หมด, ทดสอบ local ยาก, ผูกกับ vendor |
Real case — Segment ย้ายกลับไปเป็น monolith
Segment แตกระบบเป็น ~140 microservice 1 ตัวต่อ 1 destination integration ต้นทุน operational ระเบิด: upgrade shared library ข้าม 140 repo, queue 140 ชุดที่ต้อง tune, และ default ที่ต้องปรับต่อ service พวกเขารวมกลับเป็น service เดียว (ชื่อ Centrifuge) และเขียนเล่าไว้ใน Goodbye Microservices
บทเรียนไม่ใช่ "microservices ไม่ดี" แต่คือ หน่วยของการแยกควรเป็นขอบเขตของทีมและขอบเขตของการ scale ไม่ใช่ความชอบในการจัดระเบียบโค้ด ถ้า 2 service deploy พร้อมกันตลอด มันคือ service เดียวที่มี network call เกินมา
คำถาม 3 ข้อก่อนแยก service
- 2 ส่วนนี้ deploy แยกกันจริงไหม — ถ้า deploy พร้อมกันตลอด อย่าแยก
- 2 ส่วนนี้ scale ด้วย profile ต่างกันจริงไหม — ถ้าโหลดขึ้นลงพร้อมกัน อย่าแยก
- มีข้อมูลที่ต้อง transaction ร่วมกันไหม — ถ้ามีและแยก คุณกำลังแลก
BEGIN...COMMITเป็น saga พร้อม compensation (บทที่ 16) ซึ่งเป็นราคาที่แพงมากสำหรับระบบเงิน
ขอบเขต compliance เป็นเหตุผลที่ดีในการแยก
ถ้าส่วนหนึ่งของระบบแตะข้อมูลบัตร (PCI-DSS scope) การแยกมันออกเป็น service และ network zone ของตัวเอง ลดขอบเขตการตรวจ audit ลงอย่างมาก นี่เป็นเหตุผลเชิงธุรกิจที่ชัดเจน ไม่ใช่แค่ความชอบทางสถาปัตยกรรม — แต่ขอบเขตที่ว่าต้องยืนยันกับทีม compliance ไม่ใช่กำหนดเอง
Lab 6 — ทำให้ edge ของคุณพัง
ใช้ stack ในเครื่อง (API service + Postgres + Redis + broker ผ่าน Docker Compose)
ยิงโหลดด้วย k6, vegeta หรือ oha สำคัญ: เขียนคำทำนายไว้ก่อนวัดทุกครั้ง
ช่องว่างระหว่างคำทำนายกับผลวัดคือสิ่งที่คุณได้เรียน
| การทดลอง | สิ่งที่ต้องพิสูจน์ |
|---|---|
| Health check โกหก | ทำให้ /health ตอบ 200 ขณะที่ DB ต่อไม่ได้ แสดงว่า traffic ยังถูกส่งเข้า node ที่ไร้ประโยชน์ แล้วแยก liveness ออกจาก readiness แล้วรันซ้ำ |
| Deep check ฆ่าทั้ง fleet | ทำให้ทุก node ตรวจ DB ใน health check แล้วทำให้ DB ช้า 3 วินาที ดูว่า node ถูกตัดออกหมดพร้อมกันไหม |
| ช้า ไม่ใช่ตาย | เพิ่ม latency 2 วินาทีให้ DB (ใช้ toxiproxy หรือ pg_sleep) ดู connection pool และ thread pool ตัน — พิสูจน์ว่า dependency ที่ช้าแย่กว่าที่ตาย แล้วใส่ timeout ที่สั้นกว่า timeout ของ client แล้วรันซ้ำ |
| CDN cache key | เติม ?utm_source= แบบสุ่มใน request ดู hit rate ตก แล้ว normalize key แล้ววัดใหม่ |
| Sticky session | เปิด sticky แล้ว kill node ดูว่าผู้ใช้เห็น error อะไร |
Deliverable: ตาราง 1 หน้า — การทดลอง / คำทำนาย / ผลวัด / สิ่งที่แก้ / ผลวัดหลังแก้ เอกสารนี้มีค่ากับทีมของคุณมากกว่า slide ทุกชุดที่คุณจะทำในปีนี้
สรุปบทนี้
DNS ไม่ใช่ failover mechanism · cache key คือทั้งเรื่องของ CDN · health check ที่ตื้นเกินไปส่ง traffic เข้า node ที่พัง ที่ลึกเกินไปฆ่าทั้ง fleet · gateway ใส่เฉพาะสิ่งที่ต้องเหมือนกันทุกคน · stateless คือกุญแจของ scaling ทั้งหมด · และ modular monolith เป็น default ที่ถูกจนกว่าจะมีเหตุผลเรื่องทีมหรือ compliance มาหักล้าง