• Non ci sono risultati.

Realizzazione di un'applicazione distribuita a microservizi basata su container Docker e PaaS di orchestrazione per la ristorazione aziendale

N/A
N/A
Protected

Academic year: 2021

Condividi "Realizzazione di un'applicazione distribuita a microservizi basata su container Docker e PaaS di orchestrazione per la ristorazione aziendale"

Copied!
69
0
0

Testo completo

(1)

UNIVERSITÀ DEGLI STUDI DI PISA Facoltà di Scienze Matematiche, Fisiche e Naturali

Corso di Laurea Magistrale in Informatica

TESI DI LAUREA

Realizzazione di un’applicazione distribuita a microservizi basata su container Docker e PaaS di orchestrazione per la ristorazione aziendale

RELATORE Paolo Milazzo CONTRORELATORE Antonio Brogi

Candidato

Marco Loddo

ANNO ACCADEMICO 2018-2019

(2)

1

INTRODUZIONE A MICRODINER

Il progetto è iniziato come l’idea di rinnovare un vecchio progetto prototipato dalla società Extra Srl che era in corso di sviluppo con un’architettura ormai reputata inefficiente e troppo dispendiosa nel lungo termine per l’azienda, quale il modello classico monolitico. Dopo un’accurata fase di analisi dei requisiti funzionali per il progetto, i dati che dovranno essere trattati nel sistema e uno studio sulle tecnologie esistenti, si è optato per gettare le basi per una nuova versione del prototipo in una infrastruttura innovativa, quale quella a Microservizi per l’installazione su piattaforme di tipo Platform-as-a-Service, che ha preso poi il nome di microDiner. Lo sviluppo è stato condotto su sistema operativo Ubuntu, in linguaggio Java, mediante l’uso del framework Spring, ormai standard usato nel ambiente Enterprise. Sono state strutturate le basi fondamentali della logica dei componenti, i vari database e alcune componenti grafiche dedicate per alcuni test dimostrativi. I database sono stati sviluppati su DBMS PostgreSQL [12], scelta consigliata dall’azienda. Data l’affiliazione di Extra Srl con Red Hat, si è ritenuto opportuno utilizzare come base di installazione il software PaaS Red Hat Openshift. L’obiettivo a cui volge la tesi è dimostrare il vantaggio della scelta dei microservizi piuttosto che il classico modello monolitico, al fine di massimizzare elasticità e robustezza nello sviluppo software, contenere i costi di manutenzione, computazionali e creare un sistema facile da usufruire e mantenere in modo stabile e efficiente.

1.1

Il problema

Data un’azienda, viene richiesto un sistema informatico per poter gestire in maniera automat-ica una sezione mensa riservata ai dipendenti, in modo da aumentare l’efficienza del servizio e ottenere una maggior soddisfazione degli utenti. La novità che si vuole introdurre in questo progetto per essere competitivo con le sue controparti off-the-shelf deve essere strutturata in maniera tale che possa gestire più settori mensa alla volta, in modo da poter generalizzare la sua vendita senza aderire alla classica natura custom dei prodotti offerti al giorno d’oggi, ma più una installazione personalizzabile nei dati contenuti a piacimento da parte degli utenti. Un esempio sarebbe quello che possa diventare un software di gestione utilizzabile sia da catene di ristoro diffuse, fino alla mensa aziendale di un particolare edificio, senza dover

(3)

però adattare l’applicativo in maniera custom per ogni installazione.

1.2

Le realtà attuali

Le attuali soluzioni presenti sul mercato offrono installazioni custom singole per ogni azienda: ogni mensa avrà il suo database dedicato, le sue macchine dedicate per l’elaborazione delle informazioni e così altro. Il nostro applicativo si vuole distinguere in questo aspetto adot-tando una struttura cloud generica, in cui non si installa un nuovo prodotto ad hoc, ma sem-plicemente si offre l’accesso a una piattaforma distribuita con determinate funzioni già es-istenti, per cui basterà una minima parte di hardware dedicato per l’uso :terminali di cassa, totem, dispositivi NFC [13],etc.

1.3

Dal prototipo all’innovazione

Il prototipo costruito da Extra Srl mirava all’instaurazione di una gestione elettronica del settore mensa, tramite dispositivi NFC personali per l’utenza, che rendevano fruibili i pasti e l’identificazione nel sistema semplicemente passandoli su dispositivi assegnati a dei banchi contenenti il pasto voluto. Questo a fronte di automatizzare il processo di compilazione alla cassa, pagamento e prenotazione di pasti, fino anche a un’analisi di mercato per la proposta di offerte mirate a massimizzare la soddisfazione dei clienti. Questa struttura tuttavia era del genere monolitico, quindi difficilmente adatta all’ambiente cloud, e soffriva di una manuten-zione onerosa e tempi di sviluppo reputati poco efficienti. Il passaggio ai microservizi per-mette invece un balzo in avanti verso il cloud, in quanto offre vantaggi di sviluppo e di risorse computazionali considerevoli. Le piattaforme cloud al giorno d’oggi offrono di serie stru-menti molto comodi al fine prestazionale imposto sul progetto, e permettono una gestione ottimale delle risorse e della loro distribuzione nelle sue varie regioni.

1.4

Requisiti funzionali dell’utenza

Si possono identificare due figure cardine nell’utilizzo del sistema proposto: gli utenti e gli amministratori. Ognuno può usufruire di funzioni differenti, peculiari del proprio ruolo nel

(4)

sistema. Si chiede che gli utenti possano ordinare i propri pasti online, con la possibilità di pagamento con portafoglio elettronico oppure in loco, tramite l’utilizzo di dispositivi muniti di tecnologia NFC (Near Field Communication).

Dal lato amministrativo si devono poter consultare reportistiche riguardo gli acquisti fatti dai dipendenti, mantenere i menù aggiornati e offrire la possibilità di avere sconti o suggerimenti di pacchetti personalizzati in base alle esigenze dell’utente interessato. I dipendenti possono scegliere di usufruire di un prezzo scontato comprando pasti di tipologie prestabilite in una detta configurazione consultabile sia online che tramite un dispositivo in loco. Il menù del giorno deve poter essere consultabile online da un’applicazione web.

1.5

Implementazione del prototipo obsoleto - Monolite

L’approccio monolitico al problema è composto da diversi strati applicativi che si possono ri-assumere in 3 componenti essenziali: interfaccia, logica e database. Lo sviluppo si espande partendo dal database, per poi passare alla logica e all’interfaccia. Si ha un database re-lazionale unico, formato da tabelle modellate appositamente per contenere i dati interessanti su cui si baserà la logica di business, che saranno poi usufruibili tramite delle interrogazioni, o query.

La figura 1 descrive il database ipotetico del sistema di gestione della sala mensa, con le varie relazioni in una sua prima versione. Volendo, si potrebbe andare a recuperare un pasto comprato da un utente in un ordine specifico, attraversando tutte le tabelle collegate, in una sola interrogazione. In contemporanea alla stesura del modello dei dati, viene individuata la logica di business che completa la definizione del nostro dominio. La logica è tutto ciò che opera dal lato applicativo per ottenere le funzionalità desiderate. Queste vengono costruite tutte intorno alle query, che al più possono andare a scorrere tutte le tabelle interessate dal sistema. Sopra queste operazioni logiche poi viene costruito lo strato finale dell’applicativo, ossia l’interfaccia, che sarà tutto ciò che l’utente finale potrà vedere e con cui potrà interagire per poter comandare il sistema. Queste interfacce devono essere visualizzate sui vari dispos-itivi, ed essere fruibili anche tramite diversi dispositivi digitali come tablet, totem grafici, personal computer, etc.. Le interfacce nel modello monolitico sono fortemente dipendenti

(5)
(6)

dal resto del sistema, in quanto un minimo cambiamento può dar luogo a una cascata di modifiche che si propagano al resto del sistema.

(7)

2

PROGETTAZIONE DELL’APPLICAZIONE A

MICROSERVIZI

Lo sviluppo orientato ai servizi si basa su un’architettura differente. Di seguito diamo prima una definizione di alcuni termini utili.

2.1

Cosa sono i microservizi?

Un servizio è un piccolo pezzo di sistema, riservato al governo di una funzione specifica, autonoma, testabile in isolamento, ottimizzata e con un’alta affidabilità di funzionamento. In un sistema orientato ai servizi, essi comunicano tra di loro con meccanismi e protocolli snelli. La loro visione astratta per un utente esterno al sistema può essere vista come una interfaccia di metodi black box [Mustafa 2017]. utilizzabili per i propri obiettivi. Questa struttura ci porterà diversi vantaggi che verranno spiegati passo per passo.

2.2

Sviluppo Agile

