Stili architetturali in Kubernetes: 5 modi per sviluppare software

15 minutes read
11 Febbraio 2021

Chi approccia il mondo dei microservizi e di Kubernetes si trova tipicamente a porsi alcune domande: meglio separare microservizi per layer o scopo e quanto gateway devo avere? Quali linguaggi sarebbe più appropriato usare ed è necessario un pub/sub per far comunicare microservizi tra di loro?

In questo articolo vedremo alcuni stili architetturali e buone pratiche di test, deploy, monitoraggio e business continuity per creare piattaforme robuste e scalabili.

L’articolo è tratto dal talk di Giulio Roggero, CTO di Mia-Platform, tenuto al GDG Dev Fest Italia 2020, disponibile a questo link.

 

Innanzitutto proviamo a spiegare perché li chiamiamo proprio “stili” architetturali? Abbiamo scelto di utilizzare una parola affine al mondo dell’arte perché pensiamo che, come gli artisti interpretano la realtà attraverso i propri stili, così gli informatici interpretano la realtà dei dati a loro volta secondo stili.

 

Introduzione a Kubernetes

Partiamo quindi dal principio, ripassando brevemente le basi di Kubernetes e degli stili architetturali. Gli stili come li intendiamo in questo articolo sono la descrizione di un sistema informatico complesso, articolato e distribuito.

Si dice spesso che le cose facili sono difficili da realizzare; lo stile architetturale è ciò che ti permette di semplificare qualcosa di molto complesso e che si trova alla base dei nostri sistemi.

Quindi, tornando alla nostra similitudine iniziale, se gli artisti hanno colori e pennelli, gli informatici hanno Kubernetes.
Kubernetes si potrebbe definire come un “sistema operativo distribuito” per sistemi informatici moderni.

Per comprendere come funziona Kubernetes facciamo ancora un passo indietro e pensiamo a come funziona un sistema operativo:

 

Un processo

Prendiamo un processo che gira sul nostro computer (se usate linux, fate un ps-ef e vedrete i processi che girano).

Ne prendiamo in considerazione uno che chiameremo processo Giallo, e che avrà bisogno di qualcosa per poter girare. Il nostro processo Giallo non girerà finché non avrà una CPU, una Ram, il disco e il networking.

La macchina che utilizziamo e che fornisce questo hardware, unita al sistema operativo, farà girare il nostro processo Giallo, ma farà girare contemporaneamente anche altri processi. In poche parole, allocherà in modo appropriato tutte le risorse per far girare insieme i processi correttamente.

 

E se avessimo più macchine?

E se queste macchine dovessero far girare più repliche di ciascun processo? Ad esempio: due repliche del processo verde, quattro repliche del processo giallo, ecc.

 

Mia-Platform_Stili-architetturali

 

A quel punto non mi basterebbe più un singolo sistema operativo per singola macchina, ma avrei bisogno di un modo per orchestrare il tutto: qualcuno che si metta a definire cosa deve accadere e dove deve schedularlo.

L’orchestratore si prende in carico, ad esempio, due istanze del processo verde, e le schedula secondo politiche simili a quelle di un sistema operativo su due macchine A e B.

Mia-Platform_Stili-architetturali2

 

Sappiamo però che non sempre le cose procedono senza alcuna interruzione. Per questo, ipotizziamo che si rompa una macchina, la macchina A.

Il nostro orchestratore, senza perdersi d’animo, riprende quei processi e li rialloca nelle altre macchine: ecco in poche parole un sistema operativo distribuito. L’orchestratore rialloca i processi tra le risorse disponibili in modo dinamico e con regole precise.

Quanto abbiamo appena descritto è ciò che fa Kubernetes, ma ad altissimo livello: compie operazioni molto complesse che possono essere sintetizzate con l’espressione di sistema operativo distribuito.

In questo articolo andremo a vedere proprio come utilizzare Kubernetes e con quali stili.

