Il viaggio di IBiS verso il quorum code

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.

Informazioni su IBiS

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:

  • tiene traccia del ciclo di vita delle risorse in esecuzione nel cloud (macchine virtuali, cluster Kubernetes, ecc.).
  • misura e valuta l'utilizzo delle risorse (CPU, memoria, storage, ecc.).
  • mantiene aggiornati i sistemi aziendali esistenti e di terze parti (sistemi di gestione della configurazione, sistemi di fatturazione, ecc.)
  • offre varie funzioni di reportistica (ad esempio report sulla fatturazione o sulle licenze dei sistemi operativi utilizzati) per il portale clienti e i sistemi aziendali successivi.

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.

RabbitMQ all'IBiS

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.

La nostra configurazione

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.

  • Utilizziamo uno scambio di argomenti a cui inviamo nuovi messaggi.
  • Utilizziamo uno scambio di lettere morte per gestire i messaggi difettosi.
  • Ogni componente del backend dichiara due code: una per elaborare i nuovi messaggi provenienti dallo scambio di argomenti e una per rielaborare i messaggi errati provenienti dallo scambio di lettere morte.

Migrazione alle code di quorum con Spring AMQP

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

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.

Migrazione trasparente

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:

  1. creare nuove code del tipo Quorum.
  2. Termina l'instradamento verso le code speculari esistenti.
  3. elaborare i messaggi rimasti nelle code speculari esistenti (possono ancora arrivare mentre interrompiamo l'inoltro).
  4. Rimuovi le code in mirroring in modo che solo le code del quorum siano ancora disponibili.

Utilizzando la nostra architettura di messaggistica e Spring AMQP, possiamo tradurre questi passaggi nel seguente modo. Per ogni componente backend, vogliamo:

  1. fornire una nuova coda di quorum all'avvio dell'applicazione e rimuovere la coda precedente dalla configurazione. In questo modo verrà creata la nuova coda che stiamo ascoltando e verrà interrotto l'ascolto della vecchia coda.
  2. rimuovere il collegamento tra la vecchia coda e lo scambio di argomenti, cioè rimuovere le regole di routing che fanno sì che lo scambio di argomenti continui a inoltrare i messaggi alla vecchia coda.
  3. rimuove tutti i messaggi dalla coda di mirroring rifiutandoli (cioè segnalando un errore) e quindi spostandoli nella coda delle lettere morte.
  4. consuma i messaggi dalla coda delle lettere morte.
  5. Rimuovi la coda in mirroring.

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).

Conclusion

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.

Riferimenti

Adam Krajewski

Adam Krajewski

Software Engineer

Altri articoli getIT

Prêts pour Swisscom

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.

Vai ai percorsi di carriera

Vai alle posizioni vacanti cibersicurezza