สิ่งที่พบเจอได้บ่อยๆ เลย คือ การใช้ pub/sub ที่สิ้นเปลือง ใช้มันกับทุกๆ อย่าง โยนกันสนุกสนานเลย เมื่อเราเอามันมาใช้กันแบบนี้ แล้ว มันก็ตามมาด้วยปัญหาต่างๆ ที่ตามมา และถ้าเราไม่ได้ handle ปัญหาเหล่านั้น ก็คงไม่ต้องแปลกใจที่จะต้อง support incident ที่เข้ามา รวมถึงความยุ่งยากในการจัดการมัน

บอกไว้ก่อนเลยว่า ไม่ได้บอกว่า มันไม่ดี หรือ ห้ามใช้มันนะ

การที่เราจะใช้งานอะไรสักอย่าง เราต้องเข้าใจที่มาที่ไป concept และ ข้อดี/ข้อเสีย ของสิ่งเหล่านั้น เพราะในการพัฒนา software ไม่มี เครื่องมือไหนที่แก้ปัญหาได้ครอบจักวาล

ดังนั้น การเลือก concept, technology หรือ tools ต่างๆ มาใช้งาน มันเป็นเหมือนการเลือกว่า เราจะแก้ปัญหาเรื่องอะไร และ จะรับมือกับปัญหารูปแบบไหน (ที่มากับสิ่งที่เราเลือก)

งงกันมั้ยนะ ?

ผมชอบบอกทุกคนว่า เราไม่ได้ แค่เลือก สิ่งที่เราจะนำมาใช้ แต่เราเลือกว่าเราต้องการจะเจอกับปัญหา (Challenge) อะไรด้วย

ดังนั้น เราก็ดูและคิดให้ดีๆ บางอย่างมันอาจจะง่ายในการใช้งาน แต่อาจจะเป็นเรื่องยุ่งยากในมุมอื่นๆ ก็ได้

บทความนี้ผมเลยรวมปัญหาที่เราจะเจอได้ เมื่อนำเอา pub/sub มาใช้งาน


ปัญหาของ Pub/Sub

ขอดีของการใช้งาน pub/sub ก็คงไม่ต้องบอกกันแล้ว ทุกคนน่าจะรู้กันแล้วแหละ ข้ามมาพูดถึงปัญหาของ pub/sub กันเลยละกัน

ปกติผมจะเรียกว่า challence แทนที่จะเรียกว่า ปัญหา

ปัญหาที่เราจะเจอในการนำเอา pub/sub มาใช้งาน คือ

Message Loss (ข้อมูลหายระหว่างทาง)

เนื่องจากการทำงานของ pub/sub นั้น ต้นทางจะโยนข้อมูลไปเลย (publisher) โดยไม่สนใจว่าปลายทางจะได้รับข้อมูลหรือเปล่า ส่วนปลายทาง (subscriber) ทำให้ถ้าเราไม่ต้องรอ feedback จาก ปลายทางว่าได้รับข้อมูลหรือเปล่า สิ่งนี้เป็นสิ่งที่หลายๆ คนชอบ เพราะได้ไม่ต้องสนใจว่า อีกฝั่งจะเป็นอย่างไร ไม่ต้องมา handle ผลลัพธ์ ยิ่งถ้าเราพัฒนากันคนละทีม ยิ่งสบายตัว เพราะไม่ต้องคุยกับทีมอื่น ไม่ต้องรอให้อีกทีม พัฒนาของตัวเองให้เสร็จ แล้วเอามา integrate กัน หลายๆ คนเลยชอบวิธีนี้ (ซึ่งไม่ใช่วิธีที่ควรทำ)

ฟังเหมือนจะดีนะ แต่เราจะรู้ได้ไงว่า ข้อมูลส่งไปถึงแล้ว?

บ่อยครั้ง ที่การส่งข้อมูลผ่าน pub/sub นั้น มักมีการหายของข้อมูลอยู่บ่อยๆ เพราะตัว pub/sub นั้นไม่ได้การันตี (สนใจ) ว่า ข้อมูลจะส่งไปถึงปลายทางไหม

ดังนั้นเมื่อข้อความถูกส่งออกจากฝั่ง publisher แล้ว หาย ไปก่อนถึง consumer หรือ consumer ไม่สามารถประมวลผลได้สำเร็จ และ ไม่มีการส่งซ้ำ ทำให้ข้อมูลสูญหายถาวร

