SlideShare a Scribd company logo
1 of 110
Download to read offline
Studio di una
Architettura per un Sistema Distributivo
ad Alta Affidabilit`a
Autore: Dott. Roberto Peruzzo
Tutor: Prof. Alessandro Roncato
Mestre, 24 ottobre 2012
ii
Indice
1 Introduzione 3
2 Rendere affidabile un sistema 7
2.1 Reliable Server Pooling . . . . . . . . . . . . . . . . . . . . 8
2.2 Cloud Computing . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.1 Google AppEngine . . . . . . . . . . . . . . . . . . 9
2.2.2 Amazon AWS . . . . . . . . . . . . . . . . . . . . . 11
2.3 Scelta dell’infrastruttura . . . . . . . . . . . . . . . . . . . 11
3 Architettura proposta 15
3.1 Utilizzo di Java NIO . . . . . . . . . . . . . . . . . . . . . 17
3.2 Limiti nel prototipo di Pavan . . . . . . . . . . . . . . . . 21
3.3 Thread in un mondo Multiplexed . . . . . . . . . . . . . . 23
4 Implementazione 25
4.1 Come attivare i servizi Amazon AWS . . . . . . . . . . . . 25
4.2 Amazon SimpleDB . . . . . . . . . . . . . . . . . . . . . . 27
4.3 Amazon DynamoDB . . . . . . . . . . . . . . . . . . . . . 32
4.4 Il server . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
5 Prove Sperimentali 41
5.1 Preparazione dei test . . . . . . . . . . . . . . . . . . . . . 41
5.1.1 Hardware utilizzato . . . . . . . . . . . . . . . . . . 43
5.1.2 Modalit`a di esecuzione dei test . . . . . . . . . . . 43
5.2 Risultati test senza persistenza . . . . . . . . . . . . . . . 45
5.3 Risultati test con Amazon SimpleDB . . . . . . . . . . . . 47
iii
iv INDICE
5.4 Risultati test con Amazon DynamoDB . . . . . . . . . . . 49
6 Conclusioni 55
A Codice sorgente 59
A.1 Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
A.1.1 Server.java . . . . . . . . . . . . . . . . . . . . . . . 59
A.1.2 AsdaaServer.java . . . . . . . . . . . . . . . . . . . 61
A.1.3 SelectorThread.java . . . . . . . . . . . . . . . . . . 63
A.1.4 DynamoDBManager.java . . . . . . . . . . . . . . . 73
A.2 Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
A.2.1 MultiplexingClient.java . . . . . . . . . . . . . . . . 77
A.2.2 MultithreadClient.java . . . . . . . . . . . . . . . . 86
B Glossario 95
Elenco delle figure
1.1 Diagramma dell’attuale architettura da ottimizzare. . . . . 4
2.1 Google AppEngine vs. Amazon AWS. . . . . . . . . . . . . 12
2.2 Architettura con SimpleDB. . . . . . . . . . . . . . . . . . 13
2.3 Architettura con DynamoDB. . . . . . . . . . . . . . . . . 14
3.1 all’aumentare dei thread creati aumenta il tempo di crea-
zione degli stessi. . . . . . . . . . . . . . . . . . . . . . . . 16
3.2 un Thread utilizza l’API Selector per gestire 3 canali di
comunicazione. . . . . . . . . . . . . . . . . . . . . . . . . 18
3.3 diagramma degli stati del server implementato. . . . . . . 19
3.4 diagramma degli stati nella soluzione proposta da Pavan. . 22
4.1 Amazon AWS console. . . . . . . . . . . . . . . . . . . . . 26
4.2 Amazon EC2 console. . . . . . . . . . . . . . . . . . . . . . 28
4.3 elenco delle Amazon Machine Image da scegliere. . . . . . 29
4.4 finsetra per creare una nuova tabella. . . . . . . . . . . . . 30
4.5 finsetra per creare una nuova tabella. . . . . . . . . . . . . 31
4.6 diagramma della classe SimpleDBManager.java. . . . . . . 32
4.7 diagramma della classe DynamoDBManager.java. . . . . . 33
4.8 diagramma della classe SelectorThread.java. . . . . . . . . 35
4.9 diagramma della classe Server.java. . . . . . . . . . . . . . 37
5.1 diagramma della classe MultithreadClient.java. . . . . . . . 42
5.2 diagramma della classe MultiplexingClient.java. . . . . . . 44
v
vi ELENCO DELLE FIGURE
5.3 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Totale messaggi inviati 200, Tempo di risposta
medio 108,71 ms, minimo 38 ms, massimo 282 ms. . . . . . 45
5.4 rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Totale messaggi inviati 10481, Tempo di risposta
medio 261,51 ms, minimo 36 ms, massimo 57300 ms. . . . 46
5.5 rapporto tra quantit`a di dati inviati e tempi di risposta del
server. SimpleDB come persistenza dati. Totale messaggi
inviati 200, Tempo di risposta medio 3599,295 ms, minimo
203 ms, massimo 22638 ms. . . . . . . . . . . . . . . . . . 47
5.6 rapporto tra quantit`a di dati inviati e tempi di risposta del
server. SimpleDB come persistenza dati. Totale messaggi
inviati 200, Tempo di risposta medio 1494,07 ms, minimo
203 ms, massimo 4087 ms. . . . . . . . . . . . . . . . . . . 48
5.7 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 5
writes/s. Totale messaggi inviati 200, Tempo di risposta
medio 1701,3 ms, minimo 193 ms, massimo 4426 ms. . . . 50
5.8 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 320
writes/s. Totale messaggi inviati 200, Tempo di risposta
medio 904,06 ms, minimo 151 ms, massimo 2650 ms. . . . 51
5.9 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 320
writes/s. Totale messaggi inviati 800, Tempo di risposta
medio 223,38 ms, minimo 137 ms, massimo 746 ms. . . . . 52
5.10 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 320
writes/s, 10 Thread come client. Totale messaggi inviati
658, Tempo di risposta medio 315,77 ms, minimo 144 ms,
massimo 1529 ms. . . . . . . . . . . . . . . . . . . . . . . . 53
ELENCO DELLE FIGURE vii
5.11 rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 320
writes/s, 100 thread come client. Totale messaggi inviati
1707, Tempo di risposta medio 8191,05 ms, minimo 280
ms, massimo 177857 ms. . . . . . . . . . . . . . . . . . . . 54
viii ELENCO DELLE FIGURE
Elenco delle tabelle
ix
x ELENCO DELLE TABELLE
Sommario
1
2 ELENCO DELLE TABELLE
Capitolo 1
Introduzione
Questo documento rappresenta il risultato di uno studio di algoritmi
e architetture utili per implementare un sistema ad alta affidabilit`a e
alte prestazioni. Non si tratta semplicemente di un sistema capace di
garantire un’elevata continuit`a del servizio ()uptime guarantee), ma deve
essere in grado anche di non perdere informazioni in caso di guasti (fault
tollerance) e di soddisfare un numero crescente di richieste (scalable). Un
sistema per la gestione delle comunicazioni tra le centrali di smistamento
SMS e applicazioni esterne (per avere news sul meteo o sulla viabilit`a)
pu`o esserne un esempio. Per meglio comprendere la problematica da
risolvere partiamo con l’analizzare l’architettura descritta nella Figura
1.1. Un insieme di client, il cui numero `e`e destinato ad aumentare nel
tempo, comunicato con un server di controllo con il compito di effettuare
alcuni calcoli sui dati ricevuti e restituire un messaggio “ok ricevuto” al
client. Nel caso il server non sia raggiungibile a causa di un guasto alla
rete, i client invieranno i propri messaggi ad un indirizzo IP alternativo
dove un dispositivo inoltrer`a questi messaggi al server di controllo che
eseguir`a le stesse operazioni sopra descritte. La quantit`a medi di dati
che ogni singolo client invia pu`o essere stimata in 100B/h. Nel caso il
server principale, a causa di un guasto, interrompe la propria esecuzione,
tutti i client rinviano dopo cinque minuti i messaggi che non hanno avuto
risposta sperando che il guasto si risolva nel pi`u breve tempo possibile.
Il canale tra client e server utilizza una comunicazione GPRS a pa-
3
4 CAPITOLO 1. INTRODUZIONE
Un elevato numero di client (centraline)
inviano moltissimi messaggi di pochi dati
al server per essere elaborati.
Nel caso l'IP PRINCIPALE
non sia raggiungibile,
i clients inviano i loro
messaggi ad un
IP SECONDARIO.
Database
IP SECONDARIO
IP PRINCIPALE
Server che elabora
le richieste dei client
Dispositivo che esegue un
semplice forward dei messaggi
al server.
Figura 1.1: Diagramma dell’attuale architettura da ottimizzare.
5
gamento; il costo viene calcolato in base al numero di Byte inviate e
ricevuti, quindi varia in modo proporzionale al numero di messaggi che
attraversano il canale: pi`u messaggi sono spediti, pi`u alti saranno i costi.
Quindi in caso di guasti del sistema, ogni rinvio di un messaggio da parte
dei client comporta un aumento del traffico nel canale e di conseguenza
una lievitazione dei costi proporzionalmente al tempo di “blocco”.
L’obbiettivo che si vuole raggiungere `e di progettare una nuova ar-
chitettura che sia in grado di garantire un’alta affidabilit`a in modo da
minimizzare il pi`u possibile situazioni di guasto.
6 CAPITOLO 1. INTRODUZIONE
Capitolo 2
Rendere affidabile un sistema
Rendere un sistema affidabile significa garantire l’operabilit`a (uptime
guarantee) del sistema anche in presenza di guasti (fault tolerant). La
maggior parte delle tecniche di tolleranza ai guasti fa uso di tecniche
di ridondanza per replicare le sorgenti che contengono le informazioni
(Server Pooling). Esistono due tipi di ridondanza:
• spaziale, si utilizzano componenti hardware o software replicati in
grado di sostituire l’elemento guasto;
• temporale, basata sulla ripetizione di operazioni o sequenze di ope-
razioni.
Un sistema in cui le informazioni vengono replicate non offre solo una
maggiore affidabilit`a, ma `e in grado di fornire:
• migliori prestazioni, grazie ad una distribuzione del carico tra le
varie repliche; questo permette inoltre di diminuire la latenza della
comunicazione permettendo ai client di contattare “la replica” pi`u
vicina;
• una migliore disponibilit`a del servizio, se ogni replica `e isolata fi-
sicamente ed elettricamente dalle altre, il guasto di una unit`a non
influenza le altre;
7
8 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA
Il rovescio della medaglia `e che la ridondanza aumenta la complessit`a
di tutto il sistema poich´e bisogna gestire lo smistamento dei messaggi ver-
so le varie repliche, mantenere una consistenza dei dati replicati ed infine
mantenere il determinismo dello stato delle repliche, in pratica ricevere
le stesse richieste da parte dei un client nello stesso ordine temporale.
2.1 Reliable Server Pooling
Vista la complessit`a nel gestire un insieme di server replicati, nel 5 dicem-
bre 2000 prese vita un gruppo di lavoro IETF RSerPool [1] con l’obbiet-
tivo di sviluppare l’architettura e i protocolli necessari per la gestione e il
funzionamento di gruppi di server che supportano applicazioni altamente
affidabili [15].
Il framework realizzato permette ad un insieme di server con informa-
zioni replicate di essere visti come un singolo punto di accesso ai dati; in
caso di fallimenti le applicazioni che lo utilizzano possono essere deviate
verso un altro server in modo del tutto trasparente e senza il riavvio della
macchina.
Nonostante il framework aiuti gli sviluppatori ad implementare appli-
cazioni con un alto grado di affidabilit`a, presenta alcune problematiche
da risolvere:
• dal punto di vista infrastrutturale, l’acquisto e la gestione di tutta
la parte hardware e sistemistica ha un costo molto alto;
• il gruppo di lavoro `e stato chiuso il 27 aprile 2009, quindi non c’`e
la possibilit`a di avere un supporto tecnico in caso di problemi.
La soluzione a questi problemi sta nel Cloud Computing descritto nel
prossimo paragrafo.
2.2 Cloud Computing
Con la frase cloud computing si identifica quel servizio che fornisce ca-
pacit`a di calcolo e di archiviazione ad una comunit`a eterogenea di utiliz-
zatori finali in modo tale che quest’ultimi non si debbano minimamente
2.2. CLOUD COMPUTING 9
preoccupare di come gestire e garantire l’operabilit`a del sistema anche in
caso di guasti, poich´e tali sistemi utilizzano una ridondanza spaziale in
modo del tutto trasparente all’utilizzatore finale. Esistono tre tipologie
di cloud computing:
• Infrastructure as a Service (IaaS),
• Platform as a Service (PaaS),
• Software as a Service (SaaS).
Nel caso di studio sappiamo che il numero dei client `e alto e si presume
sia destinato ad aumentare con il passare del tempo, inoltre le comuni-
cazioni tra client e server devono utilizzare il minor numero di messaggi
possibili anche in caso di guasti in modo da consumare meno banda possi-
bile. In altre parole il sistema deve essere scalabile e affidabile. I servizi di
cloud computing hanno entrambi queste caratteristiche: in base al carico
di lavoro e alla quantit`a di dati da archiviare `e possibile adattare (aumen-
tando o diminuendo) la capacit`a di calcolo e di archiviazione in tempo
reale; in caso di guasti hardware i dati sono replicati in modo trasparen-
te e la capacit`a di calcolo `e garantita da un sistema di virtualizzazione.
Tutto questo viene fornito a basso costo poich´e, come l’energia elettrica,
si pagano solo le risorse consumate (modello pay-as-you-go) senza doversi
preoccupare dell’amministrazione di grossi datacenter.
Di seguito si analizzeranno alcuni tra i principali fornitori di servizi
cloud nel mercato per identificarne il pi`u adatto al caso, tenendo presente
di questi due vincoli:
1. la nostra applicazione sar`a scritta in Java visto che gran parte delle
funzionalit`a sono gi`a state scritte in questo linguaggio;
2. la nostra applicazione dovr`a implementare una comunicazione clien-
t/server tramite un socket TPC.
2.2.1 Google AppEngine
Google AppEngine [10] offre un sistema per poter sviluppare applicazioni
web a traffico elevato senza dover preoccuparsi di gestire l’infrastruttura.
10 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA
Grazie a tale strumento `e possibile realizzare applicazioni web utilizzando
tecnologie Java Standard (Java 6 JVM) e farle girare sull’infrastruttura
scalabile di Google. Questo tipo di servizio rientra nella tipologia PaaS.
Le istanze di calcolo, chiamate App Engine Backends, eseguono l’ap-
plicazione con una capacit`a di memoria che va da 128MB fino a 1GB e
una CPU con una frequenza configurabile da 600MHz a 4,8GHz. Altra
caratteristica interessante `e la possibilit`a di definire il numero massimo
di istanze attive; in base al volume di dati elaborato il sistema attiva
nuove istanze automaticamente in modo da gestire l’aumento del carico
di lavoro senza degradare le prestazioni dell’applicazione. Google Ap-
pEngine permette inoltre di servire pi`u richieste in parallelo garantendo
una corretta sincronizzazione tra i thread (thread safe).
Google AppEngine `e dotato inoltre di un servizio per la persistenza dei
dati (App Engine datastore [9]) con due diverse tipologie di data storage
in base alla disponibilit`a e alla consistenza che si vuole garantire:
• il Master/Slave datastore utilizza un sistema di replicazione master-
slave che replica in modo asincrono il dato quando viene scritto
fisicamente nel data center. Questa soluzione offre un’elevata con-
sistenza in lettura e nelle interrogazioni, a fronte di temporanei
periodi di indisponibilit`a del servizio in caso di problemi del data
center o downtime pianificati;
• nel High Replication datastore, i dati sono replicati (in modo sin-
crono) in pi`u data center tramite un meccanismo basato sul Paxos
algorithm (7). Questo sistema offre un alto grado di disponibilit`a,
ma il prezzo da pagare `e un elevata latenza nella scrittura dei dati
dovuta proprio alla replicazione distribuita.
Ogni applicazione viene eseguita in un ambiente sandboxed sicuro ed
isolato in modo da prevenire che un’applicazione interferisca con le altre.
In questo ambiente le applicazioni non possono aprire socket TCP o ac-
cedere ad un altro host direttamente, ma fare solamente richieste HTTP
o HTTPS. Questa caratteristica rappresenta una restrizione critica per
lo sviluppo di questo progetto, poich´e le classi gi`a sviluppate per la co-
municazione client/server prevedono l’utilizzo di un canale TCP ed un
2.3. SCELTA DELL’INFRASTRUTTURA 11
protocollo ad-hoc. Utilizzare una comunicazione HTTP o HTTPS il vo-
lume di ogni singolo pacchetto aumenterebbe a causa degli http headers
da aggiungere al messaggio con l’inevitabile conseguenza di un aumento
della quantit`a di dati scambiati che si traduce con un aumento dei costi
del canale. Alla luce di questa limitazione si `e deciso di analizzare le ca-
ratteristiche tecniche di un altro tra i principali fornitori di servizi cloud,
descritto nel paragrafo seguente.
2.2.2 Amazon AWS
Amazon AWS [4], a differenza di Google, offre un’infrastruttura modu-
lare che permette di scegliere i componenti che pi`u si adattano al tuo
problema creando un’architettura ad-hoc. In questo caso infatti si parla
di un’infrastruttura come servizio IaaS. Oltre a servizi per il em data-
storage (es. Amazon SimpleDB [3], Amazon DynamoDB [2]) mette a
disposizione ambienti di calcolo virtuali personalizzabili dove `e possibile
caricare differenti sistemi operativi, gestire i permessi di accesso alla re-
te e personalizzare il proprio ambiente applicativo. Altra caratteristica
importante `e che il servizio offerto `e “elastico” nel senso che `e in gra-
do di scalare automaticamente in base al carico di lavoro, configurando
opportunamente servizi di bilanciamento del carico e monitoraggio delle
risorse garantendo un uptime di 99,95% durante un’intero anno. Grazie a
queste caratteristiche `e possibile costruire un’architettura personalizzata
combinando i vari servizi offerti secondo le proprie esigenze, senza pre-
occuparsi della manutenzione dell’infrastruttura (es. upgrade, software
patch, ecc.) con un notevole risparmio monetario.
2.3 Scelta dell’infrastruttura
Dopo aver analizzato le caratteristiche di questi due sistemi (vedi Figura
2.1) la scelta `e ricaduta su Amazon.
Per il salvataggio dei dati abbiamo scelto inizialmente per Amazon
SimpleDB poich´e offriva sia un alta disponibilit`a del servizio che un’e-
levata propensione alla scalabilit`a trattandosi di una base di dati non
relazionale (NoSQL). Un vincolo imposto nel nostro progetto `e quello di
12 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA
Amazon AWS
Google
AppEngine
Java Yes Yes
Socket TCP Yes No
Latency in
data persistence
Good Bad
System Customization Yes No
martedì 23 ottobre 2012
Figura 2.1: Google AppEngine vs. Amazon AWS.
2.3. SCELTA DELL’INFRASTRUTTURA 13
Client Units
Server
(using Java NIO)
Amazon
EC2
Amazon
SimpleDB
Figura 2.2: Architettura con SimpleDB.
avere un’alta consistenza in modo da leggere sempre l’ultimo dato scritto;
SimpleDB `e in grado di garantire questo ma a discapito della latenza.
In Figura 2.2 `e descritta l’architettura di partenza da cui siamo par-
titi per implementare il server Java NIO [12] e l’interfacciamento con
SimpleDB. Dai test fatti (vedi paragrafi successivi) le performace non si
sono dimostrate soddisfacenti, quindi bisognava sostituire SimpleDB con
un servizio che permettesse di diminuire il tempo di latenza dell’intero
sistema.
Amazon AWS ha lanciato il 19 gennaio 2012 un nuovo servizio per
la persistenza dei dati chiamato Amazon DynamoDB. Questo nuovo ser-
vizio oltre ai vantaggi gi`a visti con SimpleDB, ci consente di avere una
forte consistenza nei dati che garantisce la lettura dell’ultimo dato scritto,
mantenendo una bassa latenza a qualsiasi livello di scalabilit`a. L’inter-
vento quindi `e stato indolore poich´e abbiamo introdotto una nuova classe
java in grado di gestire la comunicazione con il nuovo database (Figura
2.3).
14 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA
Client Units
Server
(using Java NIO)
Amazon
EC2
Amazon
DynamoDB
Figura 2.3: Architettura con DynamoDB.
Capitolo 3
Architettura proposta
Una delle caratteristiche fondamentali dell’architettura che sar`a svilup-
pata `e la scalabilit`a ovvero, la capacit`a del server di gestire una quantit`a
crescente di connessioni simultanee.
Un’approccio tradizionale, che si basa sull’utilizzo del thread-pooling
per gestire pi`u connessioni, trova il suo principale limite nel costante au-
mento del tempo dedicato al context switching tra i thread proporzionale
all’aumento del numero di thread stessi. Inoltre, nel caso specifico di que-
sta ricerca, ogni thread esegue operazioni I/O in modo concorrente che
comporta un’ulteriore complessit`a al sistema peggiorandone le prestazio-
ni. Il problema che nasce da un accesso concorrente sta nel fatto che la
Java VM non sa quale thread `e pronto ad eseguire un’operazione I/O,
pertanto potrebbe mettere in esecuzione un thread non ancora pronto per
un operazione I/O, mentre lasciare in attesa un thread gi`a pronto. Nel
peggior dei casi potrebbe accadere che un thread rimanga in attesa cos`ı
a lungo da andare in timeout. Inoltre all’aumentare del numero di thread
creati aumenta il carico per la CPU e la quantit`a di memoria che verr`a
riservata allo stack di ogni thread degradando le prestazioni dell’intero
sistema. Quest’ultima affermazione trova fondamento nei test fatti da
Lawery [11] in cui la creazione di nuovi thread diventa sempre pi`u lenta
all’aumentare dei thread creati (vedi Figura 3.1).
In questi test i thread creati non eseguono alcuna operazione di cal-
colo; in un caso reale dove ogni thread esegue operazioni di calcolo o di
15
16 CAPITOLO 3. ARCHITETTURA PROPOSTA
Figura 3.1: all’aumentare dei thread creati aumenta il tempo di creazione
degli stessi.
3.1. UTILIZZO DI JAVA NIO 17
I/O il degrado delle prestazioni sar`a di sicuro maggiore; questo si traduce
in un aumento del tempo di esecuzione e anche un aumento dei tempi di
risposta ai client.
Per evitare che un canale di comunicazione quindi rimanga in attesa
cos`ı a lungo da rischiare il timeout e quindi che il client non riceva la
risposta attesa, bisognerebbe procedere con l’invio del messaggio non
appena il canale `e pronto a scrivere e allo stesso tempo fare in modo che
un singolo thread sia in grado di gestire pi`u connessioni simultaneamente
(multiplexing). Questo tipo di operazioni possono essere gestita grazie
alla programmazione ad eventi offerta dalla libreira Java NIO che verr`a
descritta nel prossimo paragrafo.
3.1 Utilizzo di Java NIO
A partire dalla versione J2SE 1.4 Java introduce il concetto di socket non-
bloccanti grazie alla libreria NIO (New I/O) che permette di gestire pi`u
connessioni TCP con un unico thread (vedi Figura 3.2) senza bloccarne
l’esecuzione.
Ogni canale deve gestire i seguenti quattro eventi: connect, accept,
read e write. Quando un client invia un messaggio al server quest’ultimo
attiva un canale che solleva l’evento accept; non appena il canale accetta
la connessione viene sollevato l’evento connect. A questo punto il canale `e
pronto per poter leggere i dati inviati dal client, quindi l’evento sollevato
`e quello read; una volta letti tutti i dati ed elaborato la risposta il canale
`e pronto per inviare la risposta quindi viene sollevato l’evento write.
Questi eventi vengono gestiti dalla classe SelectorThread.java la
quale si occupa di monitorare il Selector restando in attesa di eventi I/O
e redistribuirli al gestore (handler) appropriato in base alla tipologia di
evento generato. Nella nostra architettura avremo bisogno di un gestore
per aprire connessioni in uscita, uno per accettare le connessioni in en-
trata e uno per leggere e scrivere nel canale; questi gestori dovranno
implementare rispettivamente le interfacce ConnectSelectorHandler,
AcceptSelectorHandler e ReadWriteSelectorHandler.
18 CAPITOLO 3. ARCHITETTURA PROPOSTA
Thread
Selector
Channel Channel Channel
Figura 3.2: un Thread utilizza l’API Selector per gestire 3 canali di
comunicazione.
3.1. UTILIZZO DI JAVA NIO 19
Waiting for Data
(read interest)
Reading
(no interest)
Processing
Request
(no interest)
Writing
(no interest)
Waiting to write
(write interest)
Request partially read
(enable read interest)
Ready to read
Request
read
Send reply
(enable write interest)
Ready to write
Replay partially sent
(enable write interest)
Replay sent
(enable read interest)
Start
Figura 3.3: diagramma degli stati del server implementato.
20 CAPITOLO 3. ARCHITETTURA PROPOSTA
Aspetto di fondamentale importanza in una architettura dove le ope-
razioni di scrittura e lettura non sono bloccanti `e la gestione dei dati
parzialmente letti o scritti. Quando viene generato un evento di lettura
significa che nel read-buffer del socket sono disponibili alcuni byte, ma
non `e precisato se questi byte sono una parte di un pacchetto, un pac-
chetto intero o pi`u pacchetti. Lo stesso vale in fase di scrittura, i byte
vengono scritti nel write-buffer del socket in base allo spazio disponibile
nel buffer senza preoccuparsi che i dati del pacchetto siano stati scritti
completamente.
Nello schema proposto da Nuno Santos nel suo articolo [14] sono evi-
denziati cinque stati per descrivere il ciclo di vita di un handler (vedi
Figura 3.3):
1. Waiting for data: l’handler `e in attesa di ricevere i dati provenienti
dal canale di comunicazione, quindi per prima cosa attiva l’interes-
se nel ricevere un evento di tipo lettura e attende che esso venga
generato;
2. Reading: dopo aver ricevuto un evento di lettura, l’handler riceve
i dati provenienti dal socket e inizzia a riassemblare il pacchetto.
Durante questo stato non c’`e l’interesse a ricevere nessun altro tipo
di evento. Se il pacchetto viene riassemblato completamente si
passa allo stato successivo (stato 3), altrimenti vengono salvati i
dati parziali e viene riattivato l’interesse in lettura (stato 1).
3. Processing request: non appena il pacchetto viene completamente
riassemblato, l’handler inizia a processarlo. Nel nostro caso prov-
veder`a a controllare la validit`a del CRC e poi proceder`a a salvare
il messaggio nel database. Finch´e l’handler rimane in questo stato,
tutti gli interessi in eventi di tipo I/O sono disabilitati.
4. Writing: finito di processare il pacchetto e non appena la risposta `e
pronta l’handler la spedisce immediatamente utilizzando una scrit-
tura non-bloccante. Se non c’`e spazio sufficiente nel write-buffer
del socket per contenere tutto il pacchetto, sar`a necessario spedire
il resto dei dati successivamente passando allo stato 5, altrimenti
3.2. LIMITI NEL PROTOTIPO DI PAVAN 21
l’handler si preparer`a a ricevere il pacchetto successivo tornando
allo stato 1.
5. Waiting to write: se una scrittura non-bloccante ritorna senza aver
scritto tutti i dati che compongono il pacchetto di risposta, l’hand-
ler attiva l’interesse nel ricevere un evento di scrittura. Quando il
buffer di scrittura avr`a nuovamente dello spazio disponibile, verr`a
sollevato l’evento per scrivere il resto del pacchetto.
Il ciclo di vita dell’handler permette di comprendere cosa accade quan-
do riceve o spedisce dati attraverso il canale di comunicazione. Molto
interessante risulta la gestione dell’interesse in lettura/scrittura descritta
nel diagramma di stato in Figura 3.3. Grazie a questo meccanismo `e
possibile capire:
• in fase di lettura quando un’intero pacchetto di dati `e stato ricevuto
e quindi procedere con l’elaborazione dei dati;
• in fase di scrittura quando il pacchetto di risposta `e stato inviato
completamente e quindi verificare se ci sono altri dati da leggere o
se chiudere la comunicazione.
Questo rende l’architettura riutilizzabile per risolvere svariate proble-
matiche con protocolli di comunicazione differenti tra loro implementando
l’interfaccia ProtocolDecoder.java a seconda dei casi.
3.2 Limiti nel prototipo di Pavan
Il prototipo sviluppato da Pavan e descritto nella sua tesi [13] presen-
ta l’implemetazione di un server ECHO dove ogni pacchetto inviato al
server viene immediatamente restituito per intero al client senza alcuna
elaborazione. In questo prototipo non esistono dei gestori (handlers) che
vengono invocati a seconda della tipologia di evento sollevato; il server
ServerSelect.java continua ad iterare l’insieme dei canali attivi e appe-
na ne trova uno pronto ad effettuare un’operazione di lettura, cattura
tutti i dati presenti nel buffer per poi immediatamente inviarli indietro
22 CAPITOLO 3. ARCHITETTURA PROPOSTA
Waiting to Data
Reading
Writing
Replay sent
Start
Send replay
Ready to read
Figura 3.4: diagramma degli stati nella soluzione proposta da Pavan.
al client cos`ı come sono arrivati. Il buffer del canale ha una dimensione
di 16KB ed i pacchetti possono avere una dimensione massima di 1200
Byte, quindi il buffer ha una dimensione tale per contenere l’intero mes-
saggio. Il primo limite di questa implementazione quindi `e la dimensione
dei pacchetti che possono essere scambiati tra client e server.
Nello scenario presentato nell’introduzione (capitolo 1) dove ogni mes-
saggio `e formato da pochi Byte il problema non si pone, ma con una vi-
sione aperta al futuro e alla possibilit`a di applicare una tale architettura
ad altri servizi (es. trasferimento di video da client a server o viceversa)
la soluzione di Pavan `e piuttosto limitativa. Inoltre impostare un buffer
di 16KB pu`o raggiungere facilmente run out of memory all’aumentare del
numero di connessioni e quindi non essere scalabile. Altro limite nell’im-
plementazione di Pavan deriva dalla mancanza di uno stato che disabiliti
momentaneamente la possibilit`a di sollevare altri eventi I/O durante l’e-
laborazione dei dati contenuti nel pacchetto (nel nostro caso controllo del
3.3. THREAD IN UN MONDO MULTIPLEXED 23
CRC e salvataggio dei dati). Operazioni come il salvataggio dei dati in
un database sono di tipo bloccante ed hanno un tempo di latenza pi`u o
meno lungo in base alla tipologia di tecnologia utilizzata; in questa fase
il server deve mettere in attesa il canale finch´e la risposta da inviare al
client non sar`a pronta. Il non rispetto di una tale attesa potrebbe de-
terminare situazione di perdita di dati dovuta all’interruzione prematura
delle operazioni di salvataggio.
In base a queste considerazioni il prototipo di Pavan, presentato
nel diagramma di stato illustrato nella Figura 3.4, dev’essere rivisto e
reimplementato sequendo quanto descritto nel Paragrafo 3.1.
3.3 Thread in un mondo Multiplexed
Utilizzare un singolo thread che svolga tutto il lavoro (dispatch and pro-
cess request) ha il grosso limite di non poter “nascondere” la latenza
necessaria per operazioni I/O su disco; di conseguenza lettura e scrittu-
ra su database influenzano negativamente le prestazioni di calcolo (Java
NIO non supporta operazioni non-blocking su filesystem).
Per prima cosa vediamo in modo generico quali sono le tipologie di
architettura che `e possibile realizzare per risolvere il problema:
• M dispatchers/no workers: parecchi thread di event-dispatcher che
eseguono tutto il lavoro;
• 1 dispatcher/N workers: un singolo event-dispatcher thread e unit`a
elaborative multiple;
• M dispatchers/N workers: thread multipli per lo smistamento degli
eventi e unit`a elaborative multiple;
Solitamente per risolvere un problema complesso si cerca di suddivi-
derlo in due o pi`u sotto problemi di facile risoluzione, ossia divide et im-
pera. L’idea iniziale quindi era quella di suddividere gli event-dispatcher
e i worker (unit`a elaborative) in pi`u thread; in questo modo il dispatcher
delega ai worker la parte di elaborazione e salvataggio sul database e si
24 CAPITOLO 3. ARCHITETTURA PROPOSTA
occupa solamente di gestire le connessioni con i client. Basandosi sull’e-
sperienza fatta da Santos questa non `e la soluzione migliore poich´e un’ar-
chitettura del genere presenta un grande numero di punti di interazione
tra dispatcher e worker rendendo altamente problematica la sincronizza-
zione nell’accedere al database, essendo una risorsa condivisa. Come `e
stato descritto all’inizio di questo Capitolo quando pi`u thread devono ac-
cedere in maniera concorrente una risorsa condivisa, sorge il problema di
implementare un codice thread-safe. Questo si scontra con la natura del
Selector e i suoi SelectionKey in quanto non sono “sicuri” per accessi
multi-thread. Se un SelectionKey viene modificato da un thread mentre
un altro thread sta chiamando il metodo select() dell’oggetto Selector
a cui appartiene lo stesso SelectionKey, il risultato `e che select() termi-
na brutalmente con un’eccezione. Questo solitamente accade quando un
worker chiude i canali di comunicazione e indirettamente cancella anche i
SelectionKey corrispondenti, quindi se select() viene chiamato, trover`a
il SelectionKey cancellato inaspettatamente. Santos dopo un tentativo
per rafforzare il sistema con regole pi`u severe nel controllo dei Selector, le
proprie SelectionKeys e i RegisteredChannels, si `e accorto che l’architet-
tura iniziava a diventare sempre pi`u complicata, inefficiente e propensa
agli errori.
Alla luce di queste considerazioni `e stato scelto di partire con l’archi-
tettura pi`u semplice (M dispatchers/no workers). Come abbiamo visto
all’inizio di questo capitolo il numero di thread va limitato altrimenti
le prestazioni rischiano di degenerare. Secondo Santos, l’ideale sarebbe
da mantenere costante ad un valore di 4 o 5 il rapporto tra numero di
dispatcher e CPU:
dispatchers
CPUs
= 5
Prima di inoltrarci in problematiche relative al numero di thread in
esecuzione ed il loro bilanciamento del carico, `e opportuno capire se il
codice proposto da Santos, opportunamente modificato, `e idoneo a questo
progetto. Fondamentale capire quindi se i tempi di risposta del server
sono soddisfacenti o meno.
Capitolo 4
Implementazione
In questo capitolo verr`a illustrato per prima cosa come configurare gli
ambienti di calcolo e di persistenza dei dati con Amazon AWS e il co-
dice Java utilizzato per interfacciarsi con tali servizi. Come ultima cosa
vedremo il funzionamento del Server e le modifiche apportate al codice
proposto da Santos.
4.1 Come attivare i servizi Amazon AWS
Per utilizzare l’infrastruttura offerta da Amazon bisogna creare un ac-
count utente registrandosi al sito http://aws.amazon.com. Amazon da
la possibilit`a di testare i propri servizi gratuitamente grazie al AWS Free
Usage Tier; l’infrastruttura offerta in questo caso ha alcune limitazioni
(vedi dettaglio [5]), che abbiamo dovuto superare durante i test in mo-
do da raggiungere buone performance. Come vedremo nel paragrafo dei
test, abbiamo dovuto superare questi limiti gratuiti.
Creato l’account sar`a possibile configurare i servizi di cui si ha biso-
gno grazie alla console (vedi Figura 4.1) che Amazon mette a disposizione
all’utente. Per questo progetto `e stata utilizzata un’istanza EC2 dove ab-
biamo installato il server. L’attivazione `e molto semplice, basta cliccare
sul link “EC2 Virtual Services in the Cloud” per aprire la console di ge-
stione delle istanze virtuali di EC2 (vedi Figura 4.2), cliccare sul bottone
25
26 CAPITOLO 4. IMPLEMENTAZIONE
Figura 4.1: Amazon AWS console.
4.2. AMAZON SIMPLEDB 27
“Launch Instance e scegliere la macchina virtuale che si vuole utilizzare
(vedi Figura 4.3).
Per attivare il database si deve cliccare sul link “DynamoDB” presen-
te sempre nella console (Figura 4.1) alla sezione “Database”. Una volta
entrati nella console di DymanoDB si procede con la creazione delle ta-
belle (Figura 4.4) indicando il nome della tabella e il nome della chiave
primaria. Successivamente bisogna indicare la capacit`a di lettura e scrit-
tura1
con la quale si vuole lavorare; il valore di default per il servizio
gratuito `e di 10 unit`a/sec per la lettura e 5 unit`a/sec per la scrittura
(Figura 4.5).
4.2 Amazon SimpleDB
Amazon mette a disposizione una libreria Java [6] offrendo un insieme di
API in grado di interagire con l’infrastruttura dei servizi AWS. Per gli
sviluppatori che utilizzano Eclipse Java IDE, `e possibile installare il plug-
in open source AWS Toolkit for Eclipse [7], che permette di agevolarli in
fase di sviluppo, debug e deploy dell’applicazione. Nel caso specifico di
SimpleDB, Amazon fornisce una serie di API che permettono di creare
domini (domains, astrazione che pu`o essere paragonata alle tabelle per un
database relazionale) e elementi (items, astrazione che pu`o essere parago-
nata ai record di una tabella), interrogare i dati, aggiornarli e rimuoverli
dalla base di dati. Qui di seguito metto in evidenza i frammenti di codice
utilizzati nel nostro prototipo.
Per interagire con la base di dati viene utilizzata la classe AmazonSimpleDBClient
che mette a disposizione i metodi per accedere e manipolare i dati in essa
contenuti.
// Get PropertiesCredentials
PropertiesCredentials pCredentials =
1
una unit`a di scrittura permette di effettuare una scrittura al secondo per elementi
di dimensione non superiore a 1KB. Allo stesso modo una unit`a di lettura permette
di effettuare una lettura consistente al secondo di un elemento di dimensioni non
superiori a 1KB. Se l’elemento ha una dimensione superiore allora richieder`a una
maggior capacit`a in scrittura o lettura.
28 CAPITOLO 4. IMPLEMENTAZIONE
Figura 4.2: Amazon EC2 console.
4.2. AMAZON SIMPLEDB 29
Figura 4.3: elenco delle Amazon Machine Image da scegliere.
new PropertiesCredentials(ClassLoader.getSystemResourceAsStream(
'AwsCredentials.properties'));
// Activate the Amazon SimpleDB Client
this.sdb = new AmazonSimpleDBClient(new BasicAWSCredentials(
pCredentials.getAWSAccessKeyId(),
pCredentials.getAWSSecretKey()));
this.sdb.createDomain(new CreateDomainRequest(domain));
Ogni item (oggetto di tipo ReplaceableItem) contenuto nel dominio `e
rappresentato da un nome identificativo (rappresenta la primary key) e da
un numero arbitrario di attributi (oggetto di tipo ReplaceableAttribute)
a seconda delle nostre esigenze.
List<ReplaceableItem> data = new ArrayList<ReplaceableItem>();
data.add(new ReplaceableItem().withName(String.valueOf(recordId))
.withAttributes(new ReplaceableAttribute()
.withName(”clientId”).withValue(clientId),
new ReplaceableAttribute().withName(”body”)
.withValue(String.valueOf(byteRead)),
new ReplaceableAttribute().withName(”created”).withValue(
String.valueOf(System.currentTimeMillis()))
));
30 CAPITOLO 4. IMPLEMENTAZIONE
Figura 4.4: finsetra per creare una nuova tabella.
4.2. AMAZON SIMPLEDB 31
Figura 4.5: finsetra per creare una nuova tabella.
32 CAPITOLO 4. IMPLEMENTAZIONE
Figura 4.6: diagramma della classe SimpleDBManager.java.
4.3 Amazon DynamoDB
Con l’uscita di DymanoDB Amazon ha aggiornato la propria libreria Java
[8] inserendo le nuove funzionalit`a per interagire con il nuovo servizio.
La modalit`a di operare `e la stessa vista precedentemente con SimpleDB,
cambiano ovviamente i comandi utilizzati come `e possibile vedere qui di
seguito.
// Get PropertiesCredentials
InputStream credentialsAsStream = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(”AwsCredentials.properties”);
AWSCredentials credentials = new PropertiesCredentials(
credentialsAsStream);
// Activate the Amazon DynamoDB Client
this.dynamoDB = new AmazonDynamoDBClient(credentials);
// Create an item
Map<String, AttributeValue> item =
new HashMap<String, AttributeValue>();
item.put(”messageId”, new AttributeValue(messageId));
4.4. IL SERVER 33
Figura 4.7: diagramma della classe DynamoDBManager.java.
item.put(”clientId”, new AttributeValue(clientId));
item.put(”body”, new AttributeValue(body));
item.put(”created”, new AttributeValue().withN(
Long.toString(created)));
4.4 Il server
Il cuore del server `e rappresentato dal ciclo while, riportato qui di segui-
to, responsabile della gestione degli eventi che si trova all’interno della
classe io.SelectorThread descritta dall’illustrazione Figura 4.8. Questa
classe `e stata implementata ispirandosi al modello di gestione degli eventi
utilizzato dal toolkit SWING di Java: gli eventi generati dall’interfaccia
34 CAPITOLO 4. IMPLEMENTAZIONE
utente vengono salvati in una coda che viene controllata da un thread
(istanza della classe java.awt.EventQueue) che si occupa di instradare
gli eventi in entrata verso il listener interessato. La classe EventQueue
deve assicurare che tutte le operazioni eseguite dagli oggetti AWT siano
effettuate in un unico thread. Allo stesso modo la classe SelectorThread
ha incarico simile, in particolare:
• solo il thread creato dall’istanza di questa classe ha accesso al Se-
lector e a tutti i socket da esso gestiti. Se un thread esterno vuole
avere accesso agli oggetti gestiti da questo Selector, allora dovr`a
utilizzare i metodi invockeLater() o invokeAndWait().
• il thread non dev’essere utilizzato per eseguire lunghe operazioni.
Come gi`a detto nel Capitolo 3, questo tipo di architettura `e stata scel-
ta principalmente per mantenere basso il livello di complessit`a evitando
l’onere di mantenere la sicronizzazione tra i thread.
Vediamo in dettaglio ora il funzionamento del ciclo principale.
Per prima cosa vengono eseguiti eventuali task la cui esecuzione era
stata programmata da thread esterni (tramite le chiamate invockeLater()
o invokeAndWait()).
public void run() {
// Here's where everything happens. The select method will
// return when any operations registered above have occurred, the
// thread has been interrupted, etc.
while (true) {
// Execute all the pending tasks.
doInvocations();
...
...
...
}
}
A questo punto si controlla che non ci sia una richiesta di chiusura
del Selector, in tal caso vengono portate a termine tutte le attivit`a in
corso e poi il Selector viene chiuso. Se non ci sono richieste di chiusura,
si procede con il verificare se qualcuno `e pronto ad eseguire operazioni di
4.4. IL SERVER 35
Figura 4.8: diagramma della classe SelectorThread.java.
36 CAPITOLO 4. IMPLEMENTAZIONE
I/O; tramite la chiamata select() `e possibile capire quante operazioni
(identificate da una SelectedKey) tra i vari canali registrati sono pronte
per essere eseguite.
// Time to terminate?
if (closeRequested) {
return;
}
Iterando questo insieme di selection key le operazioni vengono distri-
buite al gestore appropriato, una volta che tutte le operazioni sono state
eseguite si ritorna all’inizio del loop principale e si ripete tutto il processo
di nuovo.
// Someone is ready for IO, get the ready keys
Iterator it = selector.selectedKeys().iterator();
// Walk through the collection of ready keys and dispatch
// any active event.
while (it.hasNext()) {
SelectionKey sk = (SelectionKey)it.next();
...
...
...
}
Prendiamo ora in analisi la classe AsdaaServer.java (vedi Figura
4.9)che avvia il server. Questa classe `e un’estensione di Server.java
implementata da Santos a cui abbiamo aggiunto alcune modifiche per
adattarla al nostro problema. Due sono le fasi principali quando viene
instanziato l’oggetto di tipo AsdaaServer:
1. aprire il socket e abilitare il Selector a ricevere connessioni (chia-
mata super(listenPort));
2. aprire la connessione con la base di dati (vedi il Paragrafo 4.3 per
come creare la connessione e salvare i dati).
Vediamo cosa accade all’interno della chiamata super(listenPort).
Per prima cosa si istanzia un oggetto di tipo SelectorThread e lo si abilita
ad accettare connessioni alla porta specificata dal parametro listenPort.
4.4. IL SERVER 37
Figura 4.9: diagramma della classe Server.java.
38 CAPITOLO 4. IMPLEMENTAZIONE
public Server(int listenPort) throws Exception {
st = new SelectorThread();
Acceptor acceptor = new Acceptor(listenPort, st, this);
acceptor.openServerSocket();
logger.info(”Listening on port: ” + listenPort);
System.out.println(”Listening on port: ” + listenPort);
}
Per abilitare il Selector viene chiamato il metodo openServerSocket()
che si preoccupa di creare un socket e di registrare il nuovo canale di co-
municazione con l’istruzione registerChannelLater() abilitandolo ad
accettare streaming di dati passandogli come parametro la tipologia di
evento SelectionKey.OP ACCEPT.
public void openServerSocket() throws IOException {
ssc = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress(listenPort);
ssc.socket().bind(isa, 100);
// This method might be called from any thread. We must use
// the xxxLater methods so that the actual register operation
// is done by the selector's thread. No other thread should access
// the selector directly.
ioThread.registerChannelLater(ssc,
SelectionKey.OP_ACCEPT,
this,
new CallbackErrorHandler() {
public void handleError(Exception ex) {
listener.socketError(Acceptor.this, ex);
}
});
}
Non appena un client si connette al server viene creato un PacketChannel
per gestire la connessione e tutti i dati che verranno inviati nel canale.
Non dobbiamo trascurare il fatto che tra i parametri passati a questa
chiamata c’`e l’oggetto new SimpleProtocolDecoder(); esso identifica
il protocollo utilizzato per lo scambio di messaggi, quindi premette di
stabilire quando il server ha ricevuto per intero il messaggio inviato dal
client.
public void socketConnected(Acceptor acceptor, SocketChannel sc) {
logger.info(”[” + acceptor + ”] Socket connected: ”
+ sc.socket().getInetAddress());
try {
4.4. IL SERVER 39
// We should reduce the size of the TCP buffers or else we will
// easily run out of memory when accepting several thousands of
// connctions
sc.socket().setReceiveBufferSize(2 ∗ 1024);
sc.socket().setSendBufferSize(2 ∗ 1024);
// The contructor enables reading automatically.
PacketChannel pc = new PacketChannel(sc, st,
new SimpleProtocolDecoder(), this);
pc.resumeReading();
} catch (IOException e) {
e.printStackTrace();
logger.error(”[” + sc.socket().getInetAddress().getHostAddress()
+ ”:” + sc.socket().getPort()
+ ”] Error on socket connection: ” + e.getMessage());
}
}
Una volta che il messaggio `e stato ricevuto per intero dal server verr`a
chiamato il metodo packetArrived(PacketChannel pc, ByteBuffer pckt).
Per prima cosa si converte il ByteBuffer in un byte array, si salvano i
dati nella base di dati e per finire invia indietro al client il messaggio “ok
ricevuto”. Prima di spedire il pacchetto indietro, viene eseguita l’istru-
zione pckt.flip() reimpostanto a zero la posizione corrente del buffer.
Questa metodo dev’essere invocato ogni volta che, dopo una lettura dal
canale, si vuole preparare una sequenza di scrittura.
All’interno di questo metodo dovr`a essere inserita anche la verifica
CRC del messaggio che per ora non `e stata ancora implementata.
public void packetArrived(PacketChannel pc, ByteBuffer pckt) {
logger.info(”[” + pc.toString() + ”] Packet received. Size: ” + pckt.remaining());
// Convert ByteBuffer to bytearray
byte[] bytearray = new byte[pckt.remaining()];
pckt.get(bytearray);
// Save to the repository
dynamoDB.save(pc.toString(), new String(bytearray));
// Send packet back to the client
logger.debug(”Send packet back”);
pckt.flip();
pc.sendPacket(pckt);
logger.debug(”Packet sent.”);
}
40 CAPITOLO 4. IMPLEMENTAZIONE
Capitolo 5
Prove Sperimentali
In questo paragrafo metteremo in luce le problematiche che abbiamo in-
contrato nello sviluppo dell’integrazione tra il server Java e i servizi offerti
da Amazon AWS. Grazie ai test fatti abbiamo potuto individuare i limiti
dei servizi Amazon e trovare soluzioni alternative per poter oltrepassarli.
5.1 Preparazione dei test
Per ogni test abbiamo creato un archivio JAR per il server e uno per il
client in modo da agevolarne l’esecuzione. Nel corso del progetto abbiamo
sviluppato due tipologie di client che propongono due differenti approcci:
• MultithreadClient, (classe descritta in Figura 5.1) implementa il
metodo tradizionale un thread per connessione TCP. Questo ci
permette di simulare uno scenario che si avvicina maggiormente
alla realt`a: ogni connessione attiva invia i messaggi ad intervalli di
tempo casuali.
• MultiplexingClient, (classe descritta in Figura 5.2) utilizza la libre-
ria Java NIO che permette di gestire le connessioni multiple con
un unico thread. Questo client per prima cosa apre tutte le con-
nessioni con il server e poi abilita l’invio dei messaggi al server. I
messaggi vengono inviati in modo sequenziale, in altre parole ogni
41
42 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.1: diagramma della classe MultithreadClient.java.
5.1. PREPARAZIONE DEI TEST 43
connessione aperta attende di ricevere risposta dal server prima di
inviare un nuovo messaggio. Questo approccio permette di indi-
viduare pi`u facilmente quante connessioni il server `e in grado di
gestire simultaneamente.
Per avviare questi client vengono utilizzati due script bash ai quali
`e possibile passare il numero di porta del server e il numero di thread
da lanciare (per multithreadClient.sh) o il numero di connessioni da
aprire (per multiplexingClient.sh).
5.1.1 Hardware utilizzato
A lato server `e stato utilizzata un’istanza Amazon EC2 Linux Micro con
613MB di RAM, 8GB di spazio su disco e Linux 3.2. Per la persistenza
dei dati `e stato utilizzato inizialmente Amazon SimpleDB con a dispo-
sizione 25 ore macchina e 1GB di spazio al mese; nella seconda fase del
progetto `e stato utilizzato Amazon DynamoDB con 100MB di spazio,
una capacit`a di scrittura di 5 write/sec e una capacit`a di lettura di 10
read/sec. Per migliorare le performance dei test la capacit`a di scrittura
e di lettura sono state incrementate fino a 320 writeread/sec.
Il client —————– (recuperare dati client).
La rete utilizzata nei test `e quella del dipartimento di Informatica
dell’universit`a Ca’ Foscari che offre una banda di 100Mbps nell’intranet
(LAN) e una banda di 10GBps per la connessione verso l’esterno (WAN).
Server e Client sono installati su macchine che non appartengono alla
stessa LAN.
5.1.2 Modalit`a di esecuzione dei test
I test fatti li possiamo classificare in tre momenti:
1. un semplice test preliminare sul server realizzato senza persistenza
dei dati, lo scopo era quello di verificare la corretta comunicazione
tra client e server;
2. test per verificare le prestazioni dell’architettura persistendo i dati
con Amazon SimpleDB;
44 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.2: diagramma della classe MultiplexingClient.java.
5.2. RISULTATI TEST SENZA PERSISTENZA 45
Figura 5.3: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Totale messaggi inviati 200, Tempo di risposta medio 108,71 ms,
minimo 38 ms, massimo 282 ms.
3. test per verificare le prestazioni dell’architettura persistendo i dati
con Amazon DynamoDB;
5.2 Risultati test senza persistenza
L’andamento a “scalini” visibile nella Figura 5.3 indica come i tempi di ri-
sposta tendono ad aumentare non in maniera proporzionale all’aumentare
della dimensione del messaggio, a quanto pare esiste un intervallo entro
cui la latenza sembra stabilizzarsi. Aumentando di 50 volte il numero
di connessioni (Figura 5.4) i tempi di latenza hanno dei picchi piuttosto
alti, ma con una frequenza cos`ı bassa che la latenza media mantiene un
buon valore.
46 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.4: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Totale messaggi inviati 10481, Tempo di risposta medio 261,51
ms, minimo 36 ms, massimo 57300 ms.
5.3. RISULTATI TEST CON AMAZON SIMPLEDB 47
Figura 5.5: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. SimpleDB come persistenza dati. Totale messaggi inviati 200,
Tempo di risposta medio 3599,295 ms, minimo 203 ms, massimo 22638
ms.
5.3 Risultati test con Amazon SimpleDB
Osservando questi due test si pu`o notare come la latenza abbia un valore
molto alto gi`a con poche connessioni attive. Questi test sono stati fatti
a distanza di qualche ora e nonostante abbiamo gli stessi parametri d’in-
gresso, hanno un andamento e delle prestazioni completamente diverse;
questo `e da attribuirsi alla diversa congestione della linea internet utiliz-
zata in facolt`a. Non sono stati fatti ulteriori test con SimpleDB perch´e
ritenuti gi`a troppo poco performanti questi appena fatti.
48 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.6: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. SimpleDB come persistenza dati. Totale messaggi inviati 200,
Tempo di risposta medio 1494,07 ms, minimo 203 ms, massimo 4087 ms.
5.4. RISULTATI TEST CON AMAZON DYNAMODB 49
5.4 Risultati test con Amazon DynamoDB
Dai risultati ottenuti nel primo test con DynamoDB (Figura 5.7) sem-
bra non essere cambiato nulla rispetto alle performance di SimpleDB. Il
motivo sta nella capacit`a di scrittura nella base di dati; la configurazio-
ne base del servizio prevede al massimo 5 write/sec. Quando arrivano
messaggi con una frequenza pi`u alta, questi vengono accodati e elabo-
rati appena possibile (paradigma FIFO) determinando un rallentamento
del tempo di risposta. Una unit`a di scrittura permette di scrivere nella
base di dati un elemento di al massimo 1KB al secondo. Se l”elemento
supera questo limite `e necessario calcolare la capacit`a di cui si ha bi-
sogno; ad esempio se ogni elemento ha dimensioni di 1,5KB e si vuole
ottenere 100 scritture al secondo dovremmo approvvigionarci 100 (wri-
te/sec) x 2 (1,5KB arrotondato all’intero superiore pi`u vicino) = 200
unit`a di capacit`a. La capacit`a di scrittura ha un costo che varia in base
alla regione in cui si trova DynamoDB che si utilizza; in questa pagina
http://aws.amazon.com/dynamodb/pricing/ ci sono i prezzi che variano
da un minimo di $ 0,01 ad un massimo di $ 0,012 all’ora per ogni 10
unit`a di scrittura. Visto il limitato budget a disposizione per effettuare
i test abbiamo scelto una capacit`a di scrittura pari a 320 writes/sec che
abbiamo preventivamente stimato come una spesa media mensile di circa
$ 275,96.
L’aumento della capacit`a di scrittura ha diminuito i tempi di laten-
za (Figura 14) migliorando discretamente le prestazioni. Il sospetto di
questo risultato si fonda sull’implementazione del MultiplexingClient che
gestisce connessioni multiple sullo stesso thread e ha un degrado delle
prestazioni all’aumentare delle connessioni dovute all’overhead determi-
nato dal context-switching nel passare da una connessione all’altra. A
confermare questo sospetto `e stato il test descritto in Figura 15 dove
abbiamo utilizzato un’unica connessione. Alla luce di questo abbiamo
implementato un client multi-thread per abbattere l’overhead di gestione
e avere un singolo thread per ogni connessione.
Il risultato ottenuto in Figura 16 dimostra come i tempi di risposta
siano migliorati di quasi 2/3 rispetto a prima, confermando come la ge-
stione di connessioni multiple con un singolo thread sia un’operazione
50 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.7: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Utilizzato DynamoDB per la persistenza, 5 writes/s. Totale mes-
saggi inviati 200, Tempo di risposta medio 1701,3 ms, minimo 193 ms,
massimo 4426 ms.
5.4. RISULTATI TEST CON AMAZON DYNAMODB 51
Figura 5.8: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale
messaggi inviati 200, Tempo di risposta medio 904,06 ms, minimo 151
ms, massimo 2650 ms.
52 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.9: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale
messaggi inviati 800, Tempo di risposta medio 223,38 ms, minimo 137
ms, massimo 746 ms.
5.4. RISULTATI TEST CON AMAZON DYNAMODB 53
Figura 5.10: rapporto tra quantit`a di dati inviati e tempi di risposta del
server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 10 Thread
come client. Totale messaggi inviati 658, Tempo di risposta medio 315,77
ms, minimo 144 ms, massimo 1529 ms.
costosa. La stessa cosa si verifica anche a lato server, all’aumentare delle
connessioni simultanee da gestire i tempi di risposta di dilatano (vedi
Figura 17).
54 CAPITOLO 5. PROVE SPERIMENTALI
Figura 5.11: rapporto tra quantit`a di dati inviati e tempi di risposta
del server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 100
thread come client. Totale messaggi inviati 1707, Tempo di risposta
medio 8191,05 ms, minimo 280 ms, massimo 177857 ms.
Capitolo 6
Conclusioni
In questo documento `e stato illustrato come un server ad alte prestazioni
implementato grazie all’utilizo della libreria Java NIO, sia in grado di in-
terfacciarsi ad una base di dati dove poter salvare i dati inviati dai client.
Lo studio dell’architettura per questo progetto ha preso spunto dall’im-
plementazione fatta da Cristian Pavan e descritta nella testi “Server ad
Alte Prestazioni e ad Alta Affidabilit`a [13]. Una modifica sostanziale al
server di Pavan riguarda il differente comportamento (vedi il diagramma
di stato in Figura 3.3) nel gestire il flusso di dati in ricezione ed invio;
questo permette di avere una maggiore flessibilit`a di dimensione dei pac-
chetti scambiati tra client e server, un vantaggio non indifferente visto
che la tendenza per il futuro `e quella di scambiare moli di dati sempre
maggiori. Altra cosa che differenzia questo progetto da quanto fatto nel-
la tesi di Pavan `e l’utilizzo di un servizio di Cloud Computing sia per il
calcolo che per lo storage dei dati definendo un’architettura nuova che
non si basa sui soliti schemi dove il centro di elaborazione viene gestito
internamente.
I risultati ottenuti nei primi test senza la persistenza dei dati sono
molto buoni
55
56 CAPITOLO 6. CONCLUSIONI
Ringraziamenti
57
58 CAPITOLO 6. CONCLUSIONI
Appendice A
Codice sorgente
A.1 Server
A.1.1 Server.java
/∗
(c) 2004, Nuno Santos, nfsantos@sapo.pt
relased under terms of the GNU public license
http://www.gnu.org/licenses/licenses.html#TOCGPL
∗/
package handlers;
import io.SelectorThread;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
/∗∗
∗ A simple server for demonstrating the IO Multiplexing framework in action.
∗ After accepting a connection, it will read packets as defined by the
∗ SimpleProtocolDecoder class and echo them back.
∗
∗ This server can accept and manage large numbers of incoming connections. For
∗ added fun remove the System.out statements and try it with several thousand
∗ (>10.000) clients. You might have to increase the maximum number of sockets
∗ allowed by the operating system.
59
60 APPENDICE A. CODICE SORGENTE
∗
∗ @author Nuno Santos, modified by Roberto Peruzzo
∗/
public class Server implements AcceptorListener, PacketChannelListener {
// Log4j
static Logger logger = Logger.getLogger(Server.class);
private final SelectorThread st;
/∗∗
∗ Starts the server.
∗
∗ @param listenPort
∗ The port where to listen for incoming connections.
∗ @throws Exception
∗/
public Server(int listenPort) throws Exception {
st = new SelectorThread();
Acceptor acceptor = new Acceptor(listenPort, st, this);
acceptor.openServerSocket();
logger.info(”Listening on port: ” + listenPort);
System.out.println(”Listening on port: ” + listenPort);
}
public static void main(String[] args) throws Exception {
URL log4jURL = ClassLoader.getSystemResource(”log4jserver.xml”);
logger.debug(”Log4j config file URL: ” + log4jURL.toString());
DOMConfigurator.configure(log4jURL);
logger.info(”Log4j config loded: ” + log4jURL.toString());
int listenPort = Integer.parseInt(args[0]);
new Server(listenPort);
}
// ////////////////////////////////////////
// Implementation of the callbacks from the
// Acceptor and PacketChannel classes
// ////////////////////////////////////////
/∗∗
∗ A new client connected. Creates a PacketChannel to handle it.
∗/
public void socketConnected(Acceptor acceptor, SocketChannel sc) {
logger.info(”[” + acceptor + ”] Socket connected: ”
+ sc.socket().getInetAddress());
try {
// We should reduce the size of the TCP buffers or else we will
// easily run out of memory when accepting several thousands of
// connctions
sc.socket().setReceiveBufferSize(2 ∗ 1024);
sc.socket().setSendBufferSize(2 ∗ 1024);
// The contructor enables reading automatically.
A.1. SERVER 61
PacketChannel pc = new PacketChannel(sc, st,
new SimpleProtocolDecoder(), this);
pc.resumeReading();
} catch (IOException e) {
e.printStackTrace();
logger.error(”[” + sc.socket().getInetAddress().getHostAddress()
+ ”:” + sc.socket().getPort()
+ ”] Error on socket connection: ” + e.getMessage());
}
}
public void socketError(Acceptor acceptor, Exception ex) {
logger.error(”[” + acceptor + ”] Error: ” + ex.getMessage());
}
public void packetArrived(PacketChannel pc, ByteBuffer pckt) {
logger.debug(”[” + pc.toString() + ”] Packet received. Size: ”
+ pckt.remaining());
pc.sendPacket(pckt);
}
public void socketException(PacketChannel pc, Exception ex) {
logger.error(”[” + pc.toString() + ”] Error: ” + ex.getMessage());
}
public void socketDisconnected(PacketChannel pc) {
logger.info(”[” + pc.toString() + ”] Disconnected.”);
}
/∗∗
∗ The answer to a request was sent. Prepare to read the next request.
∗/
public void packetSent(PacketChannel pc, ByteBuffer pckt) {
try {
pc.resumeReading();
} catch (Exception e) {
e.printStackTrace();
logger.error(”[” + pc.toString() + ”] Error on resume reading: ”
+ e.getMessage());
}
}
}
A.1.2 AsdaaServer.java
/∗∗
∗
∗/
package it.studioaqua.unive.asdaa.handlers;
import handlers.PacketChannel;
62 APPENDICE A. CODICE SORGENTE
import handlers.Server;
import it.studioaqua.unive.asdaa.repository.DynamoDBManager;
import java.net.URL;
import java.nio.ByteBuffer;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
/∗∗
∗ @author roberto
∗
∗/
public class AsdaaServer extends Server {
// Log4j
static Logger logger = Logger.getLogger(AsdaaServer.class);
private DynamoDBManager dynamoDB;
/∗∗
∗ @throws Exception
∗
∗/
public AsdaaServer(int listenPort) throws Exception {
super(listenPort);
// Open the connection with Repository for saving messages
dynamoDB = new DynamoDBManager(”Messages”);
}
public static void main(String[] args) throws Exception {
URL log4jURL = ClassLoader.getSystemResource(”log4jserver.xml”);
logger.debug(”Log4j config file URL: ”+log4jURL.toString());
DOMConfigurator.configure(log4jURL);
logger.info(”Log4j config loded: ”+log4jURL.toString());
int listenPort = Integer.parseInt(args[0]);
new AsdaaServer(listenPort);
}
/∗∗
∗ The full packet was received, so resend it to the client (like echo service)
∗/
public void packetArrived(PacketChannel pc, ByteBuffer pckt) {
logger.info(”[” + pc.toString() + ”] Packet received. Size: ” + pckt.remaining()
);
// Convert ByteBuffer to bytearray
byte[] bytearray = new byte[pckt.remaining()];
A.1. SERVER 63
pckt.get(bytearray);
// Save to the repository
dynamoDB.save(pc.toString(), new String(bytearray));
// Send packet back to the client
logger.debug(”Send packet back”);
pckt.flip();
pc.sendPacket(pckt);
logger.debug(”Packet sent.”);
}
}
A.1.3 SelectorThread.java
/∗
(c) 2004, Nuno Santos, nfsantos@sapo.pt
relased under terms of the GNU public license
http://www.gnu.org/licenses/licenses.html#TOCGPL
∗/
package io;
import java.io.IOException;
import java.nio.channels.∗;
import java.util.∗;
/∗∗
∗ Event queue for I/O events raised by a selector. This class receives the
∗ lower level events raised by a Selector and dispatches them to the
∗ appropriate handler. It also manages all other operations on the selector,
∗ like registering and unregistering channels, or updating the events of
∗ interest for each monitored socket.
∗
∗ This class is inspired on the java.awt.EventQueue and follows a similar
∗ model. The EventQueue class is responsible for making sure that all
∗ operations on AWT objects are performed on a single thread, the one managed
∗ internally by EventQueue. The SelectorThread class performs a similar
∗ task. In particular:
∗
∗ − Only the thread created by instances of this class should be allowed
∗ to access the selector and all sockets managed by it. This means that
∗ all I/O operations on the sockets should be peformed on the corresponding
∗ selector's thread. If some other thread wants to access objects managed
∗ by this selector, then it should use <code>invokeLater()</code> or the
∗ <code>invokeAndWait()</code> to dispatch a runnable to this thread.
∗
∗ − This thread should not be used to perform lenghty operations. In
∗ particular, it should never be used to perform blocking I/O operations.
∗ To perform a time consuming task use a worker thread.
∗
64 APPENDICE A. CODICE SORGENTE
∗
∗ This architecture is required for two main reasons:
∗
∗ The first, is to make synchronization in the objects of a connection
∗ unnecessary. This is good for performance and essential for keeping
∗ the complexity low. Getting synchronization right within the objects
∗ of a connection would be extremely tricky.
∗
∗ The second is to make sure that all actions over the selector, its
∗ keys and related sockets are carried in the same thread. My personal
∗ experience with selectors is that they don't work well when being
∗ accessed concurrently by several threads. This is mostly the result
∗ of bugs in some of the version of Sun's Java SDK (these problems were
∗ found with version 1.4.2 02). Some of the bugs have already been
∗ identified and fixed by Sun. But it is better to work around them
∗ by avoiding multithreaded access to the selector.
∗
∗ @author Nuno Santos
∗/
final public class SelectorThread implements Runnable {
/∗∗ Selector used for I/O multiplexing ∗/
private Selector selector;
/∗∗ The thread associated with this selector ∗/
private final Thread selectorThread;
/∗∗
∗ Flag telling if this object should terminate, that is,
∗ if it should close the selector and kill the associated
∗ thread. Used for graceful termination.
∗/
private boolean closeRequested = false;
/∗∗
∗ List of tasks to be executed in the selector thread.
∗ Submitted using invokeLater() and executed in the main
∗ select loop.
∗/
@SuppressWarnings(”rawtypes”)
private final List pendingInvocations = new ArrayList(32);
/∗∗
∗ Creates a new selector and the associated thread. The thread
∗ is started by this constructor, thereby making this object
∗ ready to be used.
∗
∗ @throws IOException
∗/
public SelectorThread() throws IOException {
// Selector for incoming time requests
selector = Selector.open();
selectorThread = new Thread(this);
selectorThread.start();
A.1. SERVER 65
}
/∗∗
∗ Raises an internal flag that will result on this thread dying
∗ the next time it goes through the dispatch loop. The thread
∗ executes all pending tasks before dying.
∗/
public void requestClose() {
closeRequested = true;
// Nudges the selector.
selector.wakeup();
}
/∗∗
∗ Adds a new interest to the list of events where a channel is
∗ registered. This means that the associated event handler will
∗ start receiving events for the specified interest.
∗
∗ This method should only be called on the selector thread. Otherwise
∗ an exception is thrown. Use the addChannelInterestLater() when calling
∗ from another thread.
∗
∗ @param channel The channel to be updated. Must be registered.
∗ @param interest The interest to add. Should be one of the
∗ constants defined on SelectionKey.
∗/
public void addChannelInterestNow(SelectableChannel channel,
int interest) throws IOException {
if (Thread.currentThread() != selectorThread) {
throw new IOException(”Method can only be called from selector thread”);
}
SelectionKey sk = channel.keyFor(selector);
changeKeyInterest(sk, sk.interestOps() | interest);
}
/∗∗
∗ Like addChannelInterestNow(), but executed asynchronouly on the
∗ selector thread. It returns after scheduling the task, without
∗ waiting for it to be executed.
∗
∗ @param channel The channel to be updated. Must be registered.
∗ @param interest The new interest to add. Should be one of the
∗ constants defined on SelectionKey.
∗ @param errorHandler Callback used if an exception is raised when executing the task.
∗/
public void addChannelInterestLater(final SelectableChannel channel,
66 APPENDICE A. CODICE SORGENTE
// Add a new runnable to the list of tasks to be executed in the selector thread
invokeLater(new Runnable() {
public void run() {
try {
addChannelInterestNow(channel, interest);
} catch (IOException e) {
errorHandler.handleError(e);
}
}
});
}
/∗∗
∗ Removes an interest from the list of events where a channel is
∗ registered. The associated event handler will stop receiving events
∗ for the specified interest.
∗
∗ This method should only be called on the selector thread. Otherwise
∗ an exception is thrown. Use the removeChannelInterestLater() when calling
∗ from another thread.
∗
∗ @param channel The channel to be updated. Must be registered.
∗ @param interest The interest to be removed. Should be one of the
∗ constants defined on SelectionKey.
∗/
public void removeChannelInterestNow(SelectableChannel channel,
int interest) throws IOException {
if (Thread.currentThread() != selectorThread) {
throw new IOException(”Method can only be called from selector thread”);
}
SelectionKey sk = channel.keyFor(selector);
changeKeyInterest(sk, sk.interestOps() & ˜interest);
}
/∗∗
∗ Like removeChannelInterestNow(), but executed asynchronouly on
∗ the selector thread. This method returns after scheduling the task,
∗ without waiting for it to be executed.
∗
∗ @param channel The channel to be updated. Must be registered.
∗ @param interest The interest to remove. Should be one of the
∗ constants defined on SelectionKey.
∗ @param errorHandler Callback used if an exception is raised when
∗ executing the task.
A.1. SERVER 67
∗/
public void removeChannelInterestLater(final SelectableChannel channel,
invokeLater(new Runnable() {
public void run() {
try {
removeChannelInterestNow(channel, interest);
} catch (IOException e) {
errorHandler.handleError(e);
}
}
});
}
/∗∗
∗ Updates the interest set associated with a selection key. The
∗ old interest is discarded, being replaced by the new one.
∗
∗ @param sk The key to be updated.
∗ @param newInterest
∗ @throws IOException
∗/
private void changeKeyInterest(SelectionKey sk,
int newInterest) throws IOException {
/∗ This method might throw two unchecked exceptions:
∗ 1. IllegalArgumentException − Should never happen. It is a bug if it happens
∗ 2. CancelledKeyException − Might happen if the channel is closed while
∗ a packet is being dispatched.
∗/
try {
sk.interestOps(newInterest);
} catch (CancelledKeyException cke) {
IOException ioe = new IOException(”Failed to change channel interest.”);
ioe.initCause(cke);
throw ioe;
}
}
68 APPENDICE A. CODICE SORGENTE
/∗∗
∗ Like registerChannelLater(), but executed asynchronouly on the
∗ selector thread. It returns after scheduling the task, without
∗ waiting for it to be executed.
∗
∗ @param channel The channel to be monitored.
∗ @param selectionKeys The interest set. Should be a combination of
∗ SelectionKey constants.
∗ @param handler The handler for events raised on the registered channel.
∗ @param errorHandler Used for asynchronous error handling.
∗
∗ @throws IOException
∗/
public void registerChannelLater(final SelectableChannel channel,
final int selectionKeys,
final CallbackErrorHandler errorHandler) {
invokeLater(new Runnable() {
public void run() {
try {
registerChannelNow(channel, selectionKeys, handlerInfo);
} catch (IOException e) {
errorHandler.handleError(e);
}
}
});
}
/∗∗
∗ Registers a SelectableChannel with this selector. This channel will
∗ start to be monitored by the selector for the set of events associated
∗ with it. When an event is raised, the corresponding handler is
∗ called.
∗
∗ This method can be called multiple times with the same channel
∗ and selector. Subsequent calls update the associated interest set
∗ and selector handler to the ones given as arguments.
∗
∗ This method should only be called on the selector thread. Otherwise
∗ an exception is thrown. Use the registerChannelLater() when calling
∗ from another thread.
∗
∗ @param channel The channel to be monitored.
∗ @param selectionKeys The interest set. Should be a combination of
∗ SelectionKey constants.
∗ @param handler The handler for events raised on the registered channel.
A.1. SERVER 69
∗/
public void registerChannelNow(SelectableChannel channel,
int selectionKeys,
SelectorHandler handlerInfo) throws IOException {
if (Thread.currentThread() != selectorThread) {
throw new IOException(”Method can only be called from selector thread”);
}
if (!channel.isOpen()) {
throw new IOException(”Channel is not open.”);
}
try {
if (channel.isRegistered()) {
SelectionKey sk = channel.keyFor(selector);
assert sk != null : ”Channel is already registered with other selector”;
sk.interestOps(selectionKeys);
Object previousAttach = sk.attach(handlerInfo);
assert previousAttach != null;
} else {
channel.configureBlocking(false);
channel.register(selector, selectionKeys, handlerInfo);
}
} catch (Exception e) {
IOException ioe = new IOException(”Error registering channel.”);
ioe.initCause(e);
throw ioe;
}
}
/∗∗
∗ Executes the given task in the selector thread. This method returns
∗ as soon as the task is scheduled, without waiting for it to be
∗ executed.
∗
∗ @param run The task to be executed.
∗/
@SuppressWarnings(”unchecked”)
public void invokeLater(Runnable run) {
synchronized (pendingInvocations) {
pendingInvocations.add(run);
}
selector.wakeup();
}
/∗∗
∗ Executes the given task synchronously in the selector thread. This
∗ method schedules the task, waits for its execution and only then
∗ returns.
∗
∗ @param run The task to be executed on the selector's thread.
∗/
public void invokeAndWait(final Runnable task)
70 APPENDICE A. CODICE SORGENTE
throws InterruptedException
{
if (Thread.currentThread() == selectorThread) {
// We are in the selector's thread. No need to schedule
// execution
task.run();
} else {
// Used to deliver the notification that the task is executed
final Object latch = new Object();
synchronized (latch) {
// Uses the invokeLater method with a newly created task
this.invokeLater(new Runnable() {
public void run() {
task.run();
// Notifies
latch.notify();
}
});
// Wait for the task to complete.
latch.wait();
}
// Ok, we are done, the task was executed. Proceed.
}
}
/∗∗
∗ Executes all tasks queued for execution on the selector's thread.
∗
∗ Should be called holding the lock to <code>pendingInvocations</code>.
∗
∗/
private void doInvocations() {
synchronized (pendingInvocations) {
for (int i = 0; i < pendingInvocations.size(); i++) {
Runnable task = (Runnable) pendingInvocations.get(i);
task.run();
}
pendingInvocations.clear();
}
}
/∗∗
∗ Main cycle. This is where event processing and
∗ dispatching happens.
∗/
@SuppressWarnings(”rawtypes”)
public void run() {
// Here's where everything happens. The select method will
// return when any operations registered above have occurred, the
// thread has been interrupted, etc.
while (true) {
// Execute all the pending tasks.
doInvocations();
A.1. SERVER 71
// Time to terminate?
if (closeRequested) {
return;
}
int selectedKeys = 0;
try {
selectedKeys = selector.select();
} catch (IOException ioe) {
// Select should never throw an exception under normal
// operation. If this happens, print the error and try to
// continue working.
ioe.printStackTrace();
continue;
}
if (selectedKeys == 0) {
// Go back to the beginning of the loop
continue;
}
// Someone is ready for IO, get the ready keys
Iterator it = selector.selectedKeys().iterator();
// Walk through the collection of ready keys and dispatch
// any active event.
while (it.hasNext()) {
SelectionKey sk = (SelectionKey)it.next();
it.remove();
try {
// Obtain the interest of the key
int readyOps = sk.readyOps();
// Disable the interest for the operation that is ready.
// This prevents the same event from being raised multiple
// times.
sk.interestOps(sk.interestOps() & ˜readyOps);
SelectorHandler handler =
(SelectorHandler) sk.attachment();
// Some of the operations set in the selection key
// might no longer be valid when the handler is executed.
// So handlers should take precautions against this
// possibility.
// Check what are the interests that are active and
// dispatch the event to the appropriate method.
if (sk.isAcceptable()) {
// A connection is ready to be completed
((AcceptSelectorHandler)handler).handleAccept();
} else if (sk.isConnectable()) {
// A connection is ready to be accepted
((ConnectorSelectorHandler)handler).handleConnect();
72 APPENDICE A. CODICE SORGENTE
} else {
ReadWriteSelectorHandler rwHandler =
(ReadWriteSelectorHandler)handler;
// Readable or writable
if (sk.isReadable()) {
// It is possible to read
rwHandler.handleRead();
}
// Check if the key is still valid, since it might
// have been invalidated in the read handler
// (for instance, the socket might have been closed)
if (sk.isValid() && sk.isWritable()) {
// It is read to write
rwHandler.handleWrite();
}
}
} catch (Throwable t) {
// No exceptions should be thrown in the previous block!
// So kill everything if one is detected.
// Makes debugging easier.
closeSelectorAndChannels();
t.printStackTrace();
return;
}
}
}
}
/∗∗
∗ Closes all channels registered with the selector. Used to
∗ clean up when the selector dies and cannot be recovered.
∗/
private void closeSelectorAndChannels() {
@SuppressWarnings(”rawtypes”)
Set keys = selector.keys();
for (@SuppressWarnings(”rawtypes”)
Iterator iter = keys.iterator(); iter.hasNext();) {
SelectionKey key = (SelectionKey)iter.next();
try {
key.channel().close();
} catch (IOException e) {
// Ignore
}
}
try {
selector.close();
} catch (IOException e) {
// Ignore
}
}
}
A.1. SERVER 73
A.1.4 DynamoDBManager.java
/∗∗
∗ (c) 2011, Roberto Peruzzo, roberto.peruzzo@gmail.com
∗ relased under terms of the GNU public license
∗ http://www.gnu.org/licenses/licenses.html#TOCGPL
∗/
package it.studioaqua.unive.asdaa.repository;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.CharacterCodingException;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.dynamodb.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodb.model.AttributeValue;
import com.amazonaws.services.dynamodb.model.Condition;
import com.amazonaws.services.dynamodb.model.CreateTableRequest;
import com.amazonaws.services.dynamodb.model.DescribeTableRequest;
import com.amazonaws.services.dynamodb.model.DescribeTableResult;
import com.amazonaws.services.dynamodb.model.KeySchema;
import com.amazonaws.services.dynamodb.model.KeySchemaElement;
import com.amazonaws.services.dynamodb.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodb.model.PutItemRequest;
import com.amazonaws.services.dynamodb.model.PutItemResult;
import com.amazonaws.services.dynamodb.model.ScanRequest;
import com.amazonaws.services.dynamodb.model.ScanResult;
import com.amazonaws.services.dynamodb.model.TableDescription;
import com.amazonaws.services.dynamodb.model.TableStatus;
/∗∗
∗ This class manage the interaction between Amazon SimpleDB and SelectedServer.
∗ It persists every message received by SelectedServer.
∗
∗ @author Studio AQuA
∗
∗/
public class DynamoDBManager {
// Log4j
static Logger logger = Logger.getLogger(DynamoDBManager.class);
private AmazonDynamoDBClient dynamoDB;
private String table;
74 APPENDICE A. CODICE SORGENTE
/∗∗
∗ Constructor
∗ @param tableName
∗/
public DynamoDBManager(String tableName) {
try {
logger.debug(”Get AWS Credentials.”);
// Get PropertiesCredentials
InputStream credentialsAsStream = Thread.currentThread().
getContextClassLoader().getResourceAsStream(”AwsCredentials.
properties”);
AWSCredentials credentials = new PropertiesCredentials(
credentialsAsStream);
logger.debug(credentials.toString());
// Activate the Amazon DynamoDB Client
this.dynamoDB = new AmazonDynamoDBClient(credentials);
// Describe our new table
DescribeTableRequest describeTableRequest = new DescribeTableRequest().
withTableName(tableName);
TableDescription tableDescription = this.dynamoDB.describeTable(
describeTableRequest).getTable();
logger.info(”Table Description: ” + tableDescription);
String tableStatus = tableDescription.getTableStatus();
if (tableStatus.equals(TableStatus.ACTIVE.toString())) {
logger.info(”Table ”+tableName+ ” already exists.”);
} else {
// Create a table with a primary key named 'name', which holds a string
CreateTableRequest createTableRequest = new CreateTableRequest().
withTableName(tableName)
.withKeySchema(new KeySchema(new KeySchemaElement().
withAttributeName(”messageId”).withAttributeType(”S”)))
.withProvisionedThroughput(new ProvisionedThroughput().
withReadCapacityUnits(10L).withWriteCapacityUnits(320L));
tableDescription = this.dynamoDB.createTable(createTableRequest).
getTableDescription();
logger.info(”Created Table: ” + tableDescription);
}
this.table = tableName;
} catch (AmazonServiceException ase) {
logger.error(”Caught an AmazonServiceException, which means your request
made it ”
+ ”to AWS, but was rejected with an error response for some reason.”);
logger.error(”Error Message: ” + ase.getMessage());
logger.error(”HTTP Status Code: ” + ase.getStatusCode());
logger.error(”AWS Error Code: ” + ase.getErrorCode());
logger.error(”Error Type: ” + ase.getErrorType());
logger.error(”Request ID: ” + ase.getRequestId());
A.1. SERVER 75
} catch (AmazonClientException ace) {
logger.error(”Caught an AmazonClientException, which means the client
encountered ”
+ ”a serious internal problem while trying to communicate with AWS, ”
+ ”such as not being able to access the network.”);
logger.error(”Error Message: ” + ace.getMessage());
} catch (IOException e) {
logger.error(”RepositoryManager creation: IO exception. ”+e.
getMessage());
e.printStackTrace();
}
}
/∗∗
∗ Persists the client message into SimpleDB.
∗ @param clientId − client identifier
∗ @param body − message
∗ @return
∗ @throws CharacterCodingException
∗/
public boolean save(String clientId, String body) {
String recordId = clientId+” ”+String.valueOf(System.currentTimeMillis());
// Add an item
try {
logger.info(”Save message id ”+recordId+” on table ”+ this.table);
logger.debug(”MESSAGE BODY: ”+body);
Map<String, AttributeValue> item = newItem(recordId, clientId, body, System
.currentTimeMillis());
PutItemRequest putItemRequest = new PutItemRequest(this.table, item);
PutItemResult putItemResult = dynamoDB.putItem(putItemRequest);
logger.info(”Result: ” + putItemResult.toString());
logger.info(”Message from client ”+clientId+” saved.”);
} catch (AmazonServiceException ase) {
logger.error(”[SAVE] Caught an AmazonServiceException, which means your
request made it ”
+ ”to AWS, but was rejected with an error response for some reason.”);
logger.error(”[SAVE] Error Message: ” + ase.getMessage());
logger.error(”[SAVE] HTTP Status Code: ” + ase.getStatusCode());
logger.error(”[SAVE] AWS Error Code: ” + ase.getErrorCode());
logger.error(”[SAVE] Error Type: ” + ase.getErrorType());
logger.error(”[SAVE] Request ID: ” + ase.getRequestId());
return false;
} catch (AmazonClientException ace) {
logger.error(”[SAVE] Caught an AmazonClientException, which means the client
encountered ”
+ ”a serious internal problem while trying to communicate with AWS, ”
+ ”such as not being able to access the network.”);
logger.error(”[SAVE] Error Message: ” + ace.getMessage());
76 APPENDICE A. CODICE SORGENTE
return false;
}
return true;
}
/∗∗
∗ Get table content
∗ @param tableName
∗ @return
∗/
public ScanResult scan(String tableName, HashMap<String, Condition> scanFilter) {
ScanResult scanResult = null;
try {
ScanRequest scanRequest = new ScanRequest(tableName);
if(!scanFilter.isEmpty()) {
scanRequest.withScanFilter(scanFilter);
}
scanResult = dynamoDB.scan(scanRequest);
logger.info(”Result: ” + scanResult);
} catch (AmazonServiceException ase) {
logger.error(”[SAVE] Caught an AmazonServiceException, which means your
request made it ”
+ ”to AWS, but was rejected with an error response for some reason.”);
logger.error(”[SAVE] Error Message: ” + ase.getMessage());
logger.error(”[SAVE] HTTP Status Code: ” + ase.getStatusCode());
logger.error(”[SAVE] AWS Error Code: ” + ase.getErrorCode());
logger.error(”[SAVE] Error Type: ” + ase.getErrorType());
logger.error(”[SAVE] Request ID: ” + ase.getRequestId());
} catch (AmazonClientException ace) {
logger.error(”[SAVE] Caught an AmazonClientException, which means the client
encountered ”
+ ”a serious internal problem while trying to communicate with AWS, ”
+ ”such as not being able to access the network.”);
logger.error(”[SAVE] Error Message: ” + ace.getMessage());
}
return scanResult;
}
/∗∗
∗
∗ @param tableName
∗ @return
∗/
public Long countTableItems(String tableName) {
// Describe our new table
DescribeTableRequest describeTableRequest = new DescribeTableRequest().
withTableName(tableName);
DescribeTableResult tableDescription = this.dynamoDB.describeTable(
A.2. CLIENT 77
describeTableRequest);
return tableDescription.getTable().getItemCount();
}
/∗∗
∗
∗ @param name
∗ @param clientId
∗ @param body
∗ @param created
∗ @return
∗/
private Map<String, AttributeValue> newItem(String messageId, String clientId,
String body, long created) {
Map<String, AttributeValue> item = new HashMap<String, AttributeValue>();
item.put(”messageId”, new AttributeValue(messageId));
item.put(”clientId”, new AttributeValue(clientId));
item.put(”body”, new AttributeValue(body));
item.put(”created”, new AttributeValue().withN(Long.toString(created)));
return item;
}
}
A.2 Client
A.2.1 MultiplexingClient.java
package it.studioaqua.unive.asdaa.client;
import handlers.∗;
import io.SelectorThread;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.∗;
import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;
/∗∗
78 APPENDICE A. CODICE SORGENTE
∗ A simple test client for the I/O Multiplexing based server. This class simulates several clients
using an imaginary request− reply protocol to connect to a server. Several connections can
be established at the same time. Each one, generates a random packet to simulate a request,
sends it to the server and waits for the answer before sending the next packet. This
implementation is based on I/O Multiplexing, so it should be able to handle several
thousand of connections. Using Redhat Linux 9.0 I was able to establish about 10.000
connections before running out of memory (the system had only 256Mb of RAM).
∗ @author Nuno Santos
∗/
public class MultiplexingClient implements ConnectorListener, PacketChannelListener {
// Log4j
static Logger logger = Logger.getLogger(MultiplexingClient.class);
/∗∗
∗ A single selector for all clients
∗ @uml.property name=”st”
∗ @uml.associationEnd
∗/
private static SelectorThread st;
/∗∗ Maximum size of the packets sent ∗/
private static final int MAX_SIZE = 1200; //10 ∗ 1024;
/∗∗ Minimum size of the packets sent ∗/
private static final int MIN_SIZE = 50; //128;
/∗∗ How many packets each client should send ∗/
private static final int PACKETS_TO_SEND = 10;
/∗∗ For generating random packet sizes. ∗/
private static final Random r = new Random();
/∗∗ How many connections to created ∗/
private static int connectionCount;
/∗∗ How many connections were opened so far ∗/
private static int connectionsEstablished = 0;
private static int connectionsFailed = 0;
/∗∗ How many connections were disconnected so far ∗/
private static int connectionsClosed = 0;
/∗∗
∗ Keeps a list of connections that have been established but not yet
∗ started.
∗/
@SuppressWarnings(”rawtypes”)
private static List establishedConnections = new ArrayList(512);
/∗∗ How many packets each instance sent so far. ∗/
private int packetsSent = 0;
/∗∗
∗ Studio AQuA
∗ @uml.property name=”rigaStat”
∗ @uml.associationEnd
∗/
// Statistics
private Stats rigaStat = null;
private final static ArrayList<Stats> statistiche = new ArrayList<Stats>();
A.2. CLIENT 79
@SuppressWarnings(”unused”)
private int clientId;
private static long globalStart = 0;
/∗∗
∗ Initiates a non−blocking connection attempt to the given address.
∗
∗ @param remotePoint
∗ Where to try to connect.
∗ @throws Exception
∗/
public MultiplexingClient(InetSocketAddress remotePoint, int clientId) throws
Exception {
Connector connector = new Connector(st, remotePoint, this);
connector.connect();
logger.info(”[” + connector + ”] Connecting...”);
this.clientId = clientId;
}
/∗∗
∗ Creates a new packet with a size chosen randomly between MIN SIZE and
∗ MAX SIZE.
∗/
private ByteBuffer generateNextPacket() {
// Generate a random size between
int size = MIN_SIZE + r.nextInt(MAX_SIZE − MIN_SIZE);
ByteBuffer buffer = ByteBuffer.allocate(size);
buffer.put(SimpleProtocolDecoder.STX);
for (int i = 0; i < size − 2; i++) {
buffer.put((byte) 'a');
}
buffer.put(SimpleProtocolDecoder.ETX);
buffer.limit(buffer.position());
buffer.flip();
return buffer;
}
// ////////////////////////////////////////
// Implementation of the callbacks from the
// Acceptor and PacketChannel classes
// ////////////////////////////////////////
/∗∗
∗ A new client connected. Creates a PacketChannel to handle it.
∗/
@SuppressWarnings(”unchecked”)
public void connectionEstablished(Connector connector, SocketChannel sc) {
try {
// We should reduce the size of the TCP buffers or else we will
// easily run out of memory when accepting several thousands of
// connctions
sc.socket().setReceiveBufferSize(2 ∗ 1024);
sc.socket().setSendBufferSize(2 ∗ 1024);
80 APPENDICE A. CODICE SORGENTE
// The contructor enables reading automatically.
PacketChannel pc = new PacketChannel(sc, st,
new SimpleProtocolDecoder(), this);
// Do not start sending packets right away. Waits for all sockets
// to connect. Otherwise, the load created by sending and receiving
// packets will increase dramatically the time taken for all
// connections to be established. It is better to establish all
// connections and only then to start sending packets.
establishedConnections.add(pc);
connectionsEstablished++;
logger.info(”[” + connector + ”] Connected: ”
+ sc.socket().getInetAddress() + ” (”
+ connectionsEstablished + ”/” + connectionCount +
”)”);
// If if all connections are established.
checkAllConnected();
} catch (IOException e) {
e.printStackTrace();
}
}
public void connectionFailed(Connector connector, Exception cause) {
logger.error(”[” + connector + ”] Error: ” + cause.getMessage());
connectionsFailed++;
checkAllConnected();
}
public void packetArrived(PacketChannel pc, ByteBuffer pckt) {
logger.debug(”[”+ pc.toString() + ”] Packet arrived”);
rigaStat.setFine(System.currentTimeMillis());
rigaStat.setRicevuti(pckt.capacity());
//synchronized (statistiche) {
statistiche.add(rigaStat);
//}
if (packetsSent >= PACKETS_TO_SEND) {
// This connection sent all packets that it was supposed to send.
// Close.
logger.info(”[”
+ pc.getSocketChannel().socket().getLocalPort()
+ ”] Closed. Packets sent ” + PACKETS_TO_SEND
+ ”. Connection: ” + (connectionsClosed + 1) + ”/”
+ connectionsEstablished);
pc.close();
connectionClosed();
} else {
// Still more packets to send.
sendPacket(pc);
}
A.2. CLIENT 81
}
public void socketException(PacketChannel pc, Exception ex) {
logger.error(”[” + pc.toString() + ”] Error: ” + ex.getMessage());
connectionClosed();
}
public void socketDisconnected(PacketChannel pc) {
logger.info(”[” + pc.toString() + ”] Disconnected.”);
connectionClosed();
}
/∗∗
∗ The request was sent. Prepare to read the answer.
∗/
public void packetSent(PacketChannel pc, ByteBuffer pckt) {
logger.debug(”[” + pc.toString() + ”] Packet sent.”);
rigaStat = new Stats();
String host = pc.getSocketChannel().socket().getInetAddress().getHostAddress
()
+”:”+ String.valueOf(pc.getSocketChannel().socket().
getLocalPort());
rigaStat.setClientHost(host);
rigaStat.setMsgId(packetsSent);
rigaStat.setInviati(pckt.capacity());
rigaStat.setInizio(System.currentTimeMillis());
try {
pc.resumeReading();
} catch (Exception e) {
e.printStackTrace();
logger.error(”Packet sent error: ”+e.getMessage());
}
}
// //////////////////////////
// Helper methods
// //////////////////////////
/∗∗
∗ Called when a connection is closed. Checks if all connections have been
∗ closed and if so exits the virtual machine.
∗/
private void connectionClosed() {
connectionsClosed++;
if (connectionsClosed >= connectionsEstablished) {
st.requestClose();
System.exit(1);
}
}
/∗∗
∗ Sends a newly generated packet using the given PacketChannel
82 APPENDICE A. CODICE SORGENTE
∗
∗ @param pc
∗/
private void sendPacket(PacketChannel pc) {
// System.out.println(”[” + pc.toString() + ”] Sending packet.”);
ByteBuffer packet = generateNextPacket();
packetsSent++;
pc.sendPacket(packet);
}
/∗∗
∗ Checks if all connections have been established. If so, starts them all
∗ by sending an initial packet to all of them.
∗/
private void checkAllConnected() {
// Starts sending packets only after all connections are established.
if ((connectionsEstablished + connectionsFailed) == connectionCount) {
for (int i = 0; i < establishedConnections.size(); i++) {
PacketChannel pc = (PacketChannel) establishedConnections
.get(i);
sendPacket(pc);
}
establishedConnections.clear();
}
}
public static void main(String[] args) throws Exception {
URL log4jURL = ClassLoader.getSystemResource(”log4jclient.xml”);
logger.debug(”Log4j config file URL: ”+log4jURL.toString());
DOMConfigurator.configure(log4jURL);
logger.info(”Log4j config loded: ”+log4jURL.toString());
InetSocketAddress remotePoint = new InetSocketAddress(args[0],
Integer.parseInt(args[1]));
connectionCount = 1;
if (args.length > 2) {
connectionCount = Integer.parseInt(args[2]);
}
st = new SelectorThread();
globalStart = System.currentTimeMillis();
for (int i = 0; i < connectionCount; i++) {
new MultiplexingClient(remotePoint,i);
// Must sleep for a while between opening connections in order
// to give the remote host enough time to handle them. Otherwise,
// the remote host backlog will get full and the connection
// attemps will start to be refused.
Thread.sleep(10);
}
Runtime.getRuntime().addShutdownHook(new PrintStats());
A.2. CLIENT 83
}
private static class PrintStats extends Thread {
public void run() {
logger.info(”Execution TIME: ”+String.valueOf(System.
currentTimeMillis()−globalStart)+”ms”);
String locale = ”qualcosa”;
try {
locale = InetAddress.getLocalHost().getHostName();
try {
String filename = ”logs/”+locale + ” PPM ”
+ Calendar.getInstance().get(Calendar.
HOUR_OF_DAY)
+ ”−” + Calendar.getInstance().get(
Calendar.MINUTE)
+ ”−” + Calendar.getInstance().get(
Calendar.SECOND)
+ ” ” + currentThread().getId()
+ ”.log”;
logger.info(”nnSalvate statistiche su file: ” +
filename);
FileOutputStream stats = new FileOutputStream(
filename,
true);
PrintStream out = new PrintStream(stats);
out.println(”HosttMsgIDtByteSenttByteReceived
tStarttEndtTime”);
for (Iterator<Stats> it = statistiche.iterator(); it
.hasNext();) {
Stats riga = it.next();
out.println(riga.getClientHost() + ”t” +
riga.getMsgId()
+ ”t” + riga.getInviati() + ”
t”
+ riga.getRicevuti() + ”t” +
riga.getInizio()
+ ”t” + riga.getFine() + ”t”
+ riga.getTempo());
}
out.close();
} catch (IOException ex) {
logger.error(”Errore salvataggio dati: ” + ex.getMessage
());
}
} catch (UnknownHostException ex) {
logger.error(”Impossibile ricavare IP locale: ”
+ ex.getMessage());
}
84 APPENDICE A. CODICE SORGENTE
}
}
/∗∗
∗ @author roberto
∗/
private static class Stats {
/∗∗
∗ @uml.property name=”inviati”
∗/
private long inviati;
/∗∗
∗ @uml.property name=”ricevuti”
∗/
private long ricevuti;
/∗∗
∗ @uml.property name=”inizio”
∗/
private long inizio;
/∗∗
∗ @uml.property name=”fine”
∗/
private long fine;
/∗∗
∗ @uml.property name=”msgId”
∗/
private int msgId;
/∗∗
∗ @uml.property name=”clientHost”
∗/
private String clientHost;
/∗∗
∗ @return
∗ @uml.property name=”inizio”
∗/
public long getInizio() {
return inizio;
}
/∗∗
∗ @param inizio
∗ @uml.property name=”inizio”
∗/
public void setInizio(long inizio) {
this.inizio = inizio;
}
/∗∗
∗ @return
∗ @uml.property name=”fine”
∗/
A.2. CLIENT 85
public long getFine() {
return fine;
}
/∗∗
∗ @param fine
∗ @uml.property name=”fine”
∗/
public void setFine(long fine) {
this.fine = fine;
}
public Stats() {
}
/∗
∗ public Stats(Thread thread, int msgId, long inviati, long ricevuti,
∗ long tempo) { this.inviati = inviati; this.ricevuti = ricevuti;
∗ this.tempo = tempo; this.msgId = msgId; this.threadId =
∗ thread.getId(); }
∗/
/∗∗
∗ @return the inviati
∗ @uml.property name=”inviati”
∗/
public long getInviati() {
return inviati;
}
/∗∗
∗ @param inviati the inviati to set
∗ @uml.property name=”inviati”
∗/
public void setInviati(long inviati) {
this.inviati = inviati;
}
/∗∗
∗ @return the ricevuti
∗ @uml.property name=”ricevuti”
∗/
public long getRicevuti() {
return ricevuti;
}
/∗∗
∗ @param ricevuti the ricevuti to set
∗ @uml.property name=”ricevuti”
∗/
public void setRicevuti(long ricevuti) {
this.ricevuti = ricevuti;
}
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità
Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità

More Related Content

Similar to Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità

Anomaly detection in network traffic flows with big data analysis techniques
Anomaly detection in network traffic flows with big data analysis techniques Anomaly detection in network traffic flows with big data analysis techniques
Anomaly detection in network traffic flows with big data analysis techniques Maurizio Cacace
 
Application_level_SLA_monitoring
Application_level_SLA_monitoringApplication_level_SLA_monitoring
Application_level_SLA_monitoringNicola Mezzetti
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Francesco Komauli
 
Monitoraggio di mac address in lan
Monitoraggio di mac address in lanMonitoraggio di mac address in lan
Monitoraggio di mac address in lanCe.Se.N.A. Security
 
Profilazione utente in ambienti virtualizzati
Profilazione utente in ambienti virtualizzatiProfilazione utente in ambienti virtualizzati
Profilazione utente in ambienti virtualizzatiPietro Corona
 
Imparare asp.net 107
Imparare asp.net 107Imparare asp.net 107
Imparare asp.net 107Pi Libri
 
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...Analisi e realizzazione di uno strumento per la verifica di conformità su sis...
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...Davide Bravin
 
Extended summary of “Understanding the Performance Costs and Benefits of Pri...
Extended summary of “Understanding the Performance Costs  and Benefits of Pri...Extended summary of “Understanding the Performance Costs  and Benefits of Pri...
Extended summary of “Understanding the Performance Costs and Benefits of Pri...RiccardoDeMonte
 
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...Progetto e implementazione di una pipeline di sviluppo software con tecnologi...
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...Mattia Milleri
 
Realizzazione di un' interfaccia web per la gestione dei file di log generati...
Realizzazione di un' interfaccia web per la gestione dei file di log generati...Realizzazione di un' interfaccia web per la gestione dei file di log generati...
Realizzazione di un' interfaccia web per la gestione dei file di log generati...Marco Furlanetto
 
ASP.NET performance optimization
ASP.NET performance optimizationASP.NET performance optimization
ASP.NET performance optimizationAndrea Dottor
 
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...danieledegan
 
SESAMO (application login automator): evoluzioni applicative e considerazioni...
SESAMO (application login automator): evoluzioni applicative e considerazioni...SESAMO (application login automator): evoluzioni applicative e considerazioni...
SESAMO (application login automator): evoluzioni applicative e considerazioni...AndrijaCiric1
 
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...Alessandro Umek
 
Progettazione e sviluppo di un software applicativo su un single board computer
Progettazione e sviluppo di un software applicativo su un single board computerProgettazione e sviluppo di un software applicativo su un single board computer
Progettazione e sviluppo di un software applicativo su un single board computerAlessandro Mascherin
 
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...LorenzoFabbio
 
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...Donato Clun
 

Similar to Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità (20)

repairpdf_Oy51nCFX
repairpdf_Oy51nCFXrepairpdf_Oy51nCFX
repairpdf_Oy51nCFX
 
Anomaly detection in network traffic flows with big data analysis techniques
Anomaly detection in network traffic flows with big data analysis techniques Anomaly detection in network traffic flows with big data analysis techniques
Anomaly detection in network traffic flows with big data analysis techniques
 
Application_level_SLA_monitoring
Application_level_SLA_monitoringApplication_level_SLA_monitoring
Application_level_SLA_monitoring
 
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
Implementazione in Java di plugin Maven per algoritmi di addestramento per re...
 
Monitoraggio di mac address in lan
Monitoraggio di mac address in lanMonitoraggio di mac address in lan
Monitoraggio di mac address in lan
 
Profilazione utente in ambienti virtualizzati
Profilazione utente in ambienti virtualizzatiProfilazione utente in ambienti virtualizzati
Profilazione utente in ambienti virtualizzati
 
Imparare asp.net 107
Imparare asp.net 107Imparare asp.net 107
Imparare asp.net 107
 
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...Analisi e realizzazione di uno strumento per la verifica di conformità su sis...
Analisi e realizzazione di uno strumento per la verifica di conformità su sis...
 
Extended summary of “Understanding the Performance Costs and Benefits of Pri...
Extended summary of “Understanding the Performance Costs  and Benefits of Pri...Extended summary of “Understanding the Performance Costs  and Benefits of Pri...
Extended summary of “Understanding the Performance Costs and Benefits of Pri...
 
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...Progetto e implementazione di una pipeline di sviluppo software con tecnologi...
Progetto e implementazione di una pipeline di sviluppo software con tecnologi...
 
Realizzazione di un' interfaccia web per la gestione dei file di log generati...
Realizzazione di un' interfaccia web per la gestione dei file di log generati...Realizzazione di un' interfaccia web per la gestione dei file di log generati...
Realizzazione di un' interfaccia web per la gestione dei file di log generati...
 
ASP.NET performance optimization
ASP.NET performance optimizationASP.NET performance optimization
ASP.NET performance optimization
 
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...
Autenticazione Continua Durante la Navigazione Web Basata sulla Dinamica del ...
 
SESAMO (application login automator): evoluzioni applicative e considerazioni...
SESAMO (application login automator): evoluzioni applicative e considerazioni...SESAMO (application login automator): evoluzioni applicative e considerazioni...
SESAMO (application login automator): evoluzioni applicative e considerazioni...
 
Sat howto
Sat howtoSat howto
Sat howto
 
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...
Integrazione e sviluppo di una piattaforma per la gestione delle conformità a...
 
Progettazione e sviluppo di un software applicativo su un single board computer
Progettazione e sviluppo di un software applicativo su un single board computerProgettazione e sviluppo di un software applicativo su un single board computer
Progettazione e sviluppo di un software applicativo su un single board computer
 
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...
Progetto e realizzazione di uno strumento per la raccolta di dipendenze archi...
 
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...
Migrazione dei meccanismi di workflow di un sistema informativo assicurativo ...
 
TesiEtta
TesiEttaTesiEtta
TesiEtta
 

More from Roberto Peruzzo

COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8
COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8
COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8Roberto Peruzzo
 
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...Roberto Peruzzo
 
DrupalDay - Localizing Drupal Commerce
DrupalDay - Localizing Drupal CommerceDrupalDay - Localizing Drupal Commerce
DrupalDay - Localizing Drupal CommerceRoberto Peruzzo
 
Drupal Days 2014 - Drupal Commerce Kickstart
Drupal Days 2014 - Drupal Commerce KickstartDrupal Days 2014 - Drupal Commerce Kickstart
Drupal Days 2014 - Drupal Commerce KickstartRoberto Peruzzo
 

More from Roberto Peruzzo (6)

Welcome aboard the team
Welcome aboard the teamWelcome aboard the team
Welcome aboard the team
 
COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8
COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8
COSA SIGNIFICA CONVERTIRE UN MODULO DA D7 A D8
 
DevOps is dead
DevOps is deadDevOps is dead
DevOps is dead
 
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...
Implementazione Object-oriented del metodo AMR per il calcolo di un campo gra...
 
DrupalDay - Localizing Drupal Commerce
DrupalDay - Localizing Drupal CommerceDrupalDay - Localizing Drupal Commerce
DrupalDay - Localizing Drupal Commerce
 
Drupal Days 2014 - Drupal Commerce Kickstart
Drupal Days 2014 - Drupal Commerce KickstartDrupal Days 2014 - Drupal Commerce Kickstart
Drupal Days 2014 - Drupal Commerce Kickstart
 

Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilità

  • 1. Studio di una Architettura per un Sistema Distributivo ad Alta Affidabilit`a Autore: Dott. Roberto Peruzzo Tutor: Prof. Alessandro Roncato Mestre, 24 ottobre 2012
  • 2. ii
  • 3. Indice 1 Introduzione 3 2 Rendere affidabile un sistema 7 2.1 Reliable Server Pooling . . . . . . . . . . . . . . . . . . . . 8 2.2 Cloud Computing . . . . . . . . . . . . . . . . . . . . . . . 8 2.2.1 Google AppEngine . . . . . . . . . . . . . . . . . . 9 2.2.2 Amazon AWS . . . . . . . . . . . . . . . . . . . . . 11 2.3 Scelta dell’infrastruttura . . . . . . . . . . . . . . . . . . . 11 3 Architettura proposta 15 3.1 Utilizzo di Java NIO . . . . . . . . . . . . . . . . . . . . . 17 3.2 Limiti nel prototipo di Pavan . . . . . . . . . . . . . . . . 21 3.3 Thread in un mondo Multiplexed . . . . . . . . . . . . . . 23 4 Implementazione 25 4.1 Come attivare i servizi Amazon AWS . . . . . . . . . . . . 25 4.2 Amazon SimpleDB . . . . . . . . . . . . . . . . . . . . . . 27 4.3 Amazon DynamoDB . . . . . . . . . . . . . . . . . . . . . 32 4.4 Il server . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5 Prove Sperimentali 41 5.1 Preparazione dei test . . . . . . . . . . . . . . . . . . . . . 41 5.1.1 Hardware utilizzato . . . . . . . . . . . . . . . . . . 43 5.1.2 Modalit`a di esecuzione dei test . . . . . . . . . . . 43 5.2 Risultati test senza persistenza . . . . . . . . . . . . . . . 45 5.3 Risultati test con Amazon SimpleDB . . . . . . . . . . . . 47 iii
  • 4. iv INDICE 5.4 Risultati test con Amazon DynamoDB . . . . . . . . . . . 49 6 Conclusioni 55 A Codice sorgente 59 A.1 Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 A.1.1 Server.java . . . . . . . . . . . . . . . . . . . . . . . 59 A.1.2 AsdaaServer.java . . . . . . . . . . . . . . . . . . . 61 A.1.3 SelectorThread.java . . . . . . . . . . . . . . . . . . 63 A.1.4 DynamoDBManager.java . . . . . . . . . . . . . . . 73 A.2 Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 A.2.1 MultiplexingClient.java . . . . . . . . . . . . . . . . 77 A.2.2 MultithreadClient.java . . . . . . . . . . . . . . . . 86 B Glossario 95
  • 5. Elenco delle figure 1.1 Diagramma dell’attuale architettura da ottimizzare. . . . . 4 2.1 Google AppEngine vs. Amazon AWS. . . . . . . . . . . . . 12 2.2 Architettura con SimpleDB. . . . . . . . . . . . . . . . . . 13 2.3 Architettura con DynamoDB. . . . . . . . . . . . . . . . . 14 3.1 all’aumentare dei thread creati aumenta il tempo di crea- zione degli stessi. . . . . . . . . . . . . . . . . . . . . . . . 16 3.2 un Thread utilizza l’API Selector per gestire 3 canali di comunicazione. . . . . . . . . . . . . . . . . . . . . . . . . 18 3.3 diagramma degli stati del server implementato. . . . . . . 19 3.4 diagramma degli stati nella soluzione proposta da Pavan. . 22 4.1 Amazon AWS console. . . . . . . . . . . . . . . . . . . . . 26 4.2 Amazon EC2 console. . . . . . . . . . . . . . . . . . . . . . 28 4.3 elenco delle Amazon Machine Image da scegliere. . . . . . 29 4.4 finsetra per creare una nuova tabella. . . . . . . . . . . . . 30 4.5 finsetra per creare una nuova tabella. . . . . . . . . . . . . 31 4.6 diagramma della classe SimpleDBManager.java. . . . . . . 32 4.7 diagramma della classe DynamoDBManager.java. . . . . . 33 4.8 diagramma della classe SelectorThread.java. . . . . . . . . 35 4.9 diagramma della classe Server.java. . . . . . . . . . . . . . 37 5.1 diagramma della classe MultithreadClient.java. . . . . . . . 42 5.2 diagramma della classe MultiplexingClient.java. . . . . . . 44 v
  • 6. vi ELENCO DELLE FIGURE 5.3 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Totale messaggi inviati 200, Tempo di risposta medio 108,71 ms, minimo 38 ms, massimo 282 ms. . . . . . 45 5.4 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Totale messaggi inviati 10481, Tempo di risposta medio 261,51 ms, minimo 36 ms, massimo 57300 ms. . . . 46 5.5 rapporto tra quantit`a di dati inviati e tempi di risposta del server. SimpleDB come persistenza dati. Totale messaggi inviati 200, Tempo di risposta medio 3599,295 ms, minimo 203 ms, massimo 22638 ms. . . . . . . . . . . . . . . . . . 47 5.6 rapporto tra quantit`a di dati inviati e tempi di risposta del server. SimpleDB come persistenza dati. Totale messaggi inviati 200, Tempo di risposta medio 1494,07 ms, minimo 203 ms, massimo 4087 ms. . . . . . . . . . . . . . . . . . . 48 5.7 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 5 writes/s. Totale messaggi inviati 200, Tempo di risposta medio 1701,3 ms, minimo 193 ms, massimo 4426 ms. . . . 50 5.8 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale messaggi inviati 200, Tempo di risposta medio 904,06 ms, minimo 151 ms, massimo 2650 ms. . . . 51 5.9 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale messaggi inviati 800, Tempo di risposta medio 223,38 ms, minimo 137 ms, massimo 746 ms. . . . . 52 5.10 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 10 Thread come client. Totale messaggi inviati 658, Tempo di risposta medio 315,77 ms, minimo 144 ms, massimo 1529 ms. . . . . . . . . . . . . . . . . . . . . . . . 53
  • 7. ELENCO DELLE FIGURE vii 5.11 rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 100 thread come client. Totale messaggi inviati 1707, Tempo di risposta medio 8191,05 ms, minimo 280 ms, massimo 177857 ms. . . . . . . . . . . . . . . . . . . . 54
  • 10. x ELENCO DELLE TABELLE
  • 12. 2 ELENCO DELLE TABELLE
  • 13. Capitolo 1 Introduzione Questo documento rappresenta il risultato di uno studio di algoritmi e architetture utili per implementare un sistema ad alta affidabilit`a e alte prestazioni. Non si tratta semplicemente di un sistema capace di garantire un’elevata continuit`a del servizio ()uptime guarantee), ma deve essere in grado anche di non perdere informazioni in caso di guasti (fault tollerance) e di soddisfare un numero crescente di richieste (scalable). Un sistema per la gestione delle comunicazioni tra le centrali di smistamento SMS e applicazioni esterne (per avere news sul meteo o sulla viabilit`a) pu`o esserne un esempio. Per meglio comprendere la problematica da risolvere partiamo con l’analizzare l’architettura descritta nella Figura 1.1. Un insieme di client, il cui numero `e`e destinato ad aumentare nel tempo, comunicato con un server di controllo con il compito di effettuare alcuni calcoli sui dati ricevuti e restituire un messaggio “ok ricevuto” al client. Nel caso il server non sia raggiungibile a causa di un guasto alla rete, i client invieranno i propri messaggi ad un indirizzo IP alternativo dove un dispositivo inoltrer`a questi messaggi al server di controllo che eseguir`a le stesse operazioni sopra descritte. La quantit`a medi di dati che ogni singolo client invia pu`o essere stimata in 100B/h. Nel caso il server principale, a causa di un guasto, interrompe la propria esecuzione, tutti i client rinviano dopo cinque minuti i messaggi che non hanno avuto risposta sperando che il guasto si risolva nel pi`u breve tempo possibile. Il canale tra client e server utilizza una comunicazione GPRS a pa- 3
  • 14. 4 CAPITOLO 1. INTRODUZIONE Un elevato numero di client (centraline) inviano moltissimi messaggi di pochi dati al server per essere elaborati. Nel caso l'IP PRINCIPALE non sia raggiungibile, i clients inviano i loro messaggi ad un IP SECONDARIO. Database IP SECONDARIO IP PRINCIPALE Server che elabora le richieste dei client Dispositivo che esegue un semplice forward dei messaggi al server. Figura 1.1: Diagramma dell’attuale architettura da ottimizzare.
  • 15. 5 gamento; il costo viene calcolato in base al numero di Byte inviate e ricevuti, quindi varia in modo proporzionale al numero di messaggi che attraversano il canale: pi`u messaggi sono spediti, pi`u alti saranno i costi. Quindi in caso di guasti del sistema, ogni rinvio di un messaggio da parte dei client comporta un aumento del traffico nel canale e di conseguenza una lievitazione dei costi proporzionalmente al tempo di “blocco”. L’obbiettivo che si vuole raggiungere `e di progettare una nuova ar- chitettura che sia in grado di garantire un’alta affidabilit`a in modo da minimizzare il pi`u possibile situazioni di guasto.
  • 16. 6 CAPITOLO 1. INTRODUZIONE
  • 17. Capitolo 2 Rendere affidabile un sistema Rendere un sistema affidabile significa garantire l’operabilit`a (uptime guarantee) del sistema anche in presenza di guasti (fault tolerant). La maggior parte delle tecniche di tolleranza ai guasti fa uso di tecniche di ridondanza per replicare le sorgenti che contengono le informazioni (Server Pooling). Esistono due tipi di ridondanza: • spaziale, si utilizzano componenti hardware o software replicati in grado di sostituire l’elemento guasto; • temporale, basata sulla ripetizione di operazioni o sequenze di ope- razioni. Un sistema in cui le informazioni vengono replicate non offre solo una maggiore affidabilit`a, ma `e in grado di fornire: • migliori prestazioni, grazie ad una distribuzione del carico tra le varie repliche; questo permette inoltre di diminuire la latenza della comunicazione permettendo ai client di contattare “la replica” pi`u vicina; • una migliore disponibilit`a del servizio, se ogni replica `e isolata fi- sicamente ed elettricamente dalle altre, il guasto di una unit`a non influenza le altre; 7
  • 18. 8 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA Il rovescio della medaglia `e che la ridondanza aumenta la complessit`a di tutto il sistema poich´e bisogna gestire lo smistamento dei messaggi ver- so le varie repliche, mantenere una consistenza dei dati replicati ed infine mantenere il determinismo dello stato delle repliche, in pratica ricevere le stesse richieste da parte dei un client nello stesso ordine temporale. 2.1 Reliable Server Pooling Vista la complessit`a nel gestire un insieme di server replicati, nel 5 dicem- bre 2000 prese vita un gruppo di lavoro IETF RSerPool [1] con l’obbiet- tivo di sviluppare l’architettura e i protocolli necessari per la gestione e il funzionamento di gruppi di server che supportano applicazioni altamente affidabili [15]. Il framework realizzato permette ad un insieme di server con informa- zioni replicate di essere visti come un singolo punto di accesso ai dati; in caso di fallimenti le applicazioni che lo utilizzano possono essere deviate verso un altro server in modo del tutto trasparente e senza il riavvio della macchina. Nonostante il framework aiuti gli sviluppatori ad implementare appli- cazioni con un alto grado di affidabilit`a, presenta alcune problematiche da risolvere: • dal punto di vista infrastrutturale, l’acquisto e la gestione di tutta la parte hardware e sistemistica ha un costo molto alto; • il gruppo di lavoro `e stato chiuso il 27 aprile 2009, quindi non c’`e la possibilit`a di avere un supporto tecnico in caso di problemi. La soluzione a questi problemi sta nel Cloud Computing descritto nel prossimo paragrafo. 2.2 Cloud Computing Con la frase cloud computing si identifica quel servizio che fornisce ca- pacit`a di calcolo e di archiviazione ad una comunit`a eterogenea di utiliz- zatori finali in modo tale che quest’ultimi non si debbano minimamente
  • 19. 2.2. CLOUD COMPUTING 9 preoccupare di come gestire e garantire l’operabilit`a del sistema anche in caso di guasti, poich´e tali sistemi utilizzano una ridondanza spaziale in modo del tutto trasparente all’utilizzatore finale. Esistono tre tipologie di cloud computing: • Infrastructure as a Service (IaaS), • Platform as a Service (PaaS), • Software as a Service (SaaS). Nel caso di studio sappiamo che il numero dei client `e alto e si presume sia destinato ad aumentare con il passare del tempo, inoltre le comuni- cazioni tra client e server devono utilizzare il minor numero di messaggi possibili anche in caso di guasti in modo da consumare meno banda possi- bile. In altre parole il sistema deve essere scalabile e affidabile. I servizi di cloud computing hanno entrambi queste caratteristiche: in base al carico di lavoro e alla quantit`a di dati da archiviare `e possibile adattare (aumen- tando o diminuendo) la capacit`a di calcolo e di archiviazione in tempo reale; in caso di guasti hardware i dati sono replicati in modo trasparen- te e la capacit`a di calcolo `e garantita da un sistema di virtualizzazione. Tutto questo viene fornito a basso costo poich´e, come l’energia elettrica, si pagano solo le risorse consumate (modello pay-as-you-go) senza doversi preoccupare dell’amministrazione di grossi datacenter. Di seguito si analizzeranno alcuni tra i principali fornitori di servizi cloud nel mercato per identificarne il pi`u adatto al caso, tenendo presente di questi due vincoli: 1. la nostra applicazione sar`a scritta in Java visto che gran parte delle funzionalit`a sono gi`a state scritte in questo linguaggio; 2. la nostra applicazione dovr`a implementare una comunicazione clien- t/server tramite un socket TPC. 2.2.1 Google AppEngine Google AppEngine [10] offre un sistema per poter sviluppare applicazioni web a traffico elevato senza dover preoccuparsi di gestire l’infrastruttura.
  • 20. 10 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA Grazie a tale strumento `e possibile realizzare applicazioni web utilizzando tecnologie Java Standard (Java 6 JVM) e farle girare sull’infrastruttura scalabile di Google. Questo tipo di servizio rientra nella tipologia PaaS. Le istanze di calcolo, chiamate App Engine Backends, eseguono l’ap- plicazione con una capacit`a di memoria che va da 128MB fino a 1GB e una CPU con una frequenza configurabile da 600MHz a 4,8GHz. Altra caratteristica interessante `e la possibilit`a di definire il numero massimo di istanze attive; in base al volume di dati elaborato il sistema attiva nuove istanze automaticamente in modo da gestire l’aumento del carico di lavoro senza degradare le prestazioni dell’applicazione. Google Ap- pEngine permette inoltre di servire pi`u richieste in parallelo garantendo una corretta sincronizzazione tra i thread (thread safe). Google AppEngine `e dotato inoltre di un servizio per la persistenza dei dati (App Engine datastore [9]) con due diverse tipologie di data storage in base alla disponibilit`a e alla consistenza che si vuole garantire: • il Master/Slave datastore utilizza un sistema di replicazione master- slave che replica in modo asincrono il dato quando viene scritto fisicamente nel data center. Questa soluzione offre un’elevata con- sistenza in lettura e nelle interrogazioni, a fronte di temporanei periodi di indisponibilit`a del servizio in caso di problemi del data center o downtime pianificati; • nel High Replication datastore, i dati sono replicati (in modo sin- crono) in pi`u data center tramite un meccanismo basato sul Paxos algorithm (7). Questo sistema offre un alto grado di disponibilit`a, ma il prezzo da pagare `e un elevata latenza nella scrittura dei dati dovuta proprio alla replicazione distribuita. Ogni applicazione viene eseguita in un ambiente sandboxed sicuro ed isolato in modo da prevenire che un’applicazione interferisca con le altre. In questo ambiente le applicazioni non possono aprire socket TCP o ac- cedere ad un altro host direttamente, ma fare solamente richieste HTTP o HTTPS. Questa caratteristica rappresenta una restrizione critica per lo sviluppo di questo progetto, poich´e le classi gi`a sviluppate per la co- municazione client/server prevedono l’utilizzo di un canale TCP ed un
  • 21. 2.3. SCELTA DELL’INFRASTRUTTURA 11 protocollo ad-hoc. Utilizzare una comunicazione HTTP o HTTPS il vo- lume di ogni singolo pacchetto aumenterebbe a causa degli http headers da aggiungere al messaggio con l’inevitabile conseguenza di un aumento della quantit`a di dati scambiati che si traduce con un aumento dei costi del canale. Alla luce di questa limitazione si `e deciso di analizzare le ca- ratteristiche tecniche di un altro tra i principali fornitori di servizi cloud, descritto nel paragrafo seguente. 2.2.2 Amazon AWS Amazon AWS [4], a differenza di Google, offre un’infrastruttura modu- lare che permette di scegliere i componenti che pi`u si adattano al tuo problema creando un’architettura ad-hoc. In questo caso infatti si parla di un’infrastruttura come servizio IaaS. Oltre a servizi per il em data- storage (es. Amazon SimpleDB [3], Amazon DynamoDB [2]) mette a disposizione ambienti di calcolo virtuali personalizzabili dove `e possibile caricare differenti sistemi operativi, gestire i permessi di accesso alla re- te e personalizzare il proprio ambiente applicativo. Altra caratteristica importante `e che il servizio offerto `e “elastico” nel senso che `e in gra- do di scalare automaticamente in base al carico di lavoro, configurando opportunamente servizi di bilanciamento del carico e monitoraggio delle risorse garantendo un uptime di 99,95% durante un’intero anno. Grazie a queste caratteristiche `e possibile costruire un’architettura personalizzata combinando i vari servizi offerti secondo le proprie esigenze, senza pre- occuparsi della manutenzione dell’infrastruttura (es. upgrade, software patch, ecc.) con un notevole risparmio monetario. 2.3 Scelta dell’infrastruttura Dopo aver analizzato le caratteristiche di questi due sistemi (vedi Figura 2.1) la scelta `e ricaduta su Amazon. Per il salvataggio dei dati abbiamo scelto inizialmente per Amazon SimpleDB poich´e offriva sia un alta disponibilit`a del servizio che un’e- levata propensione alla scalabilit`a trattandosi di una base di dati non relazionale (NoSQL). Un vincolo imposto nel nostro progetto `e quello di
  • 22. 12 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA Amazon AWS Google AppEngine Java Yes Yes Socket TCP Yes No Latency in data persistence Good Bad System Customization Yes No martedì 23 ottobre 2012 Figura 2.1: Google AppEngine vs. Amazon AWS.
  • 23. 2.3. SCELTA DELL’INFRASTRUTTURA 13 Client Units Server (using Java NIO) Amazon EC2 Amazon SimpleDB Figura 2.2: Architettura con SimpleDB. avere un’alta consistenza in modo da leggere sempre l’ultimo dato scritto; SimpleDB `e in grado di garantire questo ma a discapito della latenza. In Figura 2.2 `e descritta l’architettura di partenza da cui siamo par- titi per implementare il server Java NIO [12] e l’interfacciamento con SimpleDB. Dai test fatti (vedi paragrafi successivi) le performace non si sono dimostrate soddisfacenti, quindi bisognava sostituire SimpleDB con un servizio che permettesse di diminuire il tempo di latenza dell’intero sistema. Amazon AWS ha lanciato il 19 gennaio 2012 un nuovo servizio per la persistenza dei dati chiamato Amazon DynamoDB. Questo nuovo ser- vizio oltre ai vantaggi gi`a visti con SimpleDB, ci consente di avere una forte consistenza nei dati che garantisce la lettura dell’ultimo dato scritto, mantenendo una bassa latenza a qualsiasi livello di scalabilit`a. L’inter- vento quindi `e stato indolore poich´e abbiamo introdotto una nuova classe java in grado di gestire la comunicazione con il nuovo database (Figura 2.3).
  • 24. 14 CAPITOLO 2. RENDERE AFFIDABILE UN SISTEMA Client Units Server (using Java NIO) Amazon EC2 Amazon DynamoDB Figura 2.3: Architettura con DynamoDB.
  • 25. Capitolo 3 Architettura proposta Una delle caratteristiche fondamentali dell’architettura che sar`a svilup- pata `e la scalabilit`a ovvero, la capacit`a del server di gestire una quantit`a crescente di connessioni simultanee. Un’approccio tradizionale, che si basa sull’utilizzo del thread-pooling per gestire pi`u connessioni, trova il suo principale limite nel costante au- mento del tempo dedicato al context switching tra i thread proporzionale all’aumento del numero di thread stessi. Inoltre, nel caso specifico di que- sta ricerca, ogni thread esegue operazioni I/O in modo concorrente che comporta un’ulteriore complessit`a al sistema peggiorandone le prestazio- ni. Il problema che nasce da un accesso concorrente sta nel fatto che la Java VM non sa quale thread `e pronto ad eseguire un’operazione I/O, pertanto potrebbe mettere in esecuzione un thread non ancora pronto per un operazione I/O, mentre lasciare in attesa un thread gi`a pronto. Nel peggior dei casi potrebbe accadere che un thread rimanga in attesa cos`ı a lungo da andare in timeout. Inoltre all’aumentare del numero di thread creati aumenta il carico per la CPU e la quantit`a di memoria che verr`a riservata allo stack di ogni thread degradando le prestazioni dell’intero sistema. Quest’ultima affermazione trova fondamento nei test fatti da Lawery [11] in cui la creazione di nuovi thread diventa sempre pi`u lenta all’aumentare dei thread creati (vedi Figura 3.1). In questi test i thread creati non eseguono alcuna operazione di cal- colo; in un caso reale dove ogni thread esegue operazioni di calcolo o di 15
  • 26. 16 CAPITOLO 3. ARCHITETTURA PROPOSTA Figura 3.1: all’aumentare dei thread creati aumenta il tempo di creazione degli stessi.
  • 27. 3.1. UTILIZZO DI JAVA NIO 17 I/O il degrado delle prestazioni sar`a di sicuro maggiore; questo si traduce in un aumento del tempo di esecuzione e anche un aumento dei tempi di risposta ai client. Per evitare che un canale di comunicazione quindi rimanga in attesa cos`ı a lungo da rischiare il timeout e quindi che il client non riceva la risposta attesa, bisognerebbe procedere con l’invio del messaggio non appena il canale `e pronto a scrivere e allo stesso tempo fare in modo che un singolo thread sia in grado di gestire pi`u connessioni simultaneamente (multiplexing). Questo tipo di operazioni possono essere gestita grazie alla programmazione ad eventi offerta dalla libreira Java NIO che verr`a descritta nel prossimo paragrafo. 3.1 Utilizzo di Java NIO A partire dalla versione J2SE 1.4 Java introduce il concetto di socket non- bloccanti grazie alla libreria NIO (New I/O) che permette di gestire pi`u connessioni TCP con un unico thread (vedi Figura 3.2) senza bloccarne l’esecuzione. Ogni canale deve gestire i seguenti quattro eventi: connect, accept, read e write. Quando un client invia un messaggio al server quest’ultimo attiva un canale che solleva l’evento accept; non appena il canale accetta la connessione viene sollevato l’evento connect. A questo punto il canale `e pronto per poter leggere i dati inviati dal client, quindi l’evento sollevato `e quello read; una volta letti tutti i dati ed elaborato la risposta il canale `e pronto per inviare la risposta quindi viene sollevato l’evento write. Questi eventi vengono gestiti dalla classe SelectorThread.java la quale si occupa di monitorare il Selector restando in attesa di eventi I/O e redistribuirli al gestore (handler) appropriato in base alla tipologia di evento generato. Nella nostra architettura avremo bisogno di un gestore per aprire connessioni in uscita, uno per accettare le connessioni in en- trata e uno per leggere e scrivere nel canale; questi gestori dovranno implementare rispettivamente le interfacce ConnectSelectorHandler, AcceptSelectorHandler e ReadWriteSelectorHandler.
  • 28. 18 CAPITOLO 3. ARCHITETTURA PROPOSTA Thread Selector Channel Channel Channel Figura 3.2: un Thread utilizza l’API Selector per gestire 3 canali di comunicazione.
  • 29. 3.1. UTILIZZO DI JAVA NIO 19 Waiting for Data (read interest) Reading (no interest) Processing Request (no interest) Writing (no interest) Waiting to write (write interest) Request partially read (enable read interest) Ready to read Request read Send reply (enable write interest) Ready to write Replay partially sent (enable write interest) Replay sent (enable read interest) Start Figura 3.3: diagramma degli stati del server implementato.
  • 30. 20 CAPITOLO 3. ARCHITETTURA PROPOSTA Aspetto di fondamentale importanza in una architettura dove le ope- razioni di scrittura e lettura non sono bloccanti `e la gestione dei dati parzialmente letti o scritti. Quando viene generato un evento di lettura significa che nel read-buffer del socket sono disponibili alcuni byte, ma non `e precisato se questi byte sono una parte di un pacchetto, un pac- chetto intero o pi`u pacchetti. Lo stesso vale in fase di scrittura, i byte vengono scritti nel write-buffer del socket in base allo spazio disponibile nel buffer senza preoccuparsi che i dati del pacchetto siano stati scritti completamente. Nello schema proposto da Nuno Santos nel suo articolo [14] sono evi- denziati cinque stati per descrivere il ciclo di vita di un handler (vedi Figura 3.3): 1. Waiting for data: l’handler `e in attesa di ricevere i dati provenienti dal canale di comunicazione, quindi per prima cosa attiva l’interes- se nel ricevere un evento di tipo lettura e attende che esso venga generato; 2. Reading: dopo aver ricevuto un evento di lettura, l’handler riceve i dati provenienti dal socket e inizzia a riassemblare il pacchetto. Durante questo stato non c’`e l’interesse a ricevere nessun altro tipo di evento. Se il pacchetto viene riassemblato completamente si passa allo stato successivo (stato 3), altrimenti vengono salvati i dati parziali e viene riattivato l’interesse in lettura (stato 1). 3. Processing request: non appena il pacchetto viene completamente riassemblato, l’handler inizia a processarlo. Nel nostro caso prov- veder`a a controllare la validit`a del CRC e poi proceder`a a salvare il messaggio nel database. Finch´e l’handler rimane in questo stato, tutti gli interessi in eventi di tipo I/O sono disabilitati. 4. Writing: finito di processare il pacchetto e non appena la risposta `e pronta l’handler la spedisce immediatamente utilizzando una scrit- tura non-bloccante. Se non c’`e spazio sufficiente nel write-buffer del socket per contenere tutto il pacchetto, sar`a necessario spedire il resto dei dati successivamente passando allo stato 5, altrimenti
  • 31. 3.2. LIMITI NEL PROTOTIPO DI PAVAN 21 l’handler si preparer`a a ricevere il pacchetto successivo tornando allo stato 1. 5. Waiting to write: se una scrittura non-bloccante ritorna senza aver scritto tutti i dati che compongono il pacchetto di risposta, l’hand- ler attiva l’interesse nel ricevere un evento di scrittura. Quando il buffer di scrittura avr`a nuovamente dello spazio disponibile, verr`a sollevato l’evento per scrivere il resto del pacchetto. Il ciclo di vita dell’handler permette di comprendere cosa accade quan- do riceve o spedisce dati attraverso il canale di comunicazione. Molto interessante risulta la gestione dell’interesse in lettura/scrittura descritta nel diagramma di stato in Figura 3.3. Grazie a questo meccanismo `e possibile capire: • in fase di lettura quando un’intero pacchetto di dati `e stato ricevuto e quindi procedere con l’elaborazione dei dati; • in fase di scrittura quando il pacchetto di risposta `e stato inviato completamente e quindi verificare se ci sono altri dati da leggere o se chiudere la comunicazione. Questo rende l’architettura riutilizzabile per risolvere svariate proble- matiche con protocolli di comunicazione differenti tra loro implementando l’interfaccia ProtocolDecoder.java a seconda dei casi. 3.2 Limiti nel prototipo di Pavan Il prototipo sviluppato da Pavan e descritto nella sua tesi [13] presen- ta l’implemetazione di un server ECHO dove ogni pacchetto inviato al server viene immediatamente restituito per intero al client senza alcuna elaborazione. In questo prototipo non esistono dei gestori (handlers) che vengono invocati a seconda della tipologia di evento sollevato; il server ServerSelect.java continua ad iterare l’insieme dei canali attivi e appe- na ne trova uno pronto ad effettuare un’operazione di lettura, cattura tutti i dati presenti nel buffer per poi immediatamente inviarli indietro
  • 32. 22 CAPITOLO 3. ARCHITETTURA PROPOSTA Waiting to Data Reading Writing Replay sent Start Send replay Ready to read Figura 3.4: diagramma degli stati nella soluzione proposta da Pavan. al client cos`ı come sono arrivati. Il buffer del canale ha una dimensione di 16KB ed i pacchetti possono avere una dimensione massima di 1200 Byte, quindi il buffer ha una dimensione tale per contenere l’intero mes- saggio. Il primo limite di questa implementazione quindi `e la dimensione dei pacchetti che possono essere scambiati tra client e server. Nello scenario presentato nell’introduzione (capitolo 1) dove ogni mes- saggio `e formato da pochi Byte il problema non si pone, ma con una vi- sione aperta al futuro e alla possibilit`a di applicare una tale architettura ad altri servizi (es. trasferimento di video da client a server o viceversa) la soluzione di Pavan `e piuttosto limitativa. Inoltre impostare un buffer di 16KB pu`o raggiungere facilmente run out of memory all’aumentare del numero di connessioni e quindi non essere scalabile. Altro limite nell’im- plementazione di Pavan deriva dalla mancanza di uno stato che disabiliti momentaneamente la possibilit`a di sollevare altri eventi I/O durante l’e- laborazione dei dati contenuti nel pacchetto (nel nostro caso controllo del
  • 33. 3.3. THREAD IN UN MONDO MULTIPLEXED 23 CRC e salvataggio dei dati). Operazioni come il salvataggio dei dati in un database sono di tipo bloccante ed hanno un tempo di latenza pi`u o meno lungo in base alla tipologia di tecnologia utilizzata; in questa fase il server deve mettere in attesa il canale finch´e la risposta da inviare al client non sar`a pronta. Il non rispetto di una tale attesa potrebbe de- terminare situazione di perdita di dati dovuta all’interruzione prematura delle operazioni di salvataggio. In base a queste considerazioni il prototipo di Pavan, presentato nel diagramma di stato illustrato nella Figura 3.4, dev’essere rivisto e reimplementato sequendo quanto descritto nel Paragrafo 3.1. 3.3 Thread in un mondo Multiplexed Utilizzare un singolo thread che svolga tutto il lavoro (dispatch and pro- cess request) ha il grosso limite di non poter “nascondere” la latenza necessaria per operazioni I/O su disco; di conseguenza lettura e scrittu- ra su database influenzano negativamente le prestazioni di calcolo (Java NIO non supporta operazioni non-blocking su filesystem). Per prima cosa vediamo in modo generico quali sono le tipologie di architettura che `e possibile realizzare per risolvere il problema: • M dispatchers/no workers: parecchi thread di event-dispatcher che eseguono tutto il lavoro; • 1 dispatcher/N workers: un singolo event-dispatcher thread e unit`a elaborative multiple; • M dispatchers/N workers: thread multipli per lo smistamento degli eventi e unit`a elaborative multiple; Solitamente per risolvere un problema complesso si cerca di suddivi- derlo in due o pi`u sotto problemi di facile risoluzione, ossia divide et im- pera. L’idea iniziale quindi era quella di suddividere gli event-dispatcher e i worker (unit`a elaborative) in pi`u thread; in questo modo il dispatcher delega ai worker la parte di elaborazione e salvataggio sul database e si
  • 34. 24 CAPITOLO 3. ARCHITETTURA PROPOSTA occupa solamente di gestire le connessioni con i client. Basandosi sull’e- sperienza fatta da Santos questa non `e la soluzione migliore poich´e un’ar- chitettura del genere presenta un grande numero di punti di interazione tra dispatcher e worker rendendo altamente problematica la sincronizza- zione nell’accedere al database, essendo una risorsa condivisa. Come `e stato descritto all’inizio di questo Capitolo quando pi`u thread devono ac- cedere in maniera concorrente una risorsa condivisa, sorge il problema di implementare un codice thread-safe. Questo si scontra con la natura del Selector e i suoi SelectionKey in quanto non sono “sicuri” per accessi multi-thread. Se un SelectionKey viene modificato da un thread mentre un altro thread sta chiamando il metodo select() dell’oggetto Selector a cui appartiene lo stesso SelectionKey, il risultato `e che select() termi- na brutalmente con un’eccezione. Questo solitamente accade quando un worker chiude i canali di comunicazione e indirettamente cancella anche i SelectionKey corrispondenti, quindi se select() viene chiamato, trover`a il SelectionKey cancellato inaspettatamente. Santos dopo un tentativo per rafforzare il sistema con regole pi`u severe nel controllo dei Selector, le proprie SelectionKeys e i RegisteredChannels, si `e accorto che l’architet- tura iniziava a diventare sempre pi`u complicata, inefficiente e propensa agli errori. Alla luce di queste considerazioni `e stato scelto di partire con l’archi- tettura pi`u semplice (M dispatchers/no workers). Come abbiamo visto all’inizio di questo capitolo il numero di thread va limitato altrimenti le prestazioni rischiano di degenerare. Secondo Santos, l’ideale sarebbe da mantenere costante ad un valore di 4 o 5 il rapporto tra numero di dispatcher e CPU: dispatchers CPUs = 5 Prima di inoltrarci in problematiche relative al numero di thread in esecuzione ed il loro bilanciamento del carico, `e opportuno capire se il codice proposto da Santos, opportunamente modificato, `e idoneo a questo progetto. Fondamentale capire quindi se i tempi di risposta del server sono soddisfacenti o meno.
  • 35. Capitolo 4 Implementazione In questo capitolo verr`a illustrato per prima cosa come configurare gli ambienti di calcolo e di persistenza dei dati con Amazon AWS e il co- dice Java utilizzato per interfacciarsi con tali servizi. Come ultima cosa vedremo il funzionamento del Server e le modifiche apportate al codice proposto da Santos. 4.1 Come attivare i servizi Amazon AWS Per utilizzare l’infrastruttura offerta da Amazon bisogna creare un ac- count utente registrandosi al sito http://aws.amazon.com. Amazon da la possibilit`a di testare i propri servizi gratuitamente grazie al AWS Free Usage Tier; l’infrastruttura offerta in questo caso ha alcune limitazioni (vedi dettaglio [5]), che abbiamo dovuto superare durante i test in mo- do da raggiungere buone performance. Come vedremo nel paragrafo dei test, abbiamo dovuto superare questi limiti gratuiti. Creato l’account sar`a possibile configurare i servizi di cui si ha biso- gno grazie alla console (vedi Figura 4.1) che Amazon mette a disposizione all’utente. Per questo progetto `e stata utilizzata un’istanza EC2 dove ab- biamo installato il server. L’attivazione `e molto semplice, basta cliccare sul link “EC2 Virtual Services in the Cloud” per aprire la console di ge- stione delle istanze virtuali di EC2 (vedi Figura 4.2), cliccare sul bottone 25
  • 36. 26 CAPITOLO 4. IMPLEMENTAZIONE Figura 4.1: Amazon AWS console.
  • 37. 4.2. AMAZON SIMPLEDB 27 “Launch Instance e scegliere la macchina virtuale che si vuole utilizzare (vedi Figura 4.3). Per attivare il database si deve cliccare sul link “DynamoDB” presen- te sempre nella console (Figura 4.1) alla sezione “Database”. Una volta entrati nella console di DymanoDB si procede con la creazione delle ta- belle (Figura 4.4) indicando il nome della tabella e il nome della chiave primaria. Successivamente bisogna indicare la capacit`a di lettura e scrit- tura1 con la quale si vuole lavorare; il valore di default per il servizio gratuito `e di 10 unit`a/sec per la lettura e 5 unit`a/sec per la scrittura (Figura 4.5). 4.2 Amazon SimpleDB Amazon mette a disposizione una libreria Java [6] offrendo un insieme di API in grado di interagire con l’infrastruttura dei servizi AWS. Per gli sviluppatori che utilizzano Eclipse Java IDE, `e possibile installare il plug- in open source AWS Toolkit for Eclipse [7], che permette di agevolarli in fase di sviluppo, debug e deploy dell’applicazione. Nel caso specifico di SimpleDB, Amazon fornisce una serie di API che permettono di creare domini (domains, astrazione che pu`o essere paragonata alle tabelle per un database relazionale) e elementi (items, astrazione che pu`o essere parago- nata ai record di una tabella), interrogare i dati, aggiornarli e rimuoverli dalla base di dati. Qui di seguito metto in evidenza i frammenti di codice utilizzati nel nostro prototipo. Per interagire con la base di dati viene utilizzata la classe AmazonSimpleDBClient che mette a disposizione i metodi per accedere e manipolare i dati in essa contenuti. // Get PropertiesCredentials PropertiesCredentials pCredentials = 1 una unit`a di scrittura permette di effettuare una scrittura al secondo per elementi di dimensione non superiore a 1KB. Allo stesso modo una unit`a di lettura permette di effettuare una lettura consistente al secondo di un elemento di dimensioni non superiori a 1KB. Se l’elemento ha una dimensione superiore allora richieder`a una maggior capacit`a in scrittura o lettura.
  • 38. 28 CAPITOLO 4. IMPLEMENTAZIONE Figura 4.2: Amazon EC2 console.
  • 39. 4.2. AMAZON SIMPLEDB 29 Figura 4.3: elenco delle Amazon Machine Image da scegliere. new PropertiesCredentials(ClassLoader.getSystemResourceAsStream( 'AwsCredentials.properties')); // Activate the Amazon SimpleDB Client this.sdb = new AmazonSimpleDBClient(new BasicAWSCredentials( pCredentials.getAWSAccessKeyId(), pCredentials.getAWSSecretKey())); this.sdb.createDomain(new CreateDomainRequest(domain)); Ogni item (oggetto di tipo ReplaceableItem) contenuto nel dominio `e rappresentato da un nome identificativo (rappresenta la primary key) e da un numero arbitrario di attributi (oggetto di tipo ReplaceableAttribute) a seconda delle nostre esigenze. List<ReplaceableItem> data = new ArrayList<ReplaceableItem>(); data.add(new ReplaceableItem().withName(String.valueOf(recordId)) .withAttributes(new ReplaceableAttribute() .withName(”clientId”).withValue(clientId), new ReplaceableAttribute().withName(”body”) .withValue(String.valueOf(byteRead)), new ReplaceableAttribute().withName(”created”).withValue( String.valueOf(System.currentTimeMillis())) ));
  • 40. 30 CAPITOLO 4. IMPLEMENTAZIONE Figura 4.4: finsetra per creare una nuova tabella.
  • 41. 4.2. AMAZON SIMPLEDB 31 Figura 4.5: finsetra per creare una nuova tabella.
  • 42. 32 CAPITOLO 4. IMPLEMENTAZIONE Figura 4.6: diagramma della classe SimpleDBManager.java. 4.3 Amazon DynamoDB Con l’uscita di DymanoDB Amazon ha aggiornato la propria libreria Java [8] inserendo le nuove funzionalit`a per interagire con il nuovo servizio. La modalit`a di operare `e la stessa vista precedentemente con SimpleDB, cambiano ovviamente i comandi utilizzati come `e possibile vedere qui di seguito. // Get PropertiesCredentials InputStream credentialsAsStream = Thread.currentThread() .getContextClassLoader() .getResourceAsStream(”AwsCredentials.properties”); AWSCredentials credentials = new PropertiesCredentials( credentialsAsStream); // Activate the Amazon DynamoDB Client this.dynamoDB = new AmazonDynamoDBClient(credentials); // Create an item Map<String, AttributeValue> item = new HashMap<String, AttributeValue>(); item.put(”messageId”, new AttributeValue(messageId));
  • 43. 4.4. IL SERVER 33 Figura 4.7: diagramma della classe DynamoDBManager.java. item.put(”clientId”, new AttributeValue(clientId)); item.put(”body”, new AttributeValue(body)); item.put(”created”, new AttributeValue().withN( Long.toString(created))); 4.4 Il server Il cuore del server `e rappresentato dal ciclo while, riportato qui di segui- to, responsabile della gestione degli eventi che si trova all’interno della classe io.SelectorThread descritta dall’illustrazione Figura 4.8. Questa classe `e stata implementata ispirandosi al modello di gestione degli eventi utilizzato dal toolkit SWING di Java: gli eventi generati dall’interfaccia
  • 44. 34 CAPITOLO 4. IMPLEMENTAZIONE utente vengono salvati in una coda che viene controllata da un thread (istanza della classe java.awt.EventQueue) che si occupa di instradare gli eventi in entrata verso il listener interessato. La classe EventQueue deve assicurare che tutte le operazioni eseguite dagli oggetti AWT siano effettuate in un unico thread. Allo stesso modo la classe SelectorThread ha incarico simile, in particolare: • solo il thread creato dall’istanza di questa classe ha accesso al Se- lector e a tutti i socket da esso gestiti. Se un thread esterno vuole avere accesso agli oggetti gestiti da questo Selector, allora dovr`a utilizzare i metodi invockeLater() o invokeAndWait(). • il thread non dev’essere utilizzato per eseguire lunghe operazioni. Come gi`a detto nel Capitolo 3, questo tipo di architettura `e stata scel- ta principalmente per mantenere basso il livello di complessit`a evitando l’onere di mantenere la sicronizzazione tra i thread. Vediamo in dettaglio ora il funzionamento del ciclo principale. Per prima cosa vengono eseguiti eventuali task la cui esecuzione era stata programmata da thread esterni (tramite le chiamate invockeLater() o invokeAndWait()). public void run() { // Here's where everything happens. The select method will // return when any operations registered above have occurred, the // thread has been interrupted, etc. while (true) { // Execute all the pending tasks. doInvocations(); ... ... ... } } A questo punto si controlla che non ci sia una richiesta di chiusura del Selector, in tal caso vengono portate a termine tutte le attivit`a in corso e poi il Selector viene chiuso. Se non ci sono richieste di chiusura, si procede con il verificare se qualcuno `e pronto ad eseguire operazioni di
  • 45. 4.4. IL SERVER 35 Figura 4.8: diagramma della classe SelectorThread.java.
  • 46. 36 CAPITOLO 4. IMPLEMENTAZIONE I/O; tramite la chiamata select() `e possibile capire quante operazioni (identificate da una SelectedKey) tra i vari canali registrati sono pronte per essere eseguite. // Time to terminate? if (closeRequested) { return; } Iterando questo insieme di selection key le operazioni vengono distri- buite al gestore appropriato, una volta che tutte le operazioni sono state eseguite si ritorna all’inizio del loop principale e si ripete tutto il processo di nuovo. // Someone is ready for IO, get the ready keys Iterator it = selector.selectedKeys().iterator(); // Walk through the collection of ready keys and dispatch // any active event. while (it.hasNext()) { SelectionKey sk = (SelectionKey)it.next(); ... ... ... } Prendiamo ora in analisi la classe AsdaaServer.java (vedi Figura 4.9)che avvia il server. Questa classe `e un’estensione di Server.java implementata da Santos a cui abbiamo aggiunto alcune modifiche per adattarla al nostro problema. Due sono le fasi principali quando viene instanziato l’oggetto di tipo AsdaaServer: 1. aprire il socket e abilitare il Selector a ricevere connessioni (chia- mata super(listenPort)); 2. aprire la connessione con la base di dati (vedi il Paragrafo 4.3 per come creare la connessione e salvare i dati). Vediamo cosa accade all’interno della chiamata super(listenPort). Per prima cosa si istanzia un oggetto di tipo SelectorThread e lo si abilita ad accettare connessioni alla porta specificata dal parametro listenPort.
  • 47. 4.4. IL SERVER 37 Figura 4.9: diagramma della classe Server.java.
  • 48. 38 CAPITOLO 4. IMPLEMENTAZIONE public Server(int listenPort) throws Exception { st = new SelectorThread(); Acceptor acceptor = new Acceptor(listenPort, st, this); acceptor.openServerSocket(); logger.info(”Listening on port: ” + listenPort); System.out.println(”Listening on port: ” + listenPort); } Per abilitare il Selector viene chiamato il metodo openServerSocket() che si preoccupa di creare un socket e di registrare il nuovo canale di co- municazione con l’istruzione registerChannelLater() abilitandolo ad accettare streaming di dati passandogli come parametro la tipologia di evento SelectionKey.OP ACCEPT. public void openServerSocket() throws IOException { ssc = ServerSocketChannel.open(); InetSocketAddress isa = new InetSocketAddress(listenPort); ssc.socket().bind(isa, 100); // This method might be called from any thread. We must use // the xxxLater methods so that the actual register operation // is done by the selector's thread. No other thread should access // the selector directly. ioThread.registerChannelLater(ssc, SelectionKey.OP_ACCEPT, this, new CallbackErrorHandler() { public void handleError(Exception ex) { listener.socketError(Acceptor.this, ex); } }); } Non appena un client si connette al server viene creato un PacketChannel per gestire la connessione e tutti i dati che verranno inviati nel canale. Non dobbiamo trascurare il fatto che tra i parametri passati a questa chiamata c’`e l’oggetto new SimpleProtocolDecoder(); esso identifica il protocollo utilizzato per lo scambio di messaggi, quindi premette di stabilire quando il server ha ricevuto per intero il messaggio inviato dal client. public void socketConnected(Acceptor acceptor, SocketChannel sc) { logger.info(”[” + acceptor + ”] Socket connected: ” + sc.socket().getInetAddress()); try {
  • 49. 4.4. IL SERVER 39 // We should reduce the size of the TCP buffers or else we will // easily run out of memory when accepting several thousands of // connctions sc.socket().setReceiveBufferSize(2 ∗ 1024); sc.socket().setSendBufferSize(2 ∗ 1024); // The contructor enables reading automatically. PacketChannel pc = new PacketChannel(sc, st, new SimpleProtocolDecoder(), this); pc.resumeReading(); } catch (IOException e) { e.printStackTrace(); logger.error(”[” + sc.socket().getInetAddress().getHostAddress() + ”:” + sc.socket().getPort() + ”] Error on socket connection: ” + e.getMessage()); } } Una volta che il messaggio `e stato ricevuto per intero dal server verr`a chiamato il metodo packetArrived(PacketChannel pc, ByteBuffer pckt). Per prima cosa si converte il ByteBuffer in un byte array, si salvano i dati nella base di dati e per finire invia indietro al client il messaggio “ok ricevuto”. Prima di spedire il pacchetto indietro, viene eseguita l’istru- zione pckt.flip() reimpostanto a zero la posizione corrente del buffer. Questa metodo dev’essere invocato ogni volta che, dopo una lettura dal canale, si vuole preparare una sequenza di scrittura. All’interno di questo metodo dovr`a essere inserita anche la verifica CRC del messaggio che per ora non `e stata ancora implementata. public void packetArrived(PacketChannel pc, ByteBuffer pckt) { logger.info(”[” + pc.toString() + ”] Packet received. Size: ” + pckt.remaining()); // Convert ByteBuffer to bytearray byte[] bytearray = new byte[pckt.remaining()]; pckt.get(bytearray); // Save to the repository dynamoDB.save(pc.toString(), new String(bytearray)); // Send packet back to the client logger.debug(”Send packet back”); pckt.flip(); pc.sendPacket(pckt); logger.debug(”Packet sent.”); }
  • 50. 40 CAPITOLO 4. IMPLEMENTAZIONE
  • 51. Capitolo 5 Prove Sperimentali In questo paragrafo metteremo in luce le problematiche che abbiamo in- contrato nello sviluppo dell’integrazione tra il server Java e i servizi offerti da Amazon AWS. Grazie ai test fatti abbiamo potuto individuare i limiti dei servizi Amazon e trovare soluzioni alternative per poter oltrepassarli. 5.1 Preparazione dei test Per ogni test abbiamo creato un archivio JAR per il server e uno per il client in modo da agevolarne l’esecuzione. Nel corso del progetto abbiamo sviluppato due tipologie di client che propongono due differenti approcci: • MultithreadClient, (classe descritta in Figura 5.1) implementa il metodo tradizionale un thread per connessione TCP. Questo ci permette di simulare uno scenario che si avvicina maggiormente alla realt`a: ogni connessione attiva invia i messaggi ad intervalli di tempo casuali. • MultiplexingClient, (classe descritta in Figura 5.2) utilizza la libre- ria Java NIO che permette di gestire le connessioni multiple con un unico thread. Questo client per prima cosa apre tutte le con- nessioni con il server e poi abilita l’invio dei messaggi al server. I messaggi vengono inviati in modo sequenziale, in altre parole ogni 41
  • 52. 42 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.1: diagramma della classe MultithreadClient.java.
  • 53. 5.1. PREPARAZIONE DEI TEST 43 connessione aperta attende di ricevere risposta dal server prima di inviare un nuovo messaggio. Questo approccio permette di indi- viduare pi`u facilmente quante connessioni il server `e in grado di gestire simultaneamente. Per avviare questi client vengono utilizzati due script bash ai quali `e possibile passare il numero di porta del server e il numero di thread da lanciare (per multithreadClient.sh) o il numero di connessioni da aprire (per multiplexingClient.sh). 5.1.1 Hardware utilizzato A lato server `e stato utilizzata un’istanza Amazon EC2 Linux Micro con 613MB di RAM, 8GB di spazio su disco e Linux 3.2. Per la persistenza dei dati `e stato utilizzato inizialmente Amazon SimpleDB con a dispo- sizione 25 ore macchina e 1GB di spazio al mese; nella seconda fase del progetto `e stato utilizzato Amazon DynamoDB con 100MB di spazio, una capacit`a di scrittura di 5 write/sec e una capacit`a di lettura di 10 read/sec. Per migliorare le performance dei test la capacit`a di scrittura e di lettura sono state incrementate fino a 320 writeread/sec. Il client —————– (recuperare dati client). La rete utilizzata nei test `e quella del dipartimento di Informatica dell’universit`a Ca’ Foscari che offre una banda di 100Mbps nell’intranet (LAN) e una banda di 10GBps per la connessione verso l’esterno (WAN). Server e Client sono installati su macchine che non appartengono alla stessa LAN. 5.1.2 Modalit`a di esecuzione dei test I test fatti li possiamo classificare in tre momenti: 1. un semplice test preliminare sul server realizzato senza persistenza dei dati, lo scopo era quello di verificare la corretta comunicazione tra client e server; 2. test per verificare le prestazioni dell’architettura persistendo i dati con Amazon SimpleDB;
  • 54. 44 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.2: diagramma della classe MultiplexingClient.java.
  • 55. 5.2. RISULTATI TEST SENZA PERSISTENZA 45 Figura 5.3: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Totale messaggi inviati 200, Tempo di risposta medio 108,71 ms, minimo 38 ms, massimo 282 ms. 3. test per verificare le prestazioni dell’architettura persistendo i dati con Amazon DynamoDB; 5.2 Risultati test senza persistenza L’andamento a “scalini” visibile nella Figura 5.3 indica come i tempi di ri- sposta tendono ad aumentare non in maniera proporzionale all’aumentare della dimensione del messaggio, a quanto pare esiste un intervallo entro cui la latenza sembra stabilizzarsi. Aumentando di 50 volte il numero di connessioni (Figura 5.4) i tempi di latenza hanno dei picchi piuttosto alti, ma con una frequenza cos`ı bassa che la latenza media mantiene un buon valore.
  • 56. 46 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.4: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Totale messaggi inviati 10481, Tempo di risposta medio 261,51 ms, minimo 36 ms, massimo 57300 ms.
  • 57. 5.3. RISULTATI TEST CON AMAZON SIMPLEDB 47 Figura 5.5: rapporto tra quantit`a di dati inviati e tempi di risposta del server. SimpleDB come persistenza dati. Totale messaggi inviati 200, Tempo di risposta medio 3599,295 ms, minimo 203 ms, massimo 22638 ms. 5.3 Risultati test con Amazon SimpleDB Osservando questi due test si pu`o notare come la latenza abbia un valore molto alto gi`a con poche connessioni attive. Questi test sono stati fatti a distanza di qualche ora e nonostante abbiamo gli stessi parametri d’in- gresso, hanno un andamento e delle prestazioni completamente diverse; questo `e da attribuirsi alla diversa congestione della linea internet utiliz- zata in facolt`a. Non sono stati fatti ulteriori test con SimpleDB perch´e ritenuti gi`a troppo poco performanti questi appena fatti.
  • 58. 48 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.6: rapporto tra quantit`a di dati inviati e tempi di risposta del server. SimpleDB come persistenza dati. Totale messaggi inviati 200, Tempo di risposta medio 1494,07 ms, minimo 203 ms, massimo 4087 ms.
  • 59. 5.4. RISULTATI TEST CON AMAZON DYNAMODB 49 5.4 Risultati test con Amazon DynamoDB Dai risultati ottenuti nel primo test con DynamoDB (Figura 5.7) sem- bra non essere cambiato nulla rispetto alle performance di SimpleDB. Il motivo sta nella capacit`a di scrittura nella base di dati; la configurazio- ne base del servizio prevede al massimo 5 write/sec. Quando arrivano messaggi con una frequenza pi`u alta, questi vengono accodati e elabo- rati appena possibile (paradigma FIFO) determinando un rallentamento del tempo di risposta. Una unit`a di scrittura permette di scrivere nella base di dati un elemento di al massimo 1KB al secondo. Se l”elemento supera questo limite `e necessario calcolare la capacit`a di cui si ha bi- sogno; ad esempio se ogni elemento ha dimensioni di 1,5KB e si vuole ottenere 100 scritture al secondo dovremmo approvvigionarci 100 (wri- te/sec) x 2 (1,5KB arrotondato all’intero superiore pi`u vicino) = 200 unit`a di capacit`a. La capacit`a di scrittura ha un costo che varia in base alla regione in cui si trova DynamoDB che si utilizza; in questa pagina http://aws.amazon.com/dynamodb/pricing/ ci sono i prezzi che variano da un minimo di $ 0,01 ad un massimo di $ 0,012 all’ora per ogni 10 unit`a di scrittura. Visto il limitato budget a disposizione per effettuare i test abbiamo scelto una capacit`a di scrittura pari a 320 writes/sec che abbiamo preventivamente stimato come una spesa media mensile di circa $ 275,96. L’aumento della capacit`a di scrittura ha diminuito i tempi di laten- za (Figura 14) migliorando discretamente le prestazioni. Il sospetto di questo risultato si fonda sull’implementazione del MultiplexingClient che gestisce connessioni multiple sullo stesso thread e ha un degrado delle prestazioni all’aumentare delle connessioni dovute all’overhead determi- nato dal context-switching nel passare da una connessione all’altra. A confermare questo sospetto `e stato il test descritto in Figura 15 dove abbiamo utilizzato un’unica connessione. Alla luce di questo abbiamo implementato un client multi-thread per abbattere l’overhead di gestione e avere un singolo thread per ogni connessione. Il risultato ottenuto in Figura 16 dimostra come i tempi di risposta siano migliorati di quasi 2/3 rispetto a prima, confermando come la ge- stione di connessioni multiple con un singolo thread sia un’operazione
  • 60. 50 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.7: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 5 writes/s. Totale mes- saggi inviati 200, Tempo di risposta medio 1701,3 ms, minimo 193 ms, massimo 4426 ms.
  • 61. 5.4. RISULTATI TEST CON AMAZON DYNAMODB 51 Figura 5.8: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale messaggi inviati 200, Tempo di risposta medio 904,06 ms, minimo 151 ms, massimo 2650 ms.
  • 62. 52 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.9: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s. Totale messaggi inviati 800, Tempo di risposta medio 223,38 ms, minimo 137 ms, massimo 746 ms.
  • 63. 5.4. RISULTATI TEST CON AMAZON DYNAMODB 53 Figura 5.10: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 10 Thread come client. Totale messaggi inviati 658, Tempo di risposta medio 315,77 ms, minimo 144 ms, massimo 1529 ms. costosa. La stessa cosa si verifica anche a lato server, all’aumentare delle connessioni simultanee da gestire i tempi di risposta di dilatano (vedi Figura 17).
  • 64. 54 CAPITOLO 5. PROVE SPERIMENTALI Figura 5.11: rapporto tra quantit`a di dati inviati e tempi di risposta del server. Utilizzato DynamoDB per la persistenza, 320 writes/s, 100 thread come client. Totale messaggi inviati 1707, Tempo di risposta medio 8191,05 ms, minimo 280 ms, massimo 177857 ms.
  • 65. Capitolo 6 Conclusioni In questo documento `e stato illustrato come un server ad alte prestazioni implementato grazie all’utilizo della libreria Java NIO, sia in grado di in- terfacciarsi ad una base di dati dove poter salvare i dati inviati dai client. Lo studio dell’architettura per questo progetto ha preso spunto dall’im- plementazione fatta da Cristian Pavan e descritta nella testi “Server ad Alte Prestazioni e ad Alta Affidabilit`a [13]. Una modifica sostanziale al server di Pavan riguarda il differente comportamento (vedi il diagramma di stato in Figura 3.3) nel gestire il flusso di dati in ricezione ed invio; questo permette di avere una maggiore flessibilit`a di dimensione dei pac- chetti scambiati tra client e server, un vantaggio non indifferente visto che la tendenza per il futuro `e quella di scambiare moli di dati sempre maggiori. Altra cosa che differenzia questo progetto da quanto fatto nel- la tesi di Pavan `e l’utilizzo di un servizio di Cloud Computing sia per il calcolo che per lo storage dei dati definendo un’architettura nuova che non si basa sui soliti schemi dove il centro di elaborazione viene gestito internamente. I risultati ottenuti nei primi test senza la persistenza dei dati sono molto buoni 55
  • 66. 56 CAPITOLO 6. CONCLUSIONI
  • 68. 58 CAPITOLO 6. CONCLUSIONI
  • 69. Appendice A Codice sorgente A.1 Server A.1.1 Server.java /∗ (c) 2004, Nuno Santos, nfsantos@sapo.pt relased under terms of the GNU public license http://www.gnu.org/licenses/licenses.html#TOCGPL ∗/ package handlers; import io.SelectorThread; import java.io.IOException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; /∗∗ ∗ A simple server for demonstrating the IO Multiplexing framework in action. ∗ After accepting a connection, it will read packets as defined by the ∗ SimpleProtocolDecoder class and echo them back. ∗ ∗ This server can accept and manage large numbers of incoming connections. For ∗ added fun remove the System.out statements and try it with several thousand ∗ (>10.000) clients. You might have to increase the maximum number of sockets ∗ allowed by the operating system. 59
  • 70. 60 APPENDICE A. CODICE SORGENTE ∗ ∗ @author Nuno Santos, modified by Roberto Peruzzo ∗/ public class Server implements AcceptorListener, PacketChannelListener { // Log4j static Logger logger = Logger.getLogger(Server.class); private final SelectorThread st; /∗∗ ∗ Starts the server. ∗ ∗ @param listenPort ∗ The port where to listen for incoming connections. ∗ @throws Exception ∗/ public Server(int listenPort) throws Exception { st = new SelectorThread(); Acceptor acceptor = new Acceptor(listenPort, st, this); acceptor.openServerSocket(); logger.info(”Listening on port: ” + listenPort); System.out.println(”Listening on port: ” + listenPort); } public static void main(String[] args) throws Exception { URL log4jURL = ClassLoader.getSystemResource(”log4jserver.xml”); logger.debug(”Log4j config file URL: ” + log4jURL.toString()); DOMConfigurator.configure(log4jURL); logger.info(”Log4j config loded: ” + log4jURL.toString()); int listenPort = Integer.parseInt(args[0]); new Server(listenPort); } // //////////////////////////////////////// // Implementation of the callbacks from the // Acceptor and PacketChannel classes // //////////////////////////////////////// /∗∗ ∗ A new client connected. Creates a PacketChannel to handle it. ∗/ public void socketConnected(Acceptor acceptor, SocketChannel sc) { logger.info(”[” + acceptor + ”] Socket connected: ” + sc.socket().getInetAddress()); try { // We should reduce the size of the TCP buffers or else we will // easily run out of memory when accepting several thousands of // connctions sc.socket().setReceiveBufferSize(2 ∗ 1024); sc.socket().setSendBufferSize(2 ∗ 1024); // The contructor enables reading automatically.
  • 71. A.1. SERVER 61 PacketChannel pc = new PacketChannel(sc, st, new SimpleProtocolDecoder(), this); pc.resumeReading(); } catch (IOException e) { e.printStackTrace(); logger.error(”[” + sc.socket().getInetAddress().getHostAddress() + ”:” + sc.socket().getPort() + ”] Error on socket connection: ” + e.getMessage()); } } public void socketError(Acceptor acceptor, Exception ex) { logger.error(”[” + acceptor + ”] Error: ” + ex.getMessage()); } public void packetArrived(PacketChannel pc, ByteBuffer pckt) { logger.debug(”[” + pc.toString() + ”] Packet received. Size: ” + pckt.remaining()); pc.sendPacket(pckt); } public void socketException(PacketChannel pc, Exception ex) { logger.error(”[” + pc.toString() + ”] Error: ” + ex.getMessage()); } public void socketDisconnected(PacketChannel pc) { logger.info(”[” + pc.toString() + ”] Disconnected.”); } /∗∗ ∗ The answer to a request was sent. Prepare to read the next request. ∗/ public void packetSent(PacketChannel pc, ByteBuffer pckt) { try { pc.resumeReading(); } catch (Exception e) { e.printStackTrace(); logger.error(”[” + pc.toString() + ”] Error on resume reading: ” + e.getMessage()); } } } A.1.2 AsdaaServer.java /∗∗ ∗ ∗/ package it.studioaqua.unive.asdaa.handlers; import handlers.PacketChannel;
  • 72. 62 APPENDICE A. CODICE SORGENTE import handlers.Server; import it.studioaqua.unive.asdaa.repository.DynamoDBManager; import java.net.URL; import java.nio.ByteBuffer; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; /∗∗ ∗ @author roberto ∗ ∗/ public class AsdaaServer extends Server { // Log4j static Logger logger = Logger.getLogger(AsdaaServer.class); private DynamoDBManager dynamoDB; /∗∗ ∗ @throws Exception ∗ ∗/ public AsdaaServer(int listenPort) throws Exception { super(listenPort); // Open the connection with Repository for saving messages dynamoDB = new DynamoDBManager(”Messages”); } public static void main(String[] args) throws Exception { URL log4jURL = ClassLoader.getSystemResource(”log4jserver.xml”); logger.debug(”Log4j config file URL: ”+log4jURL.toString()); DOMConfigurator.configure(log4jURL); logger.info(”Log4j config loded: ”+log4jURL.toString()); int listenPort = Integer.parseInt(args[0]); new AsdaaServer(listenPort); } /∗∗ ∗ The full packet was received, so resend it to the client (like echo service) ∗/ public void packetArrived(PacketChannel pc, ByteBuffer pckt) { logger.info(”[” + pc.toString() + ”] Packet received. Size: ” + pckt.remaining() ); // Convert ByteBuffer to bytearray byte[] bytearray = new byte[pckt.remaining()];
  • 73. A.1. SERVER 63 pckt.get(bytearray); // Save to the repository dynamoDB.save(pc.toString(), new String(bytearray)); // Send packet back to the client logger.debug(”Send packet back”); pckt.flip(); pc.sendPacket(pckt); logger.debug(”Packet sent.”); } } A.1.3 SelectorThread.java /∗ (c) 2004, Nuno Santos, nfsantos@sapo.pt relased under terms of the GNU public license http://www.gnu.org/licenses/licenses.html#TOCGPL ∗/ package io; import java.io.IOException; import java.nio.channels.∗; import java.util.∗; /∗∗ ∗ Event queue for I/O events raised by a selector. This class receives the ∗ lower level events raised by a Selector and dispatches them to the ∗ appropriate handler. It also manages all other operations on the selector, ∗ like registering and unregistering channels, or updating the events of ∗ interest for each monitored socket. ∗ ∗ This class is inspired on the java.awt.EventQueue and follows a similar ∗ model. The EventQueue class is responsible for making sure that all ∗ operations on AWT objects are performed on a single thread, the one managed ∗ internally by EventQueue. The SelectorThread class performs a similar ∗ task. In particular: ∗ ∗ − Only the thread created by instances of this class should be allowed ∗ to access the selector and all sockets managed by it. This means that ∗ all I/O operations on the sockets should be peformed on the corresponding ∗ selector's thread. If some other thread wants to access objects managed ∗ by this selector, then it should use <code>invokeLater()</code> or the ∗ <code>invokeAndWait()</code> to dispatch a runnable to this thread. ∗ ∗ − This thread should not be used to perform lenghty operations. In ∗ particular, it should never be used to perform blocking I/O operations. ∗ To perform a time consuming task use a worker thread. ∗
  • 74. 64 APPENDICE A. CODICE SORGENTE ∗ ∗ This architecture is required for two main reasons: ∗ ∗ The first, is to make synchronization in the objects of a connection ∗ unnecessary. This is good for performance and essential for keeping ∗ the complexity low. Getting synchronization right within the objects ∗ of a connection would be extremely tricky. ∗ ∗ The second is to make sure that all actions over the selector, its ∗ keys and related sockets are carried in the same thread. My personal ∗ experience with selectors is that they don't work well when being ∗ accessed concurrently by several threads. This is mostly the result ∗ of bugs in some of the version of Sun's Java SDK (these problems were ∗ found with version 1.4.2 02). Some of the bugs have already been ∗ identified and fixed by Sun. But it is better to work around them ∗ by avoiding multithreaded access to the selector. ∗ ∗ @author Nuno Santos ∗/ final public class SelectorThread implements Runnable { /∗∗ Selector used for I/O multiplexing ∗/ private Selector selector; /∗∗ The thread associated with this selector ∗/ private final Thread selectorThread; /∗∗ ∗ Flag telling if this object should terminate, that is, ∗ if it should close the selector and kill the associated ∗ thread. Used for graceful termination. ∗/ private boolean closeRequested = false; /∗∗ ∗ List of tasks to be executed in the selector thread. ∗ Submitted using invokeLater() and executed in the main ∗ select loop. ∗/ @SuppressWarnings(”rawtypes”) private final List pendingInvocations = new ArrayList(32); /∗∗ ∗ Creates a new selector and the associated thread. The thread ∗ is started by this constructor, thereby making this object ∗ ready to be used. ∗ ∗ @throws IOException ∗/ public SelectorThread() throws IOException { // Selector for incoming time requests selector = Selector.open(); selectorThread = new Thread(this); selectorThread.start();
  • 75. A.1. SERVER 65 } /∗∗ ∗ Raises an internal flag that will result on this thread dying ∗ the next time it goes through the dispatch loop. The thread ∗ executes all pending tasks before dying. ∗/ public void requestClose() { closeRequested = true; // Nudges the selector. selector.wakeup(); } /∗∗ ∗ Adds a new interest to the list of events where a channel is ∗ registered. This means that the associated event handler will ∗ start receiving events for the specified interest. ∗ ∗ This method should only be called on the selector thread. Otherwise ∗ an exception is thrown. Use the addChannelInterestLater() when calling ∗ from another thread. ∗ ∗ @param channel The channel to be updated. Must be registered. ∗ @param interest The interest to add. Should be one of the ∗ constants defined on SelectionKey. ∗/ public void addChannelInterestNow(SelectableChannel channel, int interest) throws IOException { if (Thread.currentThread() != selectorThread) { throw new IOException(”Method can only be called from selector thread”); } SelectionKey sk = channel.keyFor(selector); changeKeyInterest(sk, sk.interestOps() | interest); } /∗∗ ∗ Like addChannelInterestNow(), but executed asynchronouly on the ∗ selector thread. It returns after scheduling the task, without ∗ waiting for it to be executed. ∗ ∗ @param channel The channel to be updated. Must be registered. ∗ @param interest The new interest to add. Should be one of the ∗ constants defined on SelectionKey. ∗ @param errorHandler Callback used if an exception is raised when executing the task. ∗/ public void addChannelInterestLater(final SelectableChannel channel,
  • 76. 66 APPENDICE A. CODICE SORGENTE // Add a new runnable to the list of tasks to be executed in the selector thread invokeLater(new Runnable() { public void run() { try { addChannelInterestNow(channel, interest); } catch (IOException e) { errorHandler.handleError(e); } } }); } /∗∗ ∗ Removes an interest from the list of events where a channel is ∗ registered. The associated event handler will stop receiving events ∗ for the specified interest. ∗ ∗ This method should only be called on the selector thread. Otherwise ∗ an exception is thrown. Use the removeChannelInterestLater() when calling ∗ from another thread. ∗ ∗ @param channel The channel to be updated. Must be registered. ∗ @param interest The interest to be removed. Should be one of the ∗ constants defined on SelectionKey. ∗/ public void removeChannelInterestNow(SelectableChannel channel, int interest) throws IOException { if (Thread.currentThread() != selectorThread) { throw new IOException(”Method can only be called from selector thread”); } SelectionKey sk = channel.keyFor(selector); changeKeyInterest(sk, sk.interestOps() & ˜interest); } /∗∗ ∗ Like removeChannelInterestNow(), but executed asynchronouly on ∗ the selector thread. This method returns after scheduling the task, ∗ without waiting for it to be executed. ∗ ∗ @param channel The channel to be updated. Must be registered. ∗ @param interest The interest to remove. Should be one of the ∗ constants defined on SelectionKey. ∗ @param errorHandler Callback used if an exception is raised when ∗ executing the task.
  • 77. A.1. SERVER 67 ∗/ public void removeChannelInterestLater(final SelectableChannel channel, invokeLater(new Runnable() { public void run() { try { removeChannelInterestNow(channel, interest); } catch (IOException e) { errorHandler.handleError(e); } } }); } /∗∗ ∗ Updates the interest set associated with a selection key. The ∗ old interest is discarded, being replaced by the new one. ∗ ∗ @param sk The key to be updated. ∗ @param newInterest ∗ @throws IOException ∗/ private void changeKeyInterest(SelectionKey sk, int newInterest) throws IOException { /∗ This method might throw two unchecked exceptions: ∗ 1. IllegalArgumentException − Should never happen. It is a bug if it happens ∗ 2. CancelledKeyException − Might happen if the channel is closed while ∗ a packet is being dispatched. ∗/ try { sk.interestOps(newInterest); } catch (CancelledKeyException cke) { IOException ioe = new IOException(”Failed to change channel interest.”); ioe.initCause(cke); throw ioe; } }
  • 78. 68 APPENDICE A. CODICE SORGENTE /∗∗ ∗ Like registerChannelLater(), but executed asynchronouly on the ∗ selector thread. It returns after scheduling the task, without ∗ waiting for it to be executed. ∗ ∗ @param channel The channel to be monitored. ∗ @param selectionKeys The interest set. Should be a combination of ∗ SelectionKey constants. ∗ @param handler The handler for events raised on the registered channel. ∗ @param errorHandler Used for asynchronous error handling. ∗ ∗ @throws IOException ∗/ public void registerChannelLater(final SelectableChannel channel, final int selectionKeys, final CallbackErrorHandler errorHandler) { invokeLater(new Runnable() { public void run() { try { registerChannelNow(channel, selectionKeys, handlerInfo); } catch (IOException e) { errorHandler.handleError(e); } } }); } /∗∗ ∗ Registers a SelectableChannel with this selector. This channel will ∗ start to be monitored by the selector for the set of events associated ∗ with it. When an event is raised, the corresponding handler is ∗ called. ∗ ∗ This method can be called multiple times with the same channel ∗ and selector. Subsequent calls update the associated interest set ∗ and selector handler to the ones given as arguments. ∗ ∗ This method should only be called on the selector thread. Otherwise ∗ an exception is thrown. Use the registerChannelLater() when calling ∗ from another thread. ∗ ∗ @param channel The channel to be monitored. ∗ @param selectionKeys The interest set. Should be a combination of ∗ SelectionKey constants. ∗ @param handler The handler for events raised on the registered channel.
  • 79. A.1. SERVER 69 ∗/ public void registerChannelNow(SelectableChannel channel, int selectionKeys, SelectorHandler handlerInfo) throws IOException { if (Thread.currentThread() != selectorThread) { throw new IOException(”Method can only be called from selector thread”); } if (!channel.isOpen()) { throw new IOException(”Channel is not open.”); } try { if (channel.isRegistered()) { SelectionKey sk = channel.keyFor(selector); assert sk != null : ”Channel is already registered with other selector”; sk.interestOps(selectionKeys); Object previousAttach = sk.attach(handlerInfo); assert previousAttach != null; } else { channel.configureBlocking(false); channel.register(selector, selectionKeys, handlerInfo); } } catch (Exception e) { IOException ioe = new IOException(”Error registering channel.”); ioe.initCause(e); throw ioe; } } /∗∗ ∗ Executes the given task in the selector thread. This method returns ∗ as soon as the task is scheduled, without waiting for it to be ∗ executed. ∗ ∗ @param run The task to be executed. ∗/ @SuppressWarnings(”unchecked”) public void invokeLater(Runnable run) { synchronized (pendingInvocations) { pendingInvocations.add(run); } selector.wakeup(); } /∗∗ ∗ Executes the given task synchronously in the selector thread. This ∗ method schedules the task, waits for its execution and only then ∗ returns. ∗ ∗ @param run The task to be executed on the selector's thread. ∗/ public void invokeAndWait(final Runnable task)
  • 80. 70 APPENDICE A. CODICE SORGENTE throws InterruptedException { if (Thread.currentThread() == selectorThread) { // We are in the selector's thread. No need to schedule // execution task.run(); } else { // Used to deliver the notification that the task is executed final Object latch = new Object(); synchronized (latch) { // Uses the invokeLater method with a newly created task this.invokeLater(new Runnable() { public void run() { task.run(); // Notifies latch.notify(); } }); // Wait for the task to complete. latch.wait(); } // Ok, we are done, the task was executed. Proceed. } } /∗∗ ∗ Executes all tasks queued for execution on the selector's thread. ∗ ∗ Should be called holding the lock to <code>pendingInvocations</code>. ∗ ∗/ private void doInvocations() { synchronized (pendingInvocations) { for (int i = 0; i < pendingInvocations.size(); i++) { Runnable task = (Runnable) pendingInvocations.get(i); task.run(); } pendingInvocations.clear(); } } /∗∗ ∗ Main cycle. This is where event processing and ∗ dispatching happens. ∗/ @SuppressWarnings(”rawtypes”) public void run() { // Here's where everything happens. The select method will // return when any operations registered above have occurred, the // thread has been interrupted, etc. while (true) { // Execute all the pending tasks. doInvocations();
  • 81. A.1. SERVER 71 // Time to terminate? if (closeRequested) { return; } int selectedKeys = 0; try { selectedKeys = selector.select(); } catch (IOException ioe) { // Select should never throw an exception under normal // operation. If this happens, print the error and try to // continue working. ioe.printStackTrace(); continue; } if (selectedKeys == 0) { // Go back to the beginning of the loop continue; } // Someone is ready for IO, get the ready keys Iterator it = selector.selectedKeys().iterator(); // Walk through the collection of ready keys and dispatch // any active event. while (it.hasNext()) { SelectionKey sk = (SelectionKey)it.next(); it.remove(); try { // Obtain the interest of the key int readyOps = sk.readyOps(); // Disable the interest for the operation that is ready. // This prevents the same event from being raised multiple // times. sk.interestOps(sk.interestOps() & ˜readyOps); SelectorHandler handler = (SelectorHandler) sk.attachment(); // Some of the operations set in the selection key // might no longer be valid when the handler is executed. // So handlers should take precautions against this // possibility. // Check what are the interests that are active and // dispatch the event to the appropriate method. if (sk.isAcceptable()) { // A connection is ready to be completed ((AcceptSelectorHandler)handler).handleAccept(); } else if (sk.isConnectable()) { // A connection is ready to be accepted ((ConnectorSelectorHandler)handler).handleConnect();
  • 82. 72 APPENDICE A. CODICE SORGENTE } else { ReadWriteSelectorHandler rwHandler = (ReadWriteSelectorHandler)handler; // Readable or writable if (sk.isReadable()) { // It is possible to read rwHandler.handleRead(); } // Check if the key is still valid, since it might // have been invalidated in the read handler // (for instance, the socket might have been closed) if (sk.isValid() && sk.isWritable()) { // It is read to write rwHandler.handleWrite(); } } } catch (Throwable t) { // No exceptions should be thrown in the previous block! // So kill everything if one is detected. // Makes debugging easier. closeSelectorAndChannels(); t.printStackTrace(); return; } } } } /∗∗ ∗ Closes all channels registered with the selector. Used to ∗ clean up when the selector dies and cannot be recovered. ∗/ private void closeSelectorAndChannels() { @SuppressWarnings(”rawtypes”) Set keys = selector.keys(); for (@SuppressWarnings(”rawtypes”) Iterator iter = keys.iterator(); iter.hasNext();) { SelectionKey key = (SelectionKey)iter.next(); try { key.channel().close(); } catch (IOException e) { // Ignore } } try { selector.close(); } catch (IOException e) { // Ignore } } }
  • 83. A.1. SERVER 73 A.1.4 DynamoDBManager.java /∗∗ ∗ (c) 2011, Roberto Peruzzo, roberto.peruzzo@gmail.com ∗ relased under terms of the GNU public license ∗ http://www.gnu.org/licenses/licenses.html#TOCGPL ∗/ package it.studioaqua.unive.asdaa.repository; import java.io.IOException; import java.io.InputStream; import java.nio.charset.CharacterCodingException; import java.util.HashMap; import java.util.Map; import org.apache.log4j.Logger; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.services.dynamodb.AmazonDynamoDBClient; import com.amazonaws.services.dynamodb.model.AttributeValue; import com.amazonaws.services.dynamodb.model.Condition; import com.amazonaws.services.dynamodb.model.CreateTableRequest; import com.amazonaws.services.dynamodb.model.DescribeTableRequest; import com.amazonaws.services.dynamodb.model.DescribeTableResult; import com.amazonaws.services.dynamodb.model.KeySchema; import com.amazonaws.services.dynamodb.model.KeySchemaElement; import com.amazonaws.services.dynamodb.model.ProvisionedThroughput; import com.amazonaws.services.dynamodb.model.PutItemRequest; import com.amazonaws.services.dynamodb.model.PutItemResult; import com.amazonaws.services.dynamodb.model.ScanRequest; import com.amazonaws.services.dynamodb.model.ScanResult; import com.amazonaws.services.dynamodb.model.TableDescription; import com.amazonaws.services.dynamodb.model.TableStatus; /∗∗ ∗ This class manage the interaction between Amazon SimpleDB and SelectedServer. ∗ It persists every message received by SelectedServer. ∗ ∗ @author Studio AQuA ∗ ∗/ public class DynamoDBManager { // Log4j static Logger logger = Logger.getLogger(DynamoDBManager.class); private AmazonDynamoDBClient dynamoDB; private String table;
  • 84. 74 APPENDICE A. CODICE SORGENTE /∗∗ ∗ Constructor ∗ @param tableName ∗/ public DynamoDBManager(String tableName) { try { logger.debug(”Get AWS Credentials.”); // Get PropertiesCredentials InputStream credentialsAsStream = Thread.currentThread(). getContextClassLoader().getResourceAsStream(”AwsCredentials. properties”); AWSCredentials credentials = new PropertiesCredentials( credentialsAsStream); logger.debug(credentials.toString()); // Activate the Amazon DynamoDB Client this.dynamoDB = new AmazonDynamoDBClient(credentials); // Describe our new table DescribeTableRequest describeTableRequest = new DescribeTableRequest(). withTableName(tableName); TableDescription tableDescription = this.dynamoDB.describeTable( describeTableRequest).getTable(); logger.info(”Table Description: ” + tableDescription); String tableStatus = tableDescription.getTableStatus(); if (tableStatus.equals(TableStatus.ACTIVE.toString())) { logger.info(”Table ”+tableName+ ” already exists.”); } else { // Create a table with a primary key named 'name', which holds a string CreateTableRequest createTableRequest = new CreateTableRequest(). withTableName(tableName) .withKeySchema(new KeySchema(new KeySchemaElement(). withAttributeName(”messageId”).withAttributeType(”S”))) .withProvisionedThroughput(new ProvisionedThroughput(). withReadCapacityUnits(10L).withWriteCapacityUnits(320L)); tableDescription = this.dynamoDB.createTable(createTableRequest). getTableDescription(); logger.info(”Created Table: ” + tableDescription); } this.table = tableName; } catch (AmazonServiceException ase) { logger.error(”Caught an AmazonServiceException, which means your request made it ” + ”to AWS, but was rejected with an error response for some reason.”); logger.error(”Error Message: ” + ase.getMessage()); logger.error(”HTTP Status Code: ” + ase.getStatusCode()); logger.error(”AWS Error Code: ” + ase.getErrorCode()); logger.error(”Error Type: ” + ase.getErrorType()); logger.error(”Request ID: ” + ase.getRequestId());
  • 85. A.1. SERVER 75 } catch (AmazonClientException ace) { logger.error(”Caught an AmazonClientException, which means the client encountered ” + ”a serious internal problem while trying to communicate with AWS, ” + ”such as not being able to access the network.”); logger.error(”Error Message: ” + ace.getMessage()); } catch (IOException e) { logger.error(”RepositoryManager creation: IO exception. ”+e. getMessage()); e.printStackTrace(); } } /∗∗ ∗ Persists the client message into SimpleDB. ∗ @param clientId − client identifier ∗ @param body − message ∗ @return ∗ @throws CharacterCodingException ∗/ public boolean save(String clientId, String body) { String recordId = clientId+” ”+String.valueOf(System.currentTimeMillis()); // Add an item try { logger.info(”Save message id ”+recordId+” on table ”+ this.table); logger.debug(”MESSAGE BODY: ”+body); Map<String, AttributeValue> item = newItem(recordId, clientId, body, System .currentTimeMillis()); PutItemRequest putItemRequest = new PutItemRequest(this.table, item); PutItemResult putItemResult = dynamoDB.putItem(putItemRequest); logger.info(”Result: ” + putItemResult.toString()); logger.info(”Message from client ”+clientId+” saved.”); } catch (AmazonServiceException ase) { logger.error(”[SAVE] Caught an AmazonServiceException, which means your request made it ” + ”to AWS, but was rejected with an error response for some reason.”); logger.error(”[SAVE] Error Message: ” + ase.getMessage()); logger.error(”[SAVE] HTTP Status Code: ” + ase.getStatusCode()); logger.error(”[SAVE] AWS Error Code: ” + ase.getErrorCode()); logger.error(”[SAVE] Error Type: ” + ase.getErrorType()); logger.error(”[SAVE] Request ID: ” + ase.getRequestId()); return false; } catch (AmazonClientException ace) { logger.error(”[SAVE] Caught an AmazonClientException, which means the client encountered ” + ”a serious internal problem while trying to communicate with AWS, ” + ”such as not being able to access the network.”); logger.error(”[SAVE] Error Message: ” + ace.getMessage());
  • 86. 76 APPENDICE A. CODICE SORGENTE return false; } return true; } /∗∗ ∗ Get table content ∗ @param tableName ∗ @return ∗/ public ScanResult scan(String tableName, HashMap<String, Condition> scanFilter) { ScanResult scanResult = null; try { ScanRequest scanRequest = new ScanRequest(tableName); if(!scanFilter.isEmpty()) { scanRequest.withScanFilter(scanFilter); } scanResult = dynamoDB.scan(scanRequest); logger.info(”Result: ” + scanResult); } catch (AmazonServiceException ase) { logger.error(”[SAVE] Caught an AmazonServiceException, which means your request made it ” + ”to AWS, but was rejected with an error response for some reason.”); logger.error(”[SAVE] Error Message: ” + ase.getMessage()); logger.error(”[SAVE] HTTP Status Code: ” + ase.getStatusCode()); logger.error(”[SAVE] AWS Error Code: ” + ase.getErrorCode()); logger.error(”[SAVE] Error Type: ” + ase.getErrorType()); logger.error(”[SAVE] Request ID: ” + ase.getRequestId()); } catch (AmazonClientException ace) { logger.error(”[SAVE] Caught an AmazonClientException, which means the client encountered ” + ”a serious internal problem while trying to communicate with AWS, ” + ”such as not being able to access the network.”); logger.error(”[SAVE] Error Message: ” + ace.getMessage()); } return scanResult; } /∗∗ ∗ ∗ @param tableName ∗ @return ∗/ public Long countTableItems(String tableName) { // Describe our new table DescribeTableRequest describeTableRequest = new DescribeTableRequest(). withTableName(tableName); DescribeTableResult tableDescription = this.dynamoDB.describeTable(
  • 87. A.2. CLIENT 77 describeTableRequest); return tableDescription.getTable().getItemCount(); } /∗∗ ∗ ∗ @param name ∗ @param clientId ∗ @param body ∗ @param created ∗ @return ∗/ private Map<String, AttributeValue> newItem(String messageId, String clientId, String body, long created) { Map<String, AttributeValue> item = new HashMap<String, AttributeValue>(); item.put(”messageId”, new AttributeValue(messageId)); item.put(”clientId”, new AttributeValue(clientId)); item.put(”body”, new AttributeValue(body)); item.put(”created”, new AttributeValue().withN(Long.toString(created))); return item; } } A.2 Client A.2.1 MultiplexingClient.java package it.studioaqua.unive.asdaa.client; import handlers.∗; import io.SelectorThread; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URL; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.∗; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; /∗∗
  • 88. 78 APPENDICE A. CODICE SORGENTE ∗ A simple test client for the I/O Multiplexing based server. This class simulates several clients using an imaginary request− reply protocol to connect to a server. Several connections can be established at the same time. Each one, generates a random packet to simulate a request, sends it to the server and waits for the answer before sending the next packet. This implementation is based on I/O Multiplexing, so it should be able to handle several thousand of connections. Using Redhat Linux 9.0 I was able to establish about 10.000 connections before running out of memory (the system had only 256Mb of RAM). ∗ @author Nuno Santos ∗/ public class MultiplexingClient implements ConnectorListener, PacketChannelListener { // Log4j static Logger logger = Logger.getLogger(MultiplexingClient.class); /∗∗ ∗ A single selector for all clients ∗ @uml.property name=”st” ∗ @uml.associationEnd ∗/ private static SelectorThread st; /∗∗ Maximum size of the packets sent ∗/ private static final int MAX_SIZE = 1200; //10 ∗ 1024; /∗∗ Minimum size of the packets sent ∗/ private static final int MIN_SIZE = 50; //128; /∗∗ How many packets each client should send ∗/ private static final int PACKETS_TO_SEND = 10; /∗∗ For generating random packet sizes. ∗/ private static final Random r = new Random(); /∗∗ How many connections to created ∗/ private static int connectionCount; /∗∗ How many connections were opened so far ∗/ private static int connectionsEstablished = 0; private static int connectionsFailed = 0; /∗∗ How many connections were disconnected so far ∗/ private static int connectionsClosed = 0; /∗∗ ∗ Keeps a list of connections that have been established but not yet ∗ started. ∗/ @SuppressWarnings(”rawtypes”) private static List establishedConnections = new ArrayList(512); /∗∗ How many packets each instance sent so far. ∗/ private int packetsSent = 0; /∗∗ ∗ Studio AQuA ∗ @uml.property name=”rigaStat” ∗ @uml.associationEnd ∗/ // Statistics private Stats rigaStat = null; private final static ArrayList<Stats> statistiche = new ArrayList<Stats>();
  • 89. A.2. CLIENT 79 @SuppressWarnings(”unused”) private int clientId; private static long globalStart = 0; /∗∗ ∗ Initiates a non−blocking connection attempt to the given address. ∗ ∗ @param remotePoint ∗ Where to try to connect. ∗ @throws Exception ∗/ public MultiplexingClient(InetSocketAddress remotePoint, int clientId) throws Exception { Connector connector = new Connector(st, remotePoint, this); connector.connect(); logger.info(”[” + connector + ”] Connecting...”); this.clientId = clientId; } /∗∗ ∗ Creates a new packet with a size chosen randomly between MIN SIZE and ∗ MAX SIZE. ∗/ private ByteBuffer generateNextPacket() { // Generate a random size between int size = MIN_SIZE + r.nextInt(MAX_SIZE − MIN_SIZE); ByteBuffer buffer = ByteBuffer.allocate(size); buffer.put(SimpleProtocolDecoder.STX); for (int i = 0; i < size − 2; i++) { buffer.put((byte) 'a'); } buffer.put(SimpleProtocolDecoder.ETX); buffer.limit(buffer.position()); buffer.flip(); return buffer; } // //////////////////////////////////////// // Implementation of the callbacks from the // Acceptor and PacketChannel classes // //////////////////////////////////////// /∗∗ ∗ A new client connected. Creates a PacketChannel to handle it. ∗/ @SuppressWarnings(”unchecked”) public void connectionEstablished(Connector connector, SocketChannel sc) { try { // We should reduce the size of the TCP buffers or else we will // easily run out of memory when accepting several thousands of // connctions sc.socket().setReceiveBufferSize(2 ∗ 1024); sc.socket().setSendBufferSize(2 ∗ 1024);
  • 90. 80 APPENDICE A. CODICE SORGENTE // The contructor enables reading automatically. PacketChannel pc = new PacketChannel(sc, st, new SimpleProtocolDecoder(), this); // Do not start sending packets right away. Waits for all sockets // to connect. Otherwise, the load created by sending and receiving // packets will increase dramatically the time taken for all // connections to be established. It is better to establish all // connections and only then to start sending packets. establishedConnections.add(pc); connectionsEstablished++; logger.info(”[” + connector + ”] Connected: ” + sc.socket().getInetAddress() + ” (” + connectionsEstablished + ”/” + connectionCount + ”)”); // If if all connections are established. checkAllConnected(); } catch (IOException e) { e.printStackTrace(); } } public void connectionFailed(Connector connector, Exception cause) { logger.error(”[” + connector + ”] Error: ” + cause.getMessage()); connectionsFailed++; checkAllConnected(); } public void packetArrived(PacketChannel pc, ByteBuffer pckt) { logger.debug(”[”+ pc.toString() + ”] Packet arrived”); rigaStat.setFine(System.currentTimeMillis()); rigaStat.setRicevuti(pckt.capacity()); //synchronized (statistiche) { statistiche.add(rigaStat); //} if (packetsSent >= PACKETS_TO_SEND) { // This connection sent all packets that it was supposed to send. // Close. logger.info(”[” + pc.getSocketChannel().socket().getLocalPort() + ”] Closed. Packets sent ” + PACKETS_TO_SEND + ”. Connection: ” + (connectionsClosed + 1) + ”/” + connectionsEstablished); pc.close(); connectionClosed(); } else { // Still more packets to send. sendPacket(pc); }
  • 91. A.2. CLIENT 81 } public void socketException(PacketChannel pc, Exception ex) { logger.error(”[” + pc.toString() + ”] Error: ” + ex.getMessage()); connectionClosed(); } public void socketDisconnected(PacketChannel pc) { logger.info(”[” + pc.toString() + ”] Disconnected.”); connectionClosed(); } /∗∗ ∗ The request was sent. Prepare to read the answer. ∗/ public void packetSent(PacketChannel pc, ByteBuffer pckt) { logger.debug(”[” + pc.toString() + ”] Packet sent.”); rigaStat = new Stats(); String host = pc.getSocketChannel().socket().getInetAddress().getHostAddress () +”:”+ String.valueOf(pc.getSocketChannel().socket(). getLocalPort()); rigaStat.setClientHost(host); rigaStat.setMsgId(packetsSent); rigaStat.setInviati(pckt.capacity()); rigaStat.setInizio(System.currentTimeMillis()); try { pc.resumeReading(); } catch (Exception e) { e.printStackTrace(); logger.error(”Packet sent error: ”+e.getMessage()); } } // ////////////////////////// // Helper methods // ////////////////////////// /∗∗ ∗ Called when a connection is closed. Checks if all connections have been ∗ closed and if so exits the virtual machine. ∗/ private void connectionClosed() { connectionsClosed++; if (connectionsClosed >= connectionsEstablished) { st.requestClose(); System.exit(1); } } /∗∗ ∗ Sends a newly generated packet using the given PacketChannel
  • 92. 82 APPENDICE A. CODICE SORGENTE ∗ ∗ @param pc ∗/ private void sendPacket(PacketChannel pc) { // System.out.println(”[” + pc.toString() + ”] Sending packet.”); ByteBuffer packet = generateNextPacket(); packetsSent++; pc.sendPacket(packet); } /∗∗ ∗ Checks if all connections have been established. If so, starts them all ∗ by sending an initial packet to all of them. ∗/ private void checkAllConnected() { // Starts sending packets only after all connections are established. if ((connectionsEstablished + connectionsFailed) == connectionCount) { for (int i = 0; i < establishedConnections.size(); i++) { PacketChannel pc = (PacketChannel) establishedConnections .get(i); sendPacket(pc); } establishedConnections.clear(); } } public static void main(String[] args) throws Exception { URL log4jURL = ClassLoader.getSystemResource(”log4jclient.xml”); logger.debug(”Log4j config file URL: ”+log4jURL.toString()); DOMConfigurator.configure(log4jURL); logger.info(”Log4j config loded: ”+log4jURL.toString()); InetSocketAddress remotePoint = new InetSocketAddress(args[0], Integer.parseInt(args[1])); connectionCount = 1; if (args.length > 2) { connectionCount = Integer.parseInt(args[2]); } st = new SelectorThread(); globalStart = System.currentTimeMillis(); for (int i = 0; i < connectionCount; i++) { new MultiplexingClient(remotePoint,i); // Must sleep for a while between opening connections in order // to give the remote host enough time to handle them. Otherwise, // the remote host backlog will get full and the connection // attemps will start to be refused. Thread.sleep(10); } Runtime.getRuntime().addShutdownHook(new PrintStats());
  • 93. A.2. CLIENT 83 } private static class PrintStats extends Thread { public void run() { logger.info(”Execution TIME: ”+String.valueOf(System. currentTimeMillis()−globalStart)+”ms”); String locale = ”qualcosa”; try { locale = InetAddress.getLocalHost().getHostName(); try { String filename = ”logs/”+locale + ” PPM ” + Calendar.getInstance().get(Calendar. HOUR_OF_DAY) + ”−” + Calendar.getInstance().get( Calendar.MINUTE) + ”−” + Calendar.getInstance().get( Calendar.SECOND) + ” ” + currentThread().getId() + ”.log”; logger.info(”nnSalvate statistiche su file: ” + filename); FileOutputStream stats = new FileOutputStream( filename, true); PrintStream out = new PrintStream(stats); out.println(”HosttMsgIDtByteSenttByteReceived tStarttEndtTime”); for (Iterator<Stats> it = statistiche.iterator(); it .hasNext();) { Stats riga = it.next(); out.println(riga.getClientHost() + ”t” + riga.getMsgId() + ”t” + riga.getInviati() + ” t” + riga.getRicevuti() + ”t” + riga.getInizio() + ”t” + riga.getFine() + ”t” + riga.getTempo()); } out.close(); } catch (IOException ex) { logger.error(”Errore salvataggio dati: ” + ex.getMessage ()); } } catch (UnknownHostException ex) { logger.error(”Impossibile ricavare IP locale: ” + ex.getMessage()); }
  • 94. 84 APPENDICE A. CODICE SORGENTE } } /∗∗ ∗ @author roberto ∗/ private static class Stats { /∗∗ ∗ @uml.property name=”inviati” ∗/ private long inviati; /∗∗ ∗ @uml.property name=”ricevuti” ∗/ private long ricevuti; /∗∗ ∗ @uml.property name=”inizio” ∗/ private long inizio; /∗∗ ∗ @uml.property name=”fine” ∗/ private long fine; /∗∗ ∗ @uml.property name=”msgId” ∗/ private int msgId; /∗∗ ∗ @uml.property name=”clientHost” ∗/ private String clientHost; /∗∗ ∗ @return ∗ @uml.property name=”inizio” ∗/ public long getInizio() { return inizio; } /∗∗ ∗ @param inizio ∗ @uml.property name=”inizio” ∗/ public void setInizio(long inizio) { this.inizio = inizio; } /∗∗ ∗ @return ∗ @uml.property name=”fine” ∗/
  • 95. A.2. CLIENT 85 public long getFine() { return fine; } /∗∗ ∗ @param fine ∗ @uml.property name=”fine” ∗/ public void setFine(long fine) { this.fine = fine; } public Stats() { } /∗ ∗ public Stats(Thread thread, int msgId, long inviati, long ricevuti, ∗ long tempo) { this.inviati = inviati; this.ricevuti = ricevuti; ∗ this.tempo = tempo; this.msgId = msgId; this.threadId = ∗ thread.getId(); } ∗/ /∗∗ ∗ @return the inviati ∗ @uml.property name=”inviati” ∗/ public long getInviati() { return inviati; } /∗∗ ∗ @param inviati the inviati to set ∗ @uml.property name=”inviati” ∗/ public void setInviati(long inviati) { this.inviati = inviati; } /∗∗ ∗ @return the ricevuti ∗ @uml.property name=”ricevuti” ∗/ public long getRicevuti() { return ricevuti; } /∗∗ ∗ @param ricevuti the ricevuti to set ∗ @uml.property name=”ricevuti” ∗/ public void setRicevuti(long ricevuti) { this.ricevuti = ricevuti; }