Parlando di metodologia di sviluppo Agile, si va a creare il sistema per iterazioni incremen-tali, pianificando adattivamente le strategie di sviluppo, il tutto in piccoli lassi di tempo per ottenere così una consegna in tempi celeri e frequenti, mirando alla massima soddisfazione possibile del cliente [Sourabh Sharma 2017]. Partendo dalle specifiche dettate dal cliente, si procede in prima battuta a una definizione dettagliata di User stories, o casi d’uso del sis-tema dati direttamente dall’utente finale. In essi tipicamente si descrive il ruolo ricoperto dall’utente in questione, la funzionalità richiesta e il vantaggio di business ottenuto da ciò. Le User stories possono essere aggiornate nel tempo, in modo da avere sempre a cura la soddisfazione di chi ne usufruisce. Una volta ottenute, queste sono state ordinate per gradi di priorità e accompagnate poi da una stima in giorni uomo del tempo di sviluppo prima della consegna. Una volta consolidati i particolari delle User stories, si comincia il processo di studio del problema. Per esempio possiamo prendere la User story dell’aggiunta di un pasto al proprio conto da parte di un utente

(8)

Figure 2. Struttura di una User story

2.3

Passaggio ai Microservizi

Per prima cosa si vuole effettuare un processo di suddivisione delle regioni d’interesse del sistema, sottoponendo a uno studio accurato la possibile separazione di insiemi di dati, più o meno coesi. Procediamo a una esemplificazione per chiarire il concetto. Dati due insiemi di dati A e B, essi sono divisibili in due regioni distinte di interesse se i dati di A hanno un’affinità con i dati di B abbastanza bassa, in modo che la necessità di ottenere un insieme di dati di A non richieda categoricamente altri dati provenienti da B. Se questa affinità è abbastanza alta o totale, i due insiemi si possono considerare una regione di interesse unica dal punto di vista delle informazioni. La ragione di questa divisione è presto detta: dovendo richiedere informazioni ad A, dove per avere un dato interessante deve sempre o quasi ot-tenere dei dati da B, si dovrebbe effettuare una chiamata alla regione A, che poi dovrà chia-mare la regione B e ottenere la completezza dei suoi dati per poi restituirli all’utente. Questo dal punto di vista pratico può risultare molto costoso in termini di tempo, manutenibilità e comporta la perdita della caratteristica di autonomia dei servizi. Data la descrizione speci-ficata nel progetto si è arrivati a distinguere quattro regioni d’interesse: quella degli Utenti, dei Pasti, Ordini e Banchi. Si possono notare immediatamente delle divisioni nette rispetto al modello monolitico precedente, dove la totalità delle informazioni era interconnessa e rag-giungibile tramite le altre classi di dati. Si può pensare che essendo suddivise, ora le regioni non abbiano modo di avere interconnessioni tra loro per poter accorpare più informazioni interessanti tra di loro come in origine, ma questo è ancora possibile effettuando un piccolo accorgimento. In un modello relazionale come il nostro si hanno connessioni tra le

(9)

infor-mazioni usando dei campi chiave chiamati “Chiavi esterne”, che per ogni informazione di una classe fornisce il riferimento a una “Chiave primaria” di un’altra regione, che la identi-fica univocamente nella nostra base di dati. Essendo le nostre regioni di interesse idealmente sconnesse, quindi non risiedenti nello stesso database, si usa un tipo di referenza più elab-orato: il troncamento delle chiavi esterne. Questo consiste nel non lasciare il vincolo di chiave esterna al DBMS, ma dove entrambe le regioni di interesse conservano una copia del valore della chiave, e lo gestiscono autonomamente come fosse comunque sotto vincolo ef-fettivo del DBMS. Questo può portare certamente a degli inconvenienti, quali inconsistenze tra riferimenti in caso di aggiornamento o eliminazione, una discreta duplicazione di dati in memoria, che però sono degli inconvenienti riconosciuti e compensati dai decisivi vantaggi che verranno illustrati nel seguito, ma ovviamente tenuti sotto attento scrutinio e approcciati con attenzione. Le possibili inconsistenze che si vengono a creare possono essere del tipo che in un servizio si ha il riferimento a una chiave ormai cancellata in un altro servizio, che comporta dei possibili errori dal punto di vista del recupero e aggiornamento dei dati. Casi come questo devono essere contemplati e governati in modo accorto, così da mantenere la stabilità del sistema, senza compromettere totalmente il funzionamento. Un esempio può essere la formulazione di un sistema di codici di errore gestiti dal sistema, in modo da poter isolare il problema in maniera chiara e mirata per poi poterlo gestire secondo politiche ben definite, come la correzione dell’inconsistenza, la segnalazione all’amministratore o la più drastica (e meno preferita) eliminazione. Per poter contenere e arginare questi fenomeni si possono effettuare piccole modifiche che vengono rese molto facili dall’architettura a servizi, che fanno largo uso della indipendenza dei vari frammenti del sistema e della loro alta facilità di test qualitativo in isolamento. A questo proposito possiamo creare un piccolo schema per dare un’idea chiara di come le informazioni dipendono le une dalle altre, in modo da potersi approcciare alla creazione di test automatici e alla manutenzione del nostro sistema in modo mirato:

(10)
(11)

Dato questo schema si possono notare diversi fattori interessanti nell’architettura attuale del sistema: esistono regioni di interesse che risultano totalmente indipendenti da altre. La re-gione degli utenti e dei pasti non dipendono da nessuna delle altre, che ci lascia asserire quanto segue: un servizio, che per definizione è descritto come autonomo nelle sue fun-zioni, in caso di assenza o malfunzionamento di altri servizi sparsi nel sistema, non verrà assolutamente intaccato e continuerà il suo corretto funzionamento. Questo è comunque es-pandibile agli altri servizi con dipendenze di dato sempre per le qualità per cui si definisce un servizio, che non solo ci porta a un vantaggio funzionale enorme, ma ci aiuta anche nell’identificazione di errori o malfunzionamenti. Una volta individuate con certezza le aree di interesse dei singoli servizi e deciso quali chiavi spezzare, si parte con lo sviluppo software vero e proprio. Ogni servizio è costituito da 3 strati che portano diversi livelli di astrazione nel nostro sviluppo: uno strato Controllore al livello più alto, che sarà l’interfaccia finale con cui l’esterno potrà interagire, uno Manager che si occupa di organizzare le operazioni secondo la logica data, e uno strato Data Access Object che si occuperà delle operazioni di persistenza e lettura dei nostri dati. Ognuno di questi può essere composto da più oggetti adibiti ognuno a una particolare branca della nostra regione di interesse. Tutti i moduli ri-escono a comunicare tra di loro tramite interfacce ben definite, che poi ci offre una possibilità ancora più agile per lo sviluppo, che è la modularità delle componenti del sistema. Per esem-plificare, possiamo definire un DAOUtenti A e un DAOUtenti B, dove all’interno le funzioni interessanti sono definite nel rispetto della interfaccia DAOUtenti, ma che possono definire al loro interno comportamenti differenti, senza che il sistema debba tener conto di ulteriori accorgimenti. Un’altra possibile comodità architetturale è quella della iniezione delle dipen-denze, che sfrutta il meccanismo appena proposto. Nell’iniezione di dipendipen-denze, si cerca un modo agile e efficace di disaccoppiare le dipendenze forti nel codice, ossia si sfrutta il pattern secondo il quale nel codice non devono esserci creazioni dirette di un particolare oggetto, ma queste devono essere affidate a un gestore delle dipendenze che assegnerà l’implementazione appropriata tramite reflection. Possiamo illustrare ora una sintesi del sistema dopo le varie analisi, così da avere un’idea chiara delle differenze rispetto al modello monolitico.

Si può notare che ogni area è definita in modo isolato rispetto al resto delle regioni, fino anche a toccare lo strato di interfacciamento con esse e i loro database. Ognuna di queste aree ora è definibile come un servizio, che comunicherà con l’esterno tramite la sua

(12)

inter-Figure 4. Diagramma della struttura dei microservizi proposti

faccia in modo autonomo e agnostico rispetto agli altri. Questa divisione è uno dei punti di forza della architettura proposta, che verranno spiegati a seguire. Durante lo sviluppo dei vari servizi, una volta completate le funzioni dovute per ognuno, viene portato avanti lo sviluppo dei test di unità. Questi vengono svolti in ambienti di sicurezza isolati per conva-lidare la bontà del funzionamento in completa autonomia dagli altri test. Questi prendono le specifiche della logica di business del modulo in esame e ne convalidano la coerenza e il corretto svolgimento. Un esempio può essere: dato il servizio “A” che prende in ingresso un parametro “x” di tipo intero e dopo la sua esecuzione restituisce il doppio del suo valore, il test dovrà assicurarsi che questo sia vero. Un altro fattore importante riguardante le comu-nicazioni tra i servizi e l’esterno è l’utilizzo di oggetti chiamati Data Transfer Object, che incapsulano le informazioni essenziali richieste dal servizio per funzionare. Questi vengono usati come parametro per i servizi, in modo che in caso di una richiesta di diversi campi a seconda dell’azione interessata, non si debba cambiare il codice per tenere conto di ogni pos-sibile combinazione di parametri in ingresso. Quindi in caso di aumento o diminuzione dei parametri l’interfaccia non cambia e la logica di business deve essere leggermente modificata