Di cosa si compone Kubernetes:

  • I namespace che isolano un gruppo di servizi a livello di RBac per gli sviluppatori o operation, che possono così accedere a livello di gestione solo a un gruppo di servizi isolati all’interno del namespace. All’interno del namespace abbiamo uno o più pod;
  • Un pod è la più piccola unità gestita da k8s. Il pod è molto più complesso di un singolo processo, e dentro il pod abbiamo i container;
  • I container: all’interno dei container abbiamo, solitamente, un singolo servizio;
  • Il singolo servizio gira all’interno del sistema.

 

Dentro al pod tendenzialmente non metteremo container differenti che fanno cose differenti. Il pod fa una cosa ben specifica e precisa all’interno delle logiche di business, ma posso associargli un sidecar, un container più piccolo che esegue operazioni di gestione.

 

Mia-Platform_Stili-architetturali3

 

Fin qui abbiamo visto quali sono i nostri strumenti di partenza.

 

Stili architetturali in Kubernetes

La CNCF, la fondazione che si occupa dello sviluppo delle tecnologie cloud native nel mondo, ha aggregato nel suo landscape tantissime tecnologie, che aiutano Kubernetes a funzionare sempre meglio.

Ma partire a comprendere la materia cercando di approcciare tutte queste tecnologie può essere dispersivo. Per orientarsi è meglio seguire i principi cardine che possiamo identificare, appunto, come stili architetturali.

Per comprendere a fondo gli stili architetturali, prendiamo un caso concreto.

Nel nostro caso concreto abbiamo a disposizione alcuni canali: webapp, IoT di casa, app mobile, smartwatch, auto connessa, ecc. Per canale intendiamo qualsiasi cosa che vada a prendere informazioni e interagisca con un sistema informativo. 

Inseriamo questo sistema informativo all’interno di Kubernetes e poi:

  • Creiamo il namespace;
  • Creiamo il pod;
  • Creiamo il container e il nostro servizio.

Come prima applicazione di esempio potremmo creare un monolite all’interno di Kubernetes: questa sarebbe la cosa più semplice da fare.

In questo caso, Kubernetes forse non sarebbe neanche la soluzione ideale, ma ci aiuta ad avere una prima interpretazione. 

È come la prima pennellata sulla tela e il quadro è finito.

Proviamo quindi ad aggiungere complessità al nostro caso.

 

Le Single-Page Application

Il primo stile architetturale che vedremo in questo articolo, e il più basico della nostra panoramica, è quello delle single-page Application.

Oggi si parla moltissimo di single-page application, applicazioni che girano nel browser interagendo con la parte server e API. 

Un Gateway disaccoppia questa interazione: da un lato la parte server con le logiche di business dell’app, dall’altro gli static asset, html/CSS/JS, che girano all’interno del web browser con le loro logiche di business di presentazione e interazione utente che poi chiamano le nostre API. 

Mia-Platform_Stili-architetturali4

Ognuno dei box contornati di arancione è un pod differente gestito da kubernetes. I trattini sono il namespace.

Quella che abbiamo appena descritto potrebbe essere una prima applicazione monolitica. Se però volessimo far scalare questa applicazione, potremmo trovare all’interno del server logiche molto lente che devono scalare prima di altre, perché vengono maggiormente stressate dagli utenti. A questo punto potrebbe essere utile iniziare a dividere in microservizi.

Prendiamo quindi la nostra applicazione server e proviamo a dividerla in microservizi, ciascuno con una propria responsabilità.

Come facciamo a questo punto a far comunicare tra loro i microservizi?

 

Back-end for Front-end

Il nostro suggerimento è quello di approcciare lo stile che Sam Newman ha chiamato Back-end for Front-end (BFF): il BFF si occupa di esporre delle API che semplificano l’interazione con l’utente, mentre i microservizi si occupano di gestire le logiche di business, ben aggregate nel bounded context. 

Il BFF può coordinare la chiamata anche di più microservizi ed esporre queste informazioni aggregate verso il canale di frontend.

Il nostro static asset a destra rimane così invariato:

 

Mia-Platform_Stili-architetturali5

Ma se avessimo più canali, quindi diverse interazioni utente?

