Applicazioni distribuite con ZMQ Clone Pattern

ZMQ è una libreria di comunicazione che recentemente ha reso possibile l’utilizzo in modo semplice di pattern di messaggistica. Quando dobbiamo mettere in comunicazione due processi software, molto spesso l’unica modalità che viene in mente è il client-server, in cui la parte server fornisce un servizio ad uno o più client. In realtà questo non è un pattern vero e proprio perché ci sono molte modalità con cui un servizio può essere fornito, anzi, generalmente è un modo rapido per identificare quale parte rimane in ascolto rispetto a quella che si connette. ZMQ cerca di astrarre il modo in cui due parti si connettono tra loro, ponendo invece l’attenzione su come vogliamo che si svolga la messaggistica. I pattern built-in principali di ZMQ sono i seguenti:

ZMQ dispone di varie tipologie di socket per utilizzare i pattern base di messaggistica. Attraverso i pattern base si possono poi creare pattern di secondo livello, a seconda delle esigenze dell’applicazione che vogliamo mettere in piedi. In questo breve articolo mostrerò quali sono stati i motivi che hanno reso necessario l’utilizzo di un pattern di messaggistica avanzato come il Clone Pattern.

Problema dell’interconnessione di moduli

Quando abbiamo a che fare con un sistema distribuito, oppure vogliamo separare la logica complessa del nostro sistema su più moduli indipendenti, dobbiamo soprattutto pensare a come essi dovranno comunicare. Ciò comporta lavoro e costi in più, perciò non sempre è conveniente prendere questa decisione, soprattutto su progetti piccoli. Tuttavia l’organizzazione in moduli separati presenta alcuni vantaggi tra cui:

Siccome la logica dell’intero sistema è distribuita su più moduli, anche lo stato risulta essere distribuito. Di fatto ogni modulo o componente del sistema ha il proprio stato. Spesso la soluzione più immediata si traduce in una connessione diretta tra i moduli, come illustrato in figura.

Connessione diretta fra i moduli

Questa architettura decentralizzata risulta molto flessibile in quanto non ci sono vincoli. Spesso però una enorme flessibilità si traduce in complessità di gestione. Infatti per ogni modulo deve essere definita la modalità di comunicazione, le politiche di aggiornamento di stato e anche gli endpoint di rete. Al crescere del numero N di moduli, la tendenza è quella di ottenere collegamenti NxN, una situazione non sempre piacevole.

Per dare un taglio più strutturato a questo problema si può utilizzare un’architettura centralizzata. In questo tipo di architettura esiste un componente speciale chiamato broker che assume il ruolo di smistamento e storage dello stato dell’applicazione distribuita. Introdurre un componente centrale porta con sé una serie di pro e contro, che è importante valutare:

Nella figura seguente è mostrata un’architettura centralizzata basata su broker.

architettura centralizzata basata su broker

Condivisione dello stato

Al fine di mettere in comunicazione più moduli ed al contempo di condividere uno stato comune, il Clone Pattern di ZMQ può essere la soluzione. L’architettura del sistema è centralizzata e basata su broker. Tuttavia il compito del broker non è limitato al solo smistamento dei messaggi, bensì esso svolge anche la funzione di storage dello stato comune. Tale funzionalità di storage è una variazione rispetto al modello centralizzato generico, descritto al paragrafo precedente. Questo pattern non è uno di quelli built-in, però ne sfrutta altri di base per crearne uno più complesso. I pattern built-in utilizzati in Clone sono i seguenti:

Condivisione dello stato

La condivisione dello stato è resa possibile per mezzo del broker, dal momento che i moduli software non possono comunicare direttamente tra loro. Vediamo più in dettaglio qual è la modalità di funzionamento di questo meccanismo:

  1. Operazione di sincronizzazione. È la prima operazione che un modulo deve espletare, al momento del suo inserimento nella rete Clone. L’obiettivo è la sincronizzazione con tutto lo stato del broker (o meglio della sola sottoparte che interessa). La sincronizzazione avviene tramite una coppia di socket REQ-REP (request-reply). Il broker rimane sempre in ascolto di tali richieste di stato. Ogni qual volta ne riceve una, interrompe il suo flusso di funzionamento normale per servire la richiesta ed inviare tutto lo stato al modulo.
  2. Operazione di aggiornamento. Rappresenta lo stile di propagazione dei dati all’interno della rete ed avviene attraverso una coppia di socket PUB-SUB (publisher-subscriber). Ogni tipologia di dato prende nome di topic ed un modulo generalmente si sottoscrive solo ai topic a cui è interessato. Possiamo dire che lo stato è un insieme di dati chiave-valore, dove il topic assume il significato di chiave, mentre il valore è un payload binario generico. Il broker, durante il suo funzionamento normale, pubblica aggiornamenti ai moduli ogni volta che avviene una modifica sui dati.
  3. Operazione di modifica. Rappresenta la modalità con cui un modulo effettua una modifica sui dati di stato. In particolare ogni modulo inoltra la propria modifica di stato attraverso una coppia di socket PUSH-PULL (pipeline). Il broker a sua volta sequenzializza tutte le modifiche ricevute dai moduli e, per ognuna di esse, aggiorna il proprio stato e ripubblica infine la modifica.

