Skip to content
Tayakorn
devUpdated 2026-07-13Free

System Design Handbook

ออกแบบระบบให้รองรับโลกจริงตั้งแต่เซิร์ฟเวอร์เดียวถึง production — API, ฐานข้อมูล, caching, load balancing, auth, rate limiting และ message queue

#system-design#api#database#caching#load-balancing#security
Contents

ออกแบบระบบให้รองรับโลกจริง — ตั้งแต่เซิร์ฟเวอร์เดียวถึง Production · เอกสารสรุปเชิงลึกจากคอร์ส System Design ของ Hayk Simonyan — ครอบคลุม APIs, Databases, Caching, CDNs, Load Balancing, Authentication, Message Queues และ Production Infrastructure เรียบเรียงเป็นภาษาไทย พร้อมไดอะแกรมประกอบทุกหัวข้อ

  • ระดับ: Mid → Senior/Staff Engineer
  • เวลาอ่าน: ~75 นาที (9 บท)
  • ภาษา: ไทย พร้อมไดอะแกรม

พื้นฐานระบบ & Single Server Setup#

Foundations · Web Server · Database · Cache · CDN · ▶ ในคลิป 0:02:13 – 0:06:20

ทุกระบบใหญ่ในโลก ไม่ว่าจะเป็น Netflix หรือ Shopee ล้วนเริ่มจากจุดเดียวกัน: เซิร์ฟเวอร์หนึ่งเครื่องที่รันทุกอย่าง การเข้าใจว่า request หนึ่งครั้งเดินทางอย่างไรในระบบที่เล็กที่สุด คือรากฐานของการออกแบบระบบที่ซับซ้อนทั้งหมด เพราะเมื่อระบบโต เราไม่ได้ "เปลี่ยน" สถาปัตยกรรม — เราแค่ แยกส่วนประกอบเดิมออกจากกัน ทีละชิ้น

1.1 เส้นทางของ Request หนึ่งครั้ง

เมื่อผู้ใช้พิมพ์ www.myapp.com ในเบราว์เซอร์ สิ่งที่เกิดขึ้นตามลำดับคือ:

  1. DNS Lookup — เบราว์เซอร์ถาม DNS server ว่าโดเมนนี้ชี้ไปที่ IP อะไร (เช่น 203.0.113.10)
  2. HTTP Request — เบราว์เซอร์เปิด connection ไปที่ IP นั้น แล้วส่งคำขอ เช่น GET /home
  3. Server ประมวลผล — เว็บเซิร์ฟเวอร์รับคำขอ, รันโค้ดแอป, อ่าน/เขียนฐานข้อมูล
  4. Response กลับ — ส่ง HTML/JSON กลับไปให้เบราว์เซอร์แสดงผล
ผู้ใช้เบราว์เซอร์ / มือถือDNS Serverโดเมน → IP addressWeb Serverเครื่องเดียว รันทุกอย่างโค้ดแอป + ฐานข้อมูล203.0.113.10Database(ช่วงแรกอยู่ในเครื่องเดียวกัน)1. ถาม IP ของโดเมน2. ตอบ IP กลับ3. GET /home6. ส่ง HTML/JSON กลับ4. query ข้อมูล5. ผลลัพธ์
FIG 1.1 เส้นทาง request ในระบบ single server: DNS แปลงโดเมนเป็น IP จากนั้นเบราว์เซอร์คุยกับเซิร์ฟเวอร์ตรง ๆ

1.2 จุดแข็งและขีดจำกัดของ Single Server

ระบบเครื่องเดียวมีข้อดีที่มักถูกมองข้าม: deploy ง่าย, debug ง่าย, ไม่มี network ระหว่างส่วนประกอบ, ค่าใช้จ่ายต่ำ สำหรับ MVP หรือระบบภายในองค์กร นี่อาจเป็นคำตอบที่ถูกต้องที่สุดแล้ว แต่ขีดจำกัดจะมาถึงเมื่อ:

  • ทรัพยากรชนกัน — ฐานข้อมูลกับโค้ดแอปแย่ง CPU/RAM/Disk I/O กันเอง
  • สเกลแยกกันไม่ได้ — อยากเพิ่มกำลังเฉพาะฐานข้อมูล ก็ต้องอัปเกรดทั้งเครื่อง
  • Single Point of Failure (SPOF) — เครื่องเดียวล่ม = ระบบล่มทั้งหมด (เจาะลึกในบทที่ 3)

ก้าวแรกของการขยายระบบจึงเป็นการ แยกฐานข้อมูลออกไปอยู่เครื่องของตัวเอง — web tier กับ data tier โตอิสระจากกันได้ทันที

1.3 Cache — ความเร็วที่ซื้อด้วยหน่วยความจำ

การอ่านจากฐานข้อมูลคือต้นทุนที่แพงที่สุดของ request ส่วนใหญ่ Cache คือชั้นเก็บข้อมูลในหน่วยความจำ (in-memory เช่น Redis, Memcached) ที่ตอบได้เร็วกว่าฐานข้อมูลหลายสิบเท่า เพราะไม่ต้องอ่านดิสก์และไม่ต้องประมวลผล query

รูปแบบที่ใช้บ่อยที่สุดคือ Cache-Aside (Lazy Loading):

App Serverต้องการข้อมูล user:42Cache (Redis)in-memory · เร็วมากDatabaseบนดิสก์ · ช้ากว่า1. เช็ก cache ก่อนเสมอ2a. HIT → ตอบเลย ✓2b. MISS → อ่านจาก DB3. เขียนผลลัพธ์กลับเข้า cache(พร้อม TTL)
FIG 1.2 Cache-Aside: แอปเช็ก cache ก่อนเสมอ ถ้าไม่เจอ (miss) จึงอ่านจาก DB แล้วเก็บผลลัพธ์เข้า cache ไว้ตอบครั้งถัดไป

สิ่งที่ต้องตัดสินใจเสมอเมื่อใช้ cache:

  • TTL (Time-To-Live) — ข้อมูลใน cache อยู่ได้นานแค่ไหนก่อนหมดอายุ สั้นไป = cache แทบไม่ช่วย, นานไป = ผู้ใช้เห็นข้อมูลเก่า (stale data)
  • Cache Invalidation — เมื่อข้อมูลต้นทางเปลี่ยน จะลบ/อัปเดต cache อย่างไร นี่คือหนึ่งใน "ปัญหายากที่สุดในวงการ" เพราะพลาดแล้วผู้ใช้เห็นข้อมูลผิด
  • Eviction Policy — เมื่อหน่วยความจำเต็ม จะทิ้งตัวไหนก่อน ที่นิยมคือ LRU (Least Recently Used — ทิ้งตัวที่ไม่ถูกใช้นานที่สุด)

1.4 CDN — เอาเนื้อหาไปวางใกล้ผู้ใช้

CDN (Content Delivery Network) คือเครือข่ายเซิร์ฟเวอร์ที่กระจายอยู่ทั่วโลก (เรียกว่า edge server) ทำหน้าที่เก็บสำเนาของ static content — รูปภาพ, วิดีโอ, ไฟล์ JS/CSS — ไว้ใกล้ผู้ใช้ที่สุด ผู้ใช้ในกรุงเทพฯ โหลดรูปจาก edge ในสิงคโปร์ (~30ms) แทนที่จะวิ่งไปถึง origin server ที่อเมริกา (~250ms)

Origin ServerสหรัฐอเมริกาEdge: สิงคโปร์เก็บสำเนา static filesEdge: โตเกียวเก็บสำเนา static filesEdge: แฟรงก์เฟิร์ตเก็บสำเนา static filesผู้ใช้ กรุงเทพฯผู้ใช้ โอซาก้าผู้ใช้ ปารีส~30ms~25ms~20msดึงจาก origin เฉพาะครั้งแรก / cache หมดอายุ
FIG 1.3 CDN: ผู้ใช้แต่ละภูมิภาคโหลดไฟล์จาก edge ที่ใกล้ที่สุด edge จะวิ่งกลับ origin เฉพาะตอนยังไม่มีสำเนา

🧭 มุมมองการออกแบบ — Cache กับ CDN คือหลักการเดียวกันคนละสเกล — "เก็บสำเนาไว้ใกล้จุดที่ใช้" Cache ลดระยะทางระหว่างแอปกับข้อมูล ส่วน CDN ลดระยะทางระหว่างผู้ใช้กับเนื้อหา เวลาออกแบบให้ถามเสมอว่า "ข้อมูลชิ้นนี้ ใครอ่านบ่อย และมันอยู่ใกล้คนอ่านพอหรือยัง"

สรุปบทที่ 1 — เริ่มจากเครื่องเดียวเสมอ → แยก database ออกเป็นก้าวแรก → ใส่ cache (Redis) หน้า DB สำหรับข้อมูลอ่านบ่อย → ใช้ CDN กับ static content ทุกครั้งที่มีผู้ใช้กระจายตามภูมิศาสตร์ และจำไว้ว่าทุกชั้นที่เพิ่ม = ความซับซ้อนที่เพิ่ม ใส่เมื่อมีเหตุผลเท่านั้น

การเลือกฐานข้อมูล — SQL vs NoSQL#

Database Selection · ACID · BASE · CAP · ▶ ในคลิป 0:06:20 – 0:12:40

ฐานข้อมูลคือการตัดสินใจที่ เปลี่ยนทีหลังแพงที่สุด ในระบบ — โค้ดแอป refactor ได้ทุกสัปดาห์ แต่การย้ายข้อมูล production ข้ามชนิดฐานข้อมูลคือโปรเจกต์ระดับไตรมาส คำถามหลักของบทนี้คือ: งานของเราต้องการ "ความถูกต้องเคร่งครัด" หรือ "ความยืดหยุ่นและสเกล" มากกว่ากัน

2.1 Relational Database (SQL) — โครงสร้างคือจุดแข็ง

ฐานข้อมูลเชิงสัมพันธ์ (PostgreSQL, MySQL) เก็บข้อมูลเป็น ตารางที่มี schema ตายตัว — ทุกแถวมีคอลัมน์เหมือนกัน และตารางเชื่อมโยงกันผ่าน foreign key ทำให้ JOIN ข้อมูลข้ามตารางได้ในคำสั่งเดียว

จุดขายที่แท้จริงของ SQL คือ ACID — สัญญาว่าธุรกรรม (transaction) จะถูกต้องเสมอ:

  • A — Atomicity: ทุกขั้นในธุรกรรมสำเร็จทั้งหมด หรือไม่เกิดขึ้นเลย (โอนเงิน: หักบัญชี A และเข้าบัญชี B ต้องเกิดคู่กัน ห้ามค้างครึ่งทาง)
  • C — Consistency: ข้อมูลไม่ละเมิดกฎที่ตั้งไว้ (ยอดเงินติดลบไม่ได้, FK ต้องชี้ไปที่แถวที่มีจริง)
  • I — Isolation: ธุรกรรมที่รันพร้อมกันไม่เห็นข้อมูลครึ่ง ๆ กลาง ๆ ของกันและกัน
  • D — Durability: commit แล้วข้อมูลไม่หาย แม้ไฟดับวินาทีถัดมา