(13)

in modo mirato.

2.4

Vantaggi

2.4.1 Disaccoppiamento componenti

Uno dei vantaggi più peculiari dell’architettura proposta rispetto a quella monolitica è l’alta autonomia dei suoi componenti, che si propaga perfino sulla sua struttura intrinseca. Questa ci permette di disaccoppiare le definizioni dei componenti gli uni dagli altri e poterli sosti-tuire a piacimento (sempre nel rispetto delle loro interfacce) permettendo così di poter mod-ificare in modo ingente il codice di un servizio senza intaccare l’implementazione degli altri, o con solo minimi accorgimenti in casi di cambiamenti più radicali. La manutenibilità del codice e dell’intero applicativo è un altro aspetto che si ottiene adottando questo approccio architetturale rispetto al monolite. Possiamo modificare un servizio intero o anche una min-ima parte di esso senza dover ricompilare e reinstallare l’intera soluzione applicativa da capo. Questo ci permette una diminuzione significativa nei tempi di down del sistema per manuten-zione. Il disaccoppiamento dei componenti rende anche più facile testare l’applicativo. Ciò che viene ridefinita da questa autonomia è ciò che chiamiamo “unità di deploy”, ossia la granularità con cui un applicativo viene poi rilasciato e testato. Un applicativo monolitico è considerato come un’unica unità di deploy, in quanto nessun componente di esso è separabile e autonomo, mentre in una infrastruttura a servizi ognuno di essi è una unità di deploy a sé stante, funzionante in autonomia e testabile qualitativamente come tale.

2.4.2 Testing

Diamo innanzitutto una definizione dei vari tipi di test che vengono condotti. Ci sono di-versi tipi di test, ma i principali possono essere: di unità, dove si testano le funzioni di un componente in autonomia dal resto del sistema; di integrazione, dove simuliamo delle interazioni concrete con il servizio intero; infine troviamo quelli di non regressione, dove si va a testare nuovamente, all’atto di un update, le funzioni precedentemente definite per verificare che non vengano compromesse dalle modifiche apportate. Nel caso monolitico abbiamo che le categorie di test si estendono con una copertura globale, ossia che in caso

(14)

di update, vanno condotti test sul codice modificato e i test di non regressione sull’intero applicativo, in quanto basta che un frammento del sistema non funzioni come previsto per mettere in ginocchio l’intera infrastruttura. In questo caso i test di non regressione sono molti e costosi in termini di tempo, che significa molta più attesa prima di poter reinstal-lare e infine riportare l’applicativo online. La versione basata sui servizi invece trae largo vantaggio dall’autonomia delle sue componenti come unità di deploy indipendenti: i test da condurre sono ristretti solo al servizio o servizi che verrano reinstallati. In caso di update i test vengono condotti sulla parte aggiornata. Se il comportamento di un servizio cambia, i test devono essere minimamente riadattati, ma in maniera isolata dal resto, essendo una unità di deploy separata e quindi già testata qualitativamente. Se l’aggiornamento passa i test di integrità e quindi rientra nelle aspettative definite per il sistema, viene poi rilasciato nel sis-tema, senza dover però ricaricare tutta la soluzione e testarla nuovamente nella sua interezza. I test hanno una copertura globale in entrambe le infrastrutture soltanto in fase di impianto, dove l’intero sistema deve passare un controllo qualitativo per essere accettato come valido ed essere rilasciato interamente. Successivamente, nel sistema a servizi si riscontrano grandi risparmi di tempo di rilascio per gli aggiornamenti, mentre nel monolitico si ha un rilascio tanto costoso quanto il primo.

2.4.3 Performance di sviluppo

Seguendo la metodologia Agile, il ciclo di sviluppo è strutturato in iterazioni brevi e fre-quenti, che offrono una differenza in termini di rilascio interessante nello sviluppo di en-trambe le architetture. L’idea di fondo è quella di consegnare per prime le funzionalità che danno i maggiori vantaggi di business al cliente, senza che si debba attendere lo sviluppo del sistema completo. Adottando un’architettura a microservizi è più facile seguire questa metodologia, in quanto è più semplice definire piccole porzioni di prodotto da poter con-segnare e mostrare al cliente finale. Nel modello monolitico il compito è più complicato: è difficile identificare una porzione sufficientemente piccola e soddisfacente da poter risultare rilasciabile a breve termine. Quindi possiamo dire ciò che segue: nell’infrastruttura a servizi possiamo identificare delle funzioni o interi servizi interessanti e autonomi che possono es-sere sviluppati e rilasciati in una breve iterazione, mentre nella struttura monolitica è

(15)

neces-sario toccare più strati applicativi per avere qualcosa di soddisfacente, a discapito del tempo di sviluppo. Dato un tempo assegnato alla iterazione nella struttura a servizi, si può dedurre che il tempo richiesto da una iterazione per la struttura monolitica è sempre maggiore di quest’ultimo.

2.4.4 Affidabilità e downtime

In un sistema a servizi, uno degli aspetti fondamentali per il suo funzionamento è l’affidabilità in termini di tempo di attività. Il downtime per manutenzione, aggiornamento o guasti dif-ferisce in maniera significativa tra il modello monolitico e quello a servizi. Si può formaliz-zare come segue:

in un sistema costituito da più componenti fortemente connessi nelle loro funzioni, l’affidabilità è data dal prodotto delle affidabilità dei singoli. Se definiamo l’affidabilità in un valore tra 0 e 1, la produttoria riguardante le componenti di un sistema fortemente connesso tende rap-idamente allo 0, mentre un sistema altamente disaccoppiato nelle sue componenti mantiene un valore molto più alto. L’affidabilità di una componente cala drasticamente più è connessa con il resto del sistema, in quanto se un altro componente connesso con affidabilità bassa ha un malfunzionamento, il malfunzionamento si propaga immediatamente. Le componenti più indipendenti mantengono un più alto livello di affidabilità, in quanto il loro malfunzion-amento difficilmente impatta sul funzionmalfunzion-amento collettivo.

Possiamo notare come il modello monolitico facilmente ha un’affidabilità che tende allo zero molto velocemente, in quanto ogni componente è fortemente connesso con gli altri per definizione della sua infrastruttura. Questo infatti è molto soggetto a tempi di down in quanto se qualcosa ha un malfunzionamento o ha bisogno di un aggiornamento, l’impatto si propaga sull’intera struttura dell’applicativo, che porta a grossi problemi in termini di risorse e manutenzione. In caso di guasti alle macchine che forniscono un servizio, l’applicativo ne risente immensamente in fatto di performance. Per esemplificare: mettiamo il caso di un applicativo monolitico. La connessione all’intero database è molto forte, quindi basterebbe un malfunzionamento di quest’ultimo per far collassare il funzionamento del resto del sis-tema molto rapidamente. Nell’infrastruttura a servizi questo fenomeno è molto ridotto: se

(16)

uno o più servizi diventano inattivi per guasti o manutenzione, il funzionamento del sis-tema viene compromesso solo in parte o anche in modo del tutto impercettibile. Riferendosi alla definizione precedente, possiamo dire che, essendo ogni componente dell’infrastruttura fortemente autonomo come processo per definizione, si ha che l’affidabilità dei singoli è sempre il più vicino possibile al 100% del tempo di attività. L’unico caso in cui si ha un comportamento simile al monolite è che tutti i servizi vadano contemporaneamente in down, cosa che è fortemente improbabile. Per definizione di autonomia dei servizi, si fa in modo che essi possano funzionare il più possibile senza dover chiedere informazioni agli altri, e anche in questi casi si può ottenere un funzionamento parziale o applicare strategie per fare in modo che il sistema non abbia cali di performance. In caso di malfunzionamento o di aggiornamento di una parte del sistema, il downtime è isolato solamente alla parte intaccata, mentre il resto può continuare tranquillamente a svolgere le sue mansioni per quanto possi-bile. Riportare online i pezzi interessati del sistema richiede meno risorse in termini di tempo e materiali, data la dimensione significativamente ridotta rispetto all’intero progetto.

2.4.5 Scalabilità

All’aumentare della mole di utenza che usufruisce dello stesso sistema, l’infrastruttura ha necessità di effettuare delle operazioni in modo da poter bilanciare il carico di lavoro, senza avere una perdita di performance o compromettere la stabilità del sistema stesso. Una delle operazioni prese in esame è lo “Scaling” del sistema, ossia la variazione di dimensioni delle sue risorse in modo elastico, con successivo ribilanciamento del carico di lavoro in risposta al suo aumento. Ci sono diversi tipi di scaling:

• Scaling orizzontale: si aggiungono nuove risorse dello stesso tipo al sistema (Server, Controllers, Containers)

• Scaling verticale: si aggiungono risorse di calcolo (Ram, Cpu, Hard Drive)

