Scalable Microservice Architecture Using RabbitMQ RPC

Oded Shimon
The Startup
Published in
9 min readSep 27, 2020

Introduction

Microservices are the hot thing, in the past few years microservices have seemed to gain momentum as a very common architecture. Innovative and successful companies like Netflix and Amazon have adopted it, and for a good reason.

The main idea of microservices is to separate the application logic into loosely-coupled services, while each service has its own independent business domain. This concept, if performed accurately, solves a lot of problems that less modern architectures, such as monoliths, didn’t manage to solve: services that are easy to scale, easy to deploy, easy to test and the list goes on…

Even so, if I had to pick one reason why this architecture is so popular, I would bet that the reason is the impact on the daily routine of programmers and executives alike. For example, microservices goes hand in hand with the agile methodology. It’s easy to make changes in a single component, which is significant in the way that a programmer can even decide that he is using a different programming language than another developer working on the same system. From the eyes of the executive easy integrations are guaranteed, and in the end everyone is happy.

Microservices & Queues

We’ve talked enough about microservices, let’s talk a little about message queues. Services can communicate with each other in different ways, and we will not review them all. There is no doubt that the use of queues to communicate is also very popular, again for a good reason too. Many times, when I start the task of designing the architecture for a product or a component, I look at the whole stack. I will be a little romantic for a moment and tell you that I believe there is a reason that certain technologies connect with each other, like a chemical interaction that develops over time. A Specific programming language and a database? A particular operating system and a web server? I’m sure you can think of some!

In my humble opinion, this is the very case between message queues and microservices. Queues have a lot of benefits. They make it possible to deal with a temporary downtime of a service — in case there is no consumer messages in a queue will not be consumed, support multiple architectures and can even provide additional features like load balancing and monitoring of the load on each service.

RabbitMQ as a Message Broker

RabbitMQ is a great message-broker, it’s reliable, built for performance, supports multiple protocols, and has a strong community that makes it easy to find documentation and support.

In this article, I will explain how to build a solution based on microservices and queues using the RPC pattern. This is just a one way to do it, and in fact the RabbitMQ website has excellent documentation of 6 different patterns in which you can communicate. Why this pattern? Because I have used it with a real-time system that receives hundreds of thousands of requests per hour and found it elegant, fast, and easy to maintain. Let’s get started!

Remote Procedure Call with RabbitMQ (RPC)

While designing a microservice system, the best practice is to keep each service independent, by means its not dependent on any other microservice.

But in real life the reality is often far more complex. Imagine that you want to build a complete system made entirely out of microservices that communicate with each other via queues (due to all the benefits described above), but in addition, your system needs to provide a Web API that externalizes all the capabilities of your system using HTTP. This need could be necessary for a good architectural reason, like a front-end application that is designed to communicate with the backend through HTTP. That means that this Web API needs to get HTTP messages, hold the connection, trigger one or more microservices by sending a proper message to the RabbitMQ server, wait for the specific response and then send it back to the waiting HTTP client.

Basically, the above example describes a situation where one service has a direct dependency on another service in order to complete its job.

Now that we have understood how the RPC Pattern can be useful, I will explain how to implement it using RabbitMQ built-in features.

Message Properties

Every RabbitMQ message has a properties section that combines the message metadata. This section contains a list of optional properties, although most of them are rarely used, for our purpose we will use two of them:

  • reply_to — carries the response queue name.
  • correlation_id — carries the identifier of the sender message.
(message properties example)

It is possible to think of it like sending a letter. In a situation where we do not want to receive a reply to the letter we have sent, we do not have to indicate our address. However, if we want to receive a reply, we must indicate our address. In addition, if we do receive the letter but the letter does not start with an explanation that it is a response letter to the letter that we have sent earlier, we will not know what to do with its content. In this case the “response queue name” is our mailbox address and the Correlation ID is the explanation that it is a response letter to our first letter.

(RPC flow example)

Sample Code

One of the things I like to do the most when I recognize that there is a well-defined set of logic, which once implemented and tested can be used by me or others, is to pack it as a package. That way, I know I do not have to implement this logic over and over, and instead I can just use it with no concern in any project I work on in the future. Therefore, we will implement this functionality as an independent Python package.

The purpose of our package is to provide a single function that allows the user to send an RPC request and receive a response asynchronously as one liner.

Let’s make it reusable — aka package

Note that all the code snippets below (I have named this package “TornadoBunny”) are available on my GitHub account under:

In order to achieving high performance, we will write asynchronous code. Since the topic of the post is not asynchronous programming I will not discuss about this topic. If you are unfamiliar with the asynchronous concept, I recommend filling the gap with proper sources.

