All you need to know to get started
Fabio Pitino
Sr Software Engineer
fabio_pitino@symantec.com
(with Ruby examples)
What is a message broker?
I asked Wikipedia...
A message broker is an architectural pattern for message validation, transformation, and routing. It mediates communication among applications, minimising the mutual awareness that applications should have of each other in order to be able to exchange messages, effectively implementing decoupling.
What is RabbitMQ?
- Open source project - created by Rabbit Technologies Ltd in 2007, currently part of Pivotal Software
- Implements the AMQP (Advanced Message Queuing Protocol)
- Built in Erlang programming language
- Built using OTP (Open Telecom Platform) framework
- Gateways: AMQP, HTTP, STOMP and MQTT
- Supports many plug-ins
Basic building blocks
# producer.rb
require 'bunny'
connection = Bunny.new
connection.start
channel = connection.create_channel
exchange = channel.default_exchange
queue = channel.queue('tasks')
10.times do |n|
exchange.publish("task-#{n}", routing_key: queue.name)
end
connection.close
Basic building blocks
# consumer.rb
require 'bunny'
connection = Bunny.new
connection.start
channel = connection.create_channel
queue = channel.queue('tasks')
begin
queue.subscribe(block: true) do |_delivery_info, _properties, payload|
puts "received #{payload}"
# do some work with it...
end
rescue => error
puts error.inspect
connection.close
end
Worker queue (simple)
# worker.rb
# setup omitted...
queue = channel.queue('tasks')
# using "block: true" is like using "channel.prefetch(1)"
queue.subscribe(manual_ack: true, block: true) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
- 1 worker processes 1 message a the time
- run multiple instances of the same worker to scale out
Worker queue (with prefetch)
# worker.rb
# setup omitted...
channel.prefetch(1)
queue = channel.queue('tasks')
queue.subscribe(manual_ack: true, block: true) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
- 1 worker processes 1 message a the time
- run multiple instances of the same worker to scale out
Worker pool
# worker.rb
POOL_SIZE = 10
# setup omitted...
channel.prefetch(POOL_SIZE)
queue = channel.queue('tasks')
queue.subscribe(manual_ack: true, block: false) do |delivery_info, _, payload|
puts "received #{payload}"
# do some work with it...
channel.ack(delivery_info.delivery_tag)
end
- 1 worker processes up to 10 messages concurrently
- run 1 instance of the worker
- "block" mode must be turned off
Persistency
# queue does not survive broker reboot
channel.queue('tasks')
# queue is persisted after broker reboot but not the messages in it
channel.queue('tasks', durable: true)
# queue and messages persisted after broker reboot
queue = channel.queue('tasks', durable: true)
queue.publish(message, persistent: true)
slow
fast
- By default queues and messages are not persisted
- If a queue is declared once as "durable" it cannot be re-declared non-durable
- Queues and Exchanges can also be created using RabbitMQ Admin app.
Exchanges
- The core idea of RabbitMQ is that the Producer always publishes to an Exchange
- A Producer knows nothing about it's consumers
- Types of exchanges available: direct, topic, headers, fanout
- So far it seems like we have been publishing directly to a queue but we have been implicitly publishing to the "Default Exchange"
# so far we have been using this:
channel.queue('tasks').publish(message)
# ...which is equivalent to this:
channel.default_exchange.publish(message, routing_key: 'tasks')
Pub/Sub with a Fanout Exchange
# publisher.rb
exchange = channel.fanout('logs')
exchange.publish(message)
# subscriber.rb
# declare a temporary queue owned by the subscriber
# with auto-generated name: amq.gen-JzTY20BRgKO-HjmUJj0wLg
queue = channel.queue('', exclusive: true)
# bind queue to the specific fanout exchange
queue.bind('logs')
# at this point messages published to the "logs" exchange
# will be copied to each queue bound to it
queue.subscribe(block: true) do |_delivery_info, _, payload|
puts "received: #{payload}"
end
Routing with Direct Exchange
# publisher.rb
exchange = channel.direct('logs')
exchange.publish(message, routing_key: severity)
# consumer.rb [info] [warn] [error]
exchange = channel.direct('logs')
queue = channel.queue('', exclusive: true)
ARGV.each do |severity|
queue.bind(exchange, routing_key: severity
end
queue.subscribe(block: true) do |_delivery_info, _, payload|
puts "received #{payload}"
end
Imagine that for our logging system we have different consumers interested in different severity of messages
More flexibility with Topic Exchange
Direct exchange cannot route message based on multiple criteria.
Enter the Topic Exchange.
Messages must contain a routing_key that is a list of words delimited by "dots":
- stocks.usd.nyse
- syslog.info
- europe.ireland.dublin
Routing key can contain special characters:
- * (star) as substitute for 1 word
- # (hash) as substitute for 0 or more words
Topic Exchange routing: example
A weather forecast server streams real-time weather information to various backend services around the world.
Format of the routing key: <continent>.<country>.<city>
consumers can use various forms of routing keys to get data:
- asia.russia.moscow for data related to Moscow city - equivalent to Direct exchange
- europe.italy.* for all data related to Italy
- north_america.# for all data related to north America
- *.*.dublin for both Dublin (IE) and Dublin (OH, AM)
- # for all data - equivalent to Fanout exchange
Topic Exchange for the win!!!
Well! turns out that...
- Topic exchange is the slowest at routing messages
- Direct exchange is much faster
- Fanout exchange is the fastest
Logging system with a Topic Exchange
# publisher.rb
exchange = channel.topic('logs')
exchange.publish(message, routing_key: "#{source}.#{severity}")
# consumer.rb [key]
# e.g: consumer "rails.info"
# e.g: consumer "syslog.*" "kern.error"
# e.g: consumer "#"
exchange = channel.topic('logs')
queue = channel.queue('', exclusive: true)
ARGV.each do |key|
queue.bind(exchange, routing_key: key
end
queue.subscribe(block: true) do |delivery_info, _, payload|
puts "received #{delivery_info.routing_key}: #{payload}"
end
Let's extend the logging system with a more flexible approach
Remote procedure call via RabbitMQ
# server.rb
exchange = channel.default_exchange
queue = channel.queue('fibonacci_server')
queue.subscribe(block: true) do |_delivery, properties, payload|
result = fibonacci(payload.to_i)
exchange.publish(
result.to_s, routing_key: properties.reply_to, correlation_id: properties.correlation_id
)
end
# client.rb
call_id = SecureRandom.hex(16)
reply_queue = channel.queue('', exclusive: true)
reply_queue.subscribe do |_delivery, properties, payload|
if properties[:correlation_id] == call_id
puts "response: #{payload}"
end
end
exchange = channel.default_exchange
exchange.publish(
'30', routing_key: 'fibonacci_server', correlation_id: call_id, reply_to: reply_queue.name
)
Advanced use case
Connections VS Channels
Create Exchange
- internal if "yes" only used for exchange-to-exchange routing
- alternate-exchange if messages cannot be routed to this exchange, send them to the alternate one
Create Queue
- auto-expire is a TTL for the queue if remains unused for a period of time
- max-length is the max number of ready messages a queue can have before it starts to drop messages from the "head" of the queue (sliding window)
Virtual Hosts
Thank you
¿¿Questions??
RabbitMQ (with Ruby examples)
By Fabio Pitino
RabbitMQ (with Ruby examples)
All you need to know to get started - with Ruby examples
- 578