ที่เคยเจอ การแก้ปัญหา คือ หลายๆ คน ใช้วิธีการพ่น log (info) เพื่อไว้กลับมาดูว่ามีการส่งข้อมูลมาหรือเปล่า ซึ่งไปเปลือง cost ค่าเก็บ log อีก (ดันเอา log มาใช้เป็นตัว audit ซะงั้น)

ดังนั้นไม่ต้องแปลกใจ ถ้าหากเราต้องคอยมา support เคส ที่ว่า ข้อมูลหาย ข้อมูลไม่เข้า ต้องคอยมานั่นเช็ค log ว่า ข้อมูลมาไหม ซึ่งมันคงไม่ใช้เรื่องที่ควรจะทำเท่าไหร่

สาเหตุที่ทำให้เกิดปัญหานี้ คือ

  • At‑Most‑Once Delivery – ระบบบางประเภท (เช่น Redis Pub/Sub) ส่งข้อความเพียงครั้งเดียว ถ้า consumer ไม่ออนไลน์ หรือ connection หลุด ข้อความนั้นจะไม่ถูกส่งซ้ำ
  • ไม่มี Acknowledgement (ACK) – ฝั่ง publisher ไม่รอการยืนยันจาก consumer ว่า ได้รับแล้ว ไม่มีการ retry
  • Network Failure – ปัญหาเครือข่ายระหว่าง Publisher–Broker หรือ Broker–Consumer
  • Broker Crash / Data Eviction – ถ้า broker ไม่เก็บ persistence หรือ มีการ trim log/queue ทำให้ข้อความเก่าถูกลบก่อนที่ consumer จะอ่าน
  • Consumer Error Consumer – ล้มเหลวระหว่างประมวลผล และ ไม่มี retry หรือ DLQ (Dead Letter Queue) รองรับ

แนวทางป้องกัน/บรรเทา

  • ใช้ระบบที่รองรับ Persistence + ACK เช่น Kafka, RabbitMQ, หรือ Redis Streams (ไม่ใช่ Pub/Sub ธรรมดา) เพื่อเก็บข้อความจนกว่าจะ ACK
  • Retry Policy + Backoff ตั้งค่า retry พร้อม exponential backoff ลดโอกาสส่งซ้ำเร็วเกินไป
  • Dead Letter Queue (DLQ) เก็บข้อความที่ส่งไม่สำเร็จ เพื่อวิเคราะห์และประมวลผลภายหลัง
  • Monitoring & Alerting ตรวจจับอัตรา message loss สูงผิดปกติ
  • Idempotent Consumer ออกแบบ consumer ให้ประมวลผลซ้ำได้ โดยไม่ทำให้ state เพี้ยน เพื่อรองรับกรณี retry

Duplicate Messages (ส่งข้อมูลซ้ำ)

นอกจากข้อมูลหายแล้ว มีบ่อยครั้งที่ pub/sub จะส่งข้อมูลเดิมมา

เราจะ verify ยังไง ว่า ข้อมูลนั้นเป็นข้อมูลใหม่ หรือ ข้อมูลเก่า?

ในเคสนี้ถ้าเราไม่ได้ handle ได้ ก็จะทำให้ระบบของเราทำงานซ้ำซ้อน (double processing) ได้ ซึ่งฟังดูเผินๆ การประมวลผลข้อมูลซ้ำอาจจะดูไม่มีปัญหาอะไร (ก็บ้าแล้วววววว)

การทำระบบให้มี performance ที่ดี ใช้ resource เกินความจำเป็น เกกิดการทำงานซ้ำ โดยไม่เกิดประโยชน์อะไร ถือเป็นสิ่งที่ไม่ควรเกิดขึ้น

หนักสุด คือ ถ้าข้อมูลนั้นต้องนำมาประมวลผล อาจทำให้ข้อมูลผิดได้

