Siamo in un periodo storico dell'informatica dove tutti vanno pazzi per i Microservices. Chiunque abbia coniato il termine qualche anno addietro, chiaramente non sapeva o non gliene importava che il concetto di µServices fosse già stato introdotto nel 2010 da Peter Kriens (Software Architect presso la OSGi Alliance). La sua idea è sostanzialmente quella estratta dal suo articolo e che riporto a seguire, in cui il termine "micro" ha effettivamente senso.
What I am promoting is the idea of µServices, the concepts of an OSGi service as a design primitive. (cit. Peter Kriens)
Adesso ci troviamo nella situazione che abbiamo due termini che si pronunciano allo stesso modo ma hanno un significato diverso per quanto riguarda l'implementazione! Fortunatamente possiamo scriverli in forma diversa.
Quindi, scriveremo di µServices per fare riferimento ai piccoli ma buoni servizi Java, indipendenti e coesi, che ci consentono di raggiungere gli stessi obiettivi senza il sovraccarico dei Microservices.
Per concludere questa breve disquisizione sui termini per poi passare alla sostanza, vi consiglio la lettura dell'articolo Software mixed with marketing: micro-services pubblicato sempre da Peter Kriens.
Nel corso di questo articolo, che direi essere la prosecuzione del mio intervento Liferay as Digital Experience Platform in the context of Microservices (tenuto al Liferay Boot Camp 2020 Summer Edition), vorrei portarvi a valutare il framework OSGi come valida soluzione per l'implementazione della vostra architettura a Microservices.
Per seguire con efficacia i contenuti dell'articolo è comunque consigliabile un minimo d'infarinatura sul framework OSGi, per questo posso consigliarvi la lettura di questa sintetica presentazione OSGi e Liferay 7 del 2016 tenuta allo User Group Italiano di Liferay a Bologna.
1. OSGi µServices
I servizi OSGi o µServices sono il concetto chiave utilizzato per creare basi di codice modulare. Al livello più basso OSGi riguarda il caricamento di classe; ogni modulo (o bundle) ha il proprio caricatore di classi o class loader. Un bundle definisce le dipendenze esterne usando la direttiva Import-Package. Solo i package esportati esplicitamente attraverso la direttiva Export-Package possono essere utilizzati da altri package.
Questo livello di modularità assicura che solo le API (interfacce Java) siano condivise tra i bundle e le classi di implementazione siano strettamente nascoste.
Altri bundle possono quindi utilizzare il servizio cercandolo dall'interfaccia del servizio. Il bundle può utilizzare il servizio utilizzando la sua interfaccia, ma non deve sapere quale implementazione viene utilizzata o chi ha fornito l'implementazione. In OSGi questo problema è risolto dal registro dei servizi o Service Registry. Il registro dei servizi fa parte del framework OSGi. Un bundle può registrare un servizio in questo registro. Ciò registrerà un'istanza di una classe di implementazione nel registro con la sua interfaccia.
Volendo brevemente riassumere, potremmo dire che:
- i µServices o servizi, sono costituiti da una serie d'interfacce e classi Java registrate all'interno del Service Registry;
- un componente o component può essere pubblicato come servizio;
- un componente o component può richiedere un servizio;
- i servizi sono registrati dal Bundle Activator.
La figura a seguire mostra il Service Registry dove ogni bundle può registrare i propri service ma anche dove ogni bundle può trovare i servizi da consumare; potremmo dire che sia una sorta di "faldone".
La figura a seguire mostra il classico OSGi Service Pattern che le applicazioni modulari seguono. Una tipica applicazione è quindi constituita da:
- un bundle che definisce tutte le API (interfacce Java) e quest'ultime sono esportate verso "l'esterno" tramite la direttiva Export-Package;
- un bundle che implementa le interfacce definite dal bundle delle API. Questo utilizza la direttiva Import-Package. I servizi implementati sono registrati all'interno del Service Registry;
- un bundle Client (o Consumer) che consuma i servizi definiti dalle API. Questo utilizza la direttiva Import-Package. I riferimenti ai servizi che saranno consumati sono resi disponibili grazie all'interazione con il Service Registry.
La figura a seguire mostra un esempio di applicazione modulare OSGi in esecuzione all'interno di un container OSGi Apache Karaf dov'è evidente la separazione nei tre bundle: API, Service (impl) e Consumer (client).
Il Source Code 1 mostra invece il MANIFEST.MF contenuto all'interno del bundle delle API dov'è possibile vedere la direttiva Export-Package che esprime il package che sarà esportato (quindi visibile all'esterno) e la versione specifica.
I tre sorgenti Java a seguire, mostrano rispettivamente:
- la definizione dell'interfaccia (bundle API);
- l'implementazione del servizio la cui interfaccia è stata definita dal bundle API (bundle Impl o Service);
- uso o consumo del servizio (bundle Client o Consumer).
I µServices che abbiamo visto fino a questo momento, in maniera molto sintetica e direi anche velocemente, sono in-VM, ovvero, l'intero processo funziona all'interno di una singola JVM con un sovraccarico praticamente prossimo allo zero. Non è richiesto nessun proxy, quindi alla fine una chiamata a servizio è solo una chiamata diretta al metodo. In questo modello, una buona pratica impone di far fare una cosa sola al servizio implementato, in questo modo i servizi sono facili da sostituire e facili da riutilizzare.
Adesso è tempo della fatidica domanda. È possibile distribuire i µServices su più container OSGi mantenendo lo stesso pattern (vedi Figura 2)? Certamente! Il come lo vedremo più avanti.
2. Microservices visti in forma OSGi
Si può trovare una diversa definizione di Microservices, ma tutti riconducono al concetto di uno stile architetturale che favorisca lo sviluppo di sistemi loose-coupling dove quindi i componenti abbiamo un basso livello di accoppiamento, siano distribuibili in modo indipendente, siano in grado di comunicare attraverso protocolli leggeri e siano testabili e scalabili in modo indipendente.
Ciò implica che i Microservices abbraccino la modularità, in tal senso OSGi è il framework orientato fin dalla nascita ai µServices in VM. Per essere più precisi, la tabella a seguire mostra come i diversi concetti espressi dai Microservices sono associati al mondo OSGi.
Capacità | Funzionalità OSGi abilitante |
---|---|
Configuration Management | OSGi Config Admin definito nelle specifiche OSGi Compendium |
Service Discovery | OSGi Service Registry definito nelle specifiche OSGi Core |
Dynamic Routing | Il routing dinamico tra servizi (in moduli potenzialmente diversi) può essere stabilito mediante filtri OSGi che consentono il recupero di un riferimento di servizio utilizzando una sintassi simile a LDAP (RFC-4515) basata sulle proprietà del servizio |
API Interface | La specifica OSGi Remote Services definisce un meccanismo per l'esposizione dei servizi OSGi al mondo esterno (fornendo effettivamente un meccanismo per la definizione di un'API pubblica del modulo) |
Security | Alcune funzionalità (come il controllo delle autorizzazioni) sono fornite da Permission Admin e Conditional Permission Admin definiti dalla specifica OSGi. Ulteriori funzionalità di sicurezza sono fornite da altre specifiche come per esempio la User Admin Service |
Centralized Logging | Può essere ottenuto tramite le specifiche del Log Service di OSGi |
Packaging | I bundle OSGi sono distribuiti come artefatti JAR, pronti quindi anche per il deployment in ambienti che prevedono l'uso di container |
Deployment | Sia dalla console OSGi, dal file system (hot deploy) o tramite uno strumento/API dedicato (specifico per il runtime OSGi) |
Spostandoci fuori dai confini dell'ambiente OSGi, entrano in gioco delle sfide interessanti che includono ovviamente considerazioni circa il bilanciamento del carico (load balancing), ridimensionamento automatico (auto-scaling), resilienza, tolleranza ai guasti (fault tolerance) e back-pressure per citarne alcuni.
Tutti questi aspetti possono essere abilitati tramite le specifiche Remote Services e Remote Service Admin definite dalla specifica OSGi Compendium.
In particolare le specifiche Remote Service Admin definiscono un agente di gestione plugable chiamato Topology Manager in grado di soddisfare i diversi concetti menzionati che caratterizzano l'interazione tra i servizi OSGi remoti (effettivamente il canale di comunicazione tra i diversi container OSGi che formano l'insieme dei Microservices).
I framework più noti che forniscono un'implementazione delle due specifiche OSGi che riguardano i Remote Services, sono:
- Apache CXF Distributed OSGi
che consente di:
- offrire e consumare servizi SOAP e basati su REST. Inoltre, l'uso dei Declarative Services è il modo più semplice per interagire con tali servizi;
- comunicare in modo trasparente tra i container OSGi.
- Apache Aries RSA (Remote Service Admin);
- Eclipse Communication Framework (ECF)
Nel corso di quest'articolo faremo riferimento all'implementazione Apache Aries RSA (Remote Service Admin), perché a mio avviso è quella più semplice e flessibile da usare, nonché quella più diffusa.
Dopo aver visto quali sono le specifiche che ci abilitano a distribuire i bundle OSGi e quindi i µServices ad uscire fuori dai confini del proprio container, è arrivato il momento di presentare un possibile scenario scendendo sul molto pratico.
3. Uno scenario d'esempio di µServices Remoti
Ipotizziamo uno scenario in cui abbiamo quattro diversi container OSGi, di cui due offrono esclusivamente servizi, assumono quindi il ruolo di service provider, uno offre servizi e assume anche il ruolo di client o consumer e per finire, un ultimo che ha esclusivamente il ruolo di consumer. Su due dei tre container OSGi che agiranno da service provider, installeremo lo stesso servizio.
Volendo fare quindi la lista della spesa, dobbiamo sviluppare due classici progetti in stile OSGi.
- Uno che contenga tre bundle: API, Service e Consumer per il servizio che chiameremo Who I am.
- Uno che contenga tre bundle: API, Service e Consumer per il servizio che chiameremo Raspberry Pi.
La tabella a seguire mostra il dettaglio per servizio e di quali saranno i bundle OSGi da sviluppare.
Servizio | Bundle Name | Descrizione |
---|---|---|
Who I am Service | WhoIam API | Bundle che definisce tramite l'interfaccia Java il contratto del servizio Who I am. Il bundle esporta il package dell'interfaccia (vedi Source Code 2). |
WhoIam Service | Bundle che implementa l'interfaccia del servizio Who I am. Il bundle importa il package dell'interfaccia (vedi Source Code 3). | |
WhoIam Consumer | Bundle che consuma il servizio Who I am. Il riferimento al servizio è ottenuto in modo trasparente dal Service Registry. Il bundle importa il package dell'interfaccia (vedi Source Code 4). | |
Raspberry Pi Service | Raspberry Pi API | Bundle che definisce tramite l'interfaccia Java il contratto del servizio Raspberry Pi. Il bundle esporta il package dell'interfaccia. |
Raspberry Pi Service | Bundle che implementa l'interfaccia del servizio Raspberry Pi. Il bundle importa il package dell'interfaccia. | |
Raspberry Pi Consumer | Bundle che consuma il servizio Raspberry Pi. Il riferimento al servizio è ottenuto in modo trasparente dal Service Registry. Il bundle importa il package dell'interfaccia. |
Il diagramma a seguire mostra lo scenario che andremo a implementare utilizzando un'architettura distribuita a µServices restando nel nostro contesto OSGi. Per coloro che sono ormai avvezzi a sviluppare utilizzando il framework OSGi, apprezzeranno il fatto che alla fine non cambierà nulla nel modo di scrivere codice.
Dal diagramma è possibile notare che abbiamo a disposizione quattro OSGi Container di cui, uno è all'interno di un'istanza di Liferay Portal Platform e il resto dei tre sono tutte istanze di Apache Karaf. Al fine di rendere le cose più interessanti, ho previsto volutamente per lo scenario l'introduzione di Liferay e di un'istanza Apache Karaf in esecuzione sul Raspberry Pi.
Dal diagramma è evidente anche la distribuzione dei bundle (indicati in Tabella 2) sui quattro container OSGi. Su Liferay Portal abbiamo i bundle che consentiranno di consumare il servizio Who I am e Raspberry Pi. Su due container Apache Karaf abbiamo i bundle che consentiranno di fornire il servizio Who I am, e su uno dei due container i bundle che consentiranno di consumare il servizio Raspberry Pi. Sul terzo ed ultimo container Apache Karaf abbiamo i bundle che forniranno il servizio Raspberry Pi.
Adesso che abbiamo chiaro in mente quale sarà il nostro scenario, dobbiamo capire come abilitare i container OSGi che abbiamo a disposizione affinché siano in grado di comunicare tra loro.
4. Apache Aries RSA (Remote Service Admin)
La specifica OSGi R7 Compendium ha due sezioni relative agli OSGi Remote Services: 100 Remote Services (RS) e 122 Remote Service Admin Service Specification (RSA). Queste specifiche, tuttavia, sono destinate principalmente a chi implementa RS/RSA, piuttosto che agli sviluppatori.
Nel corso dell'articolo ho fatto sempre riferimento alla release R7 delle specifiche OSGi, vorrei però ricordare che le specifiche per gli OSGi Remote Services sono state introdotte in OSGi Service Platform Enterprise Specification, Release 4, Version 4.2.
Ricapitolando. I servizi OSGi sono semplicemente oggetti Java che espongono un certo numero di interfacce Java. Le istanze sono registrate dinamicamente in base al nome dell'interfaccia insieme alle proprietà tramite il Service Registry. Come descritto nelle specifiche, i servizi OSGi presentano numerosi vantaggi, tra cui un supporto per la dinamica, la sicurezza, una netta separazione tra contratto di servizio e implementazione, Semantic Versioning e altri. Esistono tre parti in un'istanza del servizio OSGi:
- una o più interfacce di servizio (interfaccia java);
- un'implementazione di interfacce di servizio;
- un consumatore del servizio (tramite l'interfaccia di servizio).
OSGi Remote Services sta semplicemente estendendo il Service Registry per consentire l'accesso ai servizi OSGi da fuori processo ... ovvero per l'accesso remoto.
In che cosa differisce dai servizi remoti non OSGi come HTTP/REST? In primo luogo, i servizi remoti OSGi hanno accesso a tutte le proprietà dei servizi OSGi ... ad esempio dinamica, sicurezza, controllo delle versioni, separazione dei contratti dall'implementazione, etc., nonché pieno supporto per i servizi dichiarativi o Declarative Services e altri injection framework.
Un Distribution Provider può sfruttare il loose-coupling tra i bundle per esportare un servizio registrato creando un endpoint. Viceversa, il Distribution Provider può creare un proxy che accede a un endpoint e quindi registra questo proxy come servizio importato. Un framework può contenere contemporaneamente più Distribution Provider, ciascuno dei quali importa ed esporta in modo indipendente.
Un endpoint è un meccanismo di accesso alle comunicazioni verso un servizio in un altro framework, un servizio (web), un altro processo o una coda, etc., che richiede un protocollo per le comunicazioni. La mappatura tra servizi ed endpoint nonché le loro caratteristiche di comunicazione è chiamata topologia.
Il progetto Apache Aries Remote Service Admin (RSA) consente di utilizzare in modo trasparente i servizi OSGi per la comunicazione remota. I servizi OSGi possono essere contrassegnati per l'esportazione aggiungendo una proprietà del servizio service.exported.interfaces=*. Varie altre proprietà possono essere utilizzate per personalizzare il modo in cui il servizio deve essere esposto.
La figura a seguire mostra l'architettura di Apache Aries Remote Service Admin (RSA) che se notate riporta i componenti descritti dalle specifiche OSGi Remote Service (RS e RSA). L' Endpoint descrive un servizio utilizzando le interfacce di servizio, l'URL e tutte le altre proprietà necessarie per importare correttamente il servizio remoto. L'Endpoint Listener è un servizio che deve essere avvisato quando ci sono cambiamenti sugli endpoint remoti (descritti dai filtri OSGi) sono registrati o de-registrati.
Il Topology Manager per impostazione predefinita espone tutti i servizi locali che sono opportunamente contrassegnati per l'esportazione e importa tutti i servizi interessati con gli endpoint remoti corrispondenti. Il Topology Manager può anche aggiungere proprietà con lo scopo di modificare il modo in cui i servizi sono esposti. Per i servizi da esporre chiama il Remote Service Admin, poi quest'ultimo esegue l'esportazione effettiva, successivamente notifica agli endpoint listener il nuovo endpoint. Il Topology Manager ascolta le richieste di servizio da parte dei consumer e crea gli endpoint listener.
Il Topology Manager è il posto migliore per implementare le regole di governance al livello di sistema. Alcuni esempi di cosa si può fare:
- proteggere gli endpoint remoti con SSL/TLS, servizi di autenticazione e audit;
- esportazione dei servizi OSGi con annotazioni per JAX-WS e JAX-RS anche se non appositamente contrassegnati per l'esportazione.
Per il ruolo del Topology Manager, questo non implementa direttamente queste caratteristiche ma crea tutte le chiamate necessarie a un Remote Service Admin.
Il Remote Service Admin viene chiamato dal Topology Manager per esporre i servizi locali come endpoint remoti e creare servizi proxy locali come client per endpoint remoti. Apache RSA ha un SPI (Service Provider Interface), il Distribution Provider che consente di creare facilmente nuovi meccanismi di trasporto e di serializzazione affinché i servizi OSGi siano disponibili al di fuori del container. Il diagramma a seguire mostra appunto il ruolo del Distribution Provider.
Gli attuali provider supportati da Apache Aries sono:
- Apache CXF Distributed OSGi
- utilizza Apache CXF per il trasporto;
- gli endpoint di servizio possono essere consumati anche da software != da Java grazie al supporto degli standard JAX-WS e JAX-WS.
- TCP
- serializzazione Java su TCP (una porta per servizio);
- pochissime dipendenze;
- semplice per implementare il proprio meccanismo di trasporto.
- Fastbin
- serializzazione Java Protocol Buffers (o Protobuf) ottimizzata su TCP tramite NIO (Non-blocking I/O);
- multiplexing su un'unica porta;
- gestisce in modo trasparente InputStreams e OutStreams nei servizi remoti;
- chiamate sincrone e asincrone supportate.
Il Discovery utilizza gli endpoint listener per ascoltare gli endpoint locali e li pubblica per gli altri container OSGi. Ascolta anche gli endpoint remoti e notifica agli endpoint listener la loro presenza. Le attuali implementazioni supportate da Apache RSA sono:
- Local Discovery mediante l'uso di descrittori XML;
- Discovery basato di Apache Zookeeper.
Per l'implementazione del nostro scenario (mostrato in Figura 4) utilizzeremo il Distribution Provider TCP e Apache ZooKeeper per il Discovery. Ci siamo!
5. Implementazione dello scenario µServices Remoti
Mi rendo conto che per alcuni di voi arrivare fino a qui sia stata dura, ma ci siamo, è arrivata la parte davvero molto ma molto pratica.
Dallo schema mostrato in Figura 4 qualcuno di voi avrà immaginato lo scenario di deployment, ebbene si: container Docker e SBC (Single Board Computer) Raspberry Pi.
5.1 Configurazione ambiente di deployment
Al fine di favorire tutti voi per replicare lo scenario di Figura 4, non c'è niente di meglio che mettere su i servizi necessari utilizzando docker compose. Tramite il docker compose andremo a configurare i seguenti servizi.
- Due servizi Apache Karaf (versione 4.2.7)
- Un servizio Liferay Portal Community Edition (versione 7.3 GA4)
- Un servizio Apache ZooKeeper (versione 3.6.1)
A seguire è mostrato il contenuto del file docker-compose.yaml al cui interno sono definiti i dettagli di configurazione dei servizi di cui sopra. Tutte le immagini dei servizi definiti, sono disponibili sugli account ufficiali Docker Hub dei rispettivi proprietari.
Il contenuto del docker-compose.yml dovrebbe essere abbastanza chiaro. Un'unica nota che vorrei fare riguarda il command del servizio Apache Karaf che ho voluto sovrascrive (vedi CMD) per due motivi.
- Aggiornare la configurazione dei repository Maven. In particolare il cambio di protocollo da http a https, questo per evitare l'errore HTTP/501 in caso di aggiornamento e/o installazione di nuove Apache Karaf Feature.
- Aggiornare la configurazione di Apache Karaf per installare di default le features di Apache Aries RSA. In questo modo evitiamo di doverlo fare successivamente dalla console.
- Avviare Apache Karaf con il parametro clean Cleaning the Karaf state.
In Console 1 è mostrata la struttura del progetto che consente di tirar su i servizi via docker compose. Ogni servizio ha la propria directory all'interno della quale sono poi contenute ulteriori directory (di deploy e configurazione) e file.
La directory deploy che fa riferimento al servizio Apache Karaf, contiene il file di configurazione per Apache ZooKeeper, Apache Karaf e i file JAR dei bundle che implementano il servizio Who I am (vedi Tabella 2).
La directory deploy che fa riferimento al servizio Liferay Portal, è la directory di hot-deploy dei bundle. Su questa directory andranno eventualmente posizionati i file JAR dei bundle API e Consumer dei servizi Who I am e Raspberry Pi.
La directory files/osgi/configs che fa riferimento al servizio Liferay Portal, contiene il file di configurazione per Apache ZooKeeper.
La directory files/osgi/modules che fa riferimento al servizio Liferay Portal contiene tutti bundle necessari per installare sul container OSGi di Liferay il supporto per Apache Aries RSA. Inoltre, all'interno della stessa directory, sono presenti i JAR dei bundle API e Consumer dei servizi Who I am e Raspberry Pi.
5.2 Setup di Apache Aries RSA per i container OSGi
Prima di poter esporre remotamente in nostri µServices, è necessario che sia installato Apache Aries RSA su ognuno dei container OSGi, Liferay compreso.
Per fare quest'operazione ho preferito agire direttamente a livello di docker compose, in questo modo, una volta tirati su i servizi sarà tutto pronto per assaporare i µServices remoti in azione.
Ovviamente è possibile installare successivamente Apache Aries RSA, ma per quanto possibile cerchiamo sempre di sfruttare al massimo gli strumenti a nostra disposizione.
Lato Apache Karaf, l'installazione di Apache Aries RSA è davvero semplice, bastano i seguenti file di configurazione, già inclusi nel progetto docker compose (vedi directory karaf-instance-1/deploy).
- org.apache.karaf.features.cfg. Tramite questo file di configurazione è stato aggiunto l'indirizzo della rsa-features e indicato quali bundle installare in fase di start-up di Apache Karaf.
- org.apache.aries.rsa.discovery.zookeeper.cfg. Tramite questo file è stato configurato l'indirizzo e la porta del servizio di Discovery, che in questo caso è rappresentato da Apache ZooKeeper.
A seguire parte del contenuto del file di configurazione delle feature Apacke Karaf di nostro interesse. In particolare fare riferimento alla riga 9 e dalla riga 34 alla riga 36. In questo modo Apache Karaf è istruito affinché alla partenza installi i bundle SCR (Service Component Runtime), aries-rsa-provider-tcp e aries-rsa-discovery-zookeeper.
Tramite le due property mostrate a seguire, sono impostati l'hostname e la porta del servizio di Apache ZooKeeper (vedi definizione del servizio zookeeper-instance sul file del docker compose).
Lato Liferay Portal, installare il supporto per Apache Aries RSA è un pochino più noioso. Quello che bisogna fare è scaricare i bundle di Apache Aries RSA e Apache ZooKeeper dal repository Maven e dal sito di Apache ZooKeeper, successivamente metterli all'interno della directory di hot-deploy ($LIFERAY_HOME/deploy) per l'installazione.
Il file di configurazione org.apache.aries.rsa.discovery.zookeeper.cfg per il servizio di Discovery va invece posizionato all'interno della directory $LIFERAY_HOME/osgi/configs.
Grazie al docker compose non dovrete fare nulla di tutto ciò, i servizi partiranno già configurati. Quella mostrata a seguire è la struttura del progetto docker compose, che consente di tirar su tutti i container OSGi già configurati e pronti all'uso, con installati anche i bundle OSGi dei progetti Who I am e Raspberry Pi distribuiti secondo il diagramma di Figura 4.
Per quanto riguarda invece il container OSGi Apache Karaf per il servizio Raspberry Pi, dovreste disporre un Raspberry Pi (almeno 3 Model B+), installare Apache Karaf e seguire le attività di configurazione mostrate in precedenza. L'attività d'installazione e configurazione di Apache Karaf su Raspberry Pi è davvero semplice; consiglio di leggere la guida ufficiale.
Le due figure a seguire mostrano Apache Karaf in esecuzione sul mio Raspberry Pi 3 Model B+ e la lista dei bundle di Apache Aries RSA e Apache ZooKeeper.
5.3 Start-up della soluzione via Docker Compose
Fino a questo momento credo che abbiamo visto tante belle cose interessanti. Adesso è giunta l'ora di avviare la nostra soluzione via docker compose. Gli step da fare sono veramente pochi e semplici (così come mostrato in Console 3).
Prima di avviare i servizi ed evitare d'incorrere a "strani errori", dovreste accertarvi che le risorse dedicate a Docker siano adeguate. Per questi servizi consiglio di dedicare almeno 2 CPU e 6 Gbyte di memoria.
Per quanto riguarda la versione di Docker, quella minima richiesta è la 18. Personalmente il mio ambiente è Docker Desktop con Engine versione 19.03.12 e Compose versione 1.26.2.
La prima volta consiglio di lanciare il docker compose non in modalità detached, così da seguire meglio lo start-up di tutta la soluzione. Per la maggior parte di voi, il primo start-up impiegherà parecchi minuti, spesi per il download delle immagini da Docker Hub.
Per accertarci che la soluzione sia salita senza alcun problema, potremmo
utilizzare il comando docker-compose ps
e dovremmo ottenere un risultato come
quello mostrato dalla Figura 10 a seguire. In questo caso l'output del comando
informa che tutti in container sono up e running.
La figura a seguire evidenzia in particolare le porte TCP delle console di amministrazione dei container OSGi, sia Liferay sia Apache Karaf. È evidenziata anche la porta TCP esportata per il servizio di Apache ZooKeeper.
La porta TCP per la Gogo Shell di Liferay è la 21311 per le rispettive istanze di Apache Karaf le porte TCP sono: 8104 e 8109.
La porta 2181 esportata per il servizio di Apache ZooKeeper è quella che sarà utilizza dai container OSGi per registrare le informazioni sui propri servizi.
Dal Docker Compose file (vedi Source Code 5), le porte di amministrazione esportate per le due istanze di Apache Karaf, sono impostate su base range, non è quindi assicurato che il numero di porta sia sempre lo stesso, potrebbe cambiare ad ogni start del servizio.
Cliccando su Console Sessione 1, potete vedere la sessione di terminale che fa riferimento ai comandi mostrati in Console 3 per lo start-up del Docker Compose.
Facciamo ulteriori verifiche con lo scopo di accertare che Apache Aries RSA e
Apache ZooKeeper più i bundle dei nostri servizi siano stati correttamente
installati. Per fare questa operazione e sufficiente collegarsi alla Gogo Shell
di Liferay e utilizzare il comando ls
per verificare che siano presenti i
rispettivi bundle e che siano nello stato Active. La stessa attività può essere
condotta sulle istanze di Apache Karaf, collegandosi alla console e utilizzare
il comando list
per verificare che siano presenti i rispettivi bundle e che
siano anch'essi nello stato Active.
Le due figure a seguire mostrano l'output dei comandi ls
e list
per la Gogo
Shell (Liferay) e Apache Karaf. Per la connessione alla console di Apache Karaf
utilizzare il comando ssh -p 8104 karaf@127.0.0.1
Specificare la porta
dell'istanza a cui volersi connettere (vedi Figura 10).
La password per accedere alla console di Apache Karaf è: karaf.
6. Come rendere i µServices Remoti
Tutto ciò che rende "magico" i µServices Remoti lo abbiamo visto nel dettaglio nel capitolo 4, dov'è stata introdotta la specifica OSGi Remote Services e come Apache Aries RSA renda possibile l'esportazione delle interfacce dei servizi all'esterno del container OSGi all'interno del quale vivono.
Per chi sviluppa, cambia qualcosa? Assolutamente no, a maggior ragione per chi utilizza i Declarative Services (o DS).
Prendiamo in esame il servizio Who I am. Questo servizio è molto semplice, attraverso l'interfaccia it.smc.techblog.apache.aries.rsa.examples.whoiam.api.WhoIamService (vedi Source Code 2) è stato definito il metodo whoiam(). Questo metodo restituisce qualche informazione circa l'ambiente di runtime del servizio. L'implementazione di quest'interfaccia, per così come mostrata al Source Code 3, non sarebbe candidata per l'esportazione come servizio remoto. Per far sì che questo accada, è necessario aggiungere delle proprietà al nostro componente, così come indicato a seguire.
Tramite la proprietà service.exported.interfaces=* candidiamo il componente per essere esportato come servizio remoto, inoltre occorre specificare anche il numero di porta tramite la proprietà aries.rsa.port=. Quest'ultima configurazione è specifica per il tipo di trasporto scelto, in questo caso il TCP, così come avevo anticipato nel capitolo 4 parlando del Distribution Provider.
Per noi sviluppatori la "fatica" è finita qui, non occorre fare nient'altro. Anche per il consumer non cambia nulla (vedi Source Code 4), basta inserire l'annotazione @Reference al membro dichiarato di tipo WhoIamService, come sempre fatto. Sarà poi cura del Topology Manager iniettare il riferimento corretto al servizio remoto, quello che vive all'interno dell'altro container OSGi.
Connettiamoci alla Gogo Shell di Liferay e vediamo cos'è cambiamo sotto
rispetto al solito. Con l'installazione di Apache Aries (RSA) abbiamo a nostra
disposizione il comando per poter interrogare il Remote Service Admin e capire
quali endpoint sono stati creati. Il comando si chiama rsa:endpoints
e
l'output di questo eseguito sulla Gogo Shell di Liferay dovrebbe essere simile a
quello mostrato a seguire.
Quello mostrato in figura corrisponde a quanto mi sarei aspettato, ed effettivamente i conti tornano; forse qualcuno potrebbe obiettare. Ci sono tre endpoint. perché tre endpoint se i servizi che abbiamo esportato sono due? È vero che i servizi esportati sono due ma il servizio Who I am è installato su due differenti container OSGi. Le informazioni che il comando restituisce sono:
- id: identificativo del servizio esportato espresso in formato URI. Protocollo, indirizzo ip del servizio e porta;
- interface: è il nome completo dell'interfaccia che il servizio implementa;
- framework: è l'identificato del framework OSGi all'interno del quale il servizio vive;
- comp name: è il nome del componente che corrisponde alla classe Java che implementa l'interfaccia e di conseguenza il servizio.
Notate che l'identificativo del framework è diverso per ognuno degli endpoint, corretto, perché sono servizi che vivono in tre container OSGi diversi.
Cliccando su Console Sessione 2, potete vedere la sessione di terminale dove si evidenzia il processo di registrazione del nuovo endpoint che riguarda il servizio del Raspberry Pi. Il nuovo endpoint è infatti registrato subito dopo l'avvio dell'istanza di Apache Karaf installata sul Raspberry Pi.
Quante volte avete utilizzato il comando bundle
o b
per fare l'inspection
dei bundle? Immagino parecchie volte, soprattutto quando c'era da capire perché
le cose non funzionavano. Proviamo ad eseguire il comando b
sul bundle del
consumer del servizio Who I am e vediamo cosa presenterà l'output.
Prima di eseguire il comando per l'inspection del bundle, utilizziamo il comando
lb it.smc.techblog
per ottenere la lista dei nostri bundle per poi ricavare
l'id del bundle di cui siamo interessati di fare l'inspection.
Una volta ottenuto l'id del bundle, eseguiamo il comando b 1180
. L'output
mostrato in figura mostra delle informazioni leggermente diverse rispetto al
solito, ovvero, la sezione Services in use mostra il riferimento al servizio
remoto, da cui possiamo vedere tutte le caratteristiche dell'endpoint remoto.
Qui sono entrati in gioco il Topology Manager e il Remote Service Admin.
In questo caso il binding è stato fatto con il servizio remoto che vive sul container OSGi che ha l'indirizzo 172.29.0.4.
Cosa dovrei vedere interrogando il framework OSGi tramite comando services it.smc.techblog.apache.aries.rsa.examples.whoiam.api.WhoIamService? Quello che otteniamo è l'informazione che abbiamo la disponibilità di due servizi remoti che implementano la stessa interfaccia (in questo caso WhoIamService) ma che solo uno di questi è utilizzato dal bundle del consumer.
Cosa succederebbe nel caso in cui stoppassi il servizio WhoIamService tcp://172.29.0.4:8202 attualmente referenziato dal consumer (bundle 1180)? Si verrebbe a creare un disservizio? Assolutamente no. Quello che in breve accade è questo:
- stop del servizio sull'istanza karaf-instance-2_1;
- rimozione dell'endpoint tcp://172.29.0.4:8202 da ZooKeeper;
- il servizio di Discovery provvedere a notificare l'avvenuto cambiamento sull'endpoint rimosso;
- il Topology Manager e il Remote Service Admin provvedono affinché il riferimento richiesto dal consumer del servizio WhoIamService venga associato al servizio remoto dell'istanza karaf-instance-1_1.
La figura a seguire mostra la verifica degli endpoint sull'istanza karaf-instance-2_1, l'esecuzione dello stop per il bundle Who I am Service (bundle 12) e la successiva verifica degli endpoint. Dopo lo stop del bundle è evidente come sia stato rimosso l'endpoint tcp://172.29.0.4:8202.
Sull'istanza Liferay, se andassimo a verificare gli endpoint, vedremo che
correttamente è stato rimosso anche qui l'endpoint tcp://172.29.0.4:8202 e
eseguendo nuovamente il comando services
, vedremo che il riferimento richiesto
dal consumer verso il servizio Who I am, è stato correttamente cambiato verso
l'endpoint tcp://172.29.0.3:8202.
Le due figure a seguire mostrano gli avvenuti cambiamenti lato Liferay, l'ultima in particolare mostra l'output della chiamata al metodo whoiam() del servizio remoto, dov'è evidente che il servizio remoto che ha risposto è quello che fa riferimento all'endpoint tcp://172.29.0.3:8202, in questo caso l'istanza karaf-instance-1_1.
Anche se facessimo ripartire il bundle stoppato in precedenza, il consumer rimarrebbe agganciato all'attuale endpoint. Concludiamo questo capitolo con la figura a seguire che mostra l'output della chiamata al servizio remoto Raspberry Pi Service, chiamata eseguita dall'istanza Liferay.
7. I progetti sull'account GitHub di SMC
Penso che arrivare fino a qui sia stata una bella fatica, vi lascio però un bel regalo. Tutto quello che avete avuto modo di vedere nel corso di quest'articolo è disponibile in forma di repository git. Questo progetto è organizzato in diversi repository e in particolare:
- docker-osgi-remote-services-example: È il repository che contiene tutto il progetto Docker Compose che vi consentirà di tirar su la soluzione rappresentata dal diagramma di Figura 4 (a meno dell'istanza Apache Karaf su Raspberry Pi);
- aries-rsa-whoiam-examples: Progetto Maven che contiene i bundle per il servizio Who I am Service (vedi Tabella 2);
- aries-rsa-raspberrypi-examples: Progetto Maven che contiene i bundle per il servizio Raspberry Pi Service (vedi Tabella 2).
I repositori dei due progetti OSGi contengono quelli che possiamo definire bonus, ovvero, due OSGi command per interagire con il servizio Who I am e con il servizio Raspberry Pi. Vi ricordo che gli OSGi command sono dei veri e propri comandi che possiamo richiamare dalla console dei container OSGi.
- WhoIamConsumerCommand OSGi Command per interagire con il servizio Who I am.
- RaspberryPiServiceConsumerCommand OSGi Command per interagire con il servizio Raspberry Pi. Uno dei comandi restituisce per esempio la temperatura della CPU.
Ogni repository ha al suo interno il README con le istruzioni basilari per l'uso.
8. Conclusioni
Per i temi che abbiamo trattato, questo articolo è una conseguenza dell'intervento che ho tenuto al Liferay Boot Camp Summer Edition 2020, dove in particolare ho discusso della piattaforma Liferay all'interno di contesti di Microservices.
Per quanto ho cercato di essere sintetico, con questo articolo ho voluto dimostrare come il framework OSGi con i suoi veri µServices sia in grado di favorire lo sviluppo di sistemi distribuiti leggeri, affidabili e puliti.
Mi rendo conto che sarebbe necessario più spazio, ma con quello che sono riuscito ad occupare spero di essere stato chiaro nell'esposizione e di aver suscitato la vostra curiosità sull'argomento.
Credo di poter affermare che in giro per la rete è difficile trovare articoli con questo livello di dettaglio e che hanno affrontato lo sviluppo di una soluzione pratica mettendo a vostra disposizione tutto il materiale.
Se siete arrivati fino a qui, immagino che abbiate anche accumulato una serie di domande che vi invito a lasciare in forma di commento all'articolo. Prossimamente saranno pubblicati altri articoli di approfondimento.
9. Risorse
A seguire lascio una serie di risorse che sono il punto di riferimento per i contenuti di quest'articolo.