Dead-Letter Exchanges
ปัญหา: message ที่ process ไม่ได้
หัวข้อที่มีชื่อว่า “ปัญหา: message ที่ process ไม่ได้”consumer ดึง message มา พยายาม process แล้ว fail — payload พัง, downstream service ล่ม หรือ record ที่อ้างถึงไม่มีอยู่ แล้วยังไงต่อ?
ถ้าคุณ nack แบบ requeue RabbitMQ จะเอา message กลับไปไว้หน้า queue คุณดึงมาอีก แล้วก็ fail อีก แล้วคุณก็ได้ poison message วนอยู่ในลูปแน่น ๆ เผา CPU และ block ทุกอย่างที่อยู่ข้างหลัง ถ้าคุณแค่ drop ทิ้ง คุณก็ทำข้อมูลหายแบบเงียบ ๆ ทั้งสองอย่างรับไม่ได้
ทางออกคือส่ง message ที่ fail ไปที่อื่นโดยตั้งใจ “ที่อื่น” นั้นคือ dead-letter exchange (DLX)
อะไรทำให้ message ถูก dead-letter
หัวข้อที่มีชื่อว่า “อะไรทำให้ message ถูก dead-letter”message จะถูก dead-letter — route ไปยัง DLX ที่ตั้งไว้ของ queue — เมื่อเกิดกรณีใดกรณีหนึ่ง:
- ถูก reject หรือ nack แบบ
requeue=false(consumer บอกว่า “อย่าเอาอันนี้กลับมาให้ฉัน”) - message TTL หมดอายุ ขณะนั่งอยู่ใน queue
- queue ชน limit max-length และ message ถูก drop เพื่อเปิดที่ว่าง (overflow)
คุณผูก DLX เข้ากับ queue ด้วย argument x-dead-letter-exchange (และถ้าต้องการก็ x-dead-letter-routing-key เพื่อเปลี่ยน label ตอนออก)
flowchart LR q["work queue (x-dead-letter-exchange: dlx)"] -->|"nack requeue=false or TTL expiry or overflow"| dlx["DLX (exchange)"] dlx --> dlq["dead-letter queue"] dlq --> ops["ops / inspector / retry logic"]
await channel.assertExchange('dlx', 'fanout', { durable: true });await channel.assertQueue('orders.dead', { durable: true });await channel.bindQueue('orders.dead', 'dlx', '');
// the work queue dead-letters to the DLXawait channel.assertQueue('orders', { durable: true, arguments: { 'x-dead-letter-exchange': 'dlx' },});
// in the consumer, reject a bad message WITHOUT requeue → it dead-letterschannel.nack(msg, false, false);channel.exchange_declare("dlx", exchange_type="fanout", durable=True)channel.queue_declare("orders.dead", durable=True)channel.queue_bind("orders.dead", "dlx")
# the work queue dead-letters to the DLXchannel.queue_declare("orders", durable=True, arguments={"x-dead-letter-exchange": "dlx"})
# reject a bad message WITHOUT requeue → it dead-letterschannel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)ch.ExchangeDeclare("dlx", "fanout", true, false, false, false, nil)ch.QueueDeclare("orders.dead", true, false, false, false, nil)ch.QueueBind("orders.dead", "", "dlx", false, nil)
// the work queue dead-letters to the DLXch.QueueDeclare("orders", true, false, false, false, amqp.Table{ "x-dead-letter-exchange": "dlx",})
// reject a bad message WITHOUT requeue → it dead-lettersd.Nack(false, false)Poison message: ยอมแพ้อย่างสง่างาม
หัวข้อที่มีชื่อว่า “Poison message: ยอมแพ้อย่างสง่างาม”ไม่ใช่ทุกอย่างควร retry ตลอดไป message ที่จะ ไม่มีวัน สำเร็จ — schema พัง, อ้างถึงสิ่งที่ไม่มีอยู่แล้ว — คือ poison message pattern คือ: นับว่า message ถูกลองมากี่ครั้ง (counter ใน header หรือ record x-death ที่ RabbitMQ เพิ่มให้ทุกครั้งที่ dead-letter) แล้วหลังจาก N ครั้งก็ route ไปยัง parking (หรือ “dead”) queue ที่คนหรือ alert เข้าไปดูได้ อย่าปล่อยให้ poison message วนลูป
TTL + DLX = delayed retry
หัวข้อที่มีชื่อว่า “TTL + DLX = delayed retry”นี่คือทริกฉลาด ๆ ที่ dead-lettering ปลดล็อก บางที failure เป็นแบบ ชั่วคราว — downstream service ล่มแป๊บเดียว — และคุณอยากลองใหม่ แต่ ไม่ใช่ทันที คุณอยากรอ 30 วินาทีแล้วค่อยลอง
รวม message/queue TTL กับ DLX:
- เมื่อ fail ก็ publish message ไปยัง retry queue ที่มี TTL 30 วินาที และมี DLX ชี้กลับไปยัง main exchange
- message นั่งอยู่ใน retry queue เฉย ๆ 30 วินาที
- เมื่อ TTL หมด RabbitMQ จะ dead-letter message นั้น — ซึ่ง route กลับไปยัง main queue เพื่อลองอีกครั้ง
คุณสร้าง delayed retry พร้อม backoff ด้วย queue config ล้วน ๆ — ไม่ต้องมี scheduler ไม่ต้อง sleep ใน consumer (โมดูล Consumer Patterns จะต่อยอดเป็น retry ladder เต็ม ๆ)