Delivery Guarantees
สาม guarantee กับทางเลือกจริง ๆ หนึ่งเดียว
หัวข้อที่มีชื่อว่า “สาม guarantee กับทางเลือกจริง ๆ หนึ่งเดียว”ทุก messaging system มี delivery guarantee แบบใดแบบหนึ่ง มีสามแบบที่คุณจะได้ยิน:
| Guarantee | ความหมาย | ความเสี่ยง |
|---|---|---|
| At-most-once | แต่ละ message ถูกส่ง 0 หรือ 1 ครั้ง | หาย — message หายได้ |
| At-least-once | แต่ละ message ถูกส่ง 1 ครั้งขึ้นไป | ซ้ำ — message ซ้ำได้ |
| Exactly-once | แต่ละ message ถูกส่งพอดี 1 ครั้ง | (ความฝัน) |
ความจริงที่น่าอึดอัด: ในระบบ distributed ที่มี network และ crash exactly-once delivery ทำไม่ได้ เป็น myth ดังนั้นทางเลือกจริง ๆ ในการออกแบบคือระหว่าง at-most-once กับ at-least-once — คุณกำลังเลือกว่าจะทน failure แบบไหนได้: message หาย หรือเห็น message ซ้ำสองครั้ง
ทำไม “exactly-once” ถึงเป็น myth
หัวข้อที่มีชื่อว่า “ทำไม “exactly-once” ถึงเป็น myth”ลองนึกภาพจังหวะที่ consumer ทำงานเสร็จ ตรงนั้นมีสองอย่างที่ต้องทำ: process message และ acknowledge crash เกิดขึ้น ระหว่าง สองอย่างนั้นได้ และไม่มีทางทำให้ทั้งคู่เป็น atomic ข้าม network:
sequenceDiagram participant Q as Queue participant C as Consumer Q->>C: deliver message C->>C: process (do the work) Note over C: crash HERE, before ack Note over Q: no ack arrived → redeliver Q->>C: deliver AGAIN (duplicate)
ถ้า consumer crash หลัง ทำงานเสร็จแต่ ก่อน ack ถึง RabbitMQ จะ (อย่างถูกต้องและปลอดภัย) สมมติว่างานไม่เคยเกิดขึ้นและ redeliver งานรันสองครั้ง ไม่มีความฉลาดของ broker ใดลบช่องว่างนี้ได้ — process กับ ack ไม่ใช่ขั้นตอน atomic เดียว
นั่นคือเหตุผลที่ at-least-once เป็น default ที่ซื่อสัตย์และปลอดภัย: RabbitMQ ยอม deliver สองครั้งดีกว่าเสี่ยงทำ message หาย เลือก at-most-once (auto-ack, ไม่มี persistence) เฉพาะตอนที่ message หายนาน ๆ ทีไม่สำคัญจริง ๆ — metrics, live dashboard, ephemeral event
Effectively-once: at-least-once + idempotency
หัวข้อที่มีชื่อว่า “Effectively-once: at-least-once + idempotency”ข่าวดี: จริง ๆ คุณไม่ต้องการ exactly-once delivery คุณต้องการ exactly-once effect และคุณได้สิ่งนั้นด้วยการทำให้ consumer idempotent — process message เดิมสองครั้งให้ผลเหมือน process ครั้งเดียว
วิธี idempotent ที่ใช้ได้จริง:
- Idempotency key ให้แต่ละ message มี unique id ที่เสถียร (
message_idหรือ business key อย่างorder_id) ก่อนทำอะไรก็บันทึก “ฉันจัดการ id X แล้ว” ลง database (unique constraint, upsert,SETNX) duplicate จะเจอว่า key มีอยู่แล้วและไม่ทำอะไร - operation ที่ idempotent โดยธรรมชาติ “set status = shipped” รันซ้ำสองครั้งปลอดภัย ส่วน “เพิ่ม balance 10” ไม่ปลอดภัย เลือก set/upsert แทน increment ตรง ๆ เมื่อทำได้
- dedup ตอน write ใช้ unique constraint ของ database บน business key เพื่อให้ duplicate insert fail แบบไม่มีอันตราย
รวม at-least-once delivery ของ RabbitMQ กับ idempotent consumer แล้วคุณได้ effectively-once processing — message อาจมาถึงสองครั้ง แต่ มีผล แค่ครั้งเดียว นั่นคือเป้าที่ถูกต้องและทำได้จริง