Skip to main content

RabbitMQ Queue Mirroring

Introduction

Queue mirroring is a critical feature of RabbitMQ that allows for high availability of queues across multiple nodes in a RabbitMQ cluster. In a standard RabbitMQ setup, queues exist on a single node. If that node fails, the queue and all of its messages become unavailable until the node is restored. Queue mirroring addresses this vulnerability by creating copies (mirrors) of queues across multiple nodes in a cluster, ensuring that even if one node fails, your messages remain accessible through another node.

This tutorial will explain how queue mirroring works, how to configure it properly, and provide practical examples of implementing it in your RabbitMQ environment.

Understanding Queue Mirroring Concepts

Basic Terminology

Before diving into the configuration, let's clarify some important terminology:

  • Master queue: The primary queue that receives all message operations first
  • Mirror queues: Copies of the master queue on other nodes in the cluster
  • Mirroring policy: Rules that determine which queues should be mirrored and to how many nodes

How Queue Mirroring Works

When a queue is mirrored, one node in the cluster hosts the master queue, while other nodes host mirrors of that queue. The following sequence happens for each operation:

  1. All operations for a queue are first applied to the master
  2. The master then replicates these operations to all mirrors
  3. Consumers can connect to any node, but all queue operations are routed to the master first

If the node hosting the master queue fails, an automatic failover occurs where one of the mirrors is promoted to become the new master.

Configuring Queue Mirroring

Using Policies to Configure Mirroring

Queue mirroring is configured using policies rather than per-queue parameters. This allows for flexible, centralized management of mirroring.

To create a policy that enables mirroring, you can use the RabbitMQ management UI or the rabbitmqctl command-line tool:

bash
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}' --apply-to queues

Let's break down this command:

  • ha-all is the name of the policy
  • ^ is a regular expression that matches all queues
  • {"ha-mode":"all"} specifies that queues should be mirrored to all nodes
  • --apply-to queues indicates that this policy applies to queues, not exchanges

Mirroring Modes

RabbitMQ supports several mirroring modes:

1. All nodes (ha-mode: all)

Mirrors the queue to every node in the cluster:

bash
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}' --apply-to queues

2. Specific count of nodes (ha-mode: exactly, ha-params: <count>)

Mirrors the queue to a specific number of nodes:

bash
rabbitmqctl set_policy ha-two "^" '{"ha-mode":"exactly","ha-params":2}' --apply-to queues

This creates one master and one mirror (total of 2 nodes).

3. Specific nodes (ha-mode: nodes, ha-params: [node1, node2, ...])

Mirrors the queue only to the specified nodes:

bash
rabbitmqctl set_policy ha-nodes "^" \
'{"ha-mode":"nodes","ha-params":["rabbit@node1", "rabbit@node2"]}' \
--apply-to queues

Synchronization Mode Configuration

When a new mirror joins, it needs to sync the existing messages. This can be configured using ha-sync-mode:

bash
rabbitmqctl set_policy ha-all "^" \
'{"ha-mode":"all","ha-sync-mode":"automatic"}' \
--apply-to queues

The ha-sync-mode can be:

  • manual: New mirrors won't sync automatically (default)
  • automatic: New mirrors will sync automatically

Programming with Mirrored Queues

From an application developer's perspective, mirrored queues work largely the same as regular queues. Here's a simple example using the Java client:

java
// Connection setup remains the same
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();

// Declare a durable queue (mirroring is controlled by policies, not in the code)
channel.queueDeclare("mirrored-queue", true, false, false, null);

// Publishing messages
String message = "Hello World!";
channel.basicPublish("", "mirrored-queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
System.out.println(" [x] Sent '" + message + "'");

// Consuming messages
channel.basicConsume("mirrored-queue", true, (consumerTag, delivery) -> {
String receivedMessage = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + receivedMessage + "'");
}, consumerTag -> {});

Note that the code doesn't specify anything about mirroring - that's all handled by the server's policy configuration.

Handling Node Failures

When a node hosting a master queue fails, RabbitMQ automatically promotes one of the mirrors to be the new master. This process happens quickly but isn't instantaneous.

What Happens During Failover

  1. The node with the master queue becomes unavailable
  2. RabbitMQ detects the failure
  3. One of the mirrors is promoted to master
  4. Clients reconnect to available nodes
  5. Operations resume with the new master

Client-Side Considerations

To handle failover gracefully, clients should:

  1. Use connection recovery: Most RabbitMQ client libraries support automatic connection recovery.
java
// Java client with automatic recovery
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setAutomaticRecoveryEnabled(true);
factory.setNetworkRecoveryInterval(5000); // retry every 5 seconds
Connection connection = factory.newConnection(
Arrays.asList(
new Address("192.168.1.10"),
new Address("192.168.1.11"),
new Address("192.168.1.12")
)
); // List multiple nodes for connection attempts
  1. Implement retry logic: In addition to automatic recovery, implement application-level retry logic for publishing messages.