We will be using the pika package for interacting with the RabbitMQ server and the tornado package in order to provide support for tornado-based web servers that use RPC requests.

Our library consists of 3 core classes, we will review them one by one.

AsyncConnection Class

First, we need to create a simple class that handles a single connection to a RabbitMQ server. The class Constructor gets a parameter called “io_loop”, assuming that the user who creates the connection will want to supply the io_loop by himself. This class supports both Tornado ioloop as well as asyncio ioloop. The main function of this class is `get_connection()` which begins a series of asynchronous calls until a connection object is received using the pika package.

ChannelConfiguration Class

After creating a class that handles the connection, we will need a class that handles a single channel within a RabbitMQ connection. This class users an AsyncConnection object and it provides both publish and consume capabilities. This class gets a connection (AsyncConnection from the previous paragraph), an exchange, a routing key for publishing messages to and a queue to consume from.

TornadoBunny Class

This class actually does all the magic required for the RPC, by using the classes that we have built so far along with a few other little tricks.

The class encapsulates two async channels (and two connections, respectively, for each channel). The first channel is used for publishing messages while the other one is used for consuming messages.

The class also has two dictionaries for storing the RPC related exchanges and correlation ids’ state.

The `receive()` function is responsible for consuming messages. If the received properties of consumed message are not none, it publishes result back to the queue indicated in `reply_to`.

The `publish()` function is responsible for publishing a message to the given exchange.

The `rpc()` function is the function that implements the RPC logic. First, it consumes the receiving queue, then it generates a unique uuid that will be used as the correlation id, which is therefore stored as a key in the dictionary which links between a correlation id and the caller. Afterwards, the correlation id is attached along with the reply queue to a message properties object that is attached to the message which will then be finally sent. At this stage we are yielding on the `_wait_result()` function that will be triggered only when we get the response back.

Use Case

To illustrate everything that we have described so far, we will create a system that calculates a number in the Fibonacci series. Of course, our system will maintain a scalable architecture.

The fun in this example is that it takes so little time and effort to implement a scalable system thanks to the package we have implemented earlier.

The system will consist of two microservices.

(our Fibonacci calculation system diagram)

RabbitMQ Server

For the purpose of the demonstration I ran a basic image of RabbitMQ including the management interface.

sudo docker run -d — hostname rabbit_mq — name rabbit_mq -p 8080:15672 -p 5672:5672 rabbitmq:3-management

Then, using the default password, I logged into the management interface and created a new user for the demo.

FibonacciServer.py

A Web server implemented using the tornado framework. The server gets a Uri parameter named “num” and prints the Fibonacci value of that number. For example, the request: http://127.0.0.1/?num=7 will print 13 as output. The server sends the number to calculate to the FibonacciExecutor using the `rpc()` function.

(FibonacciServer presents the result for the 7'nd element in the Fibonacci sequence)

FibonacciExecutor.py

A Microservice that gets a Fibonacci number to calculate from a known RabbitMQ queue and sends the result to the reply_to queue if given.

(FibonacciExecutor calculates the result for the seventh element in the Fibonacci sequence)

Scaling It Up

The implementation we have created allows for a number of consumers to consume messages from the incoming message queue (fic_calc_q). By default, RabbitMQ distributes the messages to its consumers using the Round-Robin scheduling algorithm.

According to Wikipedia the Round-Robin algorithm is:

“slices are assigned to each process in equal portions and in circular order, handling all processes without priority”

In other words, every consumer will receive an equal number of messages. In the next video I ran two instances of a the FibonacciExecutor, and as you can see the FibonacciServer preserves its behavior while the messages are evenly distributed between the two instances!

Conclusion

In this article we have reviewed the motivation and reasons to why the microservice and queue architecture is so popular. We explained in which cases the RPC template can be a solution for creating a scalable architecture by decoupling microservices. Later, we have implemented an independent package that provides RPC capabilities in one line, and finally we have implemented a system which uses the above package and demonstrates the theoretical concepts that we have talked about.

The purpose of the article is not to present the RPC pattern as an absolute solution to any and all communication dilemmas between microservices, but to present another approach, in order to enrich the reader’s mind with the thought that in architecture issues there is no one right answer.

All code snippets from this article are available on my GitHub account, feel free to make use of the above package as well as contact me regarding any questions.

Best Regards,
Oded Shimon.

--

--

Oded Shimon
The Startup

Software Architecture | Cyber | Open-Source | DevOps (Love my articles? you can buy me a coffee! https://www.buymeacoffee.com/OdedShimon)