In questo caso, un solo BFF con una sola API potrebbe non essere sufficiente. Sarebbe più sicuro avere diversi BFF (mobile, web, IoT) che espongono interazioni diverse in base al canale di riferimento: il BFF mobile esporrà dati più ridotti, o paginati, o comunque fatti in modo per stare dentro a uno schermo; IoT potrebbe essere minimale, ecc. I microservizi sottostanti invece continueranno a funzionare sempre allo stesso modo.

Questo schema potrebbe rappresentazione un’applicazione, a livello di business, che sta girando su Kubernetes:

 

Mia-Platform_Stili-architetturali6

 

La complessità però non finisce qui: all’interno dello stesso cluster Kubernetes possono esserci più namespace con diverse applicazioni. Posso inoltre avere più team all’interno di Kubernetes che vedono la propria partizione dell’applicazione.

Per garantire una corretta governance, in questi casi, il nostro consiglio è di mettere uno strato on top alle applicazioni, un API Gateway che intermedia e gestisce sicurezza, privacy, performance delle API esposte per tutti. Si tratta di un servizio condiviso multi-tenant da usare per tutte le applicazioni.

Per comprendere ancora più a fondo, approfondiamo nel dettaglio l’applicazione e guardiamola punto per punto.

Spesso si pensa che in questo tipo di applicazioni a microservizi tutto debba essere asincrono.

Nella semplicità però, non sempre l’asincronismo si rivela corretto perché può portare molte complessità di gestione, debugging, troubleshooting, ecc.

Perciò, quello che consigliamo è di avere un’architettura come quella descritta sopra, che scala e funziona.

Saga Pattern

Un sistema come quello appena descritto potrebbe non essere appropriato in tutti i contesti: allora potrebbe essere necessario realizzare un sistema pub/sub con un message broker

Invece di avere una comunicazione punto per punto, dove i servizi si devono conoscere tra di loro, facciamo in modo che i servizi inizino a pubblicare dei messaggi, e mettiamo altri servizi che si sottoscrivono a questi messaggi per compiere operazioni.

Potremmo avere il microservizio del catalogo prodotti con le disponibilità, il microservizio del carello e un’app di front-end semplice che invia il comando alloca questo prodotto. Quando il prodotto è allocato, il carrello va in subscribed e lo alloca sul suo database.

 

Mia-Platform_Stili-architetturali7

Il problema in questo caso è che l’utente che ha in mano il cellulare, e interagisce col frontend, aspetta che il prodotto venga allocato, e questa è una comunicazione di tipo sincrono, mentre l’http è una comunicazione che ha tipicamente un call back. 

Quando mandiamo un messaggio dal frontend che dice alloca il prodotto, ci aspettiamo una risposta dall’http request ok/ko.

In questo caso abbiamo invece un “ok, ho mandato il messaggio”. Ma cosa è successo realmente?

Una delle opzioni, forse la più complessa, applicabile in questi casi, è quella di adottare un Saga Pattern. In questo modo, ogni volta che parte un’interazione utente con una transazione distribuita, viene staccata una saga che viene poi salvata sul database proprietario, e questa saga permette di orchestrare tutti i messaggi tra di loro.

 

Mia-Platform_Stili-architetturali8

 

Arrivati a questo punto possiamo far diventare sincrono l’asincrono, ad esempio nei casi in cui ho bisogno di raccogliere i dati di una carta per effettuare un pagamento.

Se poi il pagamento non va a buon fine, la saga può fare rollback del prodotto e anche del carrello.

Questo scenario inizia ad essere quello di un’applicazione sufficientemente complessa.

 

Integrazione con sistemi Legacy

Non sempre però abbiamo la fortuna di partire da zero, dal cosiddetto green field. Più spesso capita di dover partire da soluzioni in cui sono presenti e operativi dei sistemi legacy.

Un esempio di un caso in cui dobbiamo interfacciarci con sistemi legacy è quello che può avvenire quando abbiamo la necessità di mostrare su un nuovo touchpoint le fatture pagate/non pagate che fanno riferimento a un sistema di fatturazione preesistente.