In un sistema distribuito, presa un’istanza di sistema basato su microservizi, se un servizio dell’applicativo ha necessità di scalare le sue dimensioni, l’idea di scalare orizzontalmente ha vantaggi interessanti. Si può semplicemente replicare il servizio necessario e poi fare in modo che il sistema distribuisca il traffico sulla nuova istanza del servizio. Così facendo si ha

(17)

un dispendio di risorse di computazione e tempo sicuramente minore rispetto all’applicazione dello stesso tipo di scaling a una soluzione monolitica, dove si va a creare una nuova istanza dell’intero sistema. Date le dimensioni della soluzione monolitica, il tempo richiesto per delle operazioni di scaling sono tanto lunghe quanto le operazioni di rilascio. Uno scaling orizzontale ha ovviamente un grosso impatto sulle performance, ma ha anche una grossa richiesta di risorse computazionali e/o di tempo. In caso di replica di un’istanza monolitica, si ha una richiesta di risorse di computazione pari a quelle dell’istanza precedente, quindi per avere 2 nodi uguali del sistema, si deve avere il doppio di memoria e risorse varie che si avevano prima per l’intero sistema. Le performance ottenute da questa operazione sono ovvi-amente incrementate, ma c’è un grosso spreco di risorse generali, in quanto si va a diluire l’incremento su parti non necessariamente richieste.

(18)
(19)

2.4.6 Riutilizzo del codice

Una pratica ottima durante lo sviluppo è sicuramente quella di produrre codice flessibile e riutilizzabile nel tempo. In entrambe le infrastrutture in esame questo concetto è radicato in maniera massiccia, ma con un’efficacia pratica molto differente. Prendiamo per prima la soluzione monolitica. Ogni componente è sviluppato nel contesto di funzionamento del progetto, quindi altamente aderente al campo in cui verrà utilizzato. Questa forte connes-sione può trovare riutilizzo, ma semplicemente all’interno di un campo specifico e ristretto, poco riusabile poi a livelli più alti di astrazione per contesti più complessi. Possiamo es-porre questo discorso da un punto di vista simile alla programmazione Object Oriented. Immaginiamo il Monolite come una gigantesca classe che all’interno ha delle altre classi appositamente sviluppate per il suo funzionamento. A livello più alto la classe fornirà dei metodi di interfaccia specificatamente sviluppati per i processi di business interessati. In caso un nuovo processo di business richieda una diversa logica di esecuzione, ma che riusa semplicemente sottocomponenti, i metodi esposti non sono più adatti al nuovo processo e ne-cessitano di esporre un nuovo metodo ad alto livello, appositamente creato per quel processo di business. Ciò rende difficile l’esposizione di funzioni nel lungo termine dello sviluppo continuo. A questo proposito i servizi portano alla luce ulteriori vantaggi derivati dalla loro definizione. Un servizio per definizione deve essere autonomo, piccolo e specializzato a una determinata mansione. Ciò lo rende più elastico e semplice da sviluppare il più lontano possi-bile da un’implementazione fortemente specifica allo sviluppo di un determinato progetto. I servizi vengono sviluppati con una granularità né troppo alta, né troppo bassa. Il vantaggio di questo disaccoppiamento dal contesto del progetto è che, con un’accorta definizione del suo funzionamento, si possono definire nuovi processi di business in modo molto più semplice e flessibile, utilizzando componenti precedentemente definiti senza dover andare ad adattare codice in maniera specifica e restrittiva. Questo riduce anche l’utilizzo di codice copiato e incollato, ma minimamente riadattato ad hoc. Nello sviluppo successivo per aggiornamenti poi si ha un altro vantaggio tra le due infrastrutture. I servizi godono di un processo di ver-sionamento autonomo, che quindi non impiega l’intero sistema per l’aggiornamento di una delle sue parti, rendendo lo sviluppo elastico e meno oneroso. Il monolite invece ad ogni cambiamento di uno dei suoi sottocomponenti deve andare a rivisitare e creare una nuova versione di tutta la sua struttura, portando a un aggiornamento massiccio e poco produttivo.

(20)

2.5

Svantaggi

L’infrastruttura a microservizi non è sempre la scelta più ideale per il proprio sistema, in quanto ha diversi drawback che vanno considerati. A dipendenza delle dimensioni, i servizi possono rivelarsi più uno sforzo eccessivo di sviluppo e un dispendio di risorse che non porta un beneficio rilevante. Seguendo i punti precedentemente discussi, lo studio delle zone di interesse, la creazione di database individuali per ogni zona, lo sviluppo e testing di ogni parte, etc. risultano più onerosi di creare un applicativo a struttura monolitica. Ciò ci porta alla scelta di infrastruttura su una base di studio condotto sulle dimensioni del progetto da elaborare. Ogni servizio per poter operare correttamente deve avere una opportuna configu-razione di base, tra cui possiamo citare le dipendenze e repository Maven, ambienti di test e rilascio in produzione. I servizi per formare un effettivo sistema devono poter comunicare tra loro, e questo di solito avviene attraverso messaggi che passano in rete (REST per esempio). La gestione di questi messaggi necessita di opportuni accorgimenti per poter prevenire ogni tipo di errore esterno al sistema. Un messaggio potrebbe non pervenire a un servizio che lo ha richiesto, e per restare stabile deve saper gestire la situazione. A questo punto però ci sono tecnologie che possono aiutare, che vedremo a seguire. La coerenza dei dati immagazzinati può risultare ostica. Ogni servizio essendo indipendente non ha una sincronizzazione diretta con tutte le fonti di dati a cui si può attingere, quindi in caso ci sia un dato condiviso da due o più servizi, è possibile che venga riscontrata della incoerenza tra uno o più di questi. Il problema non sorge senza soluzione però. Con opportuni accorgimenti nella struttura si può ovviare a questa incoerenza, o almeno riuscire a portarla a un livello tollerabile.

(21)

3

TECNOLOGIE

Passiamo a fare un riepilogo di tutte le tecnologie usate per sviluppare la soluzione proposta.

3.1

Spring

Questo è un framework Java per lo sviluppo enterprise di soluzioni applicative. È alla base della struttura per tutte le applicazioni create. Favorisce lo sviluppo in team, semplificando e ottimizzando il processo di creazione di un applicativo spostando il focus del team sulla logica di business desiderata, prendendosi carico di lavori tediosi ai livelli più bassi. [1] Tra le sue feature principali possiamo denotare:

• La dependency injection: è un paradigma basato sull’idea che ogni classe non dovrebbe avere delle dipendenze statiche, ma dovrebbero essere esternamente configurabili. • Ambiente di testing con mocking

• Gestione di transazioni, supporto ai DAO(Data access Object), JDBC • Orientamento all’integrazione di sistemi

3.2

Spring boot

Sotto progetto di Spring, Spring boot conferisce un ulteriore passo di semplificazione per la produzione di applicativi Spring-based. L’obiettivo dell’uso di Spring boot è quello di creare delle applicazioni Spring stand-alone con un avvio più semplice possibile, dove viene richi-esta una minima, se non nulla, configurazione di parametri, allontanando lo sviluppatore da grosse fette di tempo passato al tuning ottimo dei settaggi per un corretto funzionamento. Portiamo alla luce un esempio: prima dell’avvento di Spring boot, era necessario avviare un webserver per l’esecuzione di un applicativo. Con Spring boot è possibile invece sem-plicemente lanciare l’applicativo, mentre il framework si occupa in modo trasparente di ogni configurazione per il suo funzionamento, webserver integrato compreso. [2]

(22)

3.3

Spring Data JPA

Parte della famiglia di Spring Data, Spring data JPA offre una implementazione semplificata per l’accesso ai dati, per ridurre gli sforzi di orchestrazione necessari all’applicazione. Lo sviluppatore potrà implementare un modulo di accesso ai dati, semplicemente definendo una interfaccia di operazioni usufruibili, senza dover specificare alcuna implementazione statica. È possibile definire metodi con un pattern prestabilito da Spring Data JPA semplicemente definendo la loro firma nell’interfaccia, per poi lasciare al framework la generazione di una implementazione ottima. In caso lo si desideri si può definire una interrogazione custom che verrà poi integrata autonomamente ed eseguita in modo trasparente con la dovuta implemen-tazione. [7]

3.4

Maven

Maven fa parte del genere di software dedicati all’automazione dello sviluppo di Apache. Questo si occupa della corretta gestione delle dipendenze inerenti al progetto in sviluppo, tramite un costrutto chiamato Project Object Model (POM). Un POM è definito come un file XML che descrive tutte le dipendenze con annessa versione che sono necessarie per il funzionamento del applicativo. Maven elaborerà le informazioni all’interno del POM per poi effettuare il download di tutto il necessario richiesto nel file, tramite dei vari repository centralizzati o fonti locali. Ciò ci da un livello di indipendenza dall’ambiente di sviluppo, in quanto possiamo passare da uno all’altro avendo la sicurezza di operare sempre con la giusta versione delle dipendenze necessarie. [8]