สาเหตุมักมาจาก

  • At‑least‑once Delivery – หลาย broker (เช่น Kafka, RabbitMQ, Google Pub/Sub) เลือกส่งซ้ำเพื่อให้มั่นใจว่าข้อความถึงปลายทาง มันเลย ทำให้เกิด duplicate ได้
  • Retry Mechanism – ถ้า consumer ไม่ ack ภายในเวลาที่กำหนด broker จะส่งซ้ำ แม้ว่า consumer จะประมวลผลเสร็จแล้ว แต่ ack ช้าก็ตาม
  • Network Glitch – การส่ง ack หรือ commit ล้มเหลว ทำให้ broker คิดว่าข้อความยังไม่ถูกประมวลผล
  • Failover / Rebalance – เมื่อ consumer group มีการย้าย partition หรือ instance ล่ม อาจมีการ re‑deliver ข้อความ
  • Producer Retries – ฝั่ง producer ส่งซ้ำเพราะไม่มั่นใจว่าข้อความถูก publish สำเร็จ

แนวทางป้องกันและบรรเทา (แก้ไม่หายหรอก หึหึ)

  • Idempotent Consumer – ออกแบบให้การประมวลผลซ้ำให้ผลลัพธ์เหมือนเดิม เช่น ตรวจสอบว่ามี record นี้แล้วก่อน insert
  • Deduplication Logic – ใช้ message ID หรือ hash เก็บใน cache/DB เพื่อกรอง duplicate
  • Exactly‑once Semantics – ถ้า broker รองรับ (เช่น Kafka + transactional producer/consumer)
  • Ack ให้เร็วและถูกจุด – ย้ายขั้นตอน ack หลังจากประมวลผลสำเร็จจริง
  • Monitoring & Alert – ติดตาม metric เช่น duplicate rate, re‑delivery count เพื่อหาสาเหตุ

Ordering (ไม่การันตีลำดับข้อความ)

เมื่อการส่งข้อมูลผ่าน pub/sub เป็นเหมือนกับการหวังพึ่งโชคชะตา อีกหนึ่งปัญหาที่เจอก็ คือ ของที่ส่งมาไม่เรียงลำดับ

ถ้าเรานำ pub/sub ไปใช้กับงานที่ต้องการอัพเดท หรือ ประมวลผลแบบเป็นลำดับ ปัญหานี้ก็จะเกิดขึ้นมาในทันที การแสดงผล ก็จะสลับไปมา business logic เพี้ยน (ซึ่งสำหรับบางงานไม่ควรเกิดขึ้น)

เราจะเจอปัญหา supports เรื่องข้อมูลที่ไม่เรียงลำดับได้จากลูกค้าได้

สาเหตุเกิดจาก

  • การประมวลผลแบบขนาน (Parallelism) หลาย consumer instance อาจดึงข้อความจาก queue/topic พร้อมกัน ทำให้ ลำดับการประมวลผลไม่ตรงกับลำดับการส่ง
  • Partition/Sharding Broker อย่าง Kafka หรือ Pub/Sub จะแบ่งข้อมูลเป็นหลาย partition เพื่อเพิ่ม throughput ทำให้ การรับประกันลำดับจะมีแค่ภายใน partition เดียวเท่านั้น
  • Retry & Redelivery แน่นอนว่า ถ้าเรามีการ retry ข้อความบางตัวซ้ำ อาจถูกประมวลผลช้ากว่าข้อความที่มาทีหลังได้
  • Network & Broker Internals การส่งผ่านเครือข่าย และ การจัดคิวภายใน broker อาจทำให้เกิดการเปลี่ยนแปลงของลำดับได้

แนวทางแก้ไข

  • Key‑based Partitioning ใช้ key เดียวกัน (เช่น userId, orderId) เพื่อให้ event ที่เกี่ยวข้องอยู่ใน partition เดียว ทำให้ลำดับภายใน key ถูกคงไว้
  • Sequence Number / Versioning ใส่หมายเลขลำดับหรือ version ใน payload ให้ตัว consumer ตรวจสอบ และ discard event ที่ล้าสมัย
  • Reordering Buffer เก็บ event ชั่วคราวแล้วจัดเรียงก่อนประมวลผล ซึ่งวิธีนี้ต้องแลกมากกับ latency เพิ่ม
  • Exactly‑once Processing (ถ้าบริการรองรับนะ) ลดปัญหาซ้ำ และ ช่วยให้ logic จัดการลำดับง่ายขึ้น
  • Design for Out‑of‑Order เขียน business logic ให้ทนต่อการได้ event ไม่เรียง เช่น ใช้ state reconciliation แทนการพึ่งพาลำดับ 100%

