Routing with Ruby & ZeroMQ Devices

ZeroMQ sockets provide message-oriented messaging, support for multiple transports, transparent setup and teardown, and an entire array of routing patterns via different socket types - see quick refresher on ZeroMQ. The combination of these features, as well as the fact that we can bind or connect a single socket to multiple endpoints is what makes ZeroMQ "topology aware". We can encode and construct varying routing strategies at will, without the requirement for external routers, or message brokers.

Building a multi-threaded application? With ZeroMQ you can assemble a high-performance, in-process pub-sub fanout, a worker pipeline, or a custom worfklow combining the two with just a few lines of code. In fact, if you work with ZeroMQ you will quickly find yourself reusing similar sets of patterns. That's where ZeroMQ devices come in. The library ships with support for a few common routing patterns (queue, streamer, and forwarder), and also defines a ZDCF JSON specification to simplify their assembly.

Routing Devices: Queue, Streamer, Forwarder

If RabbitMQ is a specific message-broker implementation that codifies a set of allowed messaging patterns, then ZeroMQ is the low-level toolkit, which allows us to assemble these patterns at will. This flexibility eliminates the need for explicit message routers within our infrastructure, allowing us to assemble dynamic, P2P routing topologies. However, this also does not mean that the standalone message broker does not have its place within our infrastructure. In fact, in practice you want the flexibility of both: sometimes you want to run the router within the same runtime, and at other times, you may want to run it as a standalone process.

ZeroMQ ships with several devices out of the box which implement the most common routing patterns. ZMQ Queue is a simple forwarding device for request-reply messaging, which can allow you to aggregate requests from many requesters, and distribute them to one or more reply sockets - a chokepoint. ZMQ Streamer serves a similar function, but is designed to handle the pipeline pattern instead. And last but not least, is the ZMQ Forwarder, which allows you to aggregate data from multiple publishers, and distribute these messages via a fanout to all the connected subscribers.

Getting started with ZMQ Devices

The above patterns are simple but powerful, and best of all you can run them in memory, or as standalone processes! When you build and install ZeroMQ, you will find three ZMQ binaries on your system: zmq_forwarder, zmq_queue, and zmq_streamer. Simply pass them an XML file outlining your socket configuration, and you are ready to go.

Where would we use a standalone broker? Imagine a pub-sub scenario with multiple data centers. Each remote subscriber could bind directly to the publisher, but that would also mean a lot of expensive network hops and many open connections. Instead, we can run a pub-sub forwarding device (ZMQ Streamer) in each data center, which would bind to the remote socket, stream the messages to the local data center and make them available to the local subscribers!

Assembling Custom ZMQ Devices

While the devices shipped with ZeroMQ cover some of the most common use cases, the real power of the framework is in its flexibility to allow us to assemble arbitrary messaging patterns: mixing different socket types, defining custom routing strategies based on meta-data of request, and so on. This is where ZeroMQ’s ZDCF configuration syntax, and a Ruby DSL can do wonders:

d = ZMQ::Device::Builder.new(
  context: { iothreads: 2 },
  main: {
    type: :queue,
    frontend: { type: :SUB, option: { swap: 25 }, connect: ["tcp://127.0.0.1:5555"]},
    backend:  { type: :PUB, bind: ["tcp://127.0.0.1:5556"] }
  }
)

d.main.start do
  loop do
    msg = ZMQ::Message.new
    frontend.recv msg
    backend.send msg
  end
end
zdevice - Ruby DSL for assembling ZeroMQ routing devices

In the example above, we used ZDCF’s JSON configuration syntax to open all the socket connections, and then defined a simple relay routing strategy in our start block. Of course, you could also implement much more interesting patterns: open more than two sockets, examine the contents of the message, relay the message to select endpoints, and so on! Grab the DSL, and give it a try.

RabbitMQ and ZeroMQ work together

It is easy to mistake RabbitMQ and ZeroMQ for competitors. In reality, their features are complimentary, and the two can work together in very helpful ways! For example, by installing the rmq-0mq plugin into your RabbitMQ instance, you can effectively turn RabbitMQ into a ZeroMQ device!

RabbitMQ has proven to be an incredibly stable, high-performance message broker with support for persistence and a variety of routing topologies. So, if you are looking for a ZeroMQ device capable of handling thousands of connections, with support for persistence, and a number of other features, then the combination of ZeroMQ and RabbitMQ is definitely worth a close look.


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.