3.5

Junit

Framework dedito al unit testing in linguaggio Java. Permette lo sviluppo di test autom-atizzati, concentrandosi sullo sviluppo basato su Java 8 e versioni superiori, per dare allo sviluppatore modi innovativi di sviluppo. [9]

(23)

3.6

Mockito

Framework opensource per il mocking in fase di unit testing. La funzionalità cardine può essere descritta come segue. Data la definizione di test in unità, essi devono essere svolti in isolamento, quindi evitando ogni tipo di side effect derivato da altre classi o dati del sistema. A questo proposito vengono creati dei doppioni di dipendenze reali per i test, nel nostro caso si usa un doppione chiamato mock object. Un mock object è una falsa implementazione della dipendenza, che sia una interfaccia o una classe, dove viene poi definito un output specifico per ogni metodo possibile da chiamare. Queste operazioni vengono registrate dal mock e possono essere verificate e controllate. Una volta mockate tutte le dipendenze inerenti al progetto sotto test, si può procedere alla formulazione di una sua implementazione automat-ica, basata sulle risposte controllate fornite dagli oggetti mock, in modo da assicurare che la parte testata verifichi le condizioni di funzionamento dettate dai canoni progettuali. [5]

3.7

Hibernate

Object Relational Mapping framework che si occupa della persistenza effettiva dei moduli JPA, utilizzando metodologie naturali del paradigma Object-oriented. [11]

3.8

Docker Containers images

Un’immagine di container Docker è l’unità base che si userà su Openshift per il deploy dei nostri servizi. Questi racchiudono il codice del nostro servizio, più lo stretto essenziale per il suo funzionamento, come librerie, tool di sistema e settaggi. Una volta poi portati su una piattaforma container-based, dall’immagine viene istanziato un container. Non importa su che sistema operativo vengano eseguiti, essi opereranno allo stesso modo ovunque. Pos-siamo differenziarli da semplici Virtual Machine. Vediamolo come un’astrazione a livello applicativo: una macchina può far girare più Container che condividono tutte lo stesso sis-tema operativo, occupando meno memoria e risorsa di calcolo, per un avvio in esecuzione svelto e uno scaling particolarmente facile. Una virtual machine semplice per poter eseguire l’applicativo deve ospitare non solo il codice necessario per il progetto e le sue librerie, ma anche un sistema operativo ospite che sarà esclusivo per la gestione dell’applicativo in

(24)

questione. Queste richiedono una maggiore quantità di memoria e potenza di calcolo, senza contare lo sforzo immenso richiesto anche solo per l’avvio e lo scaling. [10]

3.9

Openshift

Openshift è un Platform-as-a-Service (PaaS) container-based prodotto da Red Hat, utilizzata per applicazioni cloud, mirato a semplificare sviluppo, deploy e scalabilità di applicativi. In esso avviene il centro dell’orchestrazione dei microservizi creati finora nel progetto. Essi verranno caricati come delle immagini Docker che poi sono istanziate in pod Kubernetes, dove in un pod possono essere replicate più istanze della stessa immagine, per aumentare le performance di quel determinato pod, che può essere contenente un database MySql, un servizio di interfaccia, etc.

3.10

Perché queste tecnologie?

Data la scelta del linguaggio Java per la creazione del progetto, si è cercata la tecnologia che desse il più alto grado di performance senza tralasciare la facilità di sviluppo annessa. Spring e JPA sono uno standard Java istituzionale che si dimostra competente e favorisce il lavoro. JPA svolge un ruolo chiave nel levare allo sviluppatore una grossa porzione di lavoro quale la gestione pura dei dati, con l’aiuto poi di Hibernate, il vantaggio in tempi e facilità d’uso è significativamente elevato. Spring boot sveltisce significativamente il lancio e la facilità di hosting dell’applicativo, minimizzando le configurazioni e fatiche di settaggio di un web server, così diventando effettivamente un applicativo che può essere “semplice-mente eseguito”. JUnit e Mockito sono standard anche essi per il linguaggio Java, che con-feriscono una grande potenza espressiva e funzionale ai test automatici, passo indispensabile per la validazione e la fortificazione del pattern di lavoro. Openshift è stato scelto data la partnership con Red Hat della Extra RED, sede ospitante e coordinatrice dello sviluppo del progetto. La scelta poi è giustificata per poter sviluppare verso il progresso dei middleware monolitici come gli Enterprise Service Bus (ESB) attualmente usati, verso un’architettura a microservizi più snella e flessibile degli standard moderni. Maven e Docker sono software di ampio utilizzo, con una larga community e supporto dietro, che conferiscono ancora più

(25)
(26)

4

PANORAMA SUI PAAS

4.1

Che cosa sono?

Un PaaS (Platform as a Service) è una categoria di cloud computing dedita ad applicazioni che necessitano tempi molto veloci, sia di sviluppo che di mantenimento. Questi sollevano l’utente e sviluppatore dal conoscere e doversi preoccupare della infrastruttura del sistema sottostante al livello applicativo e di dati, come ad esempio il server su cui girerà, il sistema operativo di base. L’obiettivo è dare degli strumenti già pronti per il deploy e il manteni-mento dei propri applicativi agli sviluppatori e utenti, allontanandoli dal dover operare re-strittivamente sulle parti più a basso livello, così da potersi concentrare di più sullo sviluppo dell’applicativo in sé.

4.2

Kubernetes

Alla base del PaaS scelto per microDiner, ovvero Openshift, gira un sistema che gestisce i container al suo interno, chiamato Kubernetes.

(27)
(28)

4.2.1 Kubernetes Cluster - Master e nodi

Questo sistema consiste in una sotto rete, chiamata Kubernetes cluster, che può essere for-mata da uno o più master e dei set di nodi. Un master consiste in uno o più host che con-tengono le componenti adatte all’orchestrazione dei nodi e al rilevamento e risposta per ogni evento in corso dentro di il cluster. Un nodo invece è un’istanza di un ambiente di esecuzione che può ospitare dei pod, in cui al loro interno possono essere istanziati più container creati da immagini Docker. Su ogni nodo gira un modulo chiamato Kublet che opera secondo le specifiche dettate dal file di definizione del Pod, chiamato PodSpec, dando diverse funzion-alità di diagnostica che vedremo poco più avanti. I master di solito vengono contenuti in un nodo del cluster. Tramite vari moduli di diagnostica che comunicano tra i nodi e i mas-ter del clusmas-ter, si può osservare il dinamismo e l’efficienza nel mantenimento di un sistema stabile. Poniamo il caso in cui il nostro sistema debba avere 4 pods con diversi container per definizione, ma che il pod numero 1 per qualche motivo non sia più disponibile ad un determinato momento. Il sistema si rende subito conto che il numero di pods richiesti è insufficiente rispetto alle specifiche date e interverrà subito tramite un componente chiam-ato Replication controller. Esso cercherà subito di riassestare la situazione, schedulando la creazione di un nuovo pod in un nodo disponibile nel cluster. Possiamo definire il Replica-tion controller come un supervisore dei pod su diversi nodi del nostro cluster.

(29)

4.3

I Pod

Questi sono gruppi di container derivati da un’immagine (generalmente Docker) che condi-vidono tra di loro memoria, rete e una specifica di configurazione con cui dovranno essere eseguiti. Ogni pod ha una definizione di stato, che ne specifica le condizioni di uno stato adeguato all’operatività dei suoi container. Un pod funge da “host logico” per i propri con-tainer e condivide tra di essi un indirizzo IP e un range di porte.Health Probes - diagnostica per i container Durante la definizione di un pod si possono istituire diversi parametri che definiscono la “salute” dei nostri container. Esistono due tipi di stato di salute del pod: vi-talità e prontezza. Questi dipendono dallo stato di tutti i container all’interno del pod. I parametri per definire se il nostro container è in salute vengono specificati nel file di deploy, se necessari. In caso l’applicativo sia in grado di chiudersi da solo in caso di problemi o entra in uno stato instabile, il modulo Kublet provvederà in automatico ad applicare una politica di restart del pod. I parametri di vitalità, se specificati, indicano entro quali termini il container è definito in uno stato di esecuzione stabile, entro i quali esso risponderà positivamente ai test eseguiti dal Kublet. In caso i test non vadano a buon fine, il container è soggetto a can-cellazione per un successivo riavvio. I parametri di prontezza specificano entro quali termini il container è definito pronto a ricevere traffico dal sistema. Se il test entro tali parametri fallisce, il pod non riceverà più del traffico fino a che il test non risulterà positivo.

4.4

I Volumi

Ogni container dentro il pod per definizione ha un suo spazio in memoria che può essere usato per la persistenza dei dati, ma che senza una specifica definizione, una volta che viene cancellato il container associato, essi vengono cancellati a loro volta. In caso quindi il nostro sistema conti su una memoria di persistenza che duri nel tempo anche, per prevenire casi perdita dati causate da un riavvio o crash, è richiesta la definizione di uno spazio permanente di memoria adibito alla persistenza. Questo spazio viene detto Volume.