Overhead จาก Loose Coupling

แม้จะดีต่อการแยกส่วน แต่ทำให้การทำความเข้าใจ flow ของระบบซับซ้อนขึ้น จริงอยู่ว่า การที่เราพยายามให้เกิด Loose Coupling ลดการพึ่งพากันของ component ต่างๆ

แต่การที่แต่ละส่วนทำงานอิสระและสื่อสารผ่าน message/event ก็ทำให้เกิด ค่าใช้จ่ายด้านการประสานงาน (Coordination Costs) และ การทำความเข้าใจระบบ มากขึ้น

เช่น

  • Latency เพิ่มขึ้น – เพราะต้องผ่าน broker/queue และ serialization/deserialization หลายชั้น
  • Observability ยากขึ้น – การ trace เส้นทางข้อมูลต้องใช้ distributed tracing และ logging ที่ซับซ้อน
  • Data Consistency – การ decouple ด้านเวลา (time autonomy) ทำให้เกิด eventual consistency ต้องมี logic จัดการเพิ่ม
  • Integration Overhead – ต้องมี schema, contract, และ versioning ที่ชัดเจน มิฉะนั้นจะเกิด breaking change ได้ง่าย
  • Operational Cost – ต้องดูแล message broker, monitoring, retry policy, DLQ ฯลฯ ซึ่งเพิ่มภาระทีม Ops

แนวทางลด Overhead

  • ออกแบบ Contract ชัดเจน – ใช้ schema registry และ enforce backward compatibility
  • เพิ่ม Observability – Distributed tracing + correlation ID ในทุก message
  • Idempotent Consumer – ป้องกันผลกระทบจาก duplicate message
  • เลือกใช้ Pub/Sub อย่างเหมาะสม – ไม่ใช้กับทุก use case โดยเฉพาะงานที่ต้องการ strong consistency หรือ synchronous feedback
  • Test ภายใต้ Load และ Failure Scenario – เพื่อดูว่าระบบรับมือ overhead ได้แค่ไหน

Backpressure

ในระบบซอฟต์แวร์ หมายถึง แรงต้านการไหลของข้อมูลใน pipeline ส่วนในบริบทของ Pub/Sub และ ระบบ event‑driven “Backpressure” คือ กลไก หรือ ภาวะที่ ฝั่ง consumer/subscriber ประมวลผลข้อมูลได้ช้ากว่าฝั่ง producer/publisher ส่งมา

จนเกิดการสะสมของข้อความในคิว หรือ บัฟเฟอร์ ซึ่งถ้าไม่จัดการให้ดี จะลุกลามเป็นปัญหา latency สูง, ใช้ memory เกิน, หรือ แม้กระทั่ง message loss

สาเหตุในระบบ Pub/SubProducer ส่งเร็วเกินไป (high throughput)Subscriber มีขั้นตอนประมวลผลหนัก เช่น I/O blocking, CPU‑bound taskNetwork หรือ broker มีข้อจำกัด bandwidth/connection

ผลลัพธ์ คือ คิวโตขึ้น → latency เพิ่ม → อาจเกิด timeout หรือ drop message

วิธีจัดการ Backpressure

  • Rate Limiting / Throttling – จำกัดอัตราการส่งจาก producer ให้สอดคล้องกับความสามารถของ consumer
  • Buffer Management – ตั้งขนาดบัฟเฟอร์ให้เหมาะสม และมีนโยบาย drop หรือ reject เมื่อเต็ม
  • Consumer Scaling – เพิ่มจำนวน consumer instance เพื่อกระจายโหลด
  • Flow Control Protocol – ใช้โปรโตคอลที่รองรับการแจ้งความพร้อม เช่น Reactive Streams, gRPC flow control
  • Prioritization – จัดลำดับความสำคัญของข้อความ เพื่อให้ critical message ได้รับการประมวลผลก่อน

Visibility & Debugging

มันความสามารถในการ “มองเห็น” และ “ติดตาม” เส้นทางของข้อความ (message flow) และสถานะของระบบ เพื่อให้สามารถวิเคราะห์ แก้ไข หรือป้องกันปัญหาได้อย่างมีประสิทธิภาพ