SQL — ตาราง + ความสัมพันธ์usersid │ name │ email42 │ Anya │ a@x.co43 │ Boon │ b@x.coordersid │ user_id │ total90 │ 42 │ 350.091 │ 42 │ 120.5foreign key → JOIN ได้NoSQL (Document) — เอกสารจบในตัว{"id": 42,"name": "Anya","email": "a@x.co","orders": [{"id":90,"total":350.0},{"id":91,"total":120.5}]}
FIG 2.1 ข้อมูลเดียวกันในสองโลก: SQL กระจายเป็นตารางเชื่อมด้วย key ส่วน document เก็บทุกอย่างที่เกี่ยวข้องไว้ในเอกสารเดียว อ่านครั้งเดียวจบแต่ทำสำเนาซ้ำซ้อนง่าย

2.2 NoSQL — สี่ตระกูล สี่โจทย์

"NoSQL" ไม่ใช่ฐานข้อมูลชนิดเดียว แต่เป็นคำเรียกรวมของฐานข้อมูลที่ สละความเคร่งครัดของ schema และ JOIN เพื่อแลกกับความยืดหยุ่นและการสเกลแนวนอน (scale-out) แบ่งเป็น 4 ตระกูลหลัก:

Key-ValueRedis · DynamoDB · Memcached"session:9f2c" → {user:42,...}"cart:42" → [sku1, sku2]เหมาะกับ: session, cache, ตะกร้าสินค้า — เร็วสุดDocumentMongoDB · CouchDB · Firestore{title:"...", tags:[...], comments:[{...},{...}]}เหมาะกับ: คอนเทนต์, แคตตาล็อก, ข้อมูลโครงสร้างยืดหยุ่นWide-ColumnCassandra · HBase · ScyllaDBrow: device42 → ts1:val ts2:val device43 → ts1:val ...เหมาะกับ: time-series, log, เขียนหนักมหาศาลGraphNeo4j · Neptuneเหมาะกับ: social graph,ระบบแนะนำ, fraud detection
FIG 2.2 สี่ตระกูล NoSQL: เลือกตามรูปร่างของข้อมูลและรูปแบบการอ่าน/เขียน ไม่ใช่ตามกระแส

2.3 Consistency ที่ต้องแลก — BASE และ CAP

NoSQL ส่วนใหญ่สเกลแนวนอนได้ดีเพราะยอมผ่อนความ consistent ลง จากโมเดล ACID ไปเป็น BASE (Basically Available, Soft state, Eventually consistent) — เขียนข้อมูลที่ node หนึ่งแล้ว node อื่นอาจเห็นค่าใหม่ "ในอีกครู่หนึ่ง" ไม่ใช่ทันที

เบื้องหลังคือ CAP Theorem: ในระบบกระจาย (distributed) เมื่อ network ขาดจากกัน (Partition) เราต้องเลือกอย่างใดอย่างหนึ่งระหว่าง

  • C — Consistency: ทุก node ตอบค่าล่าสุดเสมอ (ยอมตอบ error ถ้าไม่ชัวร์)
  • A — Availability: ตอบได้เสมอ (ยอมตอบค่าที่อาจเก่า)

ระบบธนาคารเลือก C (ยอดเงินผิดไม่ได้) ระบบ feed โซเชียลเลือก A (เห็นโพสต์ช้าไป 2 วินาทีไม่มีใครเดือดร้อน) — ไม่มีคำตอบถูกสากล มีแต่คำตอบที่ถูกกับโจทย์

2.4 ตารางตัดสินใจ

มิติSQL (Relational)NoSQL
โครงสร้างข้อมูลSchema ตายตัว ตรวจสอบเข้มยืดหยุ่น เปลี่ยนรูปร่างได้ต่อแถว
ความสัมพันธ์JOIN ได้ในตัว เหมาะข้อมูลโยงกันมากไม่มี JOIN จริง ต้อง denormalize (ทำซ้ำข้อมูล)
TransactionACID เต็มรูปแบบส่วนใหญ่ BASE / eventual consistency
การสเกลถนัด scale-up (เครื่องใหญ่ขึ้น) — scale-out ทำได้แต่ยากออกแบบมาเพื่อ scale-out ตั้งแต่ต้น
QuerySQL ทรงพลัง วิเคราะห์เฉพาะหน้า (ad-hoc) ได้ดีQuery ตามรูปแบบที่ออกแบบไว้ล่วงหน้า
เหมาะกับการเงิน, คำสั่งซื้อ, ระบบที่ผิดไม่ได้Feed, log, IoT, แคตตาล็อก, ทราฟฟิกมหาศาล

🧭 หลักเลือกในชีวิตจริงเริ่มที่ PostgreSQL ไว้ก่อนถ้าไม่แน่ใจ — ระบบส่วนใหญ่ในโลกไม่เคยโตถึงจุดที่ SQL สเกลไม่ไหว และ Postgres ยุคใหม่ทำงาน document (JSONB) ได้ด้วย ค่อยแยกบางส่วนไป NoSQL เมื่อเจอโจทย์เฉพาะจริง ๆ เช่น cache (Redis), เขียน log มหาศาล (Cassandra) — ระบบใหญ่จริงมักใช้ หลายฐานข้อมูลผสมกัน (polyglot persistence) ตามจุดแข็งของแต่ละตัว

2.5 Transaction ข้ามหลายระบบ — Saga, 2PC และ Outbox

ACID ที่เล่าไปข้างต้นใช้ได้เมื่อข้อมูลทั้งหมดอยู่ใน ฐานข้อมูลเดียว แต่ระบบแบบ microservices มักตัดขั้นตอนหนึ่ง (เช่น "สั่งซื้อ") ออกเป็นหลายบริการที่ต่างคนต่างมีฐานข้อมูลของตัวเอง — สั่งซื้อสำเร็จต้อง "สร้างออเดอร์" + "ตัดเงิน" + "จองสต็อก" ให้ครบทั้งสามที่ ถ้าที่ใดที่หนึ่งล้มเหลว ทำอย่างไรไม่ให้ข้อมูลค้างครึ่ง ๆ กลาง ๆ ข้ามระบบ

Two-Phase Commit (2PC) — ความถูกต้องเข้มแต่บล็อกทั้งระบบ

วิธีตรงไปตรงมาคือยกเครื่องมือของ ACID ข้ามระบบ: coordinator ถาม Phase 1 (Prepare) ให้ทุกฝ่าย "เตรียมพร้อม" และล็อกทรัพยากรไว้ก่อน ถ้าทุกฝ่ายตอบ "ได้" coordinator จึงสั่ง Phase 2 (Commit) พร้อมกัน — ได้ความถูกต้องระดับเดียวกับ ACID แต่ราคาที่จ่ายคือ blocking: ทุกฝ่ายต้องถือล็อกรอจนกว่า coordinator จะตัดสินใจ ถ้า coordinator ตายกลางคันหรือฝ่ายใดฝ่ายหนึ่งตอบช้า ทุกระบบที่เกี่ยวข้องค้างพร้อมกันหมด — ยิ่งมีบริการเยอะ โอกาสค้างยิ่งสูง จึงแทบไม่มีใครใช้ 2PC ข้าม microservices ในระบบที่ต้องการ availability สูง

Saga Pattern — ทำสำเร็จทีละขั้น แล้ว "แก้คืน" ถ้าพลาด

Saga เปลี่ยนโจทย์จาก "ทำทุกที่พร้อมกันแบบ atomic" เป็น ลำดับ local transaction ทีละขั้น — แต่ละขั้น commit จริงในฐานข้อมูลของตัวเอง แล้วส่ง event บอกขั้นถัดไปให้ทำงานต่อ ถ้าขั้นใดขั้นหนึ่งล้มเหลว จะไม่มีการ "ROLLBACK ข้ามระบบ" แต่ใช้ compensating transaction — ธุรกรรมที่ทำหน้าที่ "แก้คืน" ผลของขั้นที่สำเร็จไปแล้วย้อนตามลำดับ (เช่น ตัดเงินไปแล้วแต่จองสต็อกไม่ได้ → สั่ง "คืนเงิน" แทนการ rollback)

มีสองสไตล์การจัดลำดับ:

  • Choreography — ไม่มีตัวกลาง แต่ละ service ฟัง event แล้วตัดสินใจทำงานต่อเอง (event-driven ล้วน) เหมาะ flow สั้น ๆ ไม่กี่ขั้น แต่พอ flow ซับซ้อนขึ้นจะไม่มีที่เดียวที่เห็น "ภาพรวมทั้งหมด" — debug ยาก
  • Orchestration — มี "Saga Orchestrator" กลางคอยสั่งทีละขั้นและรู้ทั้ง flow ชัดเจน debug ง่ายกว่า แต่ orchestrator เองก็กลายเป็นจุดที่ต้องดูแลเรื่อง SPOF และความพร้อมใช้งาน (บทที่ 3)
Saga OrchestratorOrder ServicePayment ServiceInventory Service1. Create Order (local tx)✓ สำเร็จ2. Charge Payment (local tx)✓ สำเร็จ3. Reserve Inventory (local tx)✗ สต็อกหมด — ล้มเหลว4. Compensate: Refund Payment↩ คืนเงิน5. Compensate: Cancel Order↩ ยกเลิกออเดอร์
FIG 2.3 Saga แบบ orchestration: เมื่อขั้นตอนหนึ่งล้มเหลว (จองสต็อกไม่ได้) orchestrator สั่ง compensating transaction ย้อนกลับทุกขั้นที่สำเร็จไปแล้วตามลำดับย้อน ไม่ใช่ ROLLBACK ของฐานข้อมูลเดียว

Transactional Outbox — แก้ปัญหา "เขียน DB สำเร็จ แต่ event หลุด"

ทุกแพตเทิร์นข้างต้นพึ่งพา "ส่ง event ให้บริการถัดไปรู้" แต่การเขียนลง database กับการส่ง message ไปคนละระบบกัน — เขียนสำเร็จแต่ส่ง event ไม่สำเร็จ (หรือกลับกัน) คือ dual-write problem ทางแก้มาตรฐานคือ Outbox Pattern: เขียนทั้ง "ข้อมูลจริง" และ "event ที่จะส่ง" ลงตาราง outbox ใน transaction เดียวกัน (atomic เพราะเป็น DB เดียวกัน) แล้วให้ process แยกต่างหากอ่านตาราง outbox แล้วค่อยส่งเข้า message queue จริง — event จะถูกส่งก็ต่อเมื่อ transaction หลักสำเร็จเท่านั้น ไม่มีทางหลุดครึ่งทาง (รายละเอียดของ message queue ที่รับช่วงต่อ อยู่ในบทที่ 9)