(30)

4.5

Giustificazione di business dell’uso dei microservizi

La giustificazione è presto chiara, in quanto l’idea alla base del sistema risulterebbe troppo complessa per risultare efficiente in ambiente distribuito non usando i microservizi. Volendo porre l’idea monolitica per poi trasportarla su Openshift, si nota subito i seguenti fatti:

• per poter essere containerizzata, avrebbe un maggiore impatto nella memoria, in quanto

durante la creazione dell’immagine Docker, esso dovrà copiare tutto il codice dell’applicazione e tutte le librerie e dipendenze ad esso annesse. Ciò porta a deploy e avvii dell’applicazione containerizzata incredibilmente più lenti.

• la scalabilità risente dello stesso problema di una istanziazione singola e di altri: per creare un nuovo container, si dovrà creare una nuova istanza di tutto il sistema, a pre-scindere da quale settore di esso richieda più traffico; si può evidenziare subito uno spreco di risorse massiccio, in quanto se una minima parte del sistema risulta essere troppo carica, verrà replicato tutto il sistema, creando così grossi tempi di bilancia-mento del lavoro, dovendo attendere l’avvio di una nuova istanza, e per quanto le per-formance aumentino sensibilmente, si ha un aumento anche in settori non desiderati. • in caso di crash o instabilità di una piccola parte dell’applicativo, il sistema

can-cellerebbe e per poi replicare o estranierebbe dal traffico tutto un pod reputato mal funzionante, facendo calare a picco le performance e creando scompensi di bilancia-mento del lavoro.

• Orientamento all’integrazione di sistemi

Si può portare alla luce invece quanto i microservizi siano una soluzione efficace in un sis-tema come Openshift.

• la containerizzazione è isolata al microservizio che si va ad esaminare, copiando solo lo stretto necessario nell’immagine Docker, che in fase di deploy e avvio si hanno caricamenti molto più rapidi, data la dimensione contenuta in memoria.

• per poter scalare di dimensioni si opererà solo dove serve, scalando orizzontalmente e ottenendo performance migliorate all’occorrenza in maniera automatica grazie alle funzioni di diagnostica di Openshift, replicando solo lo stretto necessario per man-tenere dei canoni di bontà del servizio.

(31)

• in caso di crash o instabilità l’isolamento dei microservizi mantiene una consistenza nel tempo di esecuzione dell’intero applicativo più elevata, in quanto Openshift non andrà mai a cercare di buttare giù tutto l’applicativo, ma solo parte dei pod che risul-tano in uno stato di instabilità, risultando semplicemente in momentaneo disservizio parziale o in certi casi anche nullo.

(32)

5

IL SISTEMA

5.1

Schema sintetico della struttura di un microservizio

Procediamo a esporre la struttura del sistema sviluppato più nel dettaglio. La suddivisione è data dallo sviluppo in moduli con alta indipendenza, che rende il sistema elastico ai cambi-amenti oppure all’utilizzo di componenti third-party in modo trasparente e dinamico. Ogni livello può comunicare solo con lo strato soprastante o sottostante, a parte lo strato Con-troller, in quanto esso non ha un livello superiore di stratificazione. Più si sale di stratifi-cazione, più si guadagnano livelli di astrazione sul sistema, risultando nella struttura classica di un microservizio, dove all’esterno si possono solo accedere a dei metodi esposti, senza conoscerne l’esatta implementazione. Per chiarire il concetto procederemo a spiegare il fun-zionamento di ogni layer dal livello più basso nei paragrafi a seguire. I nostri microservizi comunicano con l’esterno attraverso chiamate REST, tipico sistema presente nella comuni-cazione di architetture distribuite, inviando o ricevendo delle informazioni in formato JSON distinguendo le varie operazioni a dipendenza del tipo di chiamata, che può essere di recu-pero (GET) o modifica (POST, PUT).

(33)
(34)

5.2

I DTO (Data Transfer Object)

I dati che trafficano tra la chiamata REST e l’operazione algoritmica richiesta non sono dello stesso tipo, per quanto possano contenere comunque un insieme comune di informazioni. Per la comunicazione tra l’esterno e lo strato Controller si usano degli oggetti appositi chia-mati DTO, che contengono la definizione dell’insieme dei parametri essenziali per la co-municazione in corso. Questa metodologia ci assicura una notevole elasticità nella codifica e standardizzazione delle chiamate, come vedremo più in avanti nei capitoli di implemen-tazione. Basti pensare ai casi dove viene effettuata una modifica ai parametri inerenti a una chiamata del servizio. Con una implementazione descrittiva di tutti i parametri necessari per la chiamata, in caso di aggiornamento andrebbero aggiornate tutte le chiamate a quella funzione sia da parte client che dalla parte del servizio, mentre con l’uso di un DTO che incapsula questo cambiamento, le chiamate rimangono immutate, permettendo a un aggior-namento minimo riguardante la compilazione del oggetto di trasferimento e donando una capacità di mantenimento più efficiente allo sviluppatore.

5.3

Lo strato Controller

Una volta ricevuto un DTO, lo strato Controller si occupa di prendere le informazioni in-teressanti per effettuare l’operazione richiesta e richiamare le opportune funzioni di logica di business. Una volta che la logica di business avrà terminato le sue operazioni restituirà una oggetto entità le cui informazioni verranno filtrate nel DTO che il chiamante si aspetta di ricevere. Gli strati sotto quello di Controller vedono solo operazioni inerenti entità e null’altro.

5.4

Lo strato Manager

Questo strato può contenere uno o più Manager inerenti a delle particolari unità logiche di dato, chiamate entità, che si occupano di implementare la logica di business decisa in fase di progettazione; questi vengono definiti prima come interfaccia, e poi separatamente viene fornita una implementazione che verrà iniettata in fase di avvio. L’approccio lascia spazio a fornire nuove implementazioni, sfruttando il forte accoppiamento basato sull’interfaccia

(35)

invece che all’implementazione, senza necessari adattamenti del codice chiamante. Lo strato attuale solleva il controller dal conoscere la logica di business, conoscendo solo le possibili operazioni e i risultati attesi per quella precisa regione d’interesse.

5.5

Lo strato DAO (Data Access Object)

I Dao sono uno strato residente in ogni servizio che si occupa di accedere e gestire le in-formazioni dal database e tradurre tali inin-formazioni in oggetti compatibili con lo schema definito in Java. Ogni servizio ha un Dao definito per ogni tabella a cui può accedere. Senza un Dao, il servizio non è abilitato ad accedere alle informazioni in maniera diretta. La struttura e metodi vengono ereditati estendendo l’interfaccia delle librerie JPA chiamata JpaRepository. L’estensione viene eseguita mappando la classe dell’oggetto che mappa la tabella sul database con un identificativo, definito per convenzione di tipo Long. Il risultato ci da una interfaccia con dei metodi predefiniti per interagire con il database, principal-mente il metodo findById e save. Il metodo findById permette di ottenere un elemento della tabella fornendo un identificativo non nullo, generando una query appropriata e restituendo un oggetto del tipo mappato con le informazioni dal database opportunamente compilate. È possibile creare dei metodi custom per soddisfare delle esigenze specifiche che vanno oltre le possibilità del findById senza comunque scomodare l’utente nel creare delle query SQL opportune. Tramite diversi meccanismi del framework, si possono definire dei nuovi metodi di lettura semplicemente definendo una nuova firma di metodo definita come: find-ByXAndY(...) dove X e Y stanno a simboleggiare i nomi delle proprietà dell’oggetto nec-essarie come criterio di ricerca per la ricerca. JPA genererà poi in background la query op-portuna, ma in caso la ricerca generata non abbia i risultati voluti, è possibile specificare una query puntuale custom, marcando il nuovo metodo con l’annotazione @Query(<sql query>) dove all’interno delle parentesi tonde viene specificato codice SQL che dovrà essere eseguito alla chiamata del metodo. Il metodo può essere modellato in modo molto facile anche nei riguardi delle informazioni ritornate, come in caso si voglia solo un risultato o tutti quelli che soddisfano i criteri specificati, modificando semplicemente il tipo di ritorno, come ad esempio si può scrivere come tipo di ritorno un Int per un singolo valore oppure List<Int> per ottenere una lista concatenata di tutti i valori.

(36)

5.6

Le basi dati e le entità

Una entità rappresenta un oggetto del dominio d’interesse del servizio, con una propria iden-tità e comportamento basato sulle proprietà che la caratterizzano. Ogni eniden-tità è mappata sulle informazioni di una tabella appropriatamente strutturata, mentre ogni riga della tabella rappresenta un’istanza di entità. La struttura è stata pensata in base alla necessità di indipen-denza tra i vari servizi, quindi ogni microservizio ha il suo database interrogabile. Data questa indipendenza, ci sono dei vincoli di chiave esterna che non sono organizzate a livello di database, ma vengono gestiti dalla logica del sistema. Introduciamo ora il concetto.