Una corretta implementazione di questo pattern deve tener conto delle disconnessioni, che possono avvenire tra il broker e gli altri moduli. Tale funzionalità si può implementare per mezzo di un meccanismo di diagnostica, con pacchetti di heartbeat, inviati continuamente dal broker come se fosse una normale distribuzione di dati. Ogni componente dovrà rilevare tale heartbeat e procedere con la resincronizzazione dello stato in caso di assenza di comunicazione.

Il Clone Pattern definisce soltanto il modo con cui i messaggi passano tra un modulo e un altro, ma non pone dei vincoli invece sul payload. Affinché le interfacce dei componenti software siano chiare e ben definite, il payload dei messaggi scambiati si può definire con altri formalismi come ASN.1, MessagePack, JSON ed altri ancora. Questa decisione però spetta alla business logic degli applicativi.

Utilizzando questo meccanismo di comunicazione in un sistema reale è stato sperimentato un round-trip time di 2 ms per messaggio. In presenza di un carico sufficientemente elevato, ad esempio un messaggio di circa 800 bytes ogni 200 ms, si può avere una latenza che va da 15 ms a 50 ms (dati indicativi). Questa è una performance del tutto accettabile per applicazioni non real-time o che debbano soddisfare deadline strette.

Replica dello stato

Esistono contesti in cui la replica dello stato è fondamentale per avere un certo grado di ridondanza dell’intero sistema. La ridondanza si può utilizzare per aumentare la disponibilità oppure la sicurezza di un sistema. Il caso pratico che vogliamo discutere è relativo al primo punto. Cioè vogliamo estendere questa architettura centralizzata per ottenere un meccanismo a riserva calda (hot stand-by), per aumentare la disponibilità. In questo contesto usiamo il termine sistema per definire l’insieme di tutti i moduli connessi ad un broker, in esecuzione su una macchina. Vogliamo perciò istanziare due sistemi e collegarli tra loro in qualche modo.

Una soluzione possibile è collegare i due broker tra loro. Il componente broker si può estendere ulteriormente in modo che disponga, al proprio interno, della stessa logica di connessione e gestione che ritroviamo nei moduli software. Quindi un broker si presenta all’altro come se fosse un semplice modulo (approccio in stile federation), invece che avere canali di comunicazione privilegiati (approccio in stile peering). Il broker del sistema che assume il ruolo di riserva calda (sistema B) si sottoscrive a tutto lo stato del sistema attivo (sistema A), senza alcuna limitazione. Per costruzione quindi il sistema B otterrà tutti gli aggiornamenti e manterrà allineato il proprio stato con quello del sistema A.

Replica dello stato

Il sistema B dispone degli stessi moduli del sistema A, ma essi non potranno essere in esecuzione fino a che il sistema A è attivo e funzionante. Sotto questa condizione, la sola funzione del sistema B è mantenere il suo stato allineato. D’altra parte, l’attivazione del sistema B può avvenire con lo stesso meccanismo di heartbeating definito in precedenza. Per esempio se il sistema A smette di funzionare, il sistema B se ne accorge e subentra diventando attivo. A quel punto tutti i moduli del sistema B vengono avviati e la prima operazione che svolgono è quella di sincronizzarsi con lo stato del loro broker. Ne risulta che il funzionamento globale del sistema è ridondato, aumentando la disponibilità.

Naturalmente ci sono alcuni problemi pratici da risolvere, per la decisione di eseguire il changeover tra le due unità:

Conclusioni

ZMQ è una libreria di basso livello che mette a disposizione pattern di messaggistica, che fino a pochi anni fa erano molto complessi da implementare ed utilizzare. Tuttavia focalizzando l’attenzione sulle esigenze della nostra applicazione, possiamo interconnettere in modo efficiente i componenti software. Al giorno d’oggi è decisamente più facile realizzare un’applicazione distribuita.