มิติ2PCSaga
ConsistencyStrong — atomic ข้ามระบบจริงEventual — ถูกต้องในที่สุด ระหว่างทางอาจเห็นสถานะค้าง
Availabilityต่ำ — บล็อกรอทุกฝ่ายสูง — แต่ละ service ทำงานอิสระ
ความซับซ้อนตรรกะง่าย แต่ operational เสี่ยง (deadlock/ค้าง)ต้องออกแบบ compensating tx + รับมือ retry/idempotency เอง
เหมาะกับระบบปิดที่ควบคุมทุกฝ่ายเอง ทนบล็อกได้Microservices ที่ต้องการ availability สูง

สรุปบทที่ 2 — SQL = โครงสร้าง + ACID + JOIN เหมาะกับข้อมูลที่ผิดไม่ได้ · NoSQL = ยืดหยุ่น + scale-out + eventual consistency เหมาะกับปริมาณและความเร็ว · CAP บอกว่าตอน network มีปัญหา ต้องเลือกระหว่างตอบถูกเสมอ (C) กับตอบได้เสมอ (A) · ธุรกรรมข้ามหลาย service เลือก Saga (เร็ว+พร้อมใช้งานสูง) เหนือ 2PC (บล็อก) เกือบทุกกรณีใน microservices · คำถามแรกเวลาสัมภาษณ์: "ข้อมูลนี้ยอมเก่าได้ไหม และอ่าน-เขียนหนักด้านไหน"

Scaling & Reliability — ขยายระบบและกำจัดจุดตาย#

Vertical/Horizontal Scaling · Load Balancing · SPOF · Replication · ▶ ในคลิป 0:12:40 – 0:29:50

เมื่อทราฟฟิกโตจนเครื่องเดียวรับไม่ไหว มีทางเลือกสองทาง: ทำให้เครื่อง ใหญ่ขึ้น หรือทำให้เครื่อง เยอะขึ้น บทนี้ว่าด้วยการเลือกทาง, เครื่องมือที่ทำให้ "เยอะขึ้น" เป็นจริงได้ (load balancer) และหลักคิดที่สำคัญกว่าความเร็ว — ระบบต้องไม่ตายเพราะชิ้นส่วนเดียวพัง

3.1 Vertical vs Horizontal Scaling

Vertical (Scale Up) — เครื่องใหญ่ขึ้น4 CPU16GB32 CPU256GBแพงขึ้นแบบก้าวกระโดดง่าย ไม่ต้องแก้โค้ด — แต่มีเพดาน และยังเป็นเครื่องเดียว (SPOF)Horizontal (Scale Out) — เครื่องเยอะขึ้นServer 1Server 2Server 3+ เพิ่มได้เรื่อย ๆเครื่องธรรมดาหลายตัว — ไม่มีเพดาน แต่ต้องมีตัวกระจายงานLB
FIG 3.1 Scale up ง่ายแต่มีเพดานและยังพังพร้อมกันทั้งระบบ · Scale out ไร้เพดานแต่ต้องการ load balancer และแอปที่ stateless
มิติVertical (Scale Up)Horizontal (Scale Out)
วิธีเพิ่ม CPU/RAM เครื่องเดิมเพิ่มจำนวนเครื่อง
แก้โค้ดแทบไม่ต้องต้องทำแอปให้ stateless
เพดานมี — ฮาร์ดแวร์ใหญ่สุดในตลาดแทบไม่มี
ความทนทานยังเป็น SPOFเครื่องหนึ่งตาย ที่เหลือรับแทน
ราคาแพงขึ้นแบบ exponentialเพิ่มแบบ linear ตามจำนวนเครื่อง

3.2 Load Balancer — จราจรกลางของระบบ

Load balancer (LB) คือด่านหน้าที่รับทุก request แล้วกระจายไปยังเซิร์ฟเวอร์ที่ "ยังแข็งแรง" เบื้องหลัง ทำสองหน้าที่พร้อมกัน: กระจายโหลด ให้ไม่มีเครื่องไหนล้น และ ตรวจสุขภาพ (health check) เพื่อหยุดส่งงานให้เครื่องที่ตาย

ผู้ใช้ทุก requestLoad BalancerIP เดียวที่โลกเห็นServer 1 · แข็งแรงรับงานปกติServer 2 · แข็งแรงรับงานปกติServer 3 · ตาย ✕ถูกตัดออกจากวงhealth check ไม่ตอบ→ หยุดส่งงานทันทีกระจายตาม algorithm
FIG 3.2 LB เป็น IP เดียวที่ผู้ใช้เห็น เซิร์ฟเวอร์เบื้องหลังเพิ่ม/ลด/ตายได้โดยผู้ใช้ไม่รู้สึก — นี่คือหัวใจของ horizontal scaling

อัลกอริทึมกระจายโหลดที่ต้องรู้

อัลกอริทึมวิธีทำงานเหมาะกับ
Round Robinวนส่งทีละเครื่องตามคิว 1→2→3→1...เซิร์ฟเวอร์สเปกเท่ากัน งานแต่ละชิ้นหนักพอ ๆ กัน
Weighted Round Robinวนตามคิวแต่เครื่องแรงรับสัดส่วนมากกว่าเครื่องสเปกไม่เท่ากัน (ช่วงเปลี่ยนผ่านฮาร์ดแวร์)
Least Connectionsส่งให้เครื่องที่มีงานค้างน้อยที่สุดงานแต่ละชิ้นใช้เวลาไม่เท่ากัน (เช่นมี request ช้า ๆ ปน)
IP Hashhash IP ผู้ใช้ → ผู้ใช้เดิมไปเครื่องเดิมเสมอต้องการ session affinity โดยไม่มี shared store

LB ยังแบ่งตามชั้นที่ทำงาน: L4 (Transport) ดูแค่ IP/port — เร็วมาก ส่วน L7 (Application) อ่าน HTTP ได้ จึง route ตาม path ได้ (เช่น /api/video ไปเครื่องกลุ่มหนึ่ง /api/chat ไปอีกกลุ่ม) แลกกับ overhead ที่สูงกว่า

3.3 Single Point of Failure — ศัตรูหมายเลขหนึ่ง

SPOF คือชิ้นส่วนใดก็ตามที่ถ้าพังแล้วทั้งระบบหยุด วิธีไล่หามันคือชี้ไปที่ทุกกล่องในไดอะแกรมแล้วถามว่า "ถ้ากล่องนี้หายไปตอนนี้ ระบบยังเดินไหม" — คำตอบ "ไม่" ทุกจุดคือการบ้าน และยาแก้มีตัวเดียวคือ redundancy (มีสำรอง):

  • Web/App tier — มีหลายเครื่องหลัง LB อยู่แล้วจากการ scale out
  • Load balancer เองก็เป็น SPOF! — แก้ด้วย LB คู่ (active-passive) + failover อัตโนมัติ หรือใช้ DNS กระจายไปหลาย LB
  • Database — ตัวที่มักถูกลืม แก้ด้วย replication (ถัดไป)
  • ทั้ง Data Center — ระดับสูงสุด: กระจายหลาย Availability Zone / หลาย region

คำถามที่ตามมาจากการไล่ SPOF เสมอคือ "กำจัดจนระบบทนแค่ไหนถึงจะพอ" — วงการวัดค่านี้ด้วย "จำนวนเก้า" (nines) ของ availability:

Availabilityเรียกว่าดาวน์ได้สูงสุด/ปี
99%two nines~3.65 วัน
99.9%three nines~8.8 ชั่วโมง
99.99%four nines~52 นาที
99.999%five nines~5.3 นาที

ทุก "เก้า" ที่เพิ่ม = ต้นทุนและ redundancy ที่ต้องเพิ่มเป็นเท่าตัว — กำหนดเป้าตามความสำคัญของระบบก่อนเสมอ (payment gateway ไล่ 99.99%+ ขณะที่ระบบภายในอาจพอใจ 99.9%) แล้วออกแบบ redundancy ให้พอดีเป้า ไม่ใช่มากที่สุดเท่าที่ทำได้

3.4 Database Replication — ฐานข้อมูลก็ต้องมีตัวสำรอง

โครงสร้างมาตรฐานคือ Primary–Replica (หรือ leader–follower): เขียนที่ primary ตัวเดียว แล้วข้อมูลถูกส่งต่อ (replicate) ไปยัง replica หลายตัวซึ่งรับหน้าที่ อ่าน — ได้ประโยชน์สองต่อ: กระจายโหลดอ่าน (ระบบส่วนใหญ่อ่านมากกว่าเขียนหลายเท่า) และเป็นตัวสำรองเมื่อ primary ตาย

App Serversอ่าน + เขียนPrimaryรับ “เขียน” ทั้งหมดReplica 1รับ “อ่าน”Replica 2รับ “อ่าน”WRITEREADreplicate การเปลี่ยนแปลง(มี replication lag เล็กน้อย)ถ้า primary ตาย → เลื่อนขั้นreplica เป็น primary (failover)
FIG 3.3 Primary–Replica: เขียนที่เดียว อ่านได้หลายที่ และมีแผนสำรองเมื่อ primary ล่ม

3.5 Database Sharding — เมื่อ Replication ไม่พอ

Replication ที่เล่าไปแก้ปัญหาการ อ่าน ได้ดี แต่ไม่ช่วยเรื่อง เขียน หรือ พื้นที่เก็บข้อมูล เลย — เพราะเขียนได้ที่ primary ตัวเดียวเสมอ และทุก replica ต้องเก็บสำเนาข้อมูล "ทั้งหมด" เท่ากับ primary เมื่อข้อมูลใหญ่เกินดิสก์เครื่องเดียว หรือเขียนถี่เกินที่ primary ตัวเดียวรับไหว ทางออกคือ Sharding (horizontal partitioning): ตัดแถวของตารางเดียวกันออกเป็นส่วน ๆ กระจายไปอยู่คนละเครื่อง (shard) ที่แต่ละตัวรับทั้งอ่านและเขียนของส่วนตัวเองอย่างอิสระ

กลยุทธ์วิธีแบ่งข้อแลก
Range-basedแบ่งตามช่วงของ shard key เช่น user_id 1–1M → shard 1Query แบบช่วงทำง่าย แต่เสี่ยง "shard ร้อน" ถ้าข้อมูลใหม่กระจุกที่ shard ล่าสุด
Hash-basedhash(shard key) กระจายแถวให้เกลี่ยเท่า ๆ กันทุก shardกระจายสม่ำเสมอ แต่ query แบบช่วงทำไม่ได้ และเพิ่ม/ลด shard ต้อง reshuffle ข้อมูลจำนวนมาก (บรรเทาได้ด้วย consistent hashing)
Directory-basedมี lookup table กลางคอยจำว่า key ไหนอยู่ shard ไหนยืดหยุ่นที่สุด ย้ายข้อมูลทีละส่วนได้ แต่ lookup service เองต้องดูแลเรื่อง availability เพิ่มอีกชั้น