5.7

Chiavi spezzate

Una chiave spezzata è un vincolo di chiave esterna non su un unico database, ma tra diversi database indipendenti in diverse regioni del sistema. Questa relazione è gestita a livello di logica del sistema. Per esemplificare si può avere un vincolo di chiave esterna nel servizio A che punta alla chiave primaria di una tabella nel servizio B. Il servizio A, in caso necessiti di una informazione riferita dalla chiave esterna, provvederà a effettuare una chiamata al microservizio B con il valore nella base dati di A, effettuando la query appropriata, per poi restituire le informazioni desiderate al servizio chiamante. La formazione di tali tipi di chiavi viene cercato di ridurre al minimo, in quanto creano delle dipendenze, anche se deboli, tra le varie regioni del sistema. L’idea di spezzare una chiave esterna tra due regioni d’interesse si basa su quanta sia la necessità tra di essi di un consulto tra le loro basi di dati. Se logicamente il servizio A per ogni sua interrogazione dovrà consultare B per ottenere l’informazione completa, queste due zone d’interesse non ottengono un senso nel essere divise e possono essere accorpate. La stretta dipendenza delle informazioni tra di esse spinge la struttura del database ad essere più unita. Quelle formulate nel nostro sistema sono state scelte in base appunto alla loro discreta indipendenza, senza però rinunciare alla completezza delle informazioni che potrebbe derivare dallo spezzamento delle relazioni nelle basi di dati. Questa struttura soffre di errori noti a priori. La separazione dei database porta al fatto che la relazione in questione non è espressamente controllata dal sistema delle relazioni del DBMS, portando a possibili casi di inconsistenza di dati. Possiamo esemplificare il caso in cui un si ha un record del servizio B soggetto a un vincolo di chiave spezzata nel servizio A,

(37)

dove poi il record viene cancellato. Questo crea una inconsistenza tra i dati dei due servizi, essendo indipendenti non si accorgono che la relazione è stata rotta da una cancellazione. Questo fenomeno si chiama di “Eventual Consistency”. Questo fenomeno è conosciuto a priori nella creazione del sistema e quindi si può formare in modo che sia resistente e pronto alla sua gestione, in modo che questa inconsistenza non rimanga permanente nel tempo. La gestione avviene in maniera asincrona, sia in caso di inserimento di un nuovo dato che di lettura. In fase di inserimento non consistente si può ritentare più volte l’inserimento finché l’operazione non riesce con successo. La lettura in modo analogo deve poter raggiungere la consistenza ad un certo punto, resistendo a una situazione critica: in caso di lettura non completamente consistente si può fare la rilettura in attesa della risposta completa dal sistema oppure una restituzione temporaneamente parziale dei dati. Ci possono essere altri metodi più ad alto livello per il controllo delle chiavi spezzate, ma che non verranno trattati nel nostro progetto.

5.8

Struttura entità relazione

Passiamo a illustrare la struttura Entità-Relazione delle tabelle dei vari database.

5.8.1 Base dati regione Utenti

Figure 9. Struttura delle tabelle del database sugli Utenti

La tabella Users raccoglie le varie informazioni anagrafiche di un utente, con un identifica-tivo(userId) univoco per ogni utente presente nella base dati. La tabella UserNfc contiene tutte le informazioni riguardanti i Tag NFC usati dagli utenti. La relazione che vige fra di

(38)

loro è di tipo uno a molti, in quanto un utente può avere molti NFC assegnati (data la pos-sibile perdita, guasto o furto dell’oggetto), ma un NFC può appartenere solo a un utente. L’identificativo di un UserNfc è una chiave naturale univoca per ogni Tag fornito.

5.8.2 Base dati regione pasti (Meals)

Figure 10. Struttura delle tabelle del database sui Pasti

La regione dei pasti gestisce tutta la parte riguardante le informazioni riguardanti i pasti serviti e i menù offerti dalla cucina. Ogni pietanza ha una descrizione, un prezzo e tipo, che aiuta la categorizzazione e può essere dato di interesse per lo sviluppo di strategia di mercato per pianificazione offerte. Un menù ha una data di validità giornaliera e un tipo che ne descrive in sintesi le qualità d’interesse, come “solo vegetariano”, “primo + bibita”, etc. La relazione è definita come molti a molti, ossia che un pasto può far parte di diversi Menù, ma un Menù può avere più di una pietanza. Questa viene gestita nel database come

(39)

una tabella secondaria contenente gli identificativi di entrambe le tabelle, che è collegata alle altre tabelle tramite una relazione uno a molti.

5.8.3 Base dati regione Ordini

Figure 11. Struttura delle tabelle del database sugli Ordini

Una configurazione è un meccanismo di offerta dove viene deciso un prezzo speciale per una serie di pietanze di una certa tipologia, in modo da offrire dei vantaggi all’utente ai fini di business. Ogni configurazione contiene una lista di tipi di pasti compresi nell’offerta che verranno automaticamente riconosciuti e divisi alla cassa, insieme a un prezzo speciale che verrà aggiunto al totale imponibile. Un ordine è descritto da un identificativo univoco, con

(40)

annesso prezzo totale dell’ordine, l’NFC dell’utente interessato nella compravendita, uno stato booleano che indica se l’ordine è ancora in corso o meno e un tipo che ne identifica se l’ordine è di tipo locale o online. Ogni ordine ha assegnato una lista di pietanze ordinate, registrate tramite il loro identificativo nella tabella OrderMeals. Questi serviranno poi du-rante l’elaborazione del prezzo finale per identificare i pasti scelti, per estrarne poi i tipi e controllare le possibili configurazioni corrispondenti o suggeribili. La relazione in vigore tra gli ordini e le configurazioni è di tipo molti a molti, come il caso dei menù e pietanze. Le proprietà nfcId e mealId sono chiavi spezzate rispettivamente delle tabelle Users e Meals, che risiedono nelle apposite regioni di interesse.

5.8.4 Base dati Banchi dei pasti (Corners)

Figure 12. Struttura delle tabelle del database sui Corners

Un banco è l’apposita zona di distribuzione dei pasti serviti all’utenza, a cui si possono dirigere con il loro vassoio per prendere la porzione da loro desiderata. Ogni banco ha assegnato un lettore di tag NFC che identifica univocamente la zona(e di conseguenza la pietanza) a cui un utente ha deciso di prendere qualcosa. Per sfruttare l’univocità dei lettori NFC si è scelto di assegnare una sola pietanza a ogni banco disponibile, che può essere ovviamente aggiornabile tramite un pannello di controllo di cui è dotato l’amministratore della sala mensa. Ogni corner contiene in sé l’identificatore del pasto ad esso assegnato ed è in relazione 1 a 1 con un lettore NFC.

(41)

5.8.5 Base dati Banchi dei portafogli elettronici(Wallets)

Figure 13. Struttura delle tabelle del database sui Wallets

Ogni utente viene dotato di un portafoglio elettronico con cui può effettuare delle operazioni monetarie sia sulla piattaforma online apposita che nella sala mensa. Un utente può effettuare le più comuni operazioni note in campo bancario online, come transazioni di ricarica cred-ito, spesa e visualizzazione di tutte le transazioni. Le transazioni vengono registrate tramite l’NFC assegnato all’utente per evitare mosse fraudolente da terzi, come l’accesso non au-torizzato sul pannello online. Ogni transazione ha un quantitativo di fondi assegnato, il cui segno dipende dal tipo di operazione(segno positivo per aggiunta fondi, negativo per spesa) che verrà poi usato per il calcolo dei fondi totali dell’intero portafoglio, insieme a una data. Viene registrato anche il tipo di transazione che si sta effettuando, ad esempio un’aggiunta di fondi, una spesa in sala mensa o una prenotazione di un pasto completo online. In caso di una transazione diversa da un’aggiunta fondi, si va a distinguere una transazione più specifica per la chiusura di un ordine, il cui identificativo viene memorizzato nella specializzazione Or-derTransaction. Gli identificatori userId, userNfc sono entrambi chiavi spezzate del dominio Utenti, mentre l’orderId è una chiave spezzata riferita al dominio Ordini.

(42)

5.9

Dipendenze deboli

Illustriamo ora degli schemi per rendere più chiare le dipendenze tra i contesti dei vari database presenti nel sistema completo. Le frecce rosse indicano quale tabella è soggetta a dipendenza da altre. Possiamo notare dall’immagine 13 che la tabella dei pasti ha due dipendenze, in quanto negli ordini è necessario tenere la lista dei pasti interessati e i banchi devono sapere a quale pasto essere assegnati. Il portafoglio elettronico ha ovviamente delle dipendenze dal dominio Utenti, in quanto necessita degli identificativi per ogni utente in modo da poter effettuare operazioni legali previe un riconoscimento. In caso di transazione di pagamento, si necessita dell’identificatore dell’ordine interessato per poter tenere traccia poi a livello di consulto e riepilogo delle transazioni quanti e quali ordini sono stati fatti, con la possibilità poi di una consulta dettagliata puntuale.

