สิ่งที่น่าสนใจเกี่ยวกับ RabbitMQ และ AMQP Best Practices
ด้วยแนวคิดพื้นฐานในเนื้อหา สามารถนำไปปรับใช้กับ Messaging Queue System ตัวอื่นๆ หรือ การออกแบบ Architecture ในระบบได้นะ
ด้วยตัวงานปัจจุบันต้องใช้ระบบ Messaging Queue สักตัว ที่ตอบโจทย์ได้การ Retry Mechanism การทำ Delay Queue ที่ต้องการเงื่อนไข DLX (Dead-Letter-Exchange) และ Programatic Queue ที่ซัพพอร์ต HA ด้วยนะ และ ด้วยปัญหาเหล่านี้โซลูชันเลยออกมาเป็น RabbitMQ
ด้วยความที่ยังใหม่ในการที่ต้อง Provision มันขึ้นมาเอง โดยไม่ได้ใช้ Cloud Solution ก็เลยต้อง ทดลองเองเพื่อให้เฟลแต่เนิ่นๆ ราคาจะได้ไม่แพง ดีกว่าไป ชดเชยบาปที่ Production
บทความนี้ก็เลยอยากรวบรวม ทฤษฏีที่เขาแนะนำมา Best Practice ของทีม CloudAMQP ซึ่งน่าจะเป็นเจ้าเดียว ที่ล้มลุกคลุกคลานมา ผ่านสมรภูมิการใช้แบบหนักหน่วงมาแล้ว มาแชร์ให้ผู้อ่านดูกันครับ บวกกับแชร์ไอเดีย และ ภาพประกอบที่ผมได้ลองเล่นมาให้ดูเพิ่มเติมครับ
1) Queues
Keep your queue short if possible
อย่าให้ message ในคิวต้องรอนาน message จำนวนมากที่อยู่ในคิว มีผลต่อ Memory ที่ใช้ เพื่อเป็นการ free-up memory อยู่เสมอ เราควรออกแบบ message และ consumer ให้ดี
Limit queue size, with TTL or max-length
กำหนดขนาดของ queue และอายุของคิว หรือ ความยาวสูงสุดที่รับได้ของ message ในคิว เพื่อให้คิวไม่ต้องรอนาน จะช่วยเพิ่ม high throughput ได้ ซึ่งจะกล่าวอีกครั้งในหัวข้อถัดไป
Don’t set your own names on temporary queues
ถ้าเราต้องการสร้าง temporary queue หรือ คิวที่ใช้ชั่วคราว ปล่อยให้ RabbitMQ random ชื่อมาให้ดีกว่า โดยไม่ต้องระบุชื่อ ตอน publish
Auto-delete queues you are not using
ถ้าเรารู้แน่ๆว่าคิวของเรา ออกแบบมาใช้ 28 วัน การกำหนด TTL ไปเลย จะช่วยประหยัดพื้นที่ใน server ได้
Payload — RabbitMQ message size and types
อย่าส่งข้อมูลปริมาณมากๆเข้าไปในคิว ให้ออกแบบโครงสร้างข้อมูลให้ดีก่อนจะเอาของใส่เข้าไป เพราะ มันจะทำให้เกิด bottle neck ได้ และ ทำให้คนอื่นๆรอคิวนาน
2) Connection
Connections and channels
Connection ออกแบบมา long-lived ส่วน Channels เปิด ปิดบ่อยๆได้
การเปิด 1 connection จะใช้ 100kb ใน memory ให้ระวังเรื่องการเปิด TCP connection จำนวนมาก จนเกิดเหตุการณ์ out-of-memory
เช่น เรามี 100 publisher หรือ 100 consumer ที่ทำงานเดียวกัน ควรพิจารณาเรื่องการเปิด ปิด connection บ่อยๆ ดังนั้น AMQP จึงเสนอสิ่งที่เรียกว่า channels ขึ้นมา เพื่อให้ 1 connection สามารถมีหลายๆ channels ได้
It is recommended that each process only creates one TCP connection, and uses multiple channels in that connection for different threads
คำถาม ถ้าเราต้องการ consumers worker 100 ตัว ทำงานเรื่องเดียวกัน เปิด 1 connection แต่ให้ worker แต่ละตัวเปิด channel เป็นของตัวเอง จะดีกว่าไหม?
Don’t share channels between threads
อย่าแชร์ channels ระหว่าง thread เพื่อป้องกันการเกิด race condition แทนที่แต่ละ thread จะมี local memory ของตัวเอง ถ้ามาใช้ shared memory คงเกิดการ conflict กันน่าดู
Separate connections for publisher and consumer 🐿
สร้าง connection ของใคร ของมัน ระหว่าง publisher และ consumer ที่ทำคนละหน้าที่ การแชร์ connection ให้กับ process ที่ทำงานแตกต่างกันโดยสิ้นเชิง อาจจะทำให้เกิด low throughput
3) Acknowledgments and Confirms
Unacknowledged messages
message ที่ไม่ถูก ack มันจะยังอยู่ใน moemory และ ถ้ามีมากจะเกิดสภาวะ out-of-memory อย่าลืม ack หรือ nack
4) Prefetch
กำหนดให้ Broker ส่ง message จำนวนเท่าไหร่ ให้ consumer ในเวลาเดียว
Prefetch limits how many messages the client can receive before acknowledging a message. All pre-fetched messages are removed from the queue, and invisible to other consumers.
ถ้า prefetch มาแล้ว ของจะถูกเอาออกจากคิว และ consumer ตัวอื่นจะไม่สามารถมองเห็นได้
ถ้า prefetch น้อยเกินไป ต้อง prefetch บ่อยๆ จะทำให้เกิด latency ตอนที่ Broker และ Consumer handshake กัน แต่ถ้ามากจนเกินไป จะทำให้ consumer ยุ่งอยู่ตลอด ในขณะที่ consumer อื่นอาจจะว่างงาน ดังภาพ
5) Routing (exchanges setup)
Direct exchange เร็วกว่าแบบ Topic exchange ที่เป็น wildcard routing อยู่แล้ว ดังนั้น อย่าลืมออกแบบให้เหมาะสม
6) High Performance (High Throughput)
Make sure your queues stay short
เพื่อให้เกิด optimal performance อย่าปล่อยให้ message ในคิวต้องรอนาน กว่าจะได้รับการเอาไปประมวลผล ลองคิดดูเล่นๆว่า ถ้าชีวิตจริงเราต้องต่อแถวนาน และ ไม่ถูกเรียกคิวสักที จะเกิดอะไรขึ้น คิวที่ใช้เวลานานว่าปกติ นั่นหมายความว่า message size ของเราใหญ่จนเกินความจำเป็น หรือ consumer เราทำงานช้าไปหรือเปล่า
Set a queue max-length if needed
message ที่จะเข้าคิวได้ต้องได้รับการออกแบบมาอย่างดี เพื่อไม่ให้ hit spike ของปริมาณที่รับได้ ถ้าอยากให้คิวไม่ต้องรอนาน การกำหนด max-length จะช่วยให้ discard message ที่มีขนาดเกินออกไป
Remove the policy for lazy queues
ปกติ policy lazy queue จะเอา message ลง Disk storage และ message ถูกโหลดลง memory เมื่อมันต้องการใช้เท่านั้น จะช่วยประหยัด memory ลงได้ แต่ latency ช่วง move จาก disk ไป memory จะเพิ่มขึ้นด้วย
Use transient messages
Persistent ช่วยเก็บ message ลง disk โดยอัตโนมัติ ช่วยให้แม้ว่า Node จะดับไป message ก็ยังอยู่ แต่ก็จะกระทบกับ throughput ได้นะ สัมพันธ์กับข้อก่อนหน้า
Use multiple queues and consumers
Queues are single-threaded in RabbitMQ และ มีข้อจำกัดที่ 50k messages/s เพื่อปรับปรุงให้การทำงานมีประสิทธิภาพมากขึ้น และ ใช้ประโยชน์จาก CPU / CORE เต็มเม็ดเต็มหน่วย ให้เรากระจาย Queue ไปทุกๆ Core ที่เรามี หรือ กระจายไปหลายๆ Node ถ้าเราใช้การออกแบบด้วย RabbitMQ Clustering
อย่างไรก็ดี ถ้าเราต้องมีนั่งทำเองทั้งหมด คงปวดหัวแน่ๆ ทีมงานเลยแนะนำ ปลักอินมาให้เรา 2 ตัว ได้แก่
Consistent hash exchange plugin
ปลักอินตัวนี้จะช่วยเราสร้าง Exchange ขึ้นมา และ มี Load-balancer ของ message กระจายไปทุก Queue (Eventually distribute message across queues) ที่ Binding กับ Routing Key นั้นๆ โดยมันจะสร้าง Hash Routing Key ให้ ซึ่งช่วยให้การกระจายข้อมูล และ งานไปแต่ละคิว นั้นทำได้มีประสิทธิภาพมากขึ้น
https://github.com/rabbitmq/rabbitmq-consistent-hash-exchange
RabbitMQ sharding
ถ้าต้องการเทคนิค partitioning of queues ด้วยการกระจายข้อมูลไปหลายๆ Shard Cluster หรือ การกองของให้อยู่ใกล้ๆกัน เพื่อให้การ scale มีประสิทธิภาพมากขึ้น ลองใช้ปลักอินตัวนี้ดูครับ
ถ้าใครคุ้นเคยกับ Kafka message ใน topic กระจายไปหลายๆ partitions น่าจะคุ้นเคยกับสิ่งนี้ และ ทำ topic replica factor หรือ การใช้ Message Key
Disable manual acks and publish confirms
ถ้าอยากมี high throughput ให้ปิด manual acknowledge ลง และ ใช้ auto acknowledge แทน
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
แต่ข้อเสีย คือ เราจะไม่สามารถ retry ได้ ถ้า consumer เอา message ไปใช้แล้ว เกิด fail ขึ้นมา เพราะ ของในคิวได้ถูกหยิบออกไป และ ack แล้ว
Avoid multiple nodes (HA)
ถ้าเราทำ RabbitMQ HA และ ถ้าเรามีสาม Node โดยปกติแล้ว RabbitMQ จะทำ Miror Queue ให้ เพื่อให้ข้อมูลซิงค์กันตลอดเวลา ถ้าเราไม่แคร์เรื่อง HA ก็ปิดตรงนี้ได้เลย
Disable plugins you are not using
ปลักอินอันไหน ไม่ใช่ก็ปิด เพราะ เวลาของถูกส่งเข้า Queue มันจะไปเชคกับปลักอินที่เปิดไว้ ว่าต้องทำงานหรือไม่
7) High Availability
คราวนี้เรามาลองทบทวนเรื่อง HA กันบ้างครับ ว่าแนวทางการปฏิบัติที่ดี ควรดูรายละเอียดเรืองใดบ้าง
Highly Available (Mirrored) Queues
การที่เรามี Broker เครื่องเดียวทำงาน มันคงไม่ดีแน่ ถ้าหากวันนึงเครื่องที่ใช้งานล่มไป การมี มากกว่าหนึ่งควรสำรอง มาทำงานแทนที่ จึงเป็นวิธีที่ดีกว่า การทำ HA + Mirrored Queues จึงเป็นอีกวิธีนึง ที่จะตอบโจทย์เงื่อนไขนี้ได้
ถ้าหากมีตัวใดตัวนึงดับไป ยังมีอีก 2 Nodes ถูกโปรโมทขึ้นมาทำงานแทนที่ โดยใช้หลักการของ Raft Algorithms ซึ่งเป็นส่วนหนึ่งของ Quorum
อย่างที่บอกไปว่าเบื้องหลังนั้น RabbitMQ ซิงค์ข้อมูลหากันอยู่ตลอด ถ้าหาก Node ใดล่มไป ยังมี Node สำรองขึ้นมาแทนที่ และ ข้อมูลยังสดใหม่อยู่เสมอ
https://www.rabbitmq.com/ha.html
Enable lazy queues
ถ้าอยากได้ HA ระบบล่มยังไงข้อมูลก็ยังอยู่ เปิดใช้ Policy นี้ได้ทันที ทั้งนี้ทั้งนั้น อย่าลืมด้วยว่างานของคุณเน้น throughput รึป่าว
lazy queue ช่วยให้ cluster เรา เหมาะกับงานพวก batch processing job ที่สามารถรอ publisher ทำงานนานๆได้
Cluster setup (RabbitMQ HA with 2 or more nodes)
คล้ายๆกับข้อแรกที่อธิบายไปแล้ว ถ้า node ใดเฟลไป ระบบจะทำ auto-failover ให้เอง โดยที่ client ไม่ต้องสนใจ ว่าเบื้องหลังจัดการอย่างไร
Use persistent messages and durable queues
ทุก message ที่เข้าคิวจะถูก flush จาก memory ลง disk เพื่อป้องกันข้อมูลหาย กรณี ไฟดับ ฝนตก เครื่องพัง บลาๆ แต่ก็ต้องแลกมาด้วย latency ช่วงย้ายจาก disk มา memory
Do not set RabbitMQ Management statistics rate mode to detailed in production
พวก stat ของ server ควรเปิดเฉพาะส่วนที่ต้องการเท่านั้น เพราะ การเปิดแต่ละครั้ง มันมี cost ของมัน และ อาจทำให้ระบบทำงานไม่มีประสิทธิภาพ แทนที่จะใช้ resource ในงานเกี่ยวกับ business จริงๆ
Set limited use of priority queues
RabbitMQ ถูกสร้างขึ้นมาบนพื้นฐานของ Erlang VM ดังนั้น การกำหนด priolity queue มีนส่วนสำคัญกับการใช้ resource คำแนะนำ คือ อย่ากำหนดเกิน 5 priority levels
นอกจากนี้ยังมีบทความที่น่าสนใจ ให้ลองไปศึกษาต่อ เช่น