• Non ci sono risultati.

3.3 Schema Logico Implementazione

4.3.2 MuskelContext

Come precedentemente anticipato, l’obiettivo dell’interfaccia MuskelContext `e fornire un livello di astrazione che permetta a tutti i componenti presenti nel core di MuSkel2 di trattare nello stesso modo funzionalit`a locali e remote. Come mostrato in Figura 4.15, MuskelContext `e un’interfaccia le cui imple- mentazioni principali sono LocalMuskelContext e HazelcastMuskelContext:

A grandi linee i componenti funzionali gestiti da MuskelContext sono: – Queue con nome, utilizzate dagli internals di MuSkel2 per la comuni-

cazione dei dati del data-flow tra thread o nodi remoti diversi. – Executor Service per l’allocazione dei task nei thread.

– Topic Notification per la gestione di notifiche uno a molti. LocalMuskelContext

La classe LocalMuskelContext mantiene, al suo interno, tutte le strutture dati necessarie per l’esecuzione di un programma MuSkel2 in ambiente locale.

In particolare, la classe gestisce il ciclo di vita dei seguenti componenti: – Queue che si occupa della creazione, caching e rimozione delle code

– host:porta

– 192.168.1.0-9 (range di ip address) Tabella 4.2: Parametri discovery nodi server

– Thread Pool che permette la corretta allocazione dei task all’interno di thread pool richiesti;

– Notifiche per la comunicazione di eventi a tutti i thread. HazelcastMuskelContext

L’implementazione pi`u interessante dell’interfaccia MuskelContext `e senza dubbio HazelcastMuskelContext in quanto `e il componente che si occupa, utilizzando il framework Hazelcast (vedi Sezione 2.8.1), di gestire la comuni- cazione con i nodi appartenenti al cluster MuSkel2. Le attivit`a svolte dalla classe HazelcastMuskelContext sono:

– Discovery Nodi attraverso il supporto della libreria Hazelcast, effettua il discovery dei nodi (vedi Sezione 4.3.1) e mantiene la connessione durante tutto il ciclo di vita del contesto.

– ClassLoading ovvero, un servizio client che pu`o essere richiamato, da uno dei nodi remoti, per il recupero delle classi. I dettagli implemen- tativi sono discussi nella Sezione 4.3.5.

– Executor Service che fornisce il supporto a run-time affinch´e un task possa essere elaborato lato server. Inoltre, dato il nome di un nodo o di un gruppo, cerca i possibili nodi compatibili, inoltrando loro il task.

Nome Descrizione

name Il nome da utilizzare per l’autenticazione dei

client e server.

password La password da utilizzare per l’autenticazio-

ne dei client e server.

portNumber Numero porta da aprire per la ricezione delle

connessioni in ingresso.

portAutoIncrement Se true il sistema cerca un nuova porta nel

caso fosse gi`a in uso. networkInterfaces

La lista delle interfacce di rete, separata da virgola, su cui aprire la porta del server e dove effettuare il discovery dei nodi. Esempi di utilizzo: 172.16.33.205, 172.*.*.*

sslEnabled

Se true abilita la comunicazione ssl tra i nodi e i client. Se abilitato `e necessario configurare anche le seguenti propriet`a: – sslKeyStore (nome keystore)

– sslKeyStorePassword (password keystore) – sslKeyManagerAlgorithm (esempio Su- nX509)

– sslTrustManagerAlgorithm (esempio Su- nX509)

– sslProtocol (esempio TLS) Tabella 4.3: Altri parametri Avvio Server

– Serializzazione che viene effettuata appoggiandosi a librerie specifiche e permette l’invio di dati, task e funzioni tra i nodi client e server (vedi Sezione 4.3.3).

4.3.3

Serializzazione

Un aspetto fondamentale che permette lo scambio di dati tra sistemi non appartenenti allo stesso processo `e la serializzazione. La serializzazione `e la trasformazione di un oggetto in memoria in una sequenza di byte.

In Java la serializzazione `e possibile per gli oggetti che implementano l’interfaccia Serializable, che `e un’ interfaccia “marker” in quanto non ha

Figura 4.15: Diagramma Classi MuskelContext metodi da implementare.

Per non obbligare lo sviluppatore ad implementare l’interfaccia Serializa- ble, per ogni classe la cui istanza deve essere inviata via rete, `e stato realizzato un custom serializer che, attraverso librerie esterne, `e in grado di serializzare la maggior parte degli oggetti che non implementano l’interfaccia Serializable. Una tipologia di oggetti che attualmente non si riescono a serializzare senza che implementino l’interfaccia Serializable sono le lamda expression (vedi Sezione 2.2) che possono essere utilizzate come parametri dei metodi dell’interfaccia MuskelProcessor.

Per ovviare al problema `e stata realizzata una versione Serializable di ogni tipo di classe funzionale utilizzata dall’interfaccia MuskelProcessor. Ta- le problema potrebbe essere risolto in versioni successive di Java o con la manipolazione del bytecode (vedi Capitolo 6).

4.3.4

Service Injection

La funzionalit`a di Service Injection permette, attraverso annotazioni da in- serire staticamente nel codice, la referenziazione di un servizio la cui istanza `e disponibile sul nodo server a tempo di esecuzione.

Questo consente, ad esempio, l’utilizzo di funzionalit`a presenti solo lato server, come database, servizi specifici ecc. La realizzazione di un servizio

custom, da utilizzare lato server, deve avvenire secondo le modalit`a descritte in Appendice (vedi Sezione A) e secondo le specifiche Spring (vedi Sezione 2.8.2). Come mostrato nell’Esempio 4.28, a tempo di esecuzione, Spring si occupa, automaticamente, di registrare l’interfaccia MyService associata all’implementazione MyServiceImpl.