java
// Simplified retry logic example
boolean messagePublished = false;
int attempts = 0;
while (!messagePublished && attempts < MAX_ATTEMPTS) {
try {
channel.basicPublish("", "mirrored-queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
messagePublished = true;
} catch (Exception e) {
attempts++;
Thread.sleep(1000); // Wait before retrying
}
}

Best Practices for Queue Mirroring

1. Don't Mirror Everything

Mirroring increases network traffic and CPU usage. Only mirror queues that need high availability.

bash
# Mirror only important queues
rabbitmqctl set_policy ha-important "^important\." '{"ha-mode":"all"}' --apply-to queues

2. Consider Synchronization Impact

Synchronizing large queues can impact performance. For very large queues, manual synchronization during off-peak hours might be better.

bash
# Set manual sync mode
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all","ha-sync-mode":"manual"}' --apply-to queues

# Trigger synchronization manually when appropriate
rabbitmqctl sync_queue name-of-queue

3. Use Quorum Queues for Modern Deployments

For RabbitMQ 3.8.0 and later, consider using quorum queues instead of classic mirrored queues:

bash
# Declare a quorum queue in Java
Map<String, Object> args = new HashMap<>();
args.put("x-queue-type", "quorum");
channel.queueDeclare("ha-queue", true, false, false, args);

4. Monitor Queue Synchronization Status

Regularly check if your queues are properly synchronized:

bash
rabbitmqctl list_queues name slave_pids synchronised_slave_pids

Real-World Example: Implementing a Reliable Order Processing System

Let's implement a simplified order processing system that uses mirrored queues for reliability:

  1. First, configure the mirroring policy on the RabbitMQ server:
bash
rabbitmqctl set_policy ha-orders "^orders\." '{"ha-mode":"all","ha-sync-mode":"automatic"}' --apply-to queues
  1. Here's a Node.js application that processes orders:
javascript
const amqp = require('amqplib');

async function processOrders() {
let connection;
try {
// Connect to the cluster with multiple endpoints
connection = await amqp.connect([
'amqp://user:password@rabbitmq1:5672',
'amqp://user:password@rabbitmq2:5672',
'amqp://user:password@rabbitmq3:5672'
]);

const channel = await connection.createChannel();

// Declare our durable queue
await channel.assertQueue('orders.incoming', {
durable: true
});

console.log('[*] Waiting for orders');

// Process incoming orders
await channel.consume('orders.incoming', (msg) => {
const order = JSON.parse(msg.content.toString());
console.log(`[x] Processing order ${order.id} for ${order.customer}`);

// Simulate order processing
setTimeout(() => {
console.log(`[✓] Order ${order.id} completed`);
channel.ack(msg);
}, 1000);
});
} catch (error) {
console.error('Error:', error);
setTimeout(processOrders, 5000); // Retry connection
}
}

processOrders();
  1. And a corresponding publisher:
javascript
const amqp = require('amqplib');

async function publishOrders() {
let connection;
try {
// Connect to the cluster with multiple endpoints
connection = await amqp.connect([
'amqp://user:password@rabbitmq1:5672',
'amqp://user:password@rabbitmq2:5672',
'amqp://user:password@rabbitmq3:5672'
]);

const channel = await connection.createChannel();

// Declare our durable queue
await channel.assertQueue('orders.incoming', {
durable: true
});

// Publish sample orders
for (let i = 1; i <= 10; i++) {
const order = {
id: `ORD-${Date.now()}-${i}`,
customer: `Customer ${i}`,
items: [`Product ${i}`],
total: 100 * i
};

channel.publish('', 'orders.incoming', Buffer.from(JSON.stringify(order)), {
persistent: true
});
console.log(`[x] Sent order ${order.id}`);
}

setTimeout(() => {
connection.close();
process.exit(0);
}, 500);
} catch (error) {
console.error('Error:', error);
}
}

publishOrders();

This system will continue to process orders even if one of the RabbitMQ nodes fails, demonstrating the resilience provided by queue mirroring.

Summary

RabbitMQ queue mirroring is an essential tool for building reliable, highly available messaging systems. By creating copies of queues across multiple nodes in a cluster, you ensure that your system can continue operating even when individual nodes fail.

Key points to remember:

  • Queue mirroring is configured using policies rather than in application code
  • Different mirroring modes (all, exactly, nodes) offer flexibility based on your needs
  • Synchronization can impact performance and should be configured thoughtfully
  • Client applications need proper reconnection and retry logic
  • Modern deployments should consider quorum queues over classic mirrored queues

Additional Resources

  • Practice configuring different mirroring policies and observe their behavior
  • Try setting up a 3-node RabbitMQ cluster and testing failover scenarios
  • Explore client-side recovery patterns to handle node failures gracefully
  • Compare the performance of mirrored queues vs. non-mirrored queues
  • Investigate RabbitMQ's newer quorum queues as an alternative to classic mirrored queues

Exercise

  1. Set up a two-node RabbitMQ cluster and configure queue mirroring
  2. Write a producer and consumer application using a language of your choice
  3. Test what happens when you shut down the node with the master queue
  4. Measure how long it takes for your system to recover from the failure
  5. Modify your client code to handle the failover more gracefully


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)