System Design Handbook
ออกแบบระบบให้รองรับโลกจริงตั้งแต่เซิร์ฟเวอร์เดียวถึง production — API, ฐานข้อมูล, caching, load balancing, auth, rate limiting และ message queue
สารบัญ
- 01พื้นฐานระบบ & Single Server Setup
- 02การเลือกฐานข้อมูล — SQL vs NoSQL
- 03Scaling & Reliability — ขยายระบบและกำจัดจุดตาย
- 04การออกแบบ API — สัญญาที่ดีระหว่างระบบ
- 05โปรโตคอลสื่อสาร — ภาษาที่เครื่องคุยกัน
- 06API Styles — REST vs GraphQL
- 07Authentication & Authorization — ใครเป็นใคร ทำอะไรได้
- 08Security & Rate Limiting — ปกป้องระบบใน Production
- 09Message Queue & Async Communication — เมื่อไม่ต้องรอคำตอบทันที
- ·อ่านต่อ: Harness Engineering
ออกแบบระบบให้รองรับโลกจริง — ตั้งแต่เซิร์ฟเวอร์เดียวถึง 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 ในเบราว์เซอร์ สิ่งที่เกิดขึ้นตามลำดับคือ:
- DNS Lookup — เบราว์เซอร์ถาม DNS server ว่าโดเมนนี้ชี้ไปที่ IP อะไร (เช่น
203.0.113.10) - HTTP Request — เบราว์เซอร์เปิด connection ไปที่ IP นั้น แล้วส่งคำขอ เช่น
GET /home - Server ประมวลผล — เว็บเซิร์ฟเวอร์รับคำขอ, รันโค้ดแอป, อ่าน/เขียนฐานข้อมูล
- Response กลับ — ส่ง HTML/JSON กลับไปให้เบราว์เซอร์แสดงผล
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):
สิ่งที่ต้องตัดสินใจเสมอเมื่อใช้ 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)
🧭 มุมมองการออกแบบ — 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 แล้วข้อมูลไม่หาย แม้ไฟดับวินาทีถัดมา
2.2 NoSQL — สี่ตระกูล สี่โจทย์
"NoSQL" ไม่ใช่ฐานข้อมูลชนิดเดียว แต่เป็นคำเรียกรวมของฐานข้อมูลที่ สละความเคร่งครัดของ schema และ JOIN เพื่อแลกกับความยืดหยุ่นและการสเกลแนวนอน (scale-out) แบ่งเป็น 4 ตระกูลหลัก:
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 (ทำซ้ำข้อมูล) |
| Transaction | ACID เต็มรูปแบบ | ส่วนใหญ่ BASE / eventual consistency |
| การสเกล | ถนัด scale-up (เครื่องใหญ่ขึ้น) — scale-out ทำได้แต่ยาก | ออกแบบมาเพื่อ scale-out ตั้งแต่ต้น |
| Query | SQL ทรงพลัง วิเคราะห์เฉพาะหน้า (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)
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)
| มิติ | 2PC | Saga |
|---|---|---|
| Consistency | Strong — 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) | Horizontal (Scale Out) |
|---|---|---|
| วิธี | เพิ่ม CPU/RAM เครื่องเดิม | เพิ่มจำนวนเครื่อง |
| แก้โค้ด | แทบไม่ต้อง | ต้องทำแอปให้ stateless |
| เพดาน | มี — ฮาร์ดแวร์ใหญ่สุดในตลาด | แทบไม่มี |
| ความทนทาน | ยังเป็น SPOF | เครื่องหนึ่งตาย ที่เหลือรับแทน |
| ราคา | แพงขึ้นแบบ exponential | เพิ่มแบบ linear ตามจำนวนเครื่อง |
3.2 Load Balancer — จราจรกลางของระบบ
Load balancer (LB) คือด่านหน้าที่รับทุก request แล้วกระจายไปยังเซิร์ฟเวอร์ที่ "ยังแข็งแรง" เบื้องหลัง ทำสองหน้าที่พร้อมกัน: กระจายโหลด ให้ไม่มีเครื่องไหนล้น และ ตรวจสุขภาพ (health check) เพื่อหยุดส่งงานให้เครื่องที่ตาย
อัลกอริทึมกระจายโหลดที่ต้องรู้
| อัลกอริทึม | วิธีทำงาน | เหมาะกับ |
|---|---|---|
| Round Robin | วนส่งทีละเครื่องตามคิว 1→2→3→1... | เซิร์ฟเวอร์สเปกเท่ากัน งานแต่ละชิ้นหนักพอ ๆ กัน |
| Weighted Round Robin | วนตามคิวแต่เครื่องแรงรับสัดส่วนมากกว่า | เครื่องสเปกไม่เท่ากัน (ช่วงเปลี่ยนผ่านฮาร์ดแวร์) |
| Least Connections | ส่งให้เครื่องที่มีงานค้างน้อยที่สุด | งานแต่ละชิ้นใช้เวลาไม่เท่ากัน (เช่นมี request ช้า ๆ ปน) |
| IP Hash | hash 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 ตาย
3.5 Database Sharding — เมื่อ Replication ไม่พอ
Replication ที่เล่าไปแก้ปัญหาการ อ่าน ได้ดี แต่ไม่ช่วยเรื่อง เขียน หรือ พื้นที่เก็บข้อมูล เลย — เพราะเขียนได้ที่ primary ตัวเดียวเสมอ และทุก replica ต้องเก็บสำเนาข้อมูล "ทั้งหมด" เท่ากับ primary เมื่อข้อมูลใหญ่เกินดิสก์เครื่องเดียว หรือเขียนถี่เกินที่ primary ตัวเดียวรับไหว ทางออกคือ Sharding (horizontal partitioning): ตัดแถวของตารางเดียวกันออกเป็นส่วน ๆ กระจายไปอยู่คนละเครื่อง (shard) ที่แต่ละตัวรับทั้งอ่านและเขียนของส่วนตัวเองอย่างอิสระ
| กลยุทธ์ | วิธีแบ่ง | ข้อแลก |
|---|---|---|
| Range-based | แบ่งตามช่วงของ shard key เช่น user_id 1–1M → shard 1 | Query แบบช่วงทำง่าย แต่เสี่ยง "shard ร้อน" ถ้าข้อมูลใหม่กระจุกที่ shard ล่าสุด |
| Hash-based | hash(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
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 ต่างกันชัดเจน:
4.5 Versioning — เผื่อทางให้อนาคต
สักวันเราต้องเปลี่ยนสัญญาแบบที่ client เก่าพัง (breaking change) เช่น เปลี่ยนชื่อฟิลด์ หรือโครงสร้าง response — versioning คือทางหนีไฟ มี 3 สไตล์:
| วิธี | หน้าตา | ข้อสังเกต |
|---|---|---|
| URI path | /v1/orders → /v2/orders | ชัดเจน เห็นด้วยตา cache ง่าย — นิยมที่สุด |
| Header | Accept: application/vnd.api+json;version=2 | URL สะอาด แต่ 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) — หายก็ส่งซ้ำ มาสลับลำดับก็เรียงคืนให้
5.3 UDP — เร็วก่อน ครบไม่สัญญา
UDP (User Datagram Protocol) ตรงข้ามทุกอย่าง: ไม่มี handshake ไม่มี ACK ไม่มีการเรียงลำดับ — แค่ยิง packet ออกไปแล้วหวังว่าถึง ฟังดูแย่ แต่สำหรับงานบางประเภท "ข้อมูลเก่า" แย่กว่า "ข้อมูลหาย": วิดีโอคอลที่เฟรมหายไปเฟรมหนึ่งดีกว่าภาพค้างรอส่งซ้ำ เกมออนไลน์ที่ตำแหน่งล่าสุดสำคัญกว่าตำแหน่งเมื่อ 200ms ก่อน
| มิติ | TCP | UDP |
|---|---|---|
| การเชื่อมต่อ | ต้อง 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
✅ สรุปบทที่ 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 เพิ่ม
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 ตารางเปรียบเทียบ
| มิติ | REST | GraphQL |
|---|---|---|
| Endpoint | หลายตัว ตาม resource | ตัวเดียว (/graphql) |
| รูปร่าง response | server กำหนด | client กำหนดต่อ query |
| Round trips | มักหลายรอบต่อหน้าจอ | รอบเดียว |
| Caching | HTTP/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
จุดแลกเปลี่ยนที่ต้องท่องให้ขึ้นใจ: JWT เพิกถอนกลางอากาศไม่ได้ — token ที่เซ็นแล้วจะใช้ได้จนกว่าจะหมดอายุ (exp) ต่อให้ผู้ใช้ถูกแบนไปแล้วก็ตาม ทางแก้มาตรฐานคือคู่ token:
- Access Token — อายุสั้นมาก (5–15 นาที) ใช้แนบทุก request ใน header
Authorization: Bearer <token> - Refresh Token — อายุยาว (วัน/สัปดาห์) เก็บอย่างปลอดภัย ใช้ ขอ access token ใหม่ เมื่อตัวเก่าหมดอายุ — และฝั่งเซิร์ฟเวอร์เพิกถอน refresh token ได้ (จุดนี้ stateful) ทำให้แบนผู้ใช้มีผลภายในไม่กี่นาที
| มิติ | Session + Cookie | JWT |
|---|---|---|
| เก็บ 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)
สองรายละเอียดที่ใช้จริง: 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 | นับต่อช่วงเวลาตายตัว ง่าย+ประหยัดสุด | งานภายใน ความเป๊ะไม่วิกฤต |
| 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 | Asynchronous |
|---|---|---|
| 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
9.4 เปรียบเทียบเครื่องมือจริง — Kafka, RabbitMQ, SQS
สามชื่อที่เจอบ่อยที่สุดในงานจริง มีจุดเด่นคนละแบบ ไม่ใช่ตัวไหน "ดีที่สุด" สากล:
| มิติ | Kafka | RabbitMQ | Amazon SQS |
|---|---|---|---|
| โมเดล | Distributed commit log — เก็บข้อความไว้ อ่านซ้ำ/replay ได้ | Message broker แบบดั้งเดิม — ส่งแล้วลบ | Managed queue เต็มรูปแบบ ไม่ต้องดูแล infra เอง |
| Throughput | สูงมาก (หลักล้าน msg/วินาที) | สูง แต่ต่ำกว่า Kafka | สูงมาก แต่ latency ต่อข้อความสูงกว่า |
| Routing | Topic + 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, ระบบแชต) โดยใช้ครบทุกบท