Skip to content
Tayakorn
System Design Handbook

System Design Handbook

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, ระบบแชต) โดยใช้ครบทุกบท

If this was useful —buy me a coffee

Read the full book