ด้วยแนวคิดพื้นฐานในเนื้อหา สามารถนำไปปรับใช้กับ Messaging Queue System ตัวอื่นๆ หรือ การออกแบบ Architecture ในระบบได้นะ

Photo by Roman Arkhipov on Unsplash

ด้วยตัวงานปัจจุบันต้องใช้ระบบ 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 ได้

1 connection ต่อ process และ channel ให้แต่ละ thread

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

https://medium.com/@eranda/rabbitmq-x-consistent-hashing-with-wso2-esb-27479b8d1d21

ปลักอินตัวนี้จะช่วยเราสร้าง 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

https://github.com/rabbitmq/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 จึงเป็นอีกวิธีนึง ที่จะตอบโจทย์เงื่อนไขนี้ได้

ตัวอย่าง มี 3 Nodes HA

ถ้าหากมีตัวใดตัวนึงดับไป ยังมีอีก 2 Nodes ถูกโปรโมทขึ้นมาทำงานแทนที่ โดยใช้หลักการของ Raft Algorithms ซึ่งเป็นส่วนหนึ่งของ Quorum

Data always mirroed Queues

อย่างที่บอกไปว่าเบื้องหลังนั้น 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


iamgoangle

Once a Software Engineer, always a growth engineering mindsets

Teerapong Singthong 👨🏻‍💻

Written by

LINE Engineer x Software Craftsmanship

iamgoangle

Once a Software Engineer, always a growth engineering mindsets

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade