• Non ci sono risultati.

2.8 Supporto Esecuzione Distribuita

2.8.1 Hazelcast

Hazelcast [20] `e un progetto opensource in Java che nasce come in-memory data grid (strutture dati distribuite) dove i dati sono memorizzati intera- mente nella memoria Ram del cluster. Nel tempo sono state realizzate un insieme di funzionalit`a che non la rende semplicemente una cache ma mette a disposizione diverse strutture dati in modalit`a distribuita, che includono:

• collezioni standard Java:

– Map: coppia chiave valore – List: lista di oggetti

– Set: collezione non duplicata di oggetti – Queue: collezione FIFO (First In First Out) • collezioni specifiche:

– MultiMap: mappa chiave valori

– RingBuffer : collezione che memorizza in una struttura dati ad anello.

– Topic: publish/subscribe di messaggi • gestione concorrenza:

– AtomicNumber/AtomicReference: riferimento atomico unico in tutto il cluster

– IdGenerator : generatore di numeri univoci sul cluster – Semaphore/Lock : gestione concorrenza sul cluster – CountdownLatch: contatore alla rovescia sul cluster.

Il fatto di poter gestire oggetti distribuiti, unito alla semplicit`a di poter creare dei cluster, fanno di questo prodotto un progetto molto interessante e soprattutto versatile per diverse situazioni. In MuSkel2 la libreria `e stata utilizzata a supporto nella creazione del cluster, all’esecuzione remota (sin- cronizzazione, distribuzione del carico) e per la trasmissione dei dati ai singoli nodi.

2.8.2

Spring Boot

Spring [29] `e uno dei pi`u famosi Framework per lo sviluppo di applicazio- ni enterprise in Java. Milioni di sviluppatori nel mondo usano il Framwork Spring per creare codice avente le seguenti caratteristiche: alte performan- ce, facilmente testabile e riusabile. Le funzionalit`a appartenenti al core del Framework Spring possono essere usate per lo sviluppo di qualsiasi tipo di ap- plicazione Java, esistono poi estensioni apposite per creare Web Application. Il target del Framework Spring `e quello di rendere pi`u semplice lo sviluppo di applicazione J2EE (Java 2 Enterprise Edition) e quello di usare e pro- muovere buone consuetudini di programmazione usando un modello di pro- grammazione basato sui POJO (Plain Old Java Object). La tecnologia che maggiormente caratterizza Spring `e l’iniezione delle dipendenze che realizza il pattern Inversion of Control (vedi Sezione 2.7.4) che tende a disaccoppiare i singoli componenti di un sistema.

Spring Boot [28] permette la creazione di applicazioni Spring che possono essere avviate in modalit`a standalone oppure facendo uso di un container J2EE.

2.9

Apache Maven

Apache Maven [22] `e uno strumento completo per la gestione di progetti software Java, in termini di compilazione del codice, distribuzione, docu- mentazione e collaborazione del team di sviluppo. Secondo la definizione ufficiale si tratta di un tentativo di applicare pattern ben collaudati all’in- frastruttura di build dei progetti. Maven `e contemporaneamente un insieme di standard, una struttura di repository e un’applicazione che servono alla gestione e descrizione di progetti software. Esso definisce un ciclo di vita standard per il building, il test e il deployment di file di distribuzione Java.

sitory centralizzato e scaricate localmente al primo utilizzo per il build del progetto. Lo sviluppatore pu`o utilizzare i plugin Maven per le diverse Ide, a seconda dell’ambiente di sviluppo utilizzato, pur mantenendo la consistenza del pacchetto e la totale indipendenza dalle configurazioni dell’ambiente di sviluppo.

3.1

Schema Logico

Ad alto livello MuSkel2 `e una libreria che, attraverso la composizione di ope- ratori (skeleton), permette la realizzazione di programmi data-flow asincroni in modalit`a sequenziale, parallela e distribuita.

L’utilizzatore, interagendo con le API (Application Programming Inter- face) esposte da MuSkel2, ha la possibilit`a di realizzare applicazioni che, a partire da un flusso di dati in ingresso (anche infinito), ne genera uno in uscita (vedi Figura 3.1).

Figura 3.1: Programma Muskel2 e Stream di Input - Output

A run-time la libreria si occupa di gestire tutte le fasi dell’elaborazione facendosi carico delle problematiche tipiche della programmazione parallela e distribuita come:

– allocazione e gestione del ciclo di vita dei Thread – discovery dei nodi del cluster (vedi Figura 3.2) – bilanciamento del carico

Figura 3.2: MuSkel2 Tipi di Risorse Target

Oltre a gestire il parallelismo, le API esposte permettono la manipolazione del data-flow. Nella seguente tabella `e mostrata una parte degli operatori disponibili raggruppata per categoria.

Categoria Esempio

Controllo di Flusso doOnNext, doOnError, doOnComplete, doOnEach

Calcolo Parallelo map, executeOn, flatMap, merge

Combinazioni merge, concat

Condizionali defaultIfEmpty

Filtraggio cast, filter, first, last, take, takeLast, takeFirst

Aggregazione count, reduce, toList, toSortedList

Trasformazione map, scan, flatMap, groupBy, groupBySortedToList

Tabella 3.1: Lista Operatori raggruppati per categoria

3.2

Esempi di Parallelismo

Nella seguente sezione verr`a mostrato come, a partire dagli skeleton realizzati per il calcolo parallelo di MuSkel2, sia possibile realizzare alcune fra le pi`u comuni tipologie di parallelismo conosciute in letteratura.

Uno tra i pi`u famosi pattern di programmazione parallela `e indubbiamente il pipeline, che consiste nel dividere una computazione in pi`u stadi e nell’im- plementare a cascata un insieme di processi paralleli relativi agli stadi di una elaborazione sequenziale.

In Figura 3.3 `e mostrata la funzione sequenziale R= F(G(J(x))) paralle- lizzata con un pipeline a tre stadi alla quale viene applicato uno stream di dati in ingresso ed uno in uscita.

Il MuSkel2 pu`o essere realizzato scegliendo, per ogni stadio del pipeline, la forma di parallelismo desiderata. Nelle seguenti sezioni verr`a spiegato come viene parallelizzata la funzione R in ambiente locale oppure cluster.

Computazione Locale

Le funzioni appartenenti al pipeline in esempio vengono eseguite in thread separati rispetto al programma in esecuzione. I risultati vengono man mano notificati in modo asincrono al flusso di controllo principale (vedi Figura 3.4).

Per ogni stadio del Pipeline che si desidera parallelizzare viene allocato un thread (anche appartenente a thread pool distinti) che, per ogni notifica ricevuta dallo stadio precedente, calcola la funzione sul valore in ingresso ed inoltra il risultato allo stadio successivo. Nell’Esempio 3.1 `e mostrato un frammento di codice che implementa una pipeline a tre stadi su thread pool locali e distinti. 1 MuskelProcessor.from(1, 2, 3, 4, 5, 6) 2 .executeOn(local("pool1")) 3 .map(x -> j(x)) 4 .executeOn(local("pool2")) 5 .map(b -> g(b)) 6 .executeOn(local("pool3")) 7 .map(a -> f(a)) 8 .subscribe(s -> System.out.println(s));

Esempio 3.1: Esempio di Pipeline in ambiente locale

Computazione Remota

Le funzioni appartenenti agli stadi pipeline in esempio vengono eseguite su nodi remoti del cluster rispetto al programma in esecuzione (quindi il codice si trova sul nodo Client). I risultati vengono man mano notificati in modo asincrono al flusso di controllo principale presente sul Client (vedi Figura 3.5).

Figura 3.5: Schema Logico Pipeline Remoto

La scelta del nodo da allocare per l’elaborazione del singolo stadio del pipeline pu`o avvenire:

Nell’Esempio 3.2 `e mostrato un frammento di codice che realizza un pipe- line a tre stadi utilizzando nodi remoti. In particolare nodeA, nodeB, nodeC rappresentano i nomi di server remoti dove eseguire rispettivamente le fun- zioni j(x), g(b) ed f(a). Il metodo toLocal() indica a nodeC che il risultato della singola map deve essere restituito al client. Infine, la funzione in argo- mento al metodo subscribe stampa, in modo asincrono, il risultato finale di ogni singolo elemento dello stream in ingresso.

1 MuskelProcessor.from(1, 2, 3, 4, 5, 6) 2 .executeOn(remote("nodeA")) 3 .map(x -> j(x)) 4 .executeOn(remote("nodeB")) 5 .map(b -> g(b)) 6 .executeOn(remote("nodeC")) 7 .map(a -> f(a)) 8 .toLocal() 9 .subscribe(s -> System.out.println(s));

Esempio 3.2: Esempio di Pipeline ambiente cluster Computazione Ibrida

Per ogni stadio del Pipeline `e possibile configurare se utilizzare un thread locale oppure un nodo remoto. Una soluzione ibrida potrebbe essere far s`ı che le funzioni J(x) e G(b) vengano eseguite su due thread diversi di un nodo server mentre F(a) venga elaborata lato Client.

Nell’Esempio 3.3 `e mostrato un frammento di codice che realizza un pi- peline a tre stadi utilizzando la modalit`a ibrida descritta precedentemente. In particolare nodeA rappresenta il nome di un server remoto, localThread- PoolNodeAil nome di un thread pool locale definito su nodeA e localThread- PoolClient il nome di un thread pool locale definito lato client. Il metodo toLocal() indica a nodeA che il risultato della singola map deve essere resti- tuito al client. Infine, la funzione in argomento al metodo subscribe stampa,

Figura 3.6: Generico Farm e stream di input - output

in modo asincrono, il risultato finale di ogni singolo elemento dello stream in ingresso. 1 MuskelProcessor.from(1, 2, 3, 4, 5, 6) 2 .executeOn(remote("nodeA")) 3 .map(x -> j(x)) 4 .executeOn(local("localThreadPoolNodeA")) 5 .map(b -> g(b)) 6 .toLocal() 7 .executeOn(local("localThreadPoolClient")) 8 .map(a -> f(a)) 9 .subscribe(s -> System.out.println(s));

Esempio 3.3: Esempio di Pipeline in ambiente ibrido

3.2.2

Farm

Il Farm consiste nel parallelizzare una computazione con un insieme di Wor- ker, tutti equivalenti, in cui uno stream di dati deve essere sottoposto alla stessa elaborazione. I task sono gestiti dal modulo Emitter che ha il compito di determinare i worker liberi, inviando loro il task da elaborare. I Worker, dopo aver eseguito la funzione su uno specifico dato in ingresso, inviano il risultato al Collector che riceve i dati e si occupa di riordinarli.

In Figura 3.6 `e mostrata la funzione sequenziale R= F(x) parallelizzata con tre Worker.

In MuSkel2 il Farm `e realizzato dando allo sviluppatore la possibilit`a di scegliere dove eseguire funzione: su un thread pool locale oppure su un nodo del cluster. Inoltre, la funzione di Emitter e Collector viene svolta dal client stesso. In Figura 3.7 `e mostrato un esempio di Farm con Worker remoti.

Figura 3.7: Esempio di Farm Remoto in MuSkel2

La scelta del nodo da allocare per l’elaborazione della coppia funzione, dato pu`o avvenire:

1. in modo automatico lasciando al sistema, in base a politiche Round Robin, la scelta di quale nodo allocare. Nella versione locale viene utilizzato un thread pool di default;

2. uno specifico o gruppo di nodi server o thread pool locale indicato a livello di interfaccia sviluppatore. Se non dovesse essere presente il programma restituisce un errore.

Nell’Esempio 3.4 `e mostrato un frammento di codice mostra che la fun- zione F(x) viene applicata (map), su un qualunque nodo remoto (remote()), ad una sequenza di numeri interi in ingresso (from(...)). Il risultato viene stampato in modo asincrono dal client utilizzando il metodo subscribe.

1 MuskelProcessor.from(1, 2, 3, 4, 5, 6, ...)

2 .map(x -> f(x), remote())

3 .subscribe(s -> System.out.println(s));

Esempio 3.4: Esempio di Farm in ambiente remoto

3.2.3

Generatore di Stream

Il generatore di Stream, chiamato in MuSkel2 FlatMap, consiste nel paralle- lizzare una funzione che consuma un valore e produce uno stream contenente un numero arbitrario di dati. I task sono gestiti dal modulo Emitter che ha

Figura 3.8: Generica FlatMap e Stream di Input - Output

il compito di determinare i worker liberi inviando loro il task da elaborare. I Worker eseguono la funzione su uno specifico dato in ingresso che genera uno stream asincrono di risultati che sono inviati al Merger man mano che vengono prodotti. Quest’ultimo ha il compito di mantenere le strutture dati necessarie per ricevere i risultati da n produttori e di inoltrare allo stadio seguente i risultati secondo l’ordine di arrivo. In Figura 3.8 `e mostrata la funzione sequenziale R= F(x) parallelizzata con tre Worker.

In MuSkel2 la FlatMap `e realizzata dando allo sviluppatore la possibilit`a di scegliere se l’esecuzione della funzione debba essere eseguita in un ”Thread Pool” locale oppure su un nodo remoto. La funzione di Emitter e Merger viene svolta dal client stesso. Ogni Worker ha la possibilit`a di inviare i risultati in modo completamente asincrono. In Figura 3.9 `e mostrato un esempio di FlatMap con Worker remoti.

Nell’Esempio 3.5 `e mostrato un frammento di codice in cui la funzione F(x) ritorna un’istanza di MuskelProcessor che pu`o generare un insieme, anche infinito, di valori. Tale funzione viene poi eseguita in un nodo remoto per ogni valore in ingresso.

1 MuskelProcessor.from(1, 2, 3, 4, 5, 6, ...)

2 .flatMap(x -> f(x), remote())

3 .subscribe(s -> System.out.println(s));

Figura 3.9: Esempio di FlatMap

Documenti correlati