facebook

Blog

Resta aggiornato

Vediamo come utilizzare RabbitMQ in contesti di alta disponibilità
High availability con RabbitMQ
mercoledì 17 Luglio 2019

Quando si ha a che fare con sistemi complessi, spesso è necessario prevedere meccanismi per i quali i servizi forniti risultino sempre disponibili, ovvero si richiede che un sistema sia altamente affidabile.

Nell’articolo precedente (Disaccoppiare la comunicazione con RabbitMQ), esplorando le sue caratteristiche di base, abbiamo visto come RabbitMQ sia un’ottima soluzione per far sì che applicazioni diverse possano instaurare una comunicazione.

Ma RabbitMQ mette a disposizione anche delle features che lo rendono uno strumento valido per quei sistemi che richiedono un certo livello di QoS (Quality of Service).

Un broker RabbitMQ può essere definito come l’insieme logico di uno o più nodi che eseguono l’applicazione RabbitMQ e condividono le stesse entità (code, exchanges, bindings, etc.).

L’insieme di nodi viene anche chiamato cluster. Tali nodi sono identificati all’interno del cluster mediante il loro nome, formato da un prefisso e da un hostname che, per tale motivo, deve essere unico.

Inoltre, l’hostname è necessario per l’identificazione tra i nodi stessi del cluster e quindi un hostname deve essere risolvibile, ad esempio mediante un sistema DNS (Domain Name System).

Per questo motivo, e per simulare al meglio la composizione di un cluster, possiamo definire una rete Docker di tipo bridge che fornisca una risoluzione DNS automatica tra container.

docker network create --subnet=192.168.0.0/16 cluster-network

Possiamo creare i nodi del cluster utilizzando due container Docker che eseguano RabbitMQ. I comandi sono i seguenti:

docker run -d -h node1.rabbit \
           --net cluster-network --ip 192.168.0.10\
           --name rabbitNode1\
           --add-host node2.rabbit:192.168.0.11\
           -p "4369:4369"\
           -p "5672:5672"\
           -p "15672:15672"\
           -p "25672:25672"\ 
           -p "35672:35672"\
           -e "RABBITMQ_USE_LONGNAME=true"\
           -e RABBITMQ_ERLANG_COOKIE="cookie"\
           rabbitmq:3-management
 
 
docker run -d -h node2.rabbit\
           --net cluster-network --ip 192.168.0.11\
           --name rabbitNode2\
           --add-host node1.rabbit:192.168.0.10\
           -p "4370:4369"\
           -p "5673:5672"\
           -p "15673:15672"\
           -p "25673:25672"\
           -p "35673:35672"\
           -e "RABBITMQ_USE_LONGNAME=true"\
           -e RABBITMQ_ERLANG_COOKIE="cookie"\
           rabbitmq:3-management

Attualmente, i due nodi sono entità separate:

RabbitNode1

RabbitNode2

Per fare in modo che due nodi possano appartenere allo stesso cluster bisogna assicurarsi che determinate porte siano accessibili, in particolare:

  • 4369: epdm (ERLANG PORT MAPPER DAEMON), un servizio per la scoperta dei peer, utilizzato dai nodi e dai tool CLI di RabbitMQ;
  • 5672: porta usata dal protocollo AMQP;
  • 25672: utilizzata per la comunicazione tra nodi e con la CLI;
  • 35672-35682: utilizzati dai tools CLI per la comunicazione con i nodi;
  • 15672: utilizzata ad esempio per il plugin di management UI.

Inoltre, notiamo che, tra le variabili di ambiente configurate, viene definita RABBITMQ_ERLANG_COOKIE. Si tratta di una chiave segreta, che consente a due nodi di un cluster di poter interagire tra loro.

Fermiamo l’esecuzione di RabbitMQ sul nodo rabbitNode2:

docker exec rabbitNode2 rabbitmqctl stop_app

e successivamente:

docker exec rabbitNode2 rabbitmqctl join_cluster rabbit@node1.rabbit

Facendo ripartire l’applicazione:

docker exec rabbitNode2 rabbitmqctl start_app

Otteniamo il seguente risultato:

Chiaramente, la Managment UI risulta molto comoda e pratica, ma possiamo ottenere le informazioni sul cluster anche lanciando il seguente comando:

docker exec rabbitNode1 rabbitmqctl cluster_status

Proviamo ad inviare un messaggio al nostro cluster utilizzando l’applicazione Sender, mostrata nell’articolo precedente. Una volta eseguita l’applicazione, andiamo sulle management UI dei due nodi dove possiamo notare che la coda è stata creata ed il messaggio è accodato correttamente ma tutto ciò che accade è replicato su entrambi i nodi.

RabbitNode1

RabbitNode2

Ma cosa succede se uno dei nodi smette di funzionare?

Se fermiamo l’esecuzione sul nodo rabbitNode1 mentre il messaggio è in coda e andiamo sulla management UI del nodo rabbitNode2 ci ritroviamo in questa situazione:

Creando un cluster, abbiamo sicuramente la replicazione di dati e stati necessari al funzionamento del broker, ma ciò non vale per le code, che di base risiedono su un solo nodo. Per questo motivo, terminando l’esecuzione del nodo rabbitNode1, abbiamo perso il messaggio inviato.

Per ovviare a questa situazione spiacevole, che comporta perdita di informazione, RabbitMQ consente di creare delle High Available Queue, dette anche Mirrored Queue. Una coda quindi, che risiede su di un nodo (master), può essere replicata, così come le operazioni che avvengono su di essa, sui nodi (mirrors) che compongono il cluster.

Per configurare le code del cluster affinché siano replicate, bisogna definire una policy, ovvero un pattern condiviso da tutte le code rappresentato da una regular expression.

Lanciamo il seguente comando:

docker exec rabbitNode1 rabbitmqctl set_policy ha "." '{"ha-mode":"all"}'

dove ha è il nome della policy, “.” è il pattern e ha-mode settato ad “all” indica che tutte le code devono essere high available.

Creiamo nuovamente la coda avviando l’applicazione Sender e nella sezione queues della managment UI ritroveremo quanto descritto all’interno dei dettagli della nostra coda.

Se adesso stoppiamo il container rabbitNode1, il nodo non sarà più in esecuzione ma a differenza di prima la coda è ancora disponibile e non abbiamo perso il messaggio.

Per riceverlo correttamente, bisogna però apportare una piccola modifica alla nostra applicazione consumer, definita nel precedente articolo come Receiver.

var endPointList = new List<amqptcpendpoint>
            {
                new AmqpTcpEndpoint("localhost", 5672),
                new AmqpTcpEndpoint("localhost", 5673)
            };
            var factory = new ConnectionFactory();
            using (var connection = factory.CreateConnection(endPointList))
</amqptcpendpoint>

Definiamo una lista di Endpoint (i nodi del nostro cluster), specificando le porte per il protocollo AMQP. Passiamo tale lista come parametro del metodo CreateConnection, messo a disposizione dal client RabbitMQ per .NET, il quale si occupa di verificare quale endpoint sia disponibile per la connessione al broker.

Eseguendo l’applicazione Receiver notiamo come il messaggio venga consumato correttamente:

Al prossimo articolo.