Cloud
Il percorso di IBiS verso le code di quorum con RabbitMQ
Il nostro sistema, la nostra motivazione e i passi che abbiamo seguito per migrare una serie di applicazioni basate su Java/Spring con il solo codice.
Cloud
Il nostro sistema, la nostra motivazione e i passi che abbiamo seguito per migrare una serie di applicazioni basate su Java/Spring con il solo codice.
L'anno scorso abbiamo attivato il supporto per la Quorum Queue di RabbitMQ [1] nella nostra soluzione PaaS interna a IT-Clouds, un gruppo responsabile dello sviluppo dei prodotti cloud di Swisscom. Abbiamo poi migrato uno dei nostri sistemi software per utilizzarlo in produzione. Questo post descrive il nostro sistema, la nostra motivazione e i passi che abbiamo compiuto per migrare una serie di applicazioni basate su Java/Spring utilizzando solo il codice.
IBiS è l'acronimo di "Integration of Business Services". Si tratta di un importante sistema di backend per le offerte cloud di Swisscom (Enterprise Service Cloud, Dynamic Computing Services), che supporta le funzioni di fatturazione e reporting dei nostri prodotti. Il sistema:
Per raggiungere i suoi obiettivi, IBiS ascolta tutti i tipi di eventi provenienti dai servizi cloud e reagisce di conseguenza. È costituito da una serie di componenti semplici e orientati agli eventi:
IBiS è sviluppato da Swisscom, è scritto in un moderno Java e si basa sulle ultime versioni di Spring Framework.
Requisiti per una trasmissione affidabile dei messaggi
Il cuore di IBiS è RabbitMQ [2], un broker di messaggistica open source che supporta il protocollo di messaggistica AMQP [3]. Utilizziamo RabbitMQ come servizio all'interno del nostro Application Cloud interno (iAPC), una soluzione PaaS basata su Cloud Foundry [4]. È responsabile del trasporto affidabile degli eventi all'interno del sistema, dai componenti che li ricevono (ad esempio da Apache Kafka [5]) ai componenti che li elaborano applicando la logica aziendale appropriata. Uno dei principali requisiti aziendali di IBiS è quello di tracciare con precisione ciò che accade nei Cloud di Swisscom. Per raggiungere questo obiettivo, dobbiamo garantire che gli eventi vengano trasmessi in modo affidabile. La perdita di un evento può avere conseguenze disastrose e può facilmente portare a errori di fatturazione o di reporting (ad esempio, una VM è stata cancellata nel cloud, ma IBiS non ne ha mai saputo nulla perché l'evento è andato perso).
In origine, IBiS utilizzava code con mirroring permanente per ottenere l'affidabilità desiderata. Questa soluzione è stata valida per molto tempo, ma le ultime versioni di RabbitMQ (3.8.0+) supportano le code quorum. Le code di quorum sono considerate l'alternativa moderna alle code con mirroring. Si concentrano sulla sicurezza dei dati e sono progettate specificamente per soddisfare le esigenze di sistemi come IBiS in cui l'affidabilità è fondamentale. Poiché il nostro carico di lavoro è esattamente quello per cui sono state create le code di quorum, abbiamo deciso di abbandonare le code con mirroring. Il resto dell'articolo descrive come abbiamo fatto.
Prima di parlare della migrazione in sé, è importante descrivere brevemente il sistema RabbitMQ utilizzato da IBiS. La nostra architettura interna di messaggi si basa su diversi scambi di argomenti [6] a cui vengono inviati messaggi (eventi). Ogni messaggio è accompagnato da una chiave di routing, che nel nostro caso è il tipo di evento. Gli scambi inoltrano i messaggi a una serie di code [7]. Le code sono create (dichiarate) dai nostri componenti di backend; ogni coda appartiene esattamente a un componente e ascolta un sottoinsieme di chiavi di routing. Questo rappresenta essenzialmente un sistema publish/subscribe in cui ogni componente di backend può sottoscrivere specifiche chiavi di routing (tipi di eventi) e ascoltare solo i messaggi che gli interessano. L'architettura di questo tipo di scambio è mostrata nella figura seguente:
Quando elaboriamo i messaggi, dobbiamo anche gestire gli errori (ad esempio se un evento è formato in modo errato). A questo scopo utilizziamo le code di lettera morta. Se un messaggio non può essere elaborato da un componente di backend, viene inviato a uno scambio di lettere morte [8], che lo spinge in una coda di lettere morte appartenente al componente in cui è stato rilevato il problema. Il messaggio rimane lì finché non capiamo il problema e possiamo elaborarlo manualmente.
In definitiva, la nostra architettura può essere riassunta come segue.
La nostra ricerca sul passaggio da code speculari a code quorum ha rapidamente rivelato che RabbitMQ non può aiutarci a farlo da solo. Le code di RabbitMQ non possono semplicemente cambiare tipo. Sono immutabili, cioè per passare alle code quorum dobbiamo dichiarare nuove code, spostare i messaggi dalle vecchie code e rimuovere le vecchie code. Un modo per risolvere questo problema sarebbe quello di utilizzare il plugin Shovel [9]. Tuttavia, ciò richiederebbe l'intervento del team RabbitMQ di Swisscom (pianificazione della manutenzione, installazione del plugin, test, ecc.) senza alcuna garanzia che l'approccio funzioni effettivamente per il nostro caso d'uso. Pertanto, abbiamo deciso di prendere una strada diversa e di gestire tutto con Spring AMQP come codice.
Spring AMQP [10] è un progetto Spring progettato per supportare lo sviluppo di soluzioni basate su AMQP. Fornisce astrazioni pratiche, facili da usare, che si integrano bene con il Framework Spring e che permettono di sfruttare appieno il potenziale di AMQP. Sebbene non rientri negli scopi di questo articolo entrare nel dettaglio di Spring AMQP, è importante menzionare che fornisce classi con cui possiamo dichiarare e gestire vari oggetti RabbitMQ. Ad esempio, possiamo facilmente dichiarare un nuovo scambio di argomenti, regole di routing (binding) e una nuova coda:
Una volta dichiarata la nostra configurazione (che viene poi distribuita automaticamente in modo idempotente all'avvio dell'applicazione), possiamo accedere a RabbitMQ utilizzando la classe RabbitTemplate. Proviamo a mettere insieme questi pezzi per convertire le code speculari in code quorum.
Possiamo prendere nota di ciò che deve accadere per migrare in modo trasparente e sicuro dalle code con mirroring alle code con quorum. Ecco cosa dobbiamo fare:
Utilizzando la nostra architettura di messaggistica e Spring AMQP, possiamo tradurre questi passaggi nel seguente modo. Per ogni componente backend, vogliamo:
I passaggi descritti in precedenza sono illustrati nel seguente diagramma:
Potresti chiederti perché abbiamo deciso di svuotare le vecchie code inviando i messaggi alle code lettera morta invece di consumarli direttamente. Abbiamo scelto questo approccio per motivi di manutenzione. Dato che ci affidiamo molto alle code di lettere morte per gestire i messaggi errati, abbiamo già un codice robusto e collaudato che possiamo riutilizzare. Il recupero diretto delle code speculari sarebbe possibile, ma comporta il rischio che alcuni casi limite non vengano gestiti e che quindi i messaggi vadano persi.
Ora possiamo esaminare il codice che abbiamo utilizzato per migrare le nostre code.
Definiamo innanzitutto i nomi delle code. Supponiamo di aver dichiarato la nuova coda quorum con il nome "my-quorum-queue" e che la precedente coda classica con mirroring "my-classic-mirrored-queue" esista già:
Quindi possiamo utilizzare l'oggetto RabbitTemplate autocablato (vedi i documenti di Spring AMQP per maggiori dettagli) per ottenere un client di amministrazione:
Per prima cosa verifichiamo se la coda classica esiste. Se così non fosse, non abbiamo nulla da fare.
Successivamente, utilizziamo i binding che abbiamo dichiarato nella configurazione della nostra applicazione. Se li creiamo in modo simile all'esempio della sezione Spring AMQP, possiamo creare un elenco di bind e usarlo per rimuovere i bind dalla vecchia coda. Il presupposto fondamentale è che i vincoli siano gli stessi, cioè che la nuova coda abbia lo stesso set di vincoli della vecchia.
Quindi supponiamo che la rimozione dei vincoli richiederà un po' di tempo e che alcuni messaggi potrebbero ancora arrivare mentre stiamo facendo progressi. Ecco perché aspettiamo un po'.
A questo punto, la vecchia coda dovrebbe raggiungere uno stato coerente. Si presume che tutti i messaggi siano stati ricevuti e che non ne arrivino di nuovi perché non ci sono vincoli. Pertanto, possiamo rifiutare tutti i messaggi e spostarli nella coda delle lettere morte:
In questo momento, la vecchia coda deve essere svuotata e tutti i messaggi in attesa devono essere spostati nella coda delle lettere morte. Come già detto, il nostro codice IBiS contiene la logica per la coda delle lettere morte. Questo avviene tramite l'oggetto `deadLetterQueueService` (che sotto il cofano fornisce solo alcuni metodi di utilità per leggere la coda delle lettere morte o ottenere alcune statistiche). Lo usiamo per assicurarci che la migrazione sia avvenuta con successo e per consumare i messaggi:
Infine, rimuoviamo la vecchia coda:
Abbiamo fornito questa migrazione come endpoint REST, che viene attivata automaticamente (dopo l'avvio dell'applicazione) o manualmente (se dobbiamo ripeterla dopo un errore). La migrazione è idempotente, cioè può essere attivata più volte e porta sempre allo stesso risultato.
Puoi trovare il codice completo qui(apre una nuova finestra).
In questo articolo abbiamo descritto come abbiamo migrato IBiS, una piattaforma software che stiamo sviluppando per supportare le offerte cloud di Swisscom in termini di fatturazione e reportistica, alle code quorum di RabbitMQ. Abbiamo parlato di come utilizziamo RabbitMQ internamente, dei nostri requisiti e di come abbiamo deciso di migrare. Infine, abbiamo utilizzato esempi pratici di codice per mostrare come sia possibile effettuare una migrazione di questo tipo con Java e il progetto Spring AMQP.
Software Engineer
Trouve le Job ou l’univers professionnel qui te convient. Où tu veux co-créer et évoluer.
Ce qui nous définit, c’est toi.