1 import org.springframework.stereotype.*

2 import org.springframework.beans.factory.annotation.*

3

4 @Component

5 public class MyBean {

6

7 @Bean

8 public MyService createMyService() {

9 return new MyServiceImpl();

10 } 11 12 }

Esempio 4.28: Esempio di bean di Spring

Lato client `e possibile referenziare tale servizio utilizzando l’annotazione @MuskelInjectAware sulla classe e @Inject su ogni servizio che si desidera iniettare, come mostrato nell’Esempio 4.29.

1 import it.reactive.muskel.context.MuskelInjectAware;

2 import javax.inject.Inject;

3

4 @MuskelInjectAware

5 public class MyFunction implements

SerializableFunction<String,String>{ 6

7 @Inject

8 private transient MyService myService;

9

10 public String apply(String input) {

11 12 myService.mycustomOperation(..); 13 14 ... 15 } 16 }

Esempio 4.29: Esempio di funzione con annotation

La funzione creata pu`o essere utilizzata all’interno del MuskelProcessor visto che, una volta in esecuzione sul nodo remoto, l’attributo myService verr`a popolato con il valore MyServiceImpl (vedi Esempio 4.30).

4.3.5

ClassLoading

Una delle principali novit`a di MuSkel2 `e la possibilit`a di eseguire un pro- gramma presente sul client senza che la sua classe sia disponibile in fase di startup del nodo server.

Normalmente, se si tentasse di eseguire l’Esempio 4.31 senza che il server sia in possesso della classi custom utilizzate, si incapperebbe in un errore di ClassNotFoundException durante la fase di deserializzazione delle funzioni referenziate nei metodi map, filter e subscribe.

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

2 .executeOn(remote())

3 .map(x -> j(x))

4 .filter(x -> x>3)

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

Esempio 4.31: Esempio di programma MuSkel2 e serializzazione delle

funzioni

La stessa cosa accadrebbe se lo stream di dati in ingresso non fossero interi (quindi classi presenti anche sul server) ma oggetti appartenenti a classi definite solo lato client (vedi Esempio 4.32).

1 MuskelProcessor.from(new MyCustomObj(1), new MyCustomObj(2)...)

2 .executeOn(remote())

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

Esempio 4.32: Esempio di programma MuSkel2 e serializzazione dei dati in ingresso

Questo succede perch´e chi ha il compito di caricare le classi in Java `e un oggetto, chiamato ClassLoader, che si occupa di importare i dati binari che definiscono le classi ed interfacce di un programma in esecuzione. Di solito tali dati binari si trovano in uno dei file jar (Java Archive) caricati in fase di startup del programma. Se il ClassLoader non riesce a trovare la classe lancia l’eccezione ClassNotFoundException.

Figura 4.16: Diagramma Sequenza Componenti Classloading

Un altro problema legato al classLoading `e quello derivante la presenza di pi`u versioni della stessa classe. Il ClassLoader non ammette tale eventualit`a, quindi se due programmi referenziano due versioni della stessa classe, una dei due potrebbe generare un errore.

Per ovviare a questo problema `e stato realizzato un sistema che permette, lato server, la creazione di un ClassLoader separato per ogni MuskelContext (quindi client) attivo; una sorta di ambiente protetto dove ogni client ha le proprie versioni di classi.

Inoltre, grazie ad un’implementazione custom della classe ClassLoader, le classi non presenti sul server vengono automaticamente richieste al client evitando, in questo modo, il presentarsi di errori come ClassNotFoundExcep- tion.

Per ottimizzare le prestazioni tale struttura dati viene mantenuta sul cluster fino a quando il client `e connesso ai nodi server.

Ad alto livello il processo di classloading funziona in questo modo (Figura 4.16):

client. Per ogni classe non trovata il ClassLoader invia una notifica al client in modo che restituisca il binario della classe richiesta;

– (Client) riceve il messaggio di notifica, recupera la classe dal pro- prio ClassLoader ed inserisce la rappresentazione binaria della classe all’interno di una struttura dati condivisa sul cluster;

– (Server) riceve la notifica di inserimento della classe, recupera la rap- presentazione binaria e la utilizza all’interno del proprio ClassLoader.

dati scelti ad hoc, che permette di individuarne l’andamento generale. In questo capitolo viene presentato un programma scelto come benchmark sintetico e realizzato in due varianti, confrontandone i risultati ottenuti in termini di tempo di completamento, scalabilit`a ed efficienza.

5.1

Ambiente di Lavoro

Tutti i test sono stati eseguiti su Titanic, Pianosa e Pianosau un ambiente di macchine multi-core interconnesse in rete del laboratorio di Architetture Pa- rallele del Dipartimento di Informatica dell’Universit`a di Pisa. In particolare, le caratteristiche di queste macchine sono:

Titanic:

– Cpu AMD Opteron 6176 con frequenza a 2300 MHz con 24 core fisici. – Ram 32 GB

Pianosa e Pianosau:

– Cpu Intel Xeon E5-2650 con frequenza a 2000 MH con 8 core fisici (16 contesti Hyper-Threading).

– Ram 32 GB

I software installato sulla macchina `e:

– JDK 1.8.45 Java Virtual Machine versione 8

– Jmeter 2.13 un framework per la definizione e l’esecuzione dei test

Documenti correlati