สรุปบทที่ 3 — Scale up ง่ายแต่มีเพดาน → scale out คือทางหลักของระบบใหญ่ โดยมีเงื่อนไขว่าแอปต้อง stateless · LB กระจายงาน + health check ตัดเครื่องตาย (Round Robin / Least Connections / IP Hash) · ไล่ฆ่า SPOF ทุกชั้น — อย่าลืมว่า LB และ DB ก็ตายได้ · DB ใช้ primary–replica เพื่อกระจายโหลดอ่านและรองรับ failover · เขียน/พื้นที่เก็บไม่พอค่อย sharding — แลกมาด้วย JOIN ข้าม shard ที่ยากขึ้นและ reshard ที่แพง

การออกแบบ API — สัญญาที่ดีระหว่างระบบ#

Resource Modeling · HTTP Methods · Pagination · Versioning · ▶ ในคลิป 0:29:50 – 0:46:06

API คือ สัญญา (contract) ระหว่างระบบของเรากับโลกภายนอก — และสัญญาที่ปล่อยออกไปแล้ว แก้ยากที่สุด เพราะมี client นับพันยึดตามอยู่ API ที่ดีจึงต้อง "เดาได้" (predictable): นักพัฒนาที่เห็น endpoint หนึ่งตัว ควรเดา endpoint ที่เหลือได้ถูกเกือบหมด

4.1 คิดเป็น Resource ไม่ใช่ Action

หัวใจของ REST-style API: URL แทน "สิ่งของ" (คำนาม) ส่วน "การกระทำ" (กริยา) ให้ HTTP method เป็นคนบอก — อย่าฝังกริยาลงใน URL

GET https://api.shop.com/v1/users/42/orders?status=paid&limit=20MethodการกระทำVersionกันสัญญาพังCollectionคำนามพหูพจน์Resource IDตัวระบุชิ้นเดียวNested resourceของที่อยู่ใต้ user 42Query paramsกรอง / แบ่งหน้า / เรียงลำดับ✗ ที่ไม่ควรทำ: /getUserOrders, /createOrder, /order_delete?id=42 — กริยาอยู่ผิดที่ และรูปแบบเดาไม่ได้
FIG 4.1 กายวิภาคของ URL ที่ดี: version → collection (พหูพจน์) → id → nested resource → query สำหรับการกรอง

4.2 HTTP Methods และคุณสมบัติ Idempotency

Idempotent = เรียกซ้ำกี่ครั้งผลลัพธ์สุดท้ายเหมือนเรียกครั้งเดียว — สำคัญมากเพราะ network ล้มเหลวได้เสมอ และ client ต้องรู้ว่า method ไหน "retry ได้อย่างปลอดภัย"

MethodความหมายIdempotent?ตัวอย่าง
GETอ่านข้อมูล ไม่เปลี่ยนแปลงอะไร✓ (และ safe)GET /orders/90
POSTสร้างใหม่ / สั่งประมวลผล✗ — เรียกซ้ำ = สร้างซ้ำPOST /orders
PUTแทนที่ทั้งก้อนด้วยข้อมูลใหม่PUT /users/42
PATCHแก้บางฟิลด์ขึ้นกับการออกแบบPATCH /users/42
DELETEลบ✓ (ลบซ้ำ = ก็ยังถูกลบอยู่ดี)DELETE /orders/90

🧭 Idempotency Key — เทคนิคระดับ production — POST ไม่ idempotent โดยธรรมชาติ แต่ระบบจ่ายเงินยอม "ตัดบัตรซ้ำ" ไม่ได้ — ทางแก้คือให้ client แนบ Idempotency-Key: <uuid> มากับ request ฝั่งเซิร์ฟเวอร์จำ key ที่เคยประมวลผลแล้ว ถ้าเจอซ้ำ (เช่น client retry เพราะ timeout) ให้ตอบผลลัพธ์เดิมแทนการทำงานซ้ำ — Stripe ใช้แพตเทิร์นนี้กับทุก API ที่สร้างธุรกรรม

4.3 Status Codes — ตอบให้ตรงความหมาย

  • 2xx สำเร็จ200 OK, 201 Created (สร้างสำเร็จ + ส่ง location กลับ), 204 No Content (ลบสำเร็จ ไม่มี body)
  • 4xx client ผิด400 Bad Request (payload ผิดรูป), 401 Unauthorized (ยังไม่พิสูจน์ตัวตน), 403 Forbidden (ตัวตนชัดแต่ไม่มีสิทธิ์), 404 Not Found, 429 Too Many Requests (โดน rate limit — บทที่ 8)
  • 5xx server ผิด500 Internal Server Error, 503 Service Unavailable (ล่มชั่วคราว/ปิดปรับปรุง)

ข้อผิดพลาดควรมี body ที่ machine-readable และบอกวิธีแก้ ไม่ใช่แค่ข้อความลอย ๆ:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "email ไม่ใช่รูปแบบที่ถูกต้อง",
    "field": "email",
    "request_id": "req_8f3ka"   // ไว้ตามรอยใน log
  }
}

4.4 Pagination — อย่าส่งข้อมูลล้านแถวในครั้งเดียว

ทุก endpoint ที่คืน list ต้องแบ่งหน้า มีสองแนวทางหลักที่ trade-off ต่างกันชัดเจน:

Offset-basedGET /orders?offset=20&limit=10แถว 1–10 (ข้าม)แถว 11–20 (ข้าม)แถว 21–30 ← ส่งหน้านี้+ เข้าใจง่าย กระโดดไปหน้าไหนก็ได้− ช้าลงเรื่อย ๆ เมื่อ offset ลึก (DB ต้องนับข้ามจริง)− แถวถูกแทรก/ลบระหว่างเปิดหน้า → ข้อมูลซ้ำ/หายCursor-basedGET /orders?after=ord_120&limit=10cursor: ord_120 ← จุดอ้างอิง10 แถวถัดจาก ord_120response แนบ next_cursor มาให้+ เร็วคงที่ทุกความลึก (กระโดดด้วย index)+ ทนต่อข้อมูลที่เปลี่ยนระหว่างเลื่อน — เหมาะ infinite scroll− กระโดดไป “หน้า 50” ตรง ๆ ไม่ได้กติกาเลือก: หน้าจอแบบ “เลขหน้า” (admin table) → offset · feed/timeline ที่เลื่อนไม่รู้จบ → cursor
FIG 4.2 Offset นับข้ามจากหัวตาราง ส่วน cursor กระโดดจากจุดอ้างอิงล่าสุด — Twitter/Facebook feed ล้วนใช้ cursor

4.5 Versioning — เผื่อทางให้อนาคต

สักวันเราต้องเปลี่ยนสัญญาแบบที่ client เก่าพัง (breaking change) เช่น เปลี่ยนชื่อฟิลด์ หรือโครงสร้าง response — versioning คือทางหนีไฟ มี 3 สไตล์:

วิธีหน้าตาข้อสังเกต
URI path/v1/orders/v2/ordersชัดเจน เห็นด้วยตา cache ง่าย — นิยมที่สุด
HeaderAccept: application/vnd.api+json;version=2URL สะอาด แต่ debug ยากกว่า มองไม่เห็นใน browser
Query param/orders?version=2ง่ายสุดแต่ปนกับ param อื่น ไม่ค่อยแนะนำ

กฎที่สำคัญกว่าการเลือกวิธี: เปลี่ยนแบบไม่พังได้ ไม่ต้องออกเวอร์ชันใหม่ — การ "เพิ่มฟิลด์ใหม่" ใน response ไม่ใช่ breaking change (client ที่ดีต้องเมินฟิลด์ที่ไม่รู้จัก) ออก v2 เฉพาะเมื่อจำเป็นจริง เพราะทุกเวอร์ชันที่เปิดอยู่คือภาระดูแล ×2

สรุปบทที่ 4 — URL คือคำนาม method คือกริยา · จำตาราง idempotency ให้ขึ้นใจ — มันคือเหตุผลว่าอะไร retry ได้ · ใช้ status code ให้ตรงความหมาย + error body ที่บอกวิธีแก้ · list ทุกตัวต้องแบ่งหน้า: เลขหน้า→offset, feed→cursor · เพิ่มฟิลด์ได้เสมอ แต่เปลี่ยน/ลบต้องออกเวอร์ชัน

โปรโตคอลสื่อสาร — ภาษาที่เครื่องคุยกัน#

HTTP/HTTPS · TCP vs UDP · WebSockets · Polling · SSE · ▶ ในคลิป 0:46:06 – 1:03:11

ทุกครั้งที่เลือกวิธีสื่อสาร เรากำลังตอบคำถามสองข้อ: "ยอมเสียข้อมูลบ้างไหม" (transport layer: TCP vs UDP) และ "ใครเป็นฝ่ายเริ่มพูด" (request-response vs real-time) บทนี้ไล่จากชั้นล่างขึ้นบน

5.1 ชั้นของการสื่อสาร

โปรโตคอลซ้อนกันเป็นชั้น แต่ละชั้นทำหน้าที่เดียวแล้วส่งต่อ: HTTP (ความหมายของข้อความ) วิ่งอยู่บน TCP หรือ UDP (วิธีขนส่ง) ซึ่งวิ่งบน IP (ที่อยู่ปลายทาง) — เวลาพูดว่า "เว็บช้า" ปัญหาอาจอยู่ชั้นไหนก็ได้ การรู้ชั้นช่วยให้ debug ถูกที่

5.2 TCP — ส่งถึงชัวร์ เรียงลำดับครบ

TCP (Transmission Control Protocol) เป็นโปรโตคอลแบบ connection-oriented: ก่อนส่งข้อมูลจริงต้อง "จับมือ 3 จังหวะ" (3-way handshake) เพื่อสถาปนาการเชื่อมต่อก่อน จากนั้นทุก packet ที่ส่งจะมีการตอบรับ (ACK) — หายก็ส่งซ้ำ มาสลับลำดับก็เรียงคืนให้

ClientServer1. SYN — “ขอเชื่อมต่อ”2. SYN-ACK — “ได้ มาเลย”3. ACK — “เริ่มส่งข้อมูลละนะ”— ข้อมูลจริงเริ่มไหล —ราคาที่จ่าย: 1 รอบเดินทางไป-กลับ (RTT)ก่อนได้ส่งข้อมูลจริงสิ่งที่ได้: รับประกันครบถ้วน + เรียงลำดับ
FIG 5.1 TCP 3-way handshake: จ่ายค่าตั้งวง 1 รอบ RTT แลกกับการส่งที่เชื่อถือได้ตลอดการเชื่อมต่อ