ในการใช้งาน pub/sub เพื่อให้ได้ความเป็น Distributed ทำให้ข้อมูลวิ่งผ่านหลาย service/broker ถ้าไม่มีการติดตามที่ดี จะหาต้นตอปัญหายาก

ดังนั้นจำเป็นที่จะต้องสร้างส่วนของ Incident Response ขึ้นมา เพื่อให้เรารู้ได้เร็วขึ้นว่า message หาย/ค้าง/ซ้ำ เกิดที่จุดไหน และ ช่วยลด Blind Spot ในการสื่อสารระหว่างทีมพัฒนาและทีมปฏิบัติการ

ปัญหาที่เราพบบ่อยๆ เลย คือ

  • ทำ trace ไม่ครบ – ทำให้ไม่รู้ว่า message ผ่าน service ไหนบ้าง ส่งผลให้เกิดความช้าในการแก้ปัญหา
  • Log ใช้แทน Audit – เก็บ log เยอะเกินไป เพื่อเช็คการส่ง message ทำให้ค่า storage สูง, noise เยอะ
  • ไม่มี Correlation ID – ยากต่อการเชื่อมโยง event เดียวกันข้าม service
  • Tooling ไม่สอดคล้องกัน – แต่ละทีมใช้ logging/monitoring คนละแบบ ทำให้ข้อมูลไม่เชื่อมกันอีก
  • Debug ใน Production ยาก – ไม่มีวิธี replay message หรือ จำลองเหตุการณ์ สุดท้ายวกกลับไปที่ bullet ที่ 2 ต้องไปดูที่ log แทน

แนวทางเพิ่ม Visibility

  • Correlation ID / Trace ID – ใส่ ID เดียวกันในทุก log และ message ที่เกี่ยวข้อง เพื่อให้ trace ได้ครบวงจร
  • Distributed Tracing – ใช้เครื่องมืออย่าง OpenTelemetry, Jaeger, Zipkin เพื่อดูเส้นทาง end‑to‑end
  • Structured Logging – เก็บ log ในรูปแบบ JSON/structured format เพื่อให้ query และ filter ได้ง่าย
  • Message Audit Store – เก็บสำเนา message สำคัญ (พร้อม metadata) เพื่อใช้ตรวจสอบย้อนหลัง
  • Metrics & Alerts – ติดตาม latency, backlog size, error rate และตั้ง alert เมื่อเกิน threshold
  • Replay & Simulation – มีระบบ replay message เพื่อ debug หรือทดสอบ flow โดยไม่กระทบ production

Schema / Serialization Issues

ปัญหาเรื่องโครงสร้างข้อมูล (schema) ไม่ตรงกัน ก็เป็นอีกหนึ่งปัญหา ถ้าหาก schema ของ Publisher และ Consumer ไม่สอดคล้องกัน มันก็ส่งผลให้ไม่สามารถอ่าน หรือ ประมวลผลข้อความได้ถูกต้อง

ลองนึกภาพว่า เรามีทีม 3 ทีม ที่นำข้อมูลเดียวกันไปใช้งาน แต่พบว่า มีหนึ่งทีม เปลี่ยน schema ที่ public มา แล้วไม่บอกทีมอื่นๆ ผลจะเป็นอย่างไร

จริงๆ อันนี้เป็นปัญหาเรื่องการ collaborate งานร่วมกันนะ แต่บ่อยครั้งที่พบคือ พอมันเป็น pub/sub หลายทีมก็มักจะไม่ค่อยคุยกัน

บ่อยครั้งเรามักพบปัญหาพวกนี้

  • Schema Evolution ไม่สอดคล้อง เปลี่ยนฟิลด์, ลบฟิลด์, หรือเปลี่ยนชนิดข้อมูล โดยไม่รองรับ backward/forward compatibility
  • Serialization Format ไม่ตรงกัน เช่น Publisher ใช้ Avro แต่ Consumer ใช้ JSON หรือการใช้ library/เวอร์ชันต่างกัน ก็ส่งผลได้เช่นกัน
  • Missing Schema Registry ไม่มีตัวกลางในการเก็บ schema ทำให้แต่ละ service ใช้ schema ต่างกัน
  • Custom Serializer/Deserializer Bug เขียน SerDes เองแล้ว handle edge case ไม่ครบ เช่น null, default value
  • Encoding/Decoding Mismatch เช่น UTF‑8 vs UTF‑16 หรือ big‑endian vs little‑endian

