In un articolo precedente, abbiamo accennato ad alcuni aspetti diversi che possono portarti a rifattorizzare il tuo codice verso un approccio basato sui microservizi. Uno degli ultimi aspetti che abbiamo affrontato è stata la questione di cosa fare con i dati—nelle applicazioni aziendali su larga scala, questa è spesso la questione più spinosa e che merita un trattamento più approfondito. Ciò che abbiamo visto è che, a volte, è difficile determinare quando si guarda a un problema di codifica o a un problema di data modeling che si maschera da problema di codifica.
Esamineremo diversi di questi casi e parleremo delle scelte di data modeling che puoi fare per semplificare e migliorare il tuo codice rifattorizzato. Ma prima, dobbiamo indirizzare quella che spesso è la domanda iniziale posta dai team che rifattorizzano applicazioni aziendali esistenti in microservizi.
Newsletter di settore
Resta al passo con le tendenze più importanti e interessanti del settore relative ad AI, automazione, dati e altro con la newsletter Think. Leggi l'Informativa sulla privacy IBM.
L'abbonamento sarà fornito in lingua inglese. Troverai un link per annullare l'iscrizione in tutte le newsletter. Puoi gestire i tuoi abbonamenti o annullarli qui. Per ulteriori informazioni, consulta l'Informativa sulla privacy IBM.
Se la tua applicazione è come la maggior parte delle applicazioni che stanno iniziando a essere rifattorizzate in microservizi, possiamo presumere che funzioni con un unico database relazionale di grandi dimensioni. Inoltre, c'è quasi la stessa probabilità che si tratti di un database Oracle: tutti gli altri database relazionali (DB2, SQL Server, Informix, o anche un database open source come MySQL o Postgres) suddividono il resto della quota. Anzi, abbandonare il database relazionale aziendale (di solito costoso) è uno dei benefici spesso promossi nel rifattorizzare i microservizi.
Ora, ci sono ottime ragioni per scegliere altri tipi di database, come NewSQL o NoSQL per molti microservizi. Tuttavia, abbandonare improvvisamente il database relazionale attuale è spesso una decisione avventata. Invece, potresti prendere in considerazione un approccio più incrementale per modificare il tuo database, così come noi sosteniamo un approccio incrementale per rifattorizzare il tuo codice Java esistente.
Proprio come nel caso della codifica, tuttavia, il problema principale nel seguire un approccio incrementale per il refactoring del database è capire da dove iniziare. La prima decisione da prendere una volta scelto un approccio incrementale è se utilizzare un unico grande database o molti piccoli database. A prima vista, tutto questo potrebbe sembrare una sciocchezza: ovviamente non vuoi un unico grande database, è quello che hai nel tuo monolito! Ma prima spieghiamo cosa intendiamo.
Fondamentalmente, il punto di partenza è la distinzione tra un server di database e uno schema di database. Per chi conosce i database su scala enterprise. come Oracle o Db2, questo sarà naturale, perché le aziende generalmente dispongono di un unico grande server Oracle (o un RAC, che è un grande server composto da molti server più piccoli) su cui più team ospitano i propri database separati (ognuno rappresentato da uno schema separato). Il motivo per cui ciò viene fatto è che le licenze sono spesso basate sulla CPU e l'azienda vuole massimizzare l'utilizzo che può ottenere per i propri soldi. Un modo per raggiungere questo obiettivo è riunire più team in un unico grande server. Per chi ha più familiarità con i database open source come MySQL o PostgreSQL,questo è un po' meno comune perché la distinzione è meno spesso necessaria.
Questo è importante perché quando parliamo di creare database per microservizi, è importante ridurre o eliminare l'accoppiamento nel database, ma ciò significa in realtà l'accoppiamento a livello di schema del database. I problemi sorgono quando hai due microservizi diversi che utilizzano le stesse informazioni, cioè le stesse tabelle all'interno dello stesso schema. Osservando la Figura 1, capirai cosa intendiamo.
L'equivalente del database di un'applicazione monolitica (alias la "Big Ball of Mud", dove tutto si collega a tutto il resto) è avere un unico grande schema che collega tutte le tabelle. Ogni volta che ciò accade, la quantità di lavoro necessario per separare i tavoli è enorme.
Tuttavia, quando si passa ai microservizi, è necessario rendersi conto che i problemi causati dalla condivisione di hardware e licenze a livello di server sono molto meno numerosi. In effetti, quando si fa la rifattorizzazione iniziale ai microservizi, ci sono molti vantaggi nel mantenere i nuovi schemi separati in modo più netto negli stessi server aziendali, perché le aziende di solito hanno già procedure per il backup e il ripristino del database e per gli aggiornamenti dei server di database, di cui i team possono utilizzare al meglio.
In un certo senso, ciò che l'azienda offre fornendo hardware, software e la gestione di un database aziendale è una versione limitata di un Database-as-a-Service (DBaaS). Ciò si adatta particolarmente bene a un approccio che inizia con una separazione più netta delle parti del monolito in base all'area funzionale, partendo da un approccio "monolito modulare", come mostrato nella Figura 2.
In questo esempio (pensato per mostrare un lavoro di rifattorizzazione in corso), puoi vedere come il database sia stato suddiviso separando le tabelle corrispondenti a tre nuovi schemi (A, B e C) che corrispondono a moduli specifici nell'applicazione rifattorizzata. Una volta separati in questo modo, possono essere suddivisi in microservizi distinti. Tuttavia, D ed E sono ancora in fase di refactoring: condividono ancora un unico schema con tabelle interconnesse.
Alla fine, anche il collegamento a livello di database-server potrebbe diventare un problema. Ad esempio, se scegli di utilizzare un database aziendale, sarai limitato alle caratteristiche disponibili. Anche in un modello relazionale, non tutti gli schemi hanno bisogno di tutte queste caratteristiche, oppure potrebbero richiedere funzionalità meglio supportate da un server diverso (ad esempio, uno sharding migliore viene spesso citato come motivo per usare un database NewSQL). Allo stesso modo, aggiornare un server di database condiviso da più microservizi potrebbe far disattivare più servizi contemporaneamente.
Ma il fatto è che si tratta di una decisione che può essere rimandata. Non deve essere presa necessariamente all'inizio del progetto. Quando i team iniziano a rifattorizzare inizialmente, mantenerli nello stesso server di database almeno per le fasi iniziali di un progetto di refactoring è un modo per apportare modifiche incrementali mentre il team acquisisce l'esperienza necessaria nel refactoring di codice e database.
Come accennato nella sezione precedente, man mano che si procede con il processo di refactoring, potrebbe essere opportuno pensare più attentamente sulle opzioni del database: non tutti i set di dati sono perfettamente adatti a un modello relazionale. Decidere quale sia l'approccio migliore per gestire questi set di dati spesso si riduce alla domanda "cosa stai effettivamente memorizzando nel tuo database?"
Abbiamo passato anni ad aiutare le aziende a costruire, mantenere e spesso torturare i framework di mappatura oggettuale, ma la realtà del problema era che in diversi casi i dati memorizzati non corrispondevano affatto bene a un modello di dati relazionale. Ogni volta che ciò accadeva, ci ritrovavamo a dover "alterare" il modello relazionale per adattarlo o, più probabilmente, a dover fare i salti mortali nei programmi per forzare il codice a corrispondere allo storage dei dati.
Ora che siamo entrati in un'era di scelte di persistenza poliglotta, possiamo riesaminare alcune di queste decisioni e prenderne di migliori. In particolare, vogliamo esaminare quattro casi diversi in cui è ovvio che il modello relazionale non era l'opzione migliore e poi considerare un caso in cui il modello relazionale era l'opzione migliore e mettere i dati in un'altra forma non sarebbe stato l'approccio giusto.
Molte volte abbiamo esaminato il codice di persistenza dei sistemi aziendali solo per scoprire, con nostra sorpresa, che ciò che in realtà memorizzavano nei loro database relazionali erano rappresentazioni binarie di oggetti Java serializzati. Questi vengono memorizzati in colonne "Binary Large Object" o "Blob" e sono solitamente il risultato di un team che si arrende di fronte alla complessità del tentativo di mappare i propri oggetti Java in tabelle e colonne relazionali. Lo storage presenta alcuni svantaggi: non può mai essere interrogato su base di colonna, spesso è lento ed è sensibile ai cambiamenti nella struttura degli oggetti Java stessi. Pertanto, i dati più vecchi potrebbero non essere leggibili se la struttura dell'oggetto cambia significativamente.
Se la tua applicazione (o, più probabilmente, un sottoinsieme della tua applicazione) utilizza lo storage Blob in un database relazionale, è già segnale del fatto che forse è meglio usare uno storage chiave-valore come Memcached o Redis. D'altra parte, potresti voler fare un passo indietro e riflettere un po' su cosa stai memorizzando. Se si tratta solo di un oggetto Java strutturato (forse molto strutturato, ma non binario in modo nativo), allora forse è meglio usare un memorizzare un Document come Cloudant o MongoDB. Inoltre, con un po' di impegno nel modo in cui memorizzi i tuoi documenti (ad esempio, entrambi i database sopra menzionati sono storage di documenti JSON e i parser JSON sono ampiamente disponibili e facili da personalizzare), puoi gestire qualsiasi problema di "deriva dello schema" molto più facilmente di quanto potresti fare con un approccio di archiviazione Blob, il cui meccanismo di storage è molto più opaco.
Anni fa, quando Martin Fowler scriveva "Patterns of Enterprise applicazione Architectures", avevamo una fitta corrispondenza e diverse vivaci recensioni su molti di questi modelli. Uno in particolare spiccava sempre come un elemento strano: il modello Active Record. Era strano perché, provenendo dalla comunità Java, non l'avevamo mai incontrato (anche se Martin ci ha assicurato che era comune nella comunità di programmazione Microsoft .NET). Ma ciò che ci ha colpito davvero, soprattutto quando abbiamo iniziato a vedere alcune implementazioni Java usando tecnologie open source come iBatis, è che sembrava che il miglior caso per usarlo fosse quando gli oggetti erano... beh, piatti.
Se l'oggetto che stai mappando in un database è completamente piatto, senza alcuna relazione con altri oggetti (con la limitata eccezione forse degli oggetti annidati), allora probabilmente non stai sfruttando tutte le funzionalità del modello relazionale. In realtà, è molto più probabile che tu stia memorizzando un documento. Molto spesso, nei casi in cui abbiamo assistito a questo fenomeno, i team memorizzano letteralmente versioni elettroniche di documenti cartacei, siano essi sondaggi sulla soddisfazione del cliente, ticket di risoluzione dei problemi, ecc. In tal caso, un database di documenti come Cloudant o MongoDB è probabilmente la soluzione migliore per te. Dividere il tuo codice in servizi propri che funzionano su quel tipo di database porterà a un codice molto più semplice e spesso sarà più facile a mantenere rispetto a un grande database aziendale.
Un altro schema comune che abbiamo riscontrato nei sistemi ORM è la combinazione di "dati di riferimento in una tabella inseriti in una cache in memoria". I dati di riferimento sono costituiti da elementi che non vengono aggiornati spesso (o mai), ma che vengono letti costantemente. Un buon esempio è l'elenco degli stati USA o delle province canadesi. Altri esempi includono i codici medici o le liste standard dei componenti. Questo tipo di dati viene spesso utilizzato per compilare i menu a discesa nelle GUI.
Lo schema comune è quello di iniziare leggendo l'elenco da una tabella (di solito una tabella piatta a due o, al massimo, tre colonne) ogni volta che viene utilizzato. Tuttavia, scoprirai che le prestazioni di questa operazione ogni volta sono proibitive, quindi l'applicazione la legge in una cache in-memory come EhCache all'avvio.
Ogni volta che si verifica questo problema, è necessario riorganizzarlo in un meccanismo di memorizzazione nella cache più semplice e veloce. Anche in questo caso, Memcached o Redis sarebbero perfettamente ragionevoli. Se i dati di riferimento sono indipendenti dal resto della struttura del database (e spesso sono o sono, al massimo, debolmente accoppiati), allora separare i dati e i loro servizi dal resto del sistema può aiutare.
Nel sistema di un cliente su cui abbiamo lavorato, facevamo modellazione finanziaria complessa che richiedeva query molto complesse (dell'ordine di join a sei o sette vie) solo per creare gli oggetti che il programma stava manipolando. Gli aggiornamenti erano ancora più complicati, poiché dovevamo combinare diversi livelli di controlli ottimisti di blocco solo per capire cosa fosse cambiato e se ciò che era nel database corrispondesse ancora alla struttura che avevamo creato e manipolato.
Col senno di poi, è chiaro che quello che stavamo facendo sarebbe stato modellato in modo più naturale come un grafo. Situazioni come questa (in questo caso, stavamo modellando tranche di fondi, ciascuna composta da diversi tipi di azioni e obbligazioni di debito, ciascuno prezzato in valute diverse e con scadenza in momenti diversi, con regole differenti per ogni valutazione) sono qualcosa che quasi richiede una struttura dati che ti permetta di fare facilmente ciò che vuoi davvero: attraversare il grafo su e giù e spostarne le parti a piacimento.
In questo caso, una soluzione come Apache Tinkerpop o Neo4J potrebbe rivelarsi una buona soluzione. Modellando la soluzione direttamente come un grafo, avremmo potuto evitare molto codice Java e SQL complicato e, allo stesso tempo, probabilmente migliorare significativamente le nostre prestazioni in tempo di esecuzione.
Anche se ci sono molti casi in cui i database NoSQL sono l'approccio logico giusto per particolari strutture dati, spesso è difficile superare la flessibilità e la potenza del modello relazionale. Il bello dei database relazionali è che è possibile "suddividere" in modo molto efficace gli stessi dati in forme diverse per scopi diversi. Trucchi come le Viste del database permettono di creare più mappature degli stessi dati, cosa spesso utile quando si implementa un insieme di query correlate di un modello dati complesso.
Per approfondire un confronto tra NoSQL e SQL vedi "SQL e NoSQL: qual è la differenza?"
Come abbiamo discusso in un articolo precedente, se il "limite inferiore" di un microservizio è un insieme di entità raggruppate in un aggregato insieme all'insieme associato di servizi che operano su quei dati, implementare le viste per rappresentare un insieme di query correlate è spesso più semplice e diretto con SQL.
Lo abbiamo visto per la prima volta nell'esempio del cliente che ha portato al nostro semplice esempio di Account/LineItem nell'articolo precedente. In questo caso, c'era una banca che stava cercando con tutte le sue forze di far funzionare un modello semplice simile in un database NoSQL orientato ai documenti, solo per essere sconfitto dal teorema CAP. Tuttavia, il team aveva scelto quel modello di dati per tutte le ragioni sbagliate. Avevano scelto di utilizzare il database orientato ai documenti per tutti i loro diversi microservizi, per un desiderio infondato di coerenza architettonica.
In questo caso, avevano bisogno di tutti gli attributi della modalità ACID, ma non avevano bisogno dello sharding (ad esempio, partizionamento). Il loro sistema esistente aveva funzionato per anni a un livello di prestazioni del tutto accettabile su un modello relazionale, e non prevedevano una crescita enorme. Ma, sebbene il nucleo del sistema necessitasse di transazioni ACID e non di partizionamento, ciò non era necessariamente vero per tutte le diverse parti del sistema. Ci sono alcune cose in cui i database SQL sono eccellenti, e le transazioni ACID sono una di queste. Nei sistemi di microservizi, raggruppare queste transazioni ACID attorno al più piccolo insieme di dati su cui operano è solitamente l'approccio giusto.
Tuttavia, SQL non significa necessariamente database SQL tradizionali. Può esserlo, e c'è certamente spazio per questo in molte architetture di microservizi, ma SQL è implementato anche in almeno altri due tipi di database che possono essere scelte utili per molti team che implementano microservizi. Il primo è "SQL piccolo", che è il dominio dei database open source come MySQL e Postgres. Per molti team che implementano microservizi, questi database sono una scelta perfettamente ragionevole per molte ragioni:
Il principale svantaggio dell'utilizzo di questi database SQL è che spesso non supportano lo stesso livello di scala (in particolare per quanto riguarda lo sharding) supportato dai database aziendali. Tuttavia, questo problema è stato risolto in modo ammirevole da una nuova serie di fornitori di database e progetti open source che combinano le migliori caratteristiche dei piccoli database SQL Database e la scalabilità dei database NoSQL. Tra questi rientrano CockroachDB, Apache Trofidion e Clustrix, spesso chiamati database "NewSQL".
Ogni volta che hai bisogno di tutte le transazioni ACID, di un motore SQL che supporti completamente il modello relazionale e anche di ampie funzionalità di scalabilità e sharding, i database NewSQL possono essere una buona scelta per i team. Questa scelta ha però un costo: questi database sono spesso più complessi da configurare e gestire rispetto alle vecchie soluzioni "small SQL". Inoltre, è più difficile trovare persone con competenze in questi database. In ogni caso, però, devono comunque essere considerati attentamente quando si valutano le opzioni.
Abbiamo esaminato diversi problemi di modellazione dei database che possono essere mascherati da problemi di codifica. Se scopri di avere uno o più di questi particolari problemi, allora potrebbe essere meglio separarti dal tuo attuale archivio di dati aziendale e ripensarlo con un tipo diverso di storage dei dati. In ogni caso, il nostro consiglio è di procedere in modo lento e graduale. Tieni presente che non tutte le applicazioni necessitano di tutti i tipi di modelli dati e che devi sviluppare competenze e comprendere le capacità operative dei database scelti nel tempo prima di iniziare a implementarli su larga scala.
Infine, sii consapevole che l'intero approccio a microservizi si basa sull'idea che i team che costruiscono ogni microservizio possano prendere le proprie decisioni su quale storage dei dati sia giusto per loro. Il problema più grande che abbiamo incontrato (come molte di queste storie hanno suggerito) è cercare di far funzionare un unico approccio di rappresentazione e storage dei dati per tutte le situazioni.
Molte volte ho visto team dover cercare di affrontare la realtà del teorema CAP in situazioni in cui non avrebbero nemmeno dovuto utilizzare un database NoSQL. Allo stesso modo, cercare di creare report complessi da un database NoSQL è spesso un esercizio frustrante. D'altra parte, le situazioni che abbiamo mostrato evidenziano che il modello relazionale non è onnicomprensivo. C'è spazio anche per altri modelli. Il miglior consiglio che possiamo dare è di garantire che i vostri team abbiano l'autonomia necessaria per scegliere il modello giusto per ogni microservizio.
IBM Garage è progettato per muoversi più velocemente, lavorare in modo più intelligente e innovare il servizio interrompendo le interruzioni.
Red Hat OpenShift on IBM Cloud è una OpenShift Container Platform (OCP) completamente gestita.
Utilizza il software e gli strumenti DevOps per creare, distribuire e gestire app cloud-native su più dispositivi e ambienti.
Sblocca nuove funzionalità e promuovi l'agilità aziendale con i servizi di consulenza cloud di IBM. Scopri come creare insieme soluzioni, accelerare la trasformazione digitale e ottimizzare le prestazioni attraverso strategie di hybrid cloud e partnership di esperti.