5.3 UDP — เร็วก่อน ครบไม่สัญญา

UDP (User Datagram Protocol) ตรงข้ามทุกอย่าง: ไม่มี handshake ไม่มี ACK ไม่มีการเรียงลำดับ — แค่ยิง packet ออกไปแล้วหวังว่าถึง ฟังดูแย่ แต่สำหรับงานบางประเภท "ข้อมูลเก่า" แย่กว่า "ข้อมูลหาย": วิดีโอคอลที่เฟรมหายไปเฟรมหนึ่งดีกว่าภาพค้างรอส่งซ้ำ เกมออนไลน์ที่ตำแหน่งล่าสุดสำคัญกว่าตำแหน่งเมื่อ 200ms ก่อน

มิติTCPUDP
การเชื่อมต่อต้อง handshake ก่อนยิงได้ทันที
ความครบถ้วนรับประกัน (ส่งซ้ำเมื่อหาย)ไม่รับประกัน
ลำดับเรียงให้ถูกเสมอมาก่อนได้ก่อน
ความเร็วช้ากว่า (overhead + รอ ACK)เร็วและ overhead ต่ำ
ใช้กับเว็บ, API, อีเมล, โอนไฟล์วิดีโอคอล, เกม, สตรีมมิง, DNS

🧭 จุดบรรจบของสองโลก: QUIC / HTTP-3 — HTTP/1.1 และ HTTP/2 วิ่งบน TCP ส่วน HTTP/3 วิ่งบน QUIC ซึ่งสร้างบน UDP — เอาความเร็วของ UDP มาแล้วเขียนกลไกความน่าเชื่อถือขึ้นใหม่ในชั้นบน ตัด handshake ให้สั้นลงและแก้ปัญหา head-of-line blocking ของ TCP ได้ นี่คือเหตุผลที่เว็บใหญ่ ๆ (Google, Cloudflare) ดัน HTTP/3

5.4 HTTP และ HTTPS

HTTP เป็นโปรโตคอลแบบ request–response และ stateless — แต่ละ request จบในตัว เซิร์ฟเวอร์ไม่จำอะไรระหว่าง request (เหตุผลที่ต้องมี cookie/token ในบทที่ 7) ส่วน HTTPS คือ HTTP ที่ห่อด้วย TLS: เข้ารหัสข้อมูลทั้งหมดระหว่างทาง + พิสูจน์ว่าเซิร์ฟเวอร์เป็นตัวจริงผ่าน certificate — ยุคนี้ ทุกอย่างต้องเป็น HTTPS ไม่มีข้อยกเว้น แม้แต่ระบบภายใน

วิวัฒนาการที่ควรรู้: HTTP/1.1 ใช้ 1 connection ต่อ 1 request ขณะหนึ่ง (เปลือง) → HTTP/2 multiplex หลาย request บน connection เดียว → HTTP/3 ย้ายไป QUIC ตามที่เล่าข้างบน

5.5 Real-time — เมื่อเซิร์ฟเวอร์อยากพูดก่อน

HTTP ปกติ client ถามก่อนเสมอ แต่แอปแชต ราคาหุ้น หรือแจ้งเตือน ต้องการให้ เซิร์ฟเวอร์ push ข้อมูลมาเอง — มี 4 เทคนิคไล่ระดับ:

  • Short Polling — client ถามซ้ำทุก X วินาที ("มีอะไรใหม่ไหม") ง่ายสุดแต่เปลือง: ส่วนใหญ่ได้คำตอบว่า "ไม่มี" และข้อมูลดีเลย์ได้ถึง X วินาที
  • Long Polling — client ถามแล้วเซิร์ฟเวอร์ แขวนคำตอบไว้ จนกว่าจะมีข้อมูลจริงค่อยตอบ แล้ว client ถามใหม่ทันที — ดีเลย์ต่ำลงมาก แต่ยังมี overhead ของการตั้ง request ใหม่เรื่อย ๆ
  • SSE (Server-Sent Events) — เปิด HTTP connection เดียวค้างไว้ ให้เซิร์ฟเวอร์สตรีมข้อความมาได้เรื่อย ๆ ทางเดียว — เบาและง่าย เหมาะ feed/แจ้งเตือน/ราคาหุ้น
  • WebSocket — อัปเกรดจาก HTTP เป็นท่อ สองทางถาวร (full-duplex) ทั้งสองฝั่งส่งหากันได้ทุกเมื่อ — มาตรฐานของแชต, เกม, collaborative editing
Short Polling — ถามซ้ำ ๆClientServerมีอะไรใหม่ไหม?ไม่มี (เสียเที่ยว)มีอะไรใหม่ไหม?ไม่มี (เสียเที่ยว)มี! → ได้ข้อมูลWebSocket — ท่อสองทางถาวรClientServerHTTP Upgrade ครั้งเดียว— connection เปิดค้าง —push ข้อความ Aclient ส่งกลับได้ทันทีpush ข้อความ Bไม่มีการตั้ง request ใหม่ ไม่มีเที่ยวเปล่า
FIG 5.2 Polling เผาทรัพยากรไปกับคำตอบ “ไม่มี” ส่วน WebSocket จ่ายค่าเปิดท่อครั้งเดียวแล้วคุยสองทางได้ตลอด

สรุปบทที่ 5 — TCP = ครบชัวร์แต่จ่ายค่า handshake · UDP = เร็วแต่หายได้ เหมาะงานที่ข้อมูลเก่าไร้ค่า · HTTPS คือภาคบังคับ · งาน real-time ไล่จากถูกไปแพง: short polling → long polling → SSE (ทางเดียว) → WebSocket (สองทาง) — เลือกตัวที่ "พอ" ไม่ใช่ตัวที่เก่งสุด

API Styles — REST vs GraphQL#

Over-fetching · Under-fetching · Schema · Query Language · ▶ ในคลิป 1:03:11 – 1:23:41

REST ครองโลกมานาน แต่เมื่อแอปฝั่ง client ซับซ้อนขึ้น (โดยเฉพาะมือถือ) จุดอ่อนสองข้อของมันก็ชัดขึ้นเรื่อย ๆ — GraphQL เกิดที่ Facebook เพื่อแก้สองข้อนี้โดยตรง การเข้าใจ "ปัญหาที่ GraphQL แก้" สำคัญกว่าการจำ syntax

6.1 จุดอ่อนของ REST: Over-fetching และ Under-fetching

  • Over-fetching — endpoint คืนข้อมูลตามรูปแบบตายตัว หน้าจอที่ต้องการแค่ "ชื่อกับรูปโปรไฟล์" ก็ได้ object ผู้ใช้มาทั้งก้อน 40 ฟิลด์ เปลืองแบนด์วิดท์โดยเฉพาะบนมือถือ
  • Under-fetching — หน้าจอเดียวต้องการข้อมูลหลายชนิด แต่ REST แยกเป็นคนละ endpoint จึงต้องยิงหลายรอบต่อเนื่อง (ปัญหา N+1 round trips) แต่ละรอบบวก latency เพิ่ม