แนวทางป้องกัน/บรรเทา

  • ใช้ Schema Registry เช่น Confluent Schema Registry, AWS Glue เพื่อเก็บและจัดการเวอร์ชัน schema
  • ออกแบบ Schema ให้รองรับ Evolution เช่น เพิ่มฟิลด์ใหม่ต้องมี default, หลีกเลี่ยงการลบ หรือ เปลี่ยน type ของฟิลด์เดิม
  • กำหนด Serialization Format ชัดเจน เช่น Avro, Protobuf, JSON พร้อมระบุเวอร์ชันและ library ที่ใช้ด้วย
  • Validation ก่อน Deploy ใช้ contract testing หรือ schema compatibility check
  • Graceful Error Handling จัดการ SerializationException ให้ consumer ไม่ crash และ ส่ง message ไป DLQ เพื่อวิเคราะห์
  • Monitoring เก็บ metric ของ serialization error rate เพื่อจับปัญหาเร็ว

เห็นไหมว่า ถ้าเรานำเอา pub/sub มาใช้งานให้มีประสิทธิภาพนั้น มันมี challence ที่ต้องเจอ และ ต้องทำเยอะมาก นี่ยังไม่ได้พูดถึงเรื่อง หยุมหยิมอย่าง Timeout, Client‑side bottleneck หรือ การ Scale ระบบนะ

ดังนั้น การที่เราจะหยิบเอามันมาใช้ เราควรที่จะคิดให้ดีๆ ว่า ระบบที่เรากำลังพัฒนาอยู่นั้น จำเป็นต้องใช้มันหรือเปล่า หรือ เราจะใช้มันแค่ส่วนไหน เพราะถ้าหากเราหยิบมันมาใช้โดยไม่ได้คิดให้ครบแล้ว เราก็จะต้องพบเจอกับปัญหาต่างๆ ที่กล่าวมา มากวนใจทีมพัฒนา อยู่เรื่อยๆ

ดังนั้น สำหรับผมแล้ว ระบบที่เหมาะกับการใช้งาน pub/sub ต้องลักษณะดังนี้

  • Event‑Driven / Asynchronous โมดูลไม่ต้องรอผลตอบกลับทันที หรือ ไม่ต้องรอการตอบกลับ เพื่อ ลด coupling ระหว่าง service (แต่ต้อง handle ให้ดีๆ)
  • ไม่ใช่ข้อมูล หรือ การทำงานที่สำคัญ ข้อมูลหายได้ ส่งซ้ำได้ เช่น ระบบแจ้งเตือน Notification, Email เป็นต้น
  • หลาย Consumer ใช้ข้อมูลเดียวกัน เช่น Event เดียวกระจายไปแจ้งเตือน, อัปเดต cache, เก็บ analytics พร้อมกัน
  • ต้องการ Scalability สูง เพิ่มจำนวน consumer ได้โดยไม่กระทบ publisher
  • Real‑time Notification แจ้งเตือนผู้ใช้ หรือ ระบบอื่นทันที เมื่อมีเหตุการณ์เกิดขึ้น
  • Cross‑Team Integration แต่ละทีมพัฒนา service ของตัวเองได้อิสระ (แค่ตกลง schema และ topic)

ตัวอย่างระบบก็เช่น

  • ระบบ Notification
  • ระบบ แจ้งเตือน Fraud Detection ของธนาคาร
  • Feature Flag Rollout แบบ Real‑time
  • IoT Sensor Network ที่มีการอัพเดทข้อมูลบ่อยๆ ทุก 5 วินาที
  • Supply Chain Tracking

สุดท้าย ที่บอกมา ทั้งหมด ไม่ได้บอกว่า ห้ามใช้ pub/sub แค่อยากจะบอกว่า ให้เลือกให้งาน pub/sub ให้เหมาะสมกับลักษณะของระบบที่เราทำ รวมถึง Software Architecture Style ที่เราเลือก เพราะเมื่อเราเลือกที่จะใช้มันแล้ว มันมี costs และ challences ที่ต้องจัดการ รับมือกับมัน