บทที่ 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 ต่ำ, ไม่ผูกกับ protocolpath routing, canary ตาม header, retry, sticky session, TLS termination, rate limit ต่อ route
ใช้กับฐานข้อมูล, gRPC pass-through, protocol ที่ไม่ใช่ HTTP, throughput สุดขั้วtraffic ของ application แทบทั้งหมด
Algorithmround 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 terminationbusiness 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 monolithengineer < ~30 คน, product เดียว, ความถูกต้องสำคัญกว่าการ deploy แยก — นี่คือ default ที่ถูกต้องมีวินัยรักษาขอบเขต module; scale ได้แบบทั้งก้อนหรือไม่ scale เลย
แยกไม่กี่ service ตามรอยต่อของ domainprofile การ scale ต่างกัน หรือโซน compliance ต่างกัน (เช่นแยกขอบเขตข้อมูลบัตรออกจากส่วนอื่น)distributed transaction กลายเป็น saga; debug ต้องมี tracing
Microservices จำนวนมากมีหลายทีมที่ต้องปล่อยของด้วยจังหวะของตัวเอง และคุณมี platform, CI/CD, tracing, วุฒิภาวะ on-call อยู่แล้วภาษี operational มหาศาล คุณเป็นเจ้าของ distributed system ไปแล้วไม่ว่าอยากหรือไม่
Serverless functionworkload พีคเป็นก้อน, event-driven, baseline ต่ำ; glue code; scheduled jobcold 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

  1. 2 ส่วนนี้ deploy แยกกันจริงไหม — ถ้า deploy พร้อมกันตลอด อย่าแยก
  2. 2 ส่วนนี้ scale ด้วย profile ต่างกันจริงไหม — ถ้าโหลดขึ้นลงพร้อมกัน อย่าแยก
  3. มีข้อมูลที่ต้อง 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 มาหักล้าง