REST — หน้าโปรไฟล์ต้องยิง 3 รอบMobile AppREST APIGET /users/42ได้มา 40 ฟิลด์ ใช้จริง 2 (over-fetch)GET /users/42/postsGET /users/42/followers3 round trips = latency ×3 (under-fetch)GraphQL — รอบเดียว ระบุเป๊ะMobile App/graphqlquery { user(id:42) {nameavatarUrlposts(first:5){title}followers{count}← ขอเฉพาะที่หน้าจอใช้ตอบครบทุกอย่างใน 1 round trip
FIG 6.1 โจทย์เดียวกัน (หน้าโปรไฟล์): REST ยิง 3 รอบและได้ข้อมูลเกิน ส่วน GraphQL ประกอบทุกอย่างใน query เดียว

6.2 GraphQL ทำงานอย่างไร

GraphQL มี endpoint เดียว (มักเป็น POST /graphql) และทุกอย่างถูกนิยามด้วย schema แบบมี type ชัดเจน — schema คือสัญญาที่ client กับ server แชร์กัน เครื่องมือ (autocomplete, validation, codegen) จึงเก่งมาก ปฏิบัติการมี 3 ชนิด:

  • Query — อ่านข้อมูล (เทียบ GET)
  • Mutation — เปลี่ยนแปลงข้อมูล (เทียบ POST/PUT/DELETE)
  • Subscription — สมัครรับข้อมูล real-time ผ่าน WebSocket

6.3 ราคาที่ GraphQL เรียกเก็บ

ความยืดหยุ่นของ client กลายเป็นภาระของ server:

  • HTTP caching แทบใช้ไม่ได้ — ทุกอย่างเป็น POST ที่ URL เดียว CDN/browser cache ที่ทำงานกับ GET ของ REST ใช้ไม่ได้ตรง ๆ ต้องพึ่ง client-side cache (Apollo/Relay)
  • Query อันตราย — client เขียน query ซ้อนลึกได้ตามใจ (friends { friends { friends } }) อาจถล่ม DB โดยไม่ตั้งใจ — production ต้องมี depth limit / query cost analysis
  • ปัญหา N+1 ย้ายฝั่ง — round trip ฝั่ง client หายไป แต่ resolver ฝั่ง server อาจ query DB ซ้ำซ้อนแทน ต้องใช้ DataLoader (batching) แก้
  • Setup ซับซ้อนกว่า — schema, resolvers, tooling — overkill สำหรับ API เรียบ ๆ

6.4 ตารางเปรียบเทียบ

มิติRESTGraphQL
Endpointหลายตัว ตาม resourceตัวเดียว (/graphql)
รูปร่าง responseserver กำหนดclient กำหนดต่อ query
Round tripsมักหลายรอบต่อหน้าจอรอบเดียว
CachingHTTP/CDN cache ได้เต็มที่ยาก ต้องใช้ client cache เฉพาะทาง
Type safetyต้องเสริมเอง (OpenAPI)มีในตัวผ่าน schema
เส้นโค้งการเรียนรู้ต่ำ ทุกคนรู้จักสูงกว่า ทั้งสองฝั่ง
เหมาะกับpublic API, microservices ภายใน, งาน CRUD ตรงไปตรงมาmobile/SPA ที่หน้าจอซับซ้อน, ทีม frontend หลายทีมใช้ API กลางร่วมกัน

🧭 ตัวเลือกที่สาม: gRPC — สำหรับการคุยกัน ระหว่าง microservices ภายใน ยังมี gRPC — ใช้ Protocol Buffers (binary) บน HTTP/2 เร็วกว่า JSON มาก พร้อม streaming ในตัว ภาพรวมที่พบบ่อยในบริษัทใหญ่: gRPC ภายใน · REST สำหรับ public API · GraphQL เป็นชั้นรวมข้อมูลให้ frontend (API gateway/BFF) — สามตัวอยู่ร่วมกันได้ ไม่ใช่ศึกแย่งบัลลังก์

6.5 เคสจริง — ทำไมบริษัทใหญ่เลือกไม่เหมือนกัน

Facebook (ผู้สร้าง GraphQL) — ปัญหาที่จุดประกายคือแอปมือถือ: ทีม iOS/Android ต้องประกอบหน้าจอ News Feed จากข้อมูลหลายชนิด (โพสต์, คอมเมนต์, reaction, โปรไฟล์) การยิง REST endpoint แยกทีละก้อนทำให้แอปช้าและ over-fetch หนักบนมือถือที่เน็ตไม่เสถียร ทีม Facebook จึงออกแบบภาษา query ที่ให้ client "บอกรูปร่างข้อมูลที่ต้องการ" แล้ว server ประกอบให้ในรอบเดียว ใช้ภายในตั้งแต่ปี 2012 ก่อนเปิดเป็นโอเพนซอร์สในปี 2015

GitHub (ใช้ทั้งสองแบบคู่กัน) — REST API ของ GitHub มีมาตั้งแต่ต้นและยังเป็น API หลักสำหรับงานส่วนใหญ่ ส่วน GraphQL API เปิดเพิ่มปี 2016 เพื่อให้ client ดึงข้อมูลที่เชื่อมโยงกันซับซ้อน (repo + issues + PR + reviewer ในคำขอเดียว) ได้ในรอบเดียวโดยไม่ over/under-fetch — ทุกวันนี้ GitHub เปิดเผยตรง ๆ ว่าฟีเจอร์บางอย่าง (เช่น Discussions, Projects) เข้าถึงได้ผ่าน GraphQL เท่านั้น ขณะที่บางอย่าง (เช่น logs ของ Actions) ผ่าน REST เท่านั้น — เลือกให้ตรงงาน ไม่ใช่เลือกให้ตรงเทรนด์

🧭 บทเรียนจากทั้งสองเคส — ไม่มีใคร "เปลี่ยนมาใช้ GraphQL ทั้งหมด" จริง ๆ แม้แต่ผู้คิดค้นเอง — GraphQL แก้ปัญหาการประกอบข้อมูลซับซ้อนจากหลายแหล่งให้ client ที่มีข้อจำกัด (มือถือ, แบนด์วิดท์) ส่วน REST ยังคุ้มกว่าเวลา endpoint ตรงไปตรงมาและอยากได้ประโยชน์จาก HTTP caching เต็มที่ — คำถามที่ควรถามจึงไม่ใช่ "REST หรือ GraphQL ดีกว่ากัน" แต่เป็น "endpoint ชุดนี้ client ต้องประกอบข้อมูลซับซ้อนแค่ไหน"

สรุปบทที่ 6 — REST เรียบง่าย cache เก่ง — จุดอ่อนคือ over/under-fetching · GraphQL ให้ client เลือกข้อมูลเป๊ะ ๆ ในรอบเดียว — จ่ายด้วย caching ที่ยากและ server ที่ซับซ้อน · เลือกจากรูปร่างของ client: หน้าจอซับซ้อน/มือถือ → GraphQL ได้เปรียบ, public API/CRUD → REST พอและดีกว่า

Authentication & Authorization — ใครเป็นใคร ทำอะไรได้#

Sessions · JWT · OAuth 2.0 · OpenID Connect · RBAC · ABAC · ▶ ในคลิป 1:23:41 – 1:55:51

สองคำที่หน้าตาคล้ายแต่คนละเรื่อง: Authentication (AuthN) = พิสูจน์ว่า "คุณคือใคร" ส่วน Authorization (AuthZ) = ตัดสินว่า "คุณทำสิ่งนี้ได้ไหม" — เปรียบกับสนามบิน: ด่านตรวจพาสปอร์ตคือ authentication ส่วนตั๋ว boarding pass ที่บอกว่าคุณขึ้นเครื่องลำไหน ที่นั่ง class อะไร คือ authorization · ระบบตอบ 401 เมื่อ AuthN ล้มเหลว และ 403 เมื่อ AuthZ ล้มเหลว

7.1 ปัญหาตั้งต้น: HTTP จำใครไม่ได้

HTTP เป็น stateless — login สำเร็จแล้ว request ถัดไปเซิร์ฟเวอร์ก็ไม่รู้ว่าเป็นคนเดิม เราจึงต้องมี "หลักฐาน" แนบไปกับทุก request มีสองสำนักใหญ่:

7.2 สำนักที่หนึ่ง: Session + Cookie (Stateful)

หลัง login เซิร์ฟเวอร์สร้าง session record เก็บฝั่งตัวเอง (ใน Redis/DB) แล้วส่งแค่ session ID ใส่ cookie ให้เบราว์เซอร์ — ทุก request ถัดไป เบราว์เซอร์แนบ cookie อัตโนมัติ เซิร์ฟเวอร์เอา ID ไปเปิดดูว่าเป็นใคร

  • ข้อดี — เพิกถอนได้ทันที (ลบ session ทิ้ง = logout ทุกอุปกรณ์), ข้อมูล session อัปเดตสดเสมอ
  • ข้อเสีย — ทุก request ต้องเปิด lookup หนึ่งครั้ง และใน scale-out ต้องมี shared session store ให้ทุกเครื่องอ่านร่วมกัน (บทที่ 3)

7.3 สำนักที่สอง: JWT (Stateless Token)

JWT (JSON Web Token) พลิกแนวคิด: แทนที่จะเก็บข้อมูลฝั่งเซิร์ฟเวอร์ ก็ ยัดข้อมูลลงใน token แล้วเซ็นลายเซ็นดิจิทัลกำกับ เซิร์ฟเวอร์แค่ตรวจลายเซ็นก็เชื่อเนื้อหาข้างในได้เลย — ไม่ต้องเปิด DB

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiJ9 . SflKxwRJSMeKKF2QT4...HEADER{ "alg": "HS256", "typ": "JWT" }บอกอัลกอริทึมที่ใช้เซ็นPAYLOAD (Claims){ "sub": "42", "role": "admin", "exp": 1740000000 }ตัวตน สิทธิ์ และวันหมดอายุSIGNATUREHMAC(header + payload, secret)กันการปลอมแปลงเนื้อหา⚠ เข้ารหัสแบบ Base64 = ใครก็ “อ่าน” payload ได้ — ลายเซ็นกันแค่การ “แก้ไข” ไม่ใช่การอ่านห้ามใส่ข้อมูลลับ (รหัสผ่าน, เลขบัตร) ลงใน JWT เด็ดขาด
FIG 7.1 โครงสร้าง JWT สามส่วนคั่นด้วยจุด: header.payload.signature — แก้ payload แม้ตัวอักษรเดียว ลายเซ็นจะไม่ตรงทันที

จุดแลกเปลี่ยนที่ต้องท่องให้ขึ้นใจ: JWT เพิกถอนกลางอากาศไม่ได้ — token ที่เซ็นแล้วจะใช้ได้จนกว่าจะหมดอายุ (exp) ต่อให้ผู้ใช้ถูกแบนไปแล้วก็ตาม ทางแก้มาตรฐานคือคู่ token:

  • Access Token — อายุสั้นมาก (5–15 นาที) ใช้แนบทุก request ใน header Authorization: Bearer <token>
  • Refresh Token — อายุยาว (วัน/สัปดาห์) เก็บอย่างปลอดภัย ใช้ ขอ access token ใหม่ เมื่อตัวเก่าหมดอายุ — และฝั่งเซิร์ฟเวอร์เพิกถอน refresh token ได้ (จุดนี้ stateful) ทำให้แบนผู้ใช้มีผลภายในไม่กี่นาที
มิติSession + CookieJWT
เก็บ state ที่เซิร์ฟเวอร์ (Redis/DB)ตัว token เอง (client ถือ)
ตรวจสอบlookup ทุก requestตรวจลายเซ็น ไม่แตะ DB
เพิกถอนทันทีต้องรอหมดอายุ / ใช้คู่ refresh token
Scale-outต้องมี shared storeเครื่องไหนก็ตรวจได้เอง — เหมาะ microservices
เหมาะกับเว็บแอปดั้งเดิม, ระบบที่ความปลอดภัยนำAPI, mobile, ระบบกระจายหลาย service

7.4 OAuth 2.0 — มอบสิทธิ์โดยไม่มอบรหัสผ่าน

โจทย์ของ OAuth: แอป A อยากเข้าถึงข้อมูลของคุณในบริการ B (เช่น แอปตัดต่อรูปขอเข้าถึง Google Photos) — วิธีโบราณคือเอารหัสผ่าน Google ไปกรอกในแอป ซึ่งอันตรายสุดขีด OAuth 2.0 แก้ด้วยการให้ B ออก token ที่จำกัดขอบเขต (scope) และเพิกถอนได้ ให้ A แทน — ผู้เล่นมี 4 ฝ่าย:

  • Resource Owner — ผู้ใช้ (เจ้าของข้อมูล)
  • Client — แอปที่ขอสิทธิ์ (แอปตัดต่อรูป)
  • Authorization Server — ผู้ออก token (หน้า login ของ Google)
  • Resource Server — API ที่ถือข้อมูล (Google Photos API)
ผู้ใช้Client AppAuth ServerResource API1. กด “เชื่อมต่อ Google”2. redirect ไปหน้า login + ขอ scope3. ผู้ใช้ login + กดยินยอม (consent) ที่ Google โดยตรง4. กลับมาพร้อม authorization code5. แลก code + client secret6. ได้ access token (จำกัด scope)7. เรียก API ด้วย Bearer tokenรหัสผ่านผู้ใช้ไม่เคยผ่านมือ Client App เลยแม้แต่ครั้งเดียว
FIG 7.2 OAuth 2.0 Authorization Code Flow: ผู้ใช้กรอกรหัสที่ Auth Server เท่านั้น แอปได้แค่ token ขอบเขตจำกัดที่เพิกถอนได้

สองรายละเอียดที่ใช้จริง: scope จำกัดว่า token ทำอะไรได้ (photos.read อ่านได้แต่ลบไม่ได้) และ PKCE — ส่วนขยายภาคบังคับสำหรับ mobile/SPA ที่เก็บ client secret ไม่ได้ ใช้คู่ challenge/verifier กันคนดัก authorization code ไปแลก token แทนเรา

7.5 OpenID Connect (OIDC) — ชั้นตัวตนบน OAuth

OAuth 2.0 ออกแบบมาเพื่อ "มอบสิทธิ์เข้าถึง" ไม่ใช่ "ยืนยันตัวตน" — ปุ่ม "Sign in with Google" ที่เราเห็นทุกวันคือ OIDC: ชั้นบาง ๆ บน OAuth ที่เพิ่ม ID Token (เป็น JWT) ซึ่งบรรจุข้อมูลตัวตนที่ยืนยันแล้ว (ชื่อ, อีเมล, รูป) — จำง่าย ๆ: OAuth = สิทธิ์ (authorization), OIDC = ตัวตน (authentication) และแอปจำนวนมากใช้ทั้งคู่ใน flow เดียวกัน

7.6 Access Control — RBAC vs ABAC

รู้ตัวตนแล้ว ขั้นต่อไปคือตัดสินใจว่า "ทำได้ไหม" สองโมเดลหลัก:

RBAC (Role-Based)ABAC (Attribute-Based)
หลักการผูกสิทธิ์กับ "บทบาท" แล้วแจกบทบาทให้คนเขียนกฎจาก "คุณลักษณะ" ของผู้ใช้ + ทรัพยากร + บริบท
ตัวอย่างกฎrole = editor → แก้บทความได้ทุกชิ้นแก้ได้ถ้า user.dept == doc.dept และ time ∈ เวลางาน และ doc.status != locked
ความละเอียดหยาบ — สิทธิ์เหมาเข่งตาม roleละเอียดระดับเงื่อนไขรายชิ้น
ดูแลรักษาง่าย เข้าใจไว ตรวจสอบ (audit) ง่ายกฎซับซ้อน debug ยากกว่า — แต่ยืดหยุ่นสุด
เหมาะกับองค์กรโครงสร้างชัด (admin/editor/viewer)ระบบ multi-tenant, ข้อกำหนดกำกับดูแลซับซ้อน

ของจริงมักเป็น ลูกผสม: ใช้ RBAC เป็นโครงหลัก (เข้าใจง่าย) แล้วเสริมเงื่อนไข attribute เฉพาะจุดที่ต้องละเอียด เช่น "editor แก้บทความได้ เฉพาะของทีมตัวเอง"

สรุปบทที่ 7 — AuthN = คุณคือใคร (401) · AuthZ = ทำได้ไหม (403) · Session = เพิกถอนง่าย แต่ต้อง lookup · JWT = ตรวจเองได้ไม่แตะ DB แต่เพิกถอนยาก → ใช้ access สั้น + refresh ยาว · OAuth = มอบสิทธิ์ข้ามแอปโดยไม่มอบรหัสผ่าน · OIDC = ชั้นยืนยันตัวตนบน OAuth · RBAC เริ่มง่าย เสริม ABAC เมื่อต้องละเอียด

Security & Rate Limiting — ปกป้องระบบใน Production#

Rate Limiting Algorithms · 429 · Distributed Limiter · Hardening · ▶ ในคลิป 1:55:51 – 2:03:30

ระบบที่เปิดสู่อินเทอร์เน็ตจะเจอทราฟฟิกที่ไม่ได้เชิญเสมอ — bot scrape ข้อมูล, สคริปต์เดารหัสผ่าน, client ที่เขียนพลาดยิงวนไม่หยุด, จนถึง DDoS — Rate Limiting คือด่านแรกของการป้องกัน: จำกัดว่าผู้เรียกหนึ่งราย (ระบุจาก API key, user ID หรือ IP) ส่ง request ได้กี่ครั้งต่อช่วงเวลา เกินแล้วตอบ 429 Too Many Requests พร้อม header Retry-After บอกว่าให้รอกี่วินาที

เหตุผลที่ต้องมีไม่ใช่แค่กันโจมตี: ความเป็นธรรม (ผู้ใช้รายเดียวห้ามกินทรัพยากรทั้งระบบ), ควบคุมต้นทุน (ทุก request มีราคา) และ กันระบบตัวเองพัง (bug ที่ retry ถี่ ๆ ก็ถล่มระบบได้เท่า DDoS)

8.1 วางไว้ตรงไหน

มาตรฐานคือวางที่ API Gateway / Load Balancer — ด่านหน้าสุดก่อนถึงแอป เพื่อตัดทราฟฟิกส่วนเกินตั้งแต่ยังไม่กินทรัพยากรของ service และในระบบ scale-out ตัวนับต้องเก็บใน store กลางอย่าง Redis — ถ้าแต่ละเครื่องนับของตัวเอง ผู้โจมตีที่ถูกกระจายไป 10 เครื่องจะได้โควตา ×10 ฟรี ๆ

8.2 สี่อัลกอริทึมที่ต้องรู้

Fixed Window — ช่องโหว่ตรงรอยต่อนาที 10:00 (โควตา 100)นาที 10:01 (โควตา 100)100 req100 reqยิง 100 ตอน 10:00:59 + อีก 100 ตอน 10:01:01= 200 req ใน 2 วินาที — ถูกกติกาแต่ผิดเจตนา!Sliding Window — หน้าต่างเลื่อนตาม60 วินาทีล่าสุดนับย้อนจาก “ตอนนี้” เสมอไม่มีรอยต่อให้เบิ้ลโควตาแลกกับการเก็บ timestamp มากขึ้นToken Bucket — ยอม burst อย่างมีขอบเขตเติม token คงที่ เช่น 10/วินาที1 request = ใช้ 1 tokenถังเต็มเก็บได้ = burst ที่ยอมให้Leaky Bucket — ไหลออกคงที่เสมอrequest เข้าคิวในถังประมวลผลด้วยอัตราคงที่ถังล้น = ปฏิเสธ request— โหลดขาออกเรียบสนิท
FIG 8.1 สี่อัลกอริทึม: fixed window ง่ายแต่มีช่องโหว่รอยต่อ · sliding window อุดช่องโหว่นั้น · token bucket ยอมพีคสั้น ๆ · leaky bucket บังคับอัตราเรียบ
อัลกอริทึมพฤติกรรมเลือกเมื่อ
Fixed Windowนับต่อช่วงเวลาตายตัว ง่าย+ประหยัดสุดงานภายใน ความเป๊ะไม่วิกฤต
Sliding Windowนับ 60 วิล่าสุดแบบเลื่อนตาม ไม่มีรอยต่อpublic API ที่ต้องแฟร์และแม่น
Token Bucketอัตราเฉลี่ยคงที่ + เผื่อ burst สั้น ๆทราฟฟิกจริงที่มีพีคเป็นช่วง (นิยมสุด — AWS ใช้)
Leaky Bucketขาออกเรียบคงที่ ไม่มี burst เลยระบบปลายทางรับโหลดกระชากไม่ได้

API ที่ดีควรบอกสถานะโควตาให้ client รู้ล่วงหน้าผ่าน response headers:

// ตัวอย่าง headers ที่ GitHub/Stripe ใช้
X-RateLimit-Limit:     100      // โควตาต่อหน้าต่างเวลา
X-RateLimit-Remaining: 4        // เหลือใช้ได้อีกกี่ครั้ง
X-RateLimit-Reset:     1740001  // โควตารีเซ็ตเมื่อไร (unix time)
Retry-After:           30       // (เมื่อโดน 429) รออีกกี่วินาที

🧭 ฝั่ง client ก็มีหน้าที่ — client ที่ดีเมื่อเจอ 429 ต้อง retry แบบ exponential backoff + jitter (รอ 1s → 2s → 4s... บวกสุ่มเล็กน้อย) — ถ้าทุก client retry พร้อมกันเป๊ะ จะเกิด "thundering herd" ถล่มระบบซ้ำเป็นระลอก

8.3 เช็กลิสต์ความปลอดภัยขั้นต่ำของ Production

  • HTTPS ทุกเส้นทาง — รวมถึงภายใน data center (zero-trust)
  • Validate input ทุกจุด — ป้องกัน SQL injection (ใช้ parameterized query เสมอ), XSS
  • เก็บ secret ให้ถูกที่ — secret manager / env vars, ห้าม hardcode ลงโค้ดหรือ git
  • Hash รหัสผ่านด้วย bcrypt/argon2 — ห้ามเก็บ plaintext, ห้ามใช้ MD5/SHA1
  • Least privilege ทุกชั้น — service account ของแอปไม่ควรมีสิทธิ์ DROP TABLE
  • Logging + Monitoring — ตรวจจับ pattern ผิดปกติ (login fail ถี่, ทราฟฟิกพุ่ง) ให้เห็นก่อนเป็นข่าว
  • ชั้นป้องกัน DDoS ระดับ network — ใช้ CDN/WAF (Cloudflare ฯลฯ) รับแรงปะทะแทน origin

สรุปบทที่ 8 — Rate limit ที่ gateway + ตัวนับกลางใน Redis · เลือกอัลกอริทึมตามนิสัยทราฟฟิก: token bucket คือ default ที่ดี, sliding window เมื่อต้องแฟร์เป๊ะ · ตอบ 429 พร้อม Retry-After และ headers บอกโควตา · ความปลอดภัยคือการป้องกันหลายชั้นซ้อนกัน (defense in depth) — ไม่มีชั้นเดียวที่เอาอยู่ทั้งหมด

Message Queue & Async Communication — เมื่อไม่ต้องรอคำตอบทันที#

Sync vs Async · Message Queue · Pub/Sub · Kafka · RabbitMQ · SQS · เนื้อหาเสริม — นอกคลิปต้นฉบับ เพิ่มเพื่อปิดช่องว่างสำคัญของ production infrastructure

ทุกบทที่ผ่านมาพูดถึงการสื่อสารแบบ synchronous เป็นหลัก — ผู้เรียกส่ง request แล้วรอคำตอบก่อนทำงานต่อ (บทที่ 5) แต่ไม่ใช่ทุกงานที่ต้องรอ: ส่งอีเมลยืนยัน, ตัดต่อวิดีโอ, สร้างรายงานสรุปยอดขาย — งานเหล่านี้ผู้ใช้ไม่ต้องเห็นผลทันที และถ้าให้ request หลักรอจนกว่าทุกงานจะเสร็จ ก็แค่ทำให้ระบบช้าและเปราะบางขึ้นโดยไม่จำเป็น บทนี้ว่าด้วยการสื่อสารแบบ asynchronous ผ่านตัวกลางที่เรียกว่า message queue

9.1 Synchronous vs Asynchronous — ใครรอใคร

การเรียกแบบ synchronous (ที่เห็นมาตลอดเล่มนี้ ไม่ว่า REST, GraphQL, หรือ gRPC) ผูกชะตาผู้เรียกกับผู้ถูกเรียกไว้ด้วยกัน: ถ้า service ปลายทางช้าหรือตาย ผู้เรียกก็ช้าหรือพังตามไปด้วยทันที (cascading failure) — asynchronous เปลี่ยนโจทย์โดยแทรก ตัวกลาง (broker/queue) ไว้ระหว่างกัน: ผู้ส่ง (producer) แค่วางข้อความไว้ในคิวแล้วทำงานต่อได้ทันที ไม่ต้องรู้ด้วยซ้ำว่าใครเป็นผู้รับ หรือผู้รับพร้อมหรือยัง

Synchronous — รอคำตอบก่อนไปต่อService AService Bเรียกแล้วรอ (blocking call)A ค้างรอตรงนี้ ทำอย่างอื่นไม่ได้ตอบกลับ A ถึงทำงานต่อได้B ช้า/ตาย → A ค้างหรือพังตามทันทีAsynchronous — ส่งแล้วไปต่อ ไม่รอQueueกันชนตรงกลางProducer (A)Consumer (B)วางข้อความไว้ในคิวA ทำงานต่อได้ทันที ไม่ต้องรอ BB ดึงไปทำเมื่อพร้อมB ตายชั่วคราว → ข้อความรอในคิวได้ ไม่หาย ไม่ลามถึง A
FIG 9.1 Synchronous ผูกชะตา A กับ B ไว้ด้วยกันทันที ส่วน Asynchronous ใช้คิวเป็นกันชนให้ A ไปต่อได้โดยไม่ต้องรอ B
มิติSynchronousAsynchronous
Couplingแน่น — ต้องมีทั้งสองฝั่งพร้อมกันหลวม — ผู้รับตายชั่วคราวได้โดยผู้ส่งไม่รู้สึก
Latency ที่ผู้ใช้เห็นรวมเวลาทุก service ที่เรียกต่อกันตอบผู้ใช้ได้ทันทีที่ "รับเรื่องแล้ว" งานจริงทำเบื้องหลัง
Failureลามข้าม service ได้ง่าย (cascading failure)คิวช่วยกันชน แต่ต้องออกแบบ retry/DLQ เอง
เหมาะกับผู้ใช้ต้องเห็นผลทันที (เช็กสต็อกก่อนกดจ่ายเงิน)งานที่รอได้ / ประมวลผลหนัก / ต้องแยก service ไม่ให้ผูกกัน

9.2 Message Queue พื้นฐาน — Point-to-Point

รูปแบบพื้นฐานที่สุด: producer ส่งข้อความเข้าคิว แล้ว consumer ตัวใดตัวหนึ่ง ดึงไปประมวลผล — ถ้ามี consumer หลายตัว (competing consumers) แต่ละข้อความยังถูกประมวลผล "ครั้งเดียว" โดยตัวใดตัวหนึ่งเท่านั้น เพิ่ม consumer = เพิ่มความเร็วในการระบายงานออกจากคิว (scale-out แบบเดียวกับบทที่ 3)

คำถามที่หนีไม่พ้นของทุกระบบคิวคือ "ข้อความส่งถึงกี่ครั้ง":

  • At-most-once — ส่งแค่ครั้งเดียว ไม่ retry ถ้าหายก็หายเลย เร็วที่สุดแต่เสี่ยงข้อมูลหาย
  • At-least-once — รับประกันว่าถึงแน่ ๆ แต่ retry อาจทำให้ consumer เห็นข้อความ ซ้ำ ได้ — ค่าเริ่มต้นที่ระบบส่วนใหญ่ใช้จริง
  • Exactly-once — ฟังดูในฝัน แต่ทำได้ยากมากในระบบกระจายจริง ส่วนใหญ่ที่อ้างว่า "exactly-once" คือ at-least-once + consumer ที่ออกแบบให้ idempotent (แนวคิดเดียวกับ Idempotency Key ในบทที่ 4) — เรียกซ้ำกี่ครั้งผลลัพธ์เหมือนเดิม จึงไม่สำคัญว่าจะได้รับข้อความซ้ำหรือไม่

เมื่อ consumer ประมวลผลข้อความเดิมล้มเหลวซ้ำ ๆ (เช่น payload ผิดรูปเสมอ) จะปล่อยให้มันบล็อกคิวไม่ได้ — ระบบคิวที่ดีจึงมี Dead-Letter Queue (DLQ): หลัง retry ครบจำนวนที่กำหนด ข้อความนั้นถูกย้ายไปพักไว้ต่างหากให้คนมาตรวจสอบ แทนที่จะวนซ้ำไม่รู้จบหรือหายไปเงียบ ๆ

9.3 Pub/Sub — หนึ่งข้อความ หลายผู้รับ

อีกรูปแบบคือ Publish/Subscribe: producer ส่งเข้า "topic" แล้ว ทุก subscriber ที่สนใจ topic นั้นได้รับสำเนาข้อความของตัวเอง เป็นอิสระต่อกันโดยสิ้นเชิง — ต่างจาก queue ที่ข้อความหนึ่งชิ้นมีผู้รับได้แค่รายเดียว ตัวอย่างจริง: event order.created หนึ่งชิ้น อาจต้องแจ้งทั้งบริการอีเมล, บริการวิเคราะห์ข้อมูล และบริการสต็อก — pub/sub ให้ทั้งสามบริการรับ event เดียวกันได้พร้อมกันโดยไม่ต้องรู้จักกันเลย เพิ่ม/ลบ subscriber ทีหลังก็ไม่กระทบ producer

Queue — ข้อความหนึ่งชิ้น ผู้รับหนึ่งรายProducerQueuemsg1 msg2 msg3Worker 1Worker 2msg1→Worker1, msg2→Worker2, msg3→Worker1 …เพิ่ม worker = แบ่งงานกันทำเร็วขึ้นแต่ละข้อความถูกทำ “ครั้งเดียว” โดยใครสักคนPub/Sub — ข้อความเดียว ทุกคนได้สำเนาProducerTopicorder.createdEmail ServiceAnalyticsInventoryแต่ละ service ได้สำเนา event เดียวกัน เป็นอิสระต่อกันโดยสิ้นเชิง
FIG 9.2 Queue กระจายภาระงานให้ worker หลายตัวช่วยกันทำ (แต่ละข้อความครั้งเดียว) · Pub/Sub กระจายสำเนาข้อความเดียวกันให้ subscriber ทุกตัวที่สนใจ topic นั้น

9.4 เปรียบเทียบเครื่องมือจริง — Kafka, RabbitMQ, SQS

สามชื่อที่เจอบ่อยที่สุดในงานจริง มีจุดเด่นคนละแบบ ไม่ใช่ตัวไหน "ดีที่สุด" สากล:

มิติKafkaRabbitMQAmazon SQS
โมเดลDistributed commit log — เก็บข้อความไว้ อ่านซ้ำ/replay ได้Message broker แบบดั้งเดิม — ส่งแล้วลบManaged queue เต็มรูปแบบ ไม่ต้องดูแล infra เอง
Throughputสูงมาก (หลักล้าน msg/วินาที)สูง แต่ต่ำกว่า Kafkaสูงมาก แต่ latency ต่อข้อความสูงกว่า
RoutingTopic + partition, ทำ pub/sub ผ่าน consumer group หลายกลุ่มยืดหยุ่นสุด (direct/topic/fanout exchange)เรียบง่าย ต้องพ่วง SNS ถ้าอยากได้ pub/sub
Opsดูแลเองซับซ้อน (หรือใช้ managed เช่น Confluent/MSK)ดูแลง่ายกว่า Kafkaแทบไม่ต้องดูแล — AWS จัดการให้ทั้งหมด
เหมาะกับEvent sourcing, stream processing, event backbone ขนาดใหญ่Task queue ทั่วไป, routing ซับซ้อน, ต้องการ latency ต่ำทีมที่อยู่บน AWS อยู่แล้วและอยากได้ความง่าย

🧭 เริ่มจากตรงไหนดี — ถ้าโจทย์คือ "งานเบื้องหลังธรรมดา" (ส่งอีเมล, ประมวลผลรูป) — คิวทั่วไปแบบ SQS/RabbitMQ ก็เกินพอ อย่าเริ่มด้วย Kafka เพราะมันมาพร้อม operational overhead ที่คุ้มค่าก็ต่อเมื่อต้อง เก็บ event ไว้ replay ได้ หรือมีทราฟฟิกระดับ stream ประมวลผลต่อเนื่องจริง ๆ — หลักการเดียวกับที่ย้ำมาตลอดเล่มนี้: เลือกเครื่องมือให้ตรงขนาดปัญหา ไม่ใช่ตามกระแส

9.5 ราคาที่ Async เรียกเก็บ

ความหลวมตัวที่ async ให้มา แลกมาด้วยความซับซ้อนที่ย้ายไปซ่อนอยู่จุดอื่น:

  • Ordering ไม่การันตีเสมอไป — ข้ามหลาย partition/consumer ลำดับที่ส่งอาจไม่ตรงกับลำดับที่ประมวลผลจริง ต้องออกแบบ partition key ให้ข้อความที่ต้องเรียงกัน (เช่น event ของ order เดียวกัน) ไปอยู่ partition เดียวกันเสมอ
  • Eventual consistency แทรกซึมทั่วระบบ — เชื่อมกับแนวคิด BASE/CAP ในบทที่ 2: service ที่ subscribe event อาจเห็นข้อมูล "ตามหลัง" อยู่เสี้ยววินาทีถึงหลักวินาทีเสมอ
  • Debug ยากขึ้น — ไม่มี call stack เดียวให้ไล่ตามอีกต่อไป ต้องพึ่ง correlation ID / distributed tracing ติดไปกับทุกข้อความเพื่อตามรอย flow ข้าม service
  • Consumer ต้อง idempotent เสมอ — เพราะ at-least-once คือค่าเริ่มต้นที่พบบ่อยที่สุด (9.2) การรับข้อความซ้ำคือเรื่องปกติที่ต้องออกแบบรองรับไว้ก่อน ไม่ใช่ข้อยกเว้น

สรุปบทที่ 9 — Synchronous ผูก service เข้าด้วยกันแน่น async ใช้คิวเป็นกันชนตัดการพึ่งพากันโดยตรง · Queue กระจายงานให้ผู้รับหนึ่งราย (scale ด้วย worker) ส่วน Pub/Sub กระจายสำเนาให้ทุก subscriber อิสระต่อกัน · At-least-once + idempotent consumer คือค่าเริ่มต้นที่ใช้จริงในโลก ไม่ใช่ exactly-once ในฝัน · Kafka เหมาะ event streaming ขนาดใหญ่, RabbitMQ เหมาะ task queue ทั่วไป, SQS เหมาะทีมที่อยากได้ความง่ายบน AWS · async ไม่ใช่ค่าเริ่มต้นของทุกงาน — ใช้เมื่อผู้ใช้รอได้จริง ๆ เท่านั้น

— จบหลักสูตร —

อ่านต่อ: Harness Engineering#


System Design Handbook — เรียบเรียงจากคอร์ส "System Design Explained: APIs, Databases, Caching, CDNs, Load Balancing & Production Infra" โดย Hayk Simonyan — เปิดคลิปบน YouTube

วิธีใช้เอกสารนี้ให้คุ้ม: อ่านทีละบทคู่กับช่วงเวลาในคลิป → วาดไดอะแกรมซ้ำด้วยมือเปล่า → ลองออกแบบระบบจริง (เช่น URL shortener, ระบบแชต) โดยใช้ครบทุกบท