Una soluzione potrebbe essere quella di stendere i sistemi legacy, esponendo le API sui sistemi legacy, e poi chiamarle direttamente al microservizio creato.

Sul canale sul quale vogliamo mostrare le nostre fatture avremo ipoteticamente un 1 milione di nuovi utenti, mentre sotto, fino a ieri, il mio applicativo ne accettava soltanto 100.

Mia-Platform_Stili-architetturali9

 

Stendere il sistema di fatturazione andrebbe però evitato, e non è neanche una soluzione semplice da applicare. Il percorso di API diretto potrebbe venire interrotto da molti strati, potrebbe non essere tracciato oppure non essere conosciuto da tutti.

In poche parole, quello che abbiamo fatto è stato aprire un’area B2C in un sistema pensato e utilizzato fino ad oggi per il B2B. Il B2B così “si siede”, stendendo i processi di tutta l’azienda.

 

Come possiamo evitare questa modalità?

Una soluzione che si può approcciare all’interno di un sistema distribuito come Kubernetes, è iniziare a raccogliere gli eventi in tempo reale da tutti i sistemi legacy e catturarli su un message broker. Il message broker è usato anche come data stream: prendiamo tutti gli eventi e li andiamo ad aggregare in un database, che in questo caso è un database che ha già dentro le aggregazioni di tutti i sistemi sottostanti e viene aggiornato in tempo reale mano a mano che gli eventi cambiano.

In questo modo, se vogliamo vedere tutte le fatture non andiamo più a leggerle sui sistemi sottostanti ma le abbiamo sempre aggiornate sul database.

Un database di questo tipo ha il grande vantaggio di poter scalare all’infinito, senza creare problemi e alleggerendo i sistemi sottostanti.

 

Mia-Platform_Stili-architetturali10

 

Un sistema del genere ha molte più letture che scritture (80/10 ad esempio) e questo è un ottimo modo per migliorare le performance. 

Ogni evento che viene creato, viene poi aggregato in un database tendenzialmente NoSQL documentale con Json schema annidati; un reader può leggere questi schema, e può farlo con una Rest o GraphQL, in modo più semplice di quanto facesse prima.

Questo meccanismo ci protegge dallo stendere il nostro sistema di fatturazione.

Il Canary Deploy

Ora che abbiamo il nostro sistema funzionante, e non steso, saremo contenti?

Ancora no!

Il vero progetto non finisce quando viene rilasciato; anzi, si potrebbe dire che inizia proprio quando è in produzione, perché è lì che si concentrano i costi più importanti. I veri costi di un progetto IT non sono legati alla sua realizzazione, ma alla sua manutenzione ed evoluzione nel tempo.

Vediamolo con un esempio concreto.

Abbiamo un servizio che ha 10 milioni di utenti attivi al giorno e 500 mila utenti attivi l’ora, e vogliamo cambiare il sistema di pricing. Il sistema è ancora in fase di testa e vogliamo evitare che nel catalogo compaiano per errore prodotti a costo zero.

Come possiamo gestire l’evoluzione del servizio?

Possiamo ricorrere ai sidecar pod (li avevamo nominati qualche paragrafo sopra): ognuno funge da proxy, e il catalogo prodotti non comunicherà più direttamente col servizio di pricing ma con il proxy, che a sua volta chiama il proxy del pricing, che chiama il proxy del cart che chiama il proxy del payment gateway, etc.

Mia-Platform_Stili-architetturali11

 

Così possiamo veicolare informazioni da un proxy all’altro e possiamo impostare un rilascio per il quale il 90% delle richieste va sul sistema precedente e il 10% va su quello nuovo. Questo meccanismo si chiama canary deploy, e rende molto più semplice aumentare e ridurre la gestione di versioni differenti sullo stesso sistema.

Il canary deploy è una salvezza in tantissime situazioni perché minimizza gli imprevisti che possono generarsi ad un nuovo rilascio. Si può applicare alle percentuali di traffico ma anche ad altre logiche più complesse, ad esempio lo user agent: possiamo instradare le richieste provenienti da sistemi iOS sul nuovo sistema mentre gli Android continuano a chiamare quello vecchio.