Figure 14. Dipenbdenze deboli, segnate da una freccia rossa. La freccia indica quale parametro dipende da quale altra chiave di un’altra tabella

(43)
(44)

6

FUNZIONAMENTO

Esponiamo il funzionamento passo per passo di un servizio generico. Come detto in prece-denza, un servizio espone all’esterno dei metodi che possono essere chiamati per mutare i dati persistenti o per effettuare delle operazioni di business sui dati in possesso. Questo pro-cesso inizia quando si effettua una REST Call opportunamente formata ed essa arriva con successo al servizio.

6.1

I DTO

I DTO sono una parte fondamentale per la connessione e la comunicazione dei servizi sia tra di loro che con l’esterno. Essi sono un contenitore di informazioni strutturate che viaggiano tra i servizi e l’ambiente esterno a loro, che possono essere altri servizi o altri applicativi di varia natura. Queste informazioni transitano tra gli applicativi e i servizi in formato JSON, un formato dati di scambio leggero e di facile interpretazione, a cui il framework su cui è stato basato il progetto è molto affine. Gli applicativi verso cui transita il nostro messag-gio si presuppone abbiano modo di poterlo tradurre per la propria soluzione. Nel nostro caso la traduzione sia verso che da JSON avviene spesso in background o tramite oggetti di traduzione della classe ObjectMapper della libreria Jackson fornita da Spring. In caso di ricezione di un DTO da parte di un servizio esposto, per poterlo tradurre viene usata l’annotazione @RequestBody al parametro, che segnalerà al framework che dovrà mappare il contenuto del corpo della chiamata in arrivo dentro l’oggetto definito come parametro. In caso il dato di transito venga spedito dai nostri servizi verso l’esterno, verrà automatica-mente mappata la sua struttura in formato JSON e spedita. La versatilità del JSON gioca un ruolo fondamentale nella comunicazione, perché permette una ulteriore eterogeneità della struttura e una facile integrazione con sistemi differenti. Non importa l’applicativo o sistema destinatario del DTO, finché esso avrà funzioni di traduzione dal JSON alla propria imple-mentazione e ovviamente una schematizzazione corretta del DTO, la comunicazione e le richieste potranno fluire senza eccessivi accorgimenti legati all’implementazione di partenza o destinazione dei dati.

(45)

6.2

Le chiamate REST

Esponiamo il concetto del processo di una chiamata partendo da ciò che avviene in un metodo di esempio del nostro sistema: Il metodo nell’immagine 16 fa parte del servizio di ordini e restituisce le informazioni di una configurazione d’ordine a partire da un identi-ficativo univoco. La prima riga mappa il tipo di chiamata come GET per simboleggiare una richiesta di informazioni senza modifiche, su un URI specifico per identificare il campo e il parametro di interesse, in questo caso le configurazioni e l’id. Tramite la REST Call arriverà arriverà un URI e del codice JSON, che in questo caso è vuoto, in quanto i dati necessari ven-gono incapsulati nel URI, tramite “id” per la precisione. Tutto ciò avviene tramite l’utilizzo dell’annotazione @PathVariable del parametro id. Dalla firma del metodo possiamo ricavare il tipo di informazione restituita, ossia un DTO di tipo ConfigurationDto. Una volta che i parametri vengono mappati e resi disponibili alla chiamata, l’algoritmo ha inizio. Viene creato un oggetto entity di tipo ConfigurationEntity che viene riempito con i dati forniti dal manager configManager tramite il metodo getConfiguration che prende come parametro un identificativo di tipo Long. Una volta che il manager avrà completato l’operazione, il risul-tato viene convertito in DTO in modo appropriato tramite l’ausilio di un oggetto convertitore confDtoConverter. In background il risultato restituito verrà rimandato al chiamante, oppor-tunamente convertito e mappato in codice JSON contenuto nella REST Call. Questo metodo non fa uso di un DTO per il passaggio dei dati, in quanto l’informazione necessaria può es-sere passata mappandola nell’URI per la chiamata. Esaminiamo il caso simile di un servizio dove viene usato un DTO.

Figure 16. esempio di codice di una chiamata REST GET per ottenere le configurazioni di menù con un determinato identificatore

(46)

Figure 17. esempio di codice per una chiamata REST POST per aggiungere una nuova configurazione di menù

Ogni chiamata non ha una implementazione strettamente dipendente a ciò che avviene all’altro capo della chiamata, rendendo il dato in transito estremamente flessibile, duttile e orientato ad essere multi uso. Essendo il DTO transitante in formato JSON, esso non solo è esterno ad una implementazione specifica, ma anche al linguaggio dell’implementazione per cui è richiesto quel dato. Ogni ambiente di sviluppo, indipendentemente dal linguaggio utiliz-zato o dall’implementazione può ricevere un dato dal nostro sistema, semplicemente con-vertendo il JSON contenuto nel corpo della chiamata REST e mappandolo su un oggetto appropriatamente strutturato. Dato questo fatto, l’idea di sviluppare anche l’applicazione per l’end user come un servizio si fa più nitida e convincente. Il pensiero centrale è quello di sviluppare l’applicazione di interfaccia per l’utente intorno ai dati di interesse, invece che legarli all’implementazione specifica del sistema offerto. La panoramica di sviluppo si apre a un vantaggio e facilitazione interessante. Sfruttando questa idea si può notare come l’integrazione in ambienti eterogenei di un sistema basato sui servizi sia di immediata soluzione. Il formato dei dati è agnostico e non necessita di specifiche restrizioni di forma o implementazioni, quanto semplicemente nei dati contenuti attesi dallo schema definito a liv-ello di logica di business. Si può notare un vantaggio nella dinamicità autonoma dei sistemi grazie a questa granularità nello sviluppo. Si pensi al cambio di un parametro in un DTO restituito da un servizio che viene utilizzato da una delle interfacce utente. Il team di sviluppo

(47)

del sistema può semplicemente notificare al team che si occupa dell’interfaccia grafica della presenza di un nuovo parametro in un preciso DTO, in modo che questi possano modificare semplicemente la loro implementazione riguardante quel segmento di visualizzazione e di manipolazione del dato ricevuto, senza sconvolgere le due parti in causa. Questa possibil-ità di autonomia nelle implementazioni giova alla qualpossibil-ità del sistema e ai tempi di sviluppo necessari ai team per l’aggiunta di nuove funzionalità e nuove integrazioni, riducendo le difficoltà derivanti dalla coordinazione interna del team.

(48)

7

INSTALLAZIONE SU OPENSHIFT

Una volta sviluppati i nostri microservizi, possiamo procedere al loro deploy sulla piattaforma Openshift. L’operazione è resa molto facile tramite l’uso di un plugin Maven chiamato Fabric8-maven-plugin, che si prende carico di tutti i passi necessari per una corretta instal-lazione sulla nostra piattaforma. I microservizi poi seguiranno una configurazione minima in dei file, che racchiuderanno dentro di sé le informazioni necessarie per l’interazione con il Database e gli indirizzi degli altri servizi.

7.1

Configurazione Spring Boot

Ogni microservizio per poter comunicare con l’esterno, deve avere delle informazioni pre-configurate a cui possa accedere poter instaurare le connessioni, come ad esempio l’indirizzo di rete del database, indirizzi degli altri microservizi per poter effettuare le interrogazioni, etc. Per la configurazione base di Spring Boot, esiste un file predefinito chiamato “applica-tion.properties”, dove possiamo racchiudere tutte le configurazioni più a stretto contatto del funzionamento del framework. Segue un esempio, preso dal servizio per gli ordini:

Riferimenti

Documenti correlati

Le visite vengono effettuate da uno specialista ambulatoriale interno (Dr.ssa Valentina Cima) ed in altre occasioni dai Medici della Struttura..  AMBULATORI NEUROFISIOLOGICI : EEG

(2) Riportare il codice CUI dell'intervento (nel caso in cui il CUP non sia previsto obbligatoriamente) al quale la cessione dell'immobile è associata; non indicare alcun codice

(2) Riportare il codice CUI dell'intervento (nel caso in cui il CUP non sia previsto obbligatoriamente) al quale la cessione dell'immobile è associata; non indicare alcun codice

Tale campo, come la relativa nota e tabella, compaiono solo in caso di modifica del programma (13) La somma è calcolata al netto dell'importo degli acquisti ricompresi

• Divieto di fissare una pari durata al contratto di lavoro a termine con l’agenzia e alla missione presso l’utilizzatore sino al 2003, quando, con la Riforma Hartz I, tale regola

Nel compito potrebbero essere richieste “varianti minori” di quanto segue... Tale λ viene detto moltiplicatore

ripensando alla mia giornata, possa dire “Signore, ho fatto. ciò che piace a Te, e questo mi

Ora non ci resta che unire idealmente le due stelle inferiori del grande carro e prolungando questa linea di cinque volte all'esterno trovare una stella isolata, di luminosità