Ora abbiamo la nostra app, ed è distribuita. Come facciamo ad essere certi che tutto stia funzionando bene?

Quello che consigliamo è di mettere sempre alcune rotte di controllo, tra cui le rotte di salute e di readiness. La rotta di readiness ci dice quando il pod è pronto per ricevere traffico. Fino a quando non è pronto, Kubernetes non veicola il traffico su quel pod; la rotta di salute, invece, comunica se è in salute e sta funzionando correttamente. 
Il pod potrebbe essere up and running ma non funzionare correttamente (non riesce a scodare messaggi, ecc). A quel punto, se non è in salute, Kubernetes riavvia per noi il sistema –  viene staccata anche la rotta di readiness – e riparte tutto.

Oltre a chiederci come sta, potremmo voler monitorare anche cosa sta facendo esattamente il nostro servizio in produzione.

Possiamo allora andare a misurare alcune informazioni rilevanti a livello di business: quanti messaggi sto scodando, quanti pagamenti ho effettuato, quanti utenti attivi ho in questo momento?

Queste metriche possono essere ospitate su un database sul quale possiamo andare a costruire delle dashboard di monitoraggio, e su queste dashboard possiamo impostare anche degli allarmi. Ad esempio potremmo impostare un allarme che avvisa quando ci sono troppi messaggi in coda e può esserci un rallentamento.

Questo perché quando il sistema si blocca è (relativamente) facile andare a individuare e spegnere l’incendio, molto più difficile è invece intervenire quando il sistema rallenta e non dà segnali.

Uno strumento per fare questo monitoraggio potrebbe essere Prometheus.

Un altro aspetto importante da monitorare costantemente sono i Log. Per ogni worker node possiamo collezionare i log di tutti i pod, metterli all’interno di un database, visualizzarli su dashboard e mettere allarmi sui log.

All’interno dei log potremmo inserire un request id, un conversation tracking staccato dal nostro gateway e propagato con http request su tutti i servizi, che permette di capire tutte le chiamate tra tutti i servizi.

Ci sono diversi modi di osservare la comunicazione tra servizi, questa è molto semplice, basta mettere un rec sull’id su entrambi i log e propagarli su tutti i log a livello di extra header.

 

Conclusione

Per concludere vediamo uno stile un po’ più complesso e che li racchiude in qualche modo tutti.

Partiamo dai nostri sistemi gestionali legacy, coi quali avremo a che fare nella maggior parte dei casi, e creiamo un ecosistema di microservizi (come dei mattoncini lego) ognuno con una sua responsabilità chiara e definita. Catalogo prodotti, stato degli stock, tracking del prodotto e tutti quelli che ci servono. 

Ognuno compie un’azione, che da sola non servirebbe a nulla ma che, collegata ad altre funzionalità costruisce una logica di business, e una volta esposte queste logiche costruiscono i servizi della nostra applicazione.

Un ulteriore passaggio potrebbe essere quello di inserire politiche di Fast Data e machine learning all’interno del nostro sistema.

Se i sistemi legacy dovessero avere dei problemi infatti, adottando una politica di fast data e iniziando a osservare, a livello di machine learning, tutti i collegamenti tra tutti i microservizi, potremmo iniziare a individuare problematiche ripetute sia livello operation che sulle potenzialità di business.

Con questi dati potremmo pensare a delle strategie per stimolare ulteriormente i nostri utenti finali, per sfruttare al meglio tutte le funzionalità del prodotto che stanno utilizzando.

Quella che abbiamo appena descritto è un’architettura all’interno di Kubernetes che può essere costruita nel tempo in modo incrementale ed evolutivo: un’architettura applicativa che evolve con il business.

New call-to-action
Back to start ↑
TABLE OF CONTENT
Introduzione a Kubernetes
Stili architetturali in Kubernetes
Il Canary Deploy
Conclusione