Leggi la tesi

Transcript

Leggi la tesi
Università degli studi della Calabria
Facoltà di Ingegneria
Corso di Laurea in Ingegneria Informatica
Istituto per il Calcolo e le Reti ad Alte Prestazioni – ICAR/CNR
Self-Chord:
protocollo peer to peer strutturato
basato su agenti mobili bio-ispirati
RELATORI
CANDIDATO
Ing. Carlo Mastroianni
Saverio Manti
matr. 101345
Ing. Agostino Forestiero
____________________________
Anno Accademico 2007-2008
I computer sono incredibilmente veloci, accurati e stupidi.
Gli uomini sono incredibilmente lenti, inaccurati e intelligenti.
Insieme sono una potenza che supera l'immaginazione.
(Albert Einstein)
Alla mia famiglia
e a chi ha sempre creduto in me.
2
Indice
Introduzione.......................................................................................................5
1
2
3
Le reti peer to peer......................................................................................8
1.1
Introduzione............................................................................................................8
1.2
Caratteristiche di una rete p2p...............................................................................8
1.3
Classificazione delle reti Peer-to-Peer ................................................................10
1.4
Vantaggi e svantaggi del p2p ...............................................................................11
DHT: distributed hash tables ...................................................................13
2.1
Tabella Hash ........................................................................................................13
2.2
Proprietà delle DHTs ...........................................................................................15
2.3
Partizionamento del keyspace ..............................................................................15
2.4
Alcuni esempi di protocolli basati su HDT ..........................................................17
2.4.1
CAN ..............................................................................................................17
2.4.2
Kademlia.......................................................................................................18
2.4.3
Pastry e Tapestry ..........................................................................................19
Chord.........................................................................................................20
3.1
Introduzione..........................................................................................................20
3.2
Nodi e Chiavi in Chord.........................................................................................21
3.3
Chord e le DHTs...................................................................................................23
3.4
Join e Leave ..........................................................................................................27
3.5 Fallimento di un nodo.................................................................................................31
4
Open Chord...............................................................................................32
4.1
Introduzione..........................................................................................................32
4.2
Caratteristiche di Open-Chord.............................................................................32
4.3
Configurazione .....................................................................................................33
4.4
Architettura di Open Chord..................................................................................36
4.5
Protocolli di comunicazione utilizzati ..................................................................38
3
5
4.6
Struttura delle classi.............................................................................................38
4.7
Prestazioni di Open Chord ...................................................................................44
4.8
Limiti di Open Chord............................................................................................44
Self – Chord ..............................................................................................46
5.1
Introduzione..........................................................................................................46
5.2
Funzionamento di Self-Chord...............................................................................47
5.2.1
Gli agenti ......................................................................................................48
5.2.2
Centroide ......................................................................................................49
5.2.3
Ordinamento delle chiavi .............................................................................49
5.3
Ordinamento lineare e logaritmo: confronto .......................................................53
5.4
Implementazione...................................................................................................53
5.4.1
Node..............................................................................................................59
5.4.3
InvocationThread .........................................................................................60
5.4.4
RequestHandler ............................................................................................60
5.4.5
Request e Response.......................................................................................61
5.4.6
MethodConstants..........................................................................................61
5.4.7
SocketProxy ..................................................................................................61
5.4.8
ChordImpl.....................................................................................................62
5.4.9
NodeImpl ......................................................................................................62
5.5
Principali Operazioni ...........................................................................................65
5.6
L’applicazione ......................................................................................................67
6
Simulazioni e Risultati .............................................................................69
7
Conclusioni ...............................................................................................84
Ringraziamenti ................................................................................................86
Indice Figure ...................................................................................................87
Bibliografia ......................................................................................................88
4
Introduzione
Negli ultimi anni, il mondo dell’informatica e non solo, è stato investito da un nuovo
fenomeno chiamato peer-to-peer (p2p), il quale ha trovato il maggior successo nel campo
del file sharing.
Il peer-to-peer (p2p) è un modello di comunicazione in cui ciascun membro è in rapporto
paritetico con gli altri, ovvero ha le stesse identiche funzionalità, contrariamente a quanto
avviene in un'architettura Client/Server in cui vi sono alcuni nodi (client) che richiedono
servizi e altri (server) che li offrono. I sistemi p2p si basano generalmente su un modello
topologico della rete totalmente decentralizzato (es. Freenet) ma alcuni di questi sistemi
fanno comunque riferimento a topologie gerarchiche.
Lo scopo di un sistema p2p è di permettere la condivisione, in modo non centralizzato, di
risorse e servizi (es. scambio d’informazioni, cicli di computazione, spazio su disco per i
file): ciascun peer (nodo), quindi, è responsabile del passaggio dei dati agli altri peers,
svolgendo allo stesso tempo ruolo sia di client che di server.
In poco tempo, il p2p è diventato uno dei paradigmi più potenti per quanto riguarda la
condivisione di risorse in quanto non prevede l’utilizzo di una base di dati centrale e una
suddivisione gerarchica dei nodi.
Accanto al p2p, un’altra interessante tendenza degli ultimi anni è la progettazione di griglie
cosiddette self-organizing come riportato in [1] e [2]. Esse sono griglie computazionali [9]
in cui grazie a semplici operazioni di singoli agenti bio-ispirati (si rifanno alle colonie di
formiche o agli sciami d’api) si ottiene a livello globale un’ auto-organizzazione della rete
difficile da ottenere con strategie centralizzate.
I modelli p2p modelli sono classificati in strutturati e non strutturati, in base a come i nodi
sono collegati gli uni agli altri e a come vengono disposti i dati sui nodi. In sistemi non
strutturati, le risorse sono pubblicate dai peers senza alcuna pianificazione globale. Questo
facilita la gestione della rete, ma riduce l'efficienza delle procedure di scoperta. Nei sistemi
strutturati invece, le risorse sono associate a nodi specifici, spesso tramite Distributed Hash
Tables garantendo così una maggiore efficienza in termini di tempo per la ricerca di una
risorsa e di carico del bilancio della rete.
5
Il lavoro svolto riguarda la modifica del protocollo strutturato Chord, realizzato dal MIT
[19] al fine di ottenere un nuovo sistema p2p basato su agenti i quali avranno il compito di
ordinare e organizzare le risorse sui vari peers esentando da tale lavoro i nodi stessi.
In Chord, i vari nodi si dispongono su di un anello e sono ordinati in base al valore del
loro ID. Ogni nodo può inoltre pubblicare delle risorse ad ognuna delle quali viene
associata una chiave. I nodi e le chiavi condividono lo steso spazio degli identificatori:
consistent hashing(cfr. [7] ) SHA-1 (160 bit) [18]. Una chiave k sarà quindi associata al
nodo con ID che è uguale o segue k mediante la funzione successor(k). Ogni nodo avrà
conoscenza solamente di O(log(N)) nodi presenti sulla rete (N rappresenta proprio il
numero totale dei nodi) mediante l’utilizzo di una finger table distribuite (DHT). In
particolare la finger del nodo n avrà come entry successor(n+ 2i-1) con 1≤ i ≤ m dove m
sono i bit per rappresentare l’id di un nodo. In questo modo, Chord garantisce tempi di
ricerca logaritmici oltre alla robustezza, scalabilità e bilanciamento del carico.
Self-Chord nasce dal protocollo Chord e, come già scritto, mediante l’utilizzo d’agenti
mobili bio-ispirati, dei quali si tratta in [2], si è cercato di migliorare e rendere più
efficiente la pubblicazione e la ricerca di risorse all’interno della rete. Nodi e chiavi delle
risorse non hanno più necessità di essere legati in quanto, adesso, un nodo pubblicherà la
risorsa su se stesso e successivamente, mediante il lavoro degli agenti, le risorse verranno
riorganizzate diminuendo quello che è il carico computazionale che si poteva avere in
Chord nel momento in cui più nodi accedevano contemporaneamente alla rete e iniziavano
a distribuire ai vari nodi le risorse. Gli agenti, infatti, mediante semplici operazioni basate
su calcoli probabilistici decidono di muovere o meno le risorse sull’anello con lo scopo di
ordinarle su tutta la rete in modo da riorganizzarla garantendo, come atto finale, una ricerca
ancora più efficiente.
A tal fine assume molto importanza un nuovo parametro detto centroide di un nodo,
calcolato tenendo in considerazioni le risorse presenti sul nodo e su i due adiacenti ad esso.
Un’altra novità introdotta in Self-Chord è la catalogazione delle risorse in tipi ossia in
classi. Una classe di risorse è definita come l’insieme di risorse aventi una serie di
caratteristiche in comune e per questo rappresentate con lo stesso valore di chiave.
I principali benefici introdotti da Self-Chord sono quindi:
¾ Indipendenza tra ID dei nodi e chiavi delle risorse: in Self-Chord non sarà necessario
rappresentare le due entità con lo stesso numero di bit.
6
¾ Auto-organizzazione delle risorse mediante l’azione degli agenti: non sarà più
compito del singolo nodo decidere su quale nodo dell’anello pubblicare la risorsa.
¾ In Chord quando un nodo deve accedere o lasciare la rete, sono necessarie una serie di
operazioni. In particolare, quando un nodo effettua l’ingresso nella rete, deve assegnare le
risorse che intende pubblicare ai nodi responsabili determinati mediante la funzione
successor(k) dove k è la chiave della risorsa. Per cui in caso di ingresso contemporaneo di
più nodi si potrebbe avere un elevato carico computazionali nella rete. In Self-Chord i nodi
si dovranno preoccupare solamente di notificare l’ingresso affinché gli altri nodi creino il
riferimento a tale nodo ma non sarà effettuata alcuna operazione per quanto riguarda
l’assegnazione delle risorse. Il nodo infatti pubblicherà le risorse su se stesso e sarà poi
compito degli agenti di spostare le risorse.
In particolare, nei successivi capitoli, sarà possibile, dopo un’introduzione sul mondo del
peer-to-peer al capitolo 1, andare ad osservare come, partendo dalla versione “originale” di
Chord realizzata dal MIT, si arrivi alla realizzazione di Self-Chord.
Nel capitolo 2 sarà possibile approfondire il tema delle DHT, base di molti protocolli
strutturati nonché di Chord. Successivamente, nel capitolo 3 sarà presentato Chord e in
particolare si porrà l’attenzione sul funzionamento del protocollo e sulle principali
operazioni che è necessario eseguire da ogni peer una volta che effettua l’accesso alla rete.
Dopo Chord, nel capitolo 4 invece, verrà analizzata la versione java di Open Chord
focalizzando l’attenzione su come, l’applicazione, permette l’esecuzione delle principali
operazioni che un nodo può effettuare quali join, leave, inserimento di una risorsa e lookup
di una risorsa. A tal fine, saranno riportati alcuni estratti di codice java.
La descrizione di Self-Chord trova spazio al capitolo 5, dove si discuterà in particolare di
tutte le modifiche apportate rispetto alla versione originale sia dal punto di vista del
funzionamento che del codice java stesso. Dopo una prima descrizione sulle novità del
protocollo, l’attenzione si concentrerà sull’applicazione realizzata e quindi sul codice java
della stessa descrivendo le principali classi e metodi dei vari package che costituiscono il
progetto.
Il lavoro di tesi si conclude con il sesto capitolo in cui saranno riportati alcuni importanti
risultati sui test svolti sul nuovo prototipo con i quali si mostrano le differenze
prestazionali rispetto la versione originale di Chord e con le conclusioni che costituiscono
il settimo e ultimo capitolo di questo lavoro.
7
1 Le reti peer to peer
1.1 Introduzione
Le reti peer-to-peer, come dice il nome stesso, sono reti costituite da un insieme di host
(macchine) i quali, al contrario di quanto avviene nel modello client-server, stanno in
rapporto paritetico tra loro (da qui la parola pari).
E’ quindi possibile definire una rete peer-to-peer come “una rete di computer o qualsiasi
rete informatica che non possiede nodi gerarchizzati come client o server fissi (clienti e
serventi), ma un numero di nodi equivalenti (pari, in inglese peer appunto) che fungono sia
da client che da server verso altri nodi della rete” [16].
Per cui si ha un sistema distribuito nel quale ogni nodo ha identiche capacità e
responsabilità e tutte le comunicazioni sono potenzialmente simmetriche, permettendo ai
vari peers collegati alla rete di poter scambiare tra loro risorse e/o servizi.
In breve, in una rete peer-to-peer:
¾ Non esistono nodi centrali
¾ Tutti i nodi hanno pari dignità Æ Assenza di una gerarchia tra nodi;
¾ I nodi interagiscono direttamente per lo scambio delle risorse;
¾ I nodi che mantengono la rete sono gli stessi che ne usufruiscono
1.2 Caratteristiche di una rete p2p
Per essere definito p2p, quindi, un sistema deve avere le seguenti caratteristiche:
¾ Real-time data/message trasmission: il flusso di informazioni passa direttamente da
un peer all’altro senza ritardi (delay) dovuti al passaggio tramite intermediari (server).
I network p2p garantiscono quindi trasmissioni in tempo reale;
¾ Peer come Client e Server: in un sistema p2p ogni computer è sia client (può
mandare dati) che server (può ricevere dati). Ogni macchina è quindi servent (server +
client). Questo permette al network di decentralizzare le informazioni che circolano in
esso. In un sistema client-server, se il server non funziona l’intera rete muore; in un
sistema p2p se un peer si disconnette, la funzione di tale peer nella rete sarà
riammortizzata dagli altri nodi, il network non subisce danni;
8
¾ Peer fornitori di contenuto: in ogni sistema p2p i singoli nodi forniscono le risorse al
network;
¾ Controllo ed autonomia dei peer: in funzione della decentralizzazione ogni nodo
della rete è autonomo ed indipendente, non c’è nessun controllo sulle sue attività e
risorse. Ci si può domandare “chi possiede l’hardware?” per capire il livello
d’autonomia e controllo (per es. l’e-mail non è p2p perché è l’ISP a possedere i mailserver e non gli utenti, ed invece, BitTorrent è p2p perché i singoli user sono i
proprietari della maggioranza dell’hardware (i propri pc sui quali è immagazzinato il
contenuto);
¾ Connessioni variabili ed indirizzi temporanei: i sistemi p2p sono indipendenti
dall’indirizzo IP permanente; l’ISP assegna indirizzi dinamici ai loro utenti , operando
fuori dal sistema dominante del sistema DNS (Domain Name Service). Inoltre la
natura della connessione alle varie reti p2p è transitoria, gli utenti non sono connessi
24 ore su 24, solo se sono on-line i loro nodi saranno attivi.
In sintesi:
1. I peer devono poter scoprire le altre entità pari sulla rete (peer discovery).
2. I peer devono poter condividere le risorse con gli altri peer (resource sharing).
3. I peer devono poter eseguire ricerche ed interrogazioni per scoprire le risorse messe
a disposizione dagli altri peer (resource discovery, resource querying).
4. Il sistema è dinamico. I peer si possono disconnettere e riconnettere più o meno
frequentemente.
“A decentralized system is not always better or worse than a centralized system. The
choice depends entirely on the needs of the application. The simplicity of centralized
systems makes them easier to manage and control, while decentralized systems grow better
and are more resistant to failures or shutdowns”.
"Un sistema decentrato non è sempre meglio o peggio di un sistema centralizzato. La scelta
dipende interamente le esigenze della domanda. La semplicità dei sistemi centralizzati li
rende più facili da gestire e da controllare, mentre i sistemi decentrati crescono meglio e
sono più resistenti ai guasti o arresti " [14].
Il peer-to-peer non è quindi il modello ideale per qualsiasi applicazione: le due architetture
(p2p e client-server) possono tranquillamente coesistere perché assolvono compiti diversi:
se per la distribuzione di qualsiasi tipo di contenuto i sistemi decentralizzati garantiscono
9
una maggiore efficienza, controllo e sicurezza possono essere assicurate solo dai modelli
centralizzati.
Un nodo p2p dovrà quindi essere in grado di svolgere una serie di operazioni di base. Esse,
in particolare, sono:
¾ BOOT: è la fase d’ingresso nell’infrastruttura peer-to-peer
¾ LOOKUP RISORSE: riguarda la ricerca da parte di un peer delle risorse
all’interno della rete stessa, per cui è l’individuazione della localizzazione
di una risorsa nella rete;
¾ SCAMBIO DI FILE;
( cfr. [5] , [11], [16] )
1.3 Classificazione delle reti Peer-to-Peer
All’interno delle reti peer-to-peer (p2p) è possibile effettuare due divisioni in “macrofamiglie”: la prima classificazione riguarda la distinzione tra p2p puro e ibrido mentre la
seconda divide le reti in strutturate e non strutturate.
La natura del sistema p2p dipende da come il network è costruito:
¾
p2p puro: in questo sistema non esiste nessun tipo di server. La rete è data dalla
somma di tutti i nodi connessi. Tutte le operazioni sopra elencate sono svolte dai
singoli peer. Il principale vantaggio consiste nell’eliminazione del server che
comporta non solo notevoli risparmi economici legati all’hardware, ma anche la
mancanza di un centro facilmente attaccabile (sia legalmente che fisicamente). In tali
sistemi la funzione di riconoscimento è molto complicata poiché demandata ai peer
(solo pochi nodi sono facilmente riconoscibili e collegabili da ogni punto della rete).
¾
p2p ibrido: nei sistemi ibridi si usa un server centrale (o più server distribuiti) per
l’adempimento di alcune funzioni di base. Solitamente il server centrale (nel caso di
Napster, o più server distribuiti nel caso della rete eDonkey) svolge la funzione di
riconoscimento e connessione dei peer: praticamente il server contiene una lista di
tutti i peer connessi alla rete (e del contenuto che pongono in condivisione) che si
aggiorna ogni volta che un nuovo nodo si connette alla rete tramite quel server. Altro
compito del server è di trasmettere tale lista a tutti i nodi che così sapranno
perfettamente quali sono le risorse condivise nel network. Il problema della ricerca (e
10
riconoscimento) dei peer (di difficile risoluzione nei sistemi puri) è così superato, a
discapito però della decentralizzazione!
La seconda divisione, come già in precedenza scritto, riguarda:
¾
Reti non strutturate: la locazione delle risorse non è legata alla topologia della rete.
Ogni peer pubblica le risorse autonomamente, quindi non c’è modo di sapere quale
peer potrebbe possedere una risorsa (Napster, Gnutella, Morpheus) ;
¾
Reti strutturate: la locazione delle risorse è legata alla topologia della rete. Esiste un
legame tra l’identificativo di una risorsa e l’indirizzo del peer che possiede tale
risorsa. È quindi possibile eseguire ricerche mirate delle risorse (Chord, CAN);
1.4 Vantaggi e svantaggi del p2p
Come in tutte le cose, accanto ai vantaggi vi sono anche dei punti di debolezza. Si elencano
ora quelli che sono i punti di forza e gli svantaggi delle reti peer-to-peer.
Vantaggi:
¾ edge: i sistemi p2p fanno leva sulla risorse inutilizzate che si trovano sui centinaia di
milioni di computer (e altri devices); rappresentano la periferia (the edge) di Internet;
¾ fast delivery: il p2p permette una più veloce distribuzione delle informazioni da nodo
a nodo bypassando qualsiasi “filtro” centrale;
¾ free bandwidth: il p2p permette una migliore gestione della banda disponibile, a
differenza del modello client-server in cui si verificano, all’aumentare delle richieste
dei client, frequenti colli di bottiglia (bottleneck);
¾ personal efficiency: gli utenti non devono aspettare lunghe code per i task essenziali
poiché l’attività viene intrapresa a discrezione dell’utente;
¾ cost saving: il p2p garantisce risparmi economici notevoli. Essendo le risorse e la
potenza di calcolo dei computer distribuita su tutta la rete, non c’è bisogno di
costosissimi server centrali; tutto ciò determina altresì la riduzione dei costi di
gestione centralizzata e di storage;
11
¾ No centralized control: non c’è nessuno big brother nei sistemi p2p, nessun controllo
dal centro;
¾ scalability: è sicuramente una della caratteristiche più importanti del modello p2p. Se
nel modello centrale l’aumentare degli utenti determina problemi di gestione, traffico
e controllo del network, nel p2p la rete può crescere a dismisura senza che si
verifichino problemi di scalabilità; anzi, l’efficienza e la ricchezza del network è
direttamente proporzionale alla sua grandezza;
¾ fault tolerance: si intende la capacità del sistema di resistere alla perdita di alcune sue
parti. Nel p2p nessun nodo è indispensabile al funzionamento dell’architettura; anche
se il network perde parte dei suoi peer (che si scollegano) rimane comunque
funzionante;
¾ privacy: il livello di privacy nei sistemi p2p è sicuramente maggiore poiché ogni tipo
di informazione è scambiato direttamente da pc a pc;
¾ flexibility: i modelli p2p sono più flessibili ed adattabili dei tradizionali client-server.
( cfr.[15], [16 ])
Svantaggi:
¾ nel p2p non vi è garanzia che il contenuto dei nodi sia sempre disponibile. Se un utente
scollega il proprio pc dalla rete, tutte le risorse che possiede e che determinano la
ricchezza di quel determinato peer non sono più accessibili dal network;
¾ difficoltà di fermare la condivisione e lo scambio di contenuto protetto da copyright
(una possibile soluzione è data dai sistemi di DRM, se ne parlerà in seguito);
¾ mancanza di standard a livello tecnologico di protocolli, infrastrutture e supporti
(anche se finalmente vengono prodotti software open source con licenza Creative
Commons come Limewire) e a livello etico/sociale;
¾ estrema facilità nel propagare qualsiasi tipo di dati benevoli ma anche malevoli (virus,
materiale pedo-pornografico, spyware, adware, misinformation, contenuti protetti,
ecc);
¾ essendo il p2p un sistema aperto è facilmente attaccabile dagli hackers; mancanza di
sicurezza;
12
2 DHT: distributed hash tables
Prima di iniziare a parlare di Chord e di Self-Chord è necessario spiegare il significato e
l’utilità che ricoprono le tabelle hash distribuite all’interno del protocollo strutturato: sia
l’inserimento che la ricerca di risorse, infatti, si basano su tabelle hash.
Le tabelle di hash distribuite (in inglese distributed hash tables, indicate anche come
DHTs) sono una classe di sistemi distribuiti decentralizzati che partizionano
l’appartenenza di un set di chiavi tra i nodi partecipanti, e possono inoltrare in maniera
efficiente i messaggi all’unico proprietario di una determinata chiave. Ciascun nodo è
l’analogo di un array slot in un’hash table [10].
Originariamente la ricerca sulle DHT fu in parte motivata da sistemi peer-to-peer quali
Napster, Gnutella e Freenet, che si basavano su risorse distribuite in Internet per offrire
l’applicazione desiderata. In particolare questi sistemi si avvantaggiavano dell’aumento di
larghezza di banda e sull’incremento degli spazi degli hard disk per fornire un sistema di
file sharing.
Le tabelle di hash distribuite utilizzano un tipo di routing basato sulle chiavi maggiormente
strutturato al fine di ottenere sia la decentralizzazione di Gnutella e Freenet, sia l’efficienza
e l’affidabilità nelle ricerche offerte da Napster. Un aspetto negativo è che, come in
Freenet, le DHT offrono una ricerca solo per titoli esatti e non su parole chiave, sebbene
questo tipo di funzionalità possa essere inserita in uno strato superiore della DHT (cfr.
[13], [14]).
2.1 Tabella Hash
Una DHT si basa quindi su una tabella hash.
Una tabella hash è una struttura dati che associa delle chiavi a dei valori (per questo motivo
spesso si fa riferimento alla coppia chiave-valore). Un valore è un oggetto, un dato o una
qualsiasi informazione che si vuole memorizzare, mentre una chiave è un’identificatore
univoco associato ad un particolare valore.
Una funzione hash è una funzione univoca che stabilisce la corrispondenza tra valore e
chiave. In particolare, essa riceve una stringa di una qualsiasi dimensione associata al
13
valore che si vuole memorizzare e restituisce una stringa di dimensioni fisse che non ha
alcuna correlazione col valore. La funzione opera, infatti, sui bit che costituiscono la
stringa-valore e dato il valore hash non sarà più possibile recuperare con una funzione
inversa il valore della stringa “madre”.
L’operazione più importante supportata dalle tabelle hash è il lookup ossia l’operazione di
get: data una chiave viene restituito il valore associato alla chiave.
L’altra operazione è l’insert ossia l’inserimento di un risorsa nella tabella mediante il
calcolo della chiave.
Entrambe le operazioni hanno un costo temporale costante pari a O(1).
Il primo passo per realizzare algoritmi di ricerca tramite hashing è quello di determinare la
funzione di hash: il dato da indicizzare viene trasformato da un'apposita funzione di hash in
un intero compreso tra 0 ed n-1 che viene utilizzato come indice in un array di lunghezza n.
Idealmente, chiavi diverse dovrebbero essere trasformate in indirizzi differenti, ma poiché
non esiste la funzione di hash perfetta, ovvero totalmente iniettiva, è possibile che due o
più chiavi diverse siano convertite nello stesso indirizzo. Il caso in cui la funzione hash
applicata a due chiavi diverse genera un medesimo indirizzo viene chiamato collisione e
può essere gestito in vari modi. La scelta di una buona funzione di hash è indispensabile
per ridurre al minimo le collisioni e garantire prestazioni sempre ottimali. Il risultato
migliore si ha con funzioni pseudo-casuali che distribuiscono i dati in input in modo
uniforme. Molto spesso però, una buona funzione di hash non può bastare, infatti, le
prestazioni di un’hash table sono molto legate anche al cosiddetto fattore di carico (load
factor) calcolato come Celle libere/Elementi presenti e che ci dice quanta probabilità ha un
nuovo elemento di collidere con uno già presente nella tabella. È bene dunque mantenere il
load factor il più basso possibile (di solito un valore di 0.75 è quello ottimale) per ridurre al
minimo il numero di collisioni. Questo può essere fatto, ad esempio, ridimensionando
l'array ogni volta che si supera il load factor desiderato. (cfr. [12] ).
14
2.2 Proprietà delle DHTs
Le caratteristiche delle DHT enfatizzano le seguenti proprietà:
•
Decentralizzazione: i nodi formano collettivamente il sistema senza alcun
coordinamento centrale.
•
Scalabilità: il sistema è predisposto per un funzionamento efficiente anche con
centinaia di milioni di nodi.
•
Tolleranza ai guasti: il sistema dovrebbe risultare affidabile anche in presenza di
nodi che entrano, escono dalla rete o sono soggetti a malfunzionamenti con elevata
frequenza.
La tecnica chiave utilizzata per raggiungere questi scopi è costituita dal fatto che ciascun
nodo ha bisogno di rimanere in contatto solo con un piccolo numero d’altri nodi della rete;
in questa maniera la rete è sottoposta ad una minima quantità di lavoro per ogni modifica
che avviene nel set di nodi che la costituiscono.
2.3 Partizionamento del keyspace
Molte DHT usano alcune varianti del consistent hashing per mappare le chiavi ai nodi.
Questa tecnica impiega una funzione δ(k1,k2) che definisce la nozione astratta di distanza
tra la chiave k1 e la chiave k2 . A ciascun nodo è assegnata una chiave che è detta
identificativo (ID). Ad un nodo con ID i appartengono tutte le chiavi per cui i è l’ID più
vicino, misurando in base a δ.
L’algoritmo Chord (di cui si parlerà nei prossimi capitoli) considera le chiavi come punti
su una circonferenza, e δ(k1,k2) è la distanza percorsa (in senso orario) sul cerchio tra k1 e
k2. Perciò, il keyspace circolare è diviso in segmenti contigui i cui punti terminali sono gli
identificativi di nodo. Se i1 e i2 sono due IDs adiacenti, allora il nodo con ID i2 è
proprietario di tutte le chiavi che cadono tra i1 e i2.
Il consistent hashing ha la caratteristica fondamentale che la rimozione o l’addizione di un
nodo modifica solo il set di chiavi posseduti dai nodi con IDs adiacenti, senza coinvolgere
tutti gli altri nodi. Tutto ciò al contrario di una hash table tradizionale, nella quale
l’addizione o la rimozione di un bucket causa un rimappaggio di quasi tutto l’intero
15
keyspace. Dal momento che ogni modifica nel set di chiavi di cui un nodo è responsabile
corrisponde tipicamente ad un intenso (per quanto riguarda la larghezza di banda)
movimento da un nodo all’altro d’oggetti immagazzinati nella DHT, una minimizzazione
di questi movimenti di riorganizzazione è necessaria al fine di far fronte in maniera
efficiente a peers che hanno un comportamento molto dinamico (elevato numero di
ingressi, uscite o malfunzionamenti).
Si è dimostrato che grazie all’utilizzo di protocolli p2p di seconda generazione che
supportano DHT è stata migliorata la scalabilità 1 del sistema [12].
Messaggi necessari
per trovare una
chiave
Anello
N-1
Chord
Grafo
totalmente
connesso
Log(N)
1
1
Log(N)
N-1
Dimensione tabella di routing
Figura 1: scalabilità del sistema; rapporto tra dimensione tabelle di routing e messaggi da
scambiare per la ricerca di una chiave
1
Scalabilità: capacità di un sistema di incrementare le proprie prestazioni (il proprio
throughput nel caso di sistemi trasmissivi) se a tale sistema vengono fornite nuove risorse
(nuovi nodi nel sistema)
16
2.4 Alcuni esempi di protocolli basati su HDT
2.4.1 CAN
CAN (Content-Addressable Network, [21]) riporta i nodi della rete in uno spazio virtuale
d-dimensionale di coordinate cartesiane. L’intero spazio di coordinate è partizionano
dinamicamente tra tutti i nodi della rete, in modo che ogni peer possieda una propria zona
individuale e distinta all’interno dello spazio complessivo: lo spazio, è infatti, diviso in
blocchi d-dimensionali e ogni nodo è proprietario di un singolo blocco. Un nodo mantiene
una tabella di routing che contiene l’indirizzo IP e le coordinate virtuali di ogni zona ad
esso vicina (ciascun blocco conosce i blocchi ad esse contigui, perché ne mantiene alcune
informazioni).
Figura 2: un esempio di rete CAN con 5 nodi in uno spazio 2-d
Una chiave è deterministicamente associata ad un punto P nello spazio delle coordinate,
usando una funzione hash, e la corrispondente coppia chiave-valore è memorizzata nel
nodo che è il proprietario della zona all’interno del quale giace il punto P. Durante la fase
di ricerca di un valore, i nodi instradano un messaggio verso la destinazione usando un
semplice algoritmo greedy: ogni nodo inoltra il messaggio al nodo che risiede nella zona
con le coordinare più vicine alla destinazione.
17
2.4.2 Kademlia
Kademlia [17] è un protocollo p2p ideato per una rete di computer decentralizzata e basata
sulla tecnologia DHT. In Kademlia i nodi comunicano tra di loro utilizzando il protocollo
di trasporto UDO.
L’algoritmo Kademlia utilizza un approccio alternativo per il calcolo della “distanza” tra
due nodi (questa distanza non ha nulla a che vedere con la localizzazione geografica dei
nodi, ma rappresenta la distanza nello spazio degli identificatori): equivale al risultato
dell’OR esclusivo (XOR) tra gli identificatori dei due nodi.
Figura 3: Esempio di albero binario in Kademlia
Kademlia tratta i nodi come foglie di un albero binario, in cui la posizione di ogni nodo è
determinata dal più breve prefisso unico del suo identificativo. Per ogni nodo, l’albero
vinario è diviso in una serie di sotto-alberi sempre più bassi che non contengono il nodo. Il
sotto-albero più alto consiste nella metà dell’albero che non contiene il nodo, il successivo
sotto-albero consiste nella metà del rimanente albero che non contiene il nodo e così via.
Ogni nodo contiene un riferimento ad almeno un nodo di ogni sotto-albero; questo
garantisce che ogni nodo possa contattare qualsiasi altro nodo (si veda la Figura 3, in cui
un punto nero mostra la posizione del nodo con prefisso 0011 nell’albero mentre gli ovali
evidenziano i sotto-alberi nei quali quel nodo deve avere un contatto).
Quando si cerca una chiave, l’algoritmo esplora la rete in passi successivi ed ad ogni
passaggio salta ad un sotto-albero diverso sempre più piccolo, avvicinandosi sempre più al
18
nodo responsabile della chiave cercata. Il numero di nodi contattati durante la ricerca
dipende solo marginalmente dalla dimensione della rete.
Kademlia è usata da molti programmi di file sharing, come eMule, eDonkey e vari client
BitTorrent.
2.4.3 Pastry e Tapestry
Durante la costruzione delle tabelle di routing, né Chord né CAN tengono in
considerazione le distanze che separano i nodi nella rete. Contrariamente, Tapestry [4]
costruisce tabelle di routing localmente ottimali fin dall’inizio, e poi le mantiene per
ridurre l’estensione del routing. Tapestry assegna dinamicamente ogni identificatore
(chiave) ad un unico nodo attivo, chiamato root di quell’identificatore. Per consegnare i
messaggi , ogni nodo mantiene una tabella di routing che contiene gli identificatori dei
nodi con cui comunica (i suoi vicini) e i loro rispettivi indirizzi IP. I messaggi vengono
inoltrati verso destinazione considerando una cifra alla volta della chiave cercata, trovando
il nodo appropriato nella tabella di routing. Quando ad una cifra non corrisponde alcun
nodo nella tabella di routing, Tapestry esegue il routing “surrogato” individuando la cifra
più simile nella tabella di routing ed individuando il messaggio al nodo corrispondente.
Pastry [3], pur essendo un progetto indipendente da Tapestry si basa sugli stesi principi.
19
3 Chord
3.1 Introduzione
Chord [19] è un sistema p2p puro e strutturato 2 nato dallo studio di alcuni studiosi del
MIT (Massachusetts Institute of Technology) a Cambridge nel Massachusetts.
Chord è stato sviluppato per cercare di migliorare le prestazioni di ricerca di una
particolare risorsa all’interno di un insieme di nodi che vanno a formare la rete p2p. Il
componente base del progetto Chord è l'algoritmo di ricerca distribuita basata su tabelle di
hash (DHT).
I vari peer si dispongono in maniera da formare un anello e sono ordinati in senso orario in
base all’id assegnato a ciascun nodo. Ogni nodo possiede quindi un successore e un
predecessore. Il nodo successore di un nodo n è il peer che in senso orario viene subito
dopo n.
Il protocollo supporta una sola operazione: data una chiave essa viene mappata su un nodo,
o meglio sul nodo con ID uguale o maggiore al valore della chiave. Per il calcolo della
chiave, in Chord è stata usata una variante del consistent hashing [12]. Mediante il
consistent hashing si cerca di mantenere il carico bilanciato e quindi di distribuire tra i vari
nodi lo stesso numero di chiavi evitando, nel momento in cui un nudo entra o esce dal
sistema, di muovere un gran numero di chiavi [6].
Il primo lavoro svolto sulle consistent hashing assumeva che ogni nodo doveva conoscere
tutti gli altri nodi appartenenti alla rete o comunque della maggior parte dei nodi [12]. Al
contrario, in Chord, ogni nodo necessita delle informazioni di "Routing" su solo pochi altri
nodi. Essendo, infatti, la tabella di routine distribuita, un nodo riesce ad avere conoscenza
di tutti i nodi della rete solamente comunicando con un paio d’altri nodi.
In stato stazionario, quindi quando il sistema non è sottoposto a grandi cambiamenti, in una
rete di N peers, ogni nodo mantiene informazioni solo su O(log N) altri nodi, e riesce a
risolvere tutte le ricerche tramite O(log N) messaggi scambiati con gli altri nodi. Nel
momento in cui un nodo entra o lascia la rete, con alta probabilità, saranno necessari non
più di O(log2 N) messaggi per l’aggiornamento della varie tabelle di routing ( cfr. [19]).
2
Si veda il capitolo 1
20
Dopo la breve introduzione, nei paragrafi che seguiranno, si cercherà di mettere in luce
ancor meglio il funzionamento del protocollo scendendo ad un livello di maggiore
dettaglio.
3.2 Nodi e Chiavi in Chord
Si è in già parlato, nel capitolo precedente, delle tabelle di hashing distribuite, delle loro
caratteristiche e della loro funzione. Ci si concentrerà adesso sulla particolare funzione che
è stata utilizzata nel protocollo Chord.
La funzione di consistent hashing assegna ad ogni nodo un identificatore di m-bit
utilizzando una funzione di base come SHA-1.
SHA-1 (Secure Hash Algorithm 1) è stato progettato dal NIST (l’algoritmo SHA originale
è stato sostituito con SHA-1 dal NIST stesso per via di una vulnerabilità non pubblicata
come spiegato in [7]).
Il messaggio (che deve essere di lunghezza inferiore a 264 bit) è elaborato a blocchi di 512
bit e in output viene rilasciato un blocco di 160 bit [18].
L’identificatore di un qualsiasi nodo nell’anello viene calcolato applicando la funzione
hash all’indirizzo IP del nodo stesso. Esso dovrà essere abbastanza grande da rendere
trascurabile la probabilità di avere due nodi con lo stesso ID.
Si descrive di seguito come avviene l’assegnazione delle chiavi ai vari nodi.
Gli dentificatori sono ordinati in un cerchio in ordine crescente in senso orario. La chiave k
a cui è associato un valore v è assegnata al primo nodo il cui identificatore è uguale o
segue (l'identificatore di) k nell’anello. Questo nodo è chiamato il "nodo successore" della
chiave k, indicato da successor(k).
La figura di seguito riportata mostra un anello con alcuni peer collegati alla rete (quelli con
il puntino rosso).
21
Figura 4: Anello di peers (in rosso)
Attraverso la Figura 4 si cerca di capire come avviene l’assegnamento di una risorsa. Se la
chiave ha valore 10 essa sarà assegnata al nodo succesor(10) che è il nodo con ID 10.
Qualora invece debba essere mappata una risorsa con chiave 3, succesor(3) sarà il nodo 5.
Per mantenere il mapping basato su consistent hashing, quando un nodo n entra a far parte
della rete, alcune chiavi in precedenza assegnate al successore del nodo n adesso vengono
spostate sul nodo n. Al contrario, quando un nodo lascia la rete, le chiavi che gli erano state
assegnate vengono spostate sul nodo predecessore e successore. In questo modo si cerca di
garantire un bilanciamento del carico dal momento che tutti i peers dell’anello hanno circa
lo stesso numero di chiavi e quindi di risorse.
TEOREMA 1:
Per ogni set di N nodi e K chiavi, con alta probabilità:
1. ogni nodo è responsabile al più di (1 + ε )
K
chiavi
N
2. quando un (N+1)-esimo nodo entra o lascia la rete saranno spostate
⎛K⎞
O⎜ ⎟ chiavi
⎝N⎠
Quando la funzione di consistent hashing viene applicata come descritto in precedenza, il
teorema mostra che ε assume un valore O(log N) ma che può comunque essere ridotto ad
un costante arbitrariamente piccola.
22
La frase "con elevata probabilità" è dovuta la fatto che comunque il calcolo delle chiavi è
sempre qualcosa di casuale basato sul valore che viene passato in input per il calcolo della
chiave e quindi, anche se con bassa probabilità, si potrebbero ottenere chiavi uguali. E’
proprio per garantire che le chiavi siano diverse e che siano distribuite in maniera
equilibrate che viene utilizzata la variante SHA-1 [18]. Questo rende il protocollo
deterministico, in modo che le rivendicazioni di "alta probabilità" non ha più alcun senso.
Ma per mantenere il bilanciamento del carico tra i vari nodi, per il calcolo delle chiavi,
sarebbe meglio conoscere a priori il numero di nodi che possono accedere alla rete che può
essere difficile da determinare.
Naturalmente, si potrebbe scegliere di utilizzare un numero limite di peer a priori fornendo
così ottimi risultati per quanto riguarda il bilanciamento del carico.
3.3 Chord e le DHTs
All’interno della rete, ciascun nodo ha il bisogno di avere conoscenza di un particolare
numero di nodi per poter bilanciare il carico tra i vari peer e per poter rispondere in
maniera efficiente alle ricerche di lookup che su questo peer vengono inoltrate.
Ciò potrebbe portare all’idea che ogni nodo debba conoscere tutti gli altri nodi, ma questo
vorrebbe dire avere un elevato carico d’informazioni da mantenere, né è pensabile che ogni
nodo conosca solo il suo successore in quanto lo scambio d’informazioni in questo caso
risulterebbe molto lento e macchinoso.
Per questo, sfruttando le Distributed Hash Table (DHT) ogni nodo manterrà informazioni
solo su un certo numero d’altri nodi. In particolare l’i-esimo peer avrà conoscenza solo dei
successori con posizione potenza di 2 oltre che del nodo precedente. Questo permette che,
in una rete di N nodi, ogni nodo conoscerà solo un numero di nodi dell’ordine di O(log(N))
e risolve la ricerca di una risorsa al più mediante lo scambio di O(log(N)) messaggi con gli
altri nodi.
Se quindi le chiavi o gli identificatori dei nodi saranno costituiti da m bit, la finger table di
ogni nodo avrà al più m entries.
A titolo di esempio si consideri la Figura 5.
In base a quanto detto prima, essendo 16 i peer appartenenti alla rete, un nodo deve avere
conoscenza solo di log(16) peers.
23
Un qualsiasi nodo n in una rete con N=2m peers avrà quindi come successori quelli in
posizione (n + 2i-1 ) mod 2m con i che va da 1 a m. Se si prende in considerazione il peer
con ID 1, esso conoscerà i peer con ID 1+20, 1+21, 1+22 e1+23 purché essi siano presenti.
Se infatti consideriamo l’immagine a lato e consideriamo tutti i peer collegati alla rete
ecco come sarà la DHT del peer.
ID
succ.
2
2
3
3
5
5
9
9
Figura 5: esempio di finger table per il nodo con ID 1 e relativa finger table
Ogni entry di una finger table, sarà quindi costituita dall’ID del nodo nell’anello e
dall’indirizzo
IP
del
informazione
necessaria
nodo
per
stesso,
poter
“contattare” il nodo. Si noti che proprio per
come vengono gestite le entry il primo nodo
sarà quello in posizione ID+20 dove 20=1
per cui esso sarà sempre il successore diretto
ossia il nodo che viene subito dopo
nell’anello.
In questo modo però, un nodo, come già
scritto
in
precedenza,
non
ha
piena
conoscenza della rete, per cui bisogna capire
come si comporterà nel momento in cui
Figura 6: anello di Chord
all’i-simo nodo per qualche ragione debba comunicare con un nodo non presente nella sua
finger table. Considerando la Figura 6, s’immagini che il nodo 1 debba comunicare col
24
nodo 13. In questo caso, il nodo n, che dovrà inoltrare la richiesta ad m, cercherà nella
finger table, il nodo che immediatamente precede m e inoltrerà ad esso la richiesta.
Nell’esempio in considerazione, il nodo 1 trasmetterà la richiesta al nodo 9 e quest’ultimo,
avendo conoscenza del nodo 13 potrà facilmente inoltrare la richiesta.
Figura 7: passi dell’algoritmo per la ricerca nella finger del nodo responsabile per una chiave.
TEOREMA 2: in una rete di N peers, il numero di nodi che dovrà essere contattato per la
ricerca di un successore sarà, con alta probabilità, O (log N). ( cfr. [19])
A dimostrazione del teorema consideriamo il caso peggiore per la ricerca di un nodo
successore. Considerando nuovamente l’immagine precedente, il caso peggiore è, partendo
dal nodo 0 è la ricerca del nodo 15 (si suppone che ogni nodo non abbia conoscenza del
nodo precedente).
Il nodo 0, nella propria finger avrà come entries i nodi 1,2,4 e 8. Esso inoltra la richiesta al
nodo 8 e questi al nodo 12 il quale conoscerà 13 e 14. La richiesta passa quindi al nodo 14
che come diretto successore avrà proprio il nodo 15.
Facendo un veloce calcolo si nota come i peer contattati sono 4 che è proprio il logaritmo
di 16 (numero dei nodi totali dell’anello).
25
Figura 8 : esempio di passi logaritmici per la ricerca di un nodo successore
Di seguito si riporta lo pseudocodice delle operazioni principali per la ricerca del nodo
successore per un particolare id e del nodo predecessore.
// ask node n to find the successor of id
n.find_successor(id)
if (id (n, successor])
return successor;
else
// forward the query around the circle
n0 = closest_preceding_node(id);
return n0.find_successor(id);
// search the local table for the highest predecessor of id
n.closest_preceding_node(id)
for i = m downto 1
if (finger[i] (n,id))
return finger[i];
return n;
26
3.4 Join e Leave
Le due operazioni fondamentali che interessano un qualsiasi nodo in Chord [19] sono la
join (ingresso nella rete) e la leave (uscita dalla rete); tali operazioni, proprio per la
dinamicità di Chord, potranno essere effettuate da ciascun nodo in ogni momento.
Obiettivo principale, nel momento in cui un nodo entra o esce dalla rete, è quello di evitare
di spostare un elevato numero di chiavi mantenendo però ordinato il mapping delle chiavi
sui nodi per garantire di rendere possibile il lookup delle risorse. Per fare ciò, Chord deve
garantire che:
1. Ogni successore di un nodo deve essere costantemente verificato e mantenuto
correttamente;
2. Per ogni chiave k, il nodo successor(k) dovrà essere responsabile per k;
3. Le finger table devono costantemente essere aggiornate in modo da garantire in
maniera efficace il lookup delle risorse;
TEOREMA 3: Con alta probabilità, ogni nodo che entra o lascia la rete costituita da N
nodi, userà O(log2 N) messaggi per riordinare le risorse e ristabilire le nuove entries per
le finger table .
Inoltre, come proposto dal MIT in [19], ogni nodo oltre ad avere conoscenza dei nodi
successori, conosce pure il nodo immediatamente precedente permettendo così di potersi
muovere lungo i nodi della rete in senso antiorario.
Nel momento in cui un nodo n accede alla rete sarà necessario effettuare alcune operazioni
indispensabili per mantenere il sistema stabile ed efficiente:
1. inizializzazione del nodo predecessore e della finger table del nodo n;
2. aggiornamento del nodo predecessore e delle entries delle finger tables degli altri
nodi;
3. spostamento sul nodo n delle risorse per le quali sarà responsabile;
27
Inizializzazione predecessore e fingers
Calcolare la finger table è banale: come già in precedenza scritto, per determinare le varie
entry r il nodo n, sarà necessario applicare sempre la stessa formula n+2 i-1 con i compreso
tra 1 e m con m= ⎡log(N )⎤ dove N è il numero dei nodi che costituiscono l’anello.
Per calcolare il predecessore invece dovranno essere fatte tre operazioni:
1. n chiede ad un nodo qualsiasi di calcolare n’=successor (n);
2. n chiede a n’ chi è il suo predecessore
3. Il predecessore di n’ diventa ora predecessore di n
Per ottimizzare il processo, il nodo appena entrato può chiedere ad un suo vicino il suo
nodo precedente e una copia della finger: usando tali dati può ricostruire la propria finger
dal momento che quella del nodo vicino sarà molto simile, riducendo così i tempi per la
costruzione della nuova finger dell’ordine di O(log N).
Aggiornamento predecessori e fingers degli altri nodi nella rete
Ipotizziamo che, dopo l’ingresso di n nella rete, esso debba diventare l’i-esimo finger di un
nodo p., questo può accadere se e solo se:
¾ p precede n di almeno 2 i-1 chiavi
¾ il nodo m puntato dall’i-esimo finger di p soddisfa m=successor(n)
Il primo nodo che può soddisfare entrambe queste condizioni è successor(n-2 i-1).
Trasferimento delle chiavi
Questa operazione può essere portata a termine con una semplice ispezione di tutte le
chiavi gestite da successor(n).
Si riportano, di seguito, in pseudocodice le operazioni che interessano la join e la
stabilizzazione di un nodo:
¾ CREATE: crea un nuovo anello che andrà a contenere i vari nodi della rete
n.create()
predecessor = nil;
successor = n;
28
¾ JOIN: permette ad un nodo di accedere ad una rete già esistente
Un nodo, appena entrato nella rete chiede ad un qualsiasi nodo m di trovare il suo diretto
successore.
n.join(m)
predecessor = nil;
successor = m.find_successor(n);
¾ STABILIZE: operazione chiamata periodicamente per aggiornare lo stato della rete e
per notificare l’aggiunta di nuovi nodi. A tal fine viene determinato il successore del
predecessore di n e se si sono aggiunti nuovi nodi viene fatta la notifica.
n.stabilize()
x = successor.predecessor;
if (x ∈ (n, successor))
successor = x;
successor.notify(n);
¾ NOTIFY: al nodo n viene settato come predecessore il nodo m
n.notify(m)
if (predecessor is nil or m ∈ (predecessor, n))
predecessor = m;
¾ FIX_FINGER: periodicamente viene chiamato questo metodo per aggiornare le
entries di ciascuna finger table.
n.fix_fingers()
next = next + 1 ;
if (next > m)
next = 1 ;
finger[next] = find_successor(n + 2next-1);
29
¾ CHECK_PREDECESSOR:
periodicamente
è
controllata
la
presenza
del
predecessore.
Consente ad n di resettare il proprio predecessor, nel caso che questo sia fallito. In caso di
fallimento, il predecessor viene settato quando n riceve una notify (durante una procedura
stabilize).
n.check_predecessor()
if (predecessor has failed)
predecessor = null;
Si riporta di seguito lo pseudocodice in cui sono riportate tutte le operazioni da eseguire
dopo la join o leave di un nodo per ristabilizzare l’anello.
// create a new Chord ring.
n.create()
predecessor = nil;
successor = n;
// join a Chord ring containing node n'.
n.join(n')
predecessor = nil;
successor = n'.find_successor(n);
// called periodically. verifies n’s immediate
// successor, and tells the successor about n.
n.stabilize()
x = successor.predecessor;
if (x (n, successor))
successor = x;
successor.notify(n);
// n' thinks it might be our predecessor.
n.notify(n')
if (predecessor is nil or n' (predecessor, n))
predecessor = n';
// called periodically. refreshes finger table entries.
// next stores the index of the finger to fix
n.fix_fingers()
next = next + 1;
if (next > m)
next = 1;
finger[next] = find_successor(n+2next − 1);
// called periodically. checks whether predecessor has failed.
n.check_predecessor()
if (predecessor has failed)
predecessor = nil;
30
3.5 Fallimento di un nodo
Quando un nodo n per un qualche motivo fallisce e quindi deve lasciare la rete, tutti gli
altri nodi che hanno nelle loro fingers il riferimento al nodo n, dovranno modificare le
entries della finger e dovranno andare a determinare il successore di n. Inoltre, il fallimento
di un nodo non deve andare a perturbare le query che sono in corso in quel momento: per
questo motivo quindi il sistema si deve subito ristabilizzare. A tal fine, ogni nodo contiene
oltre alla finger table una lista dei successori e, qualora il diretto successore dovesse per un
qualche motivo abbandonare per fallimento l’anello, mediante la lista dei successori potrà
aggiornare e modificare il nodo diretto successore.
Potrebbe anche succedere che nell’intervallo di tempo tra il fallimento di un nodo e la
completa stabilizzazione della rete, un qualche altro nodo potrebbe inviare richieste al
nodo caduto o servirsi del nodo caduto per effettuare lookup su un altro nodo: per evitare
ciò, idealmente la richiesta dovrebbe essere processata, dopo un certo timeout, passando
per un altro percorso che non è interessato da fallimento. Ciò in molti casi avviene grazie
ad un’ulteriore lista di nodi facilmente costruibile a partire dalla finger del nodo precedente
decaduto. Ciò trova le proprie basi nei due teoremi di seguito riportati:
TEOREMA 4: se si usa una lista di successori di lunghezza r=O(log N) in una rete
inizialmente stabile e in cui successivamente un nodo fallisce con probabilità 0.5, allora
con alta probabilità find_successor(k) sarà allora il nodo ancora attivo più vicino a k.
TEOREMA 5: se si usa una lista di successori di lunghezza r=O(log N) in una rete
inizialmente stabile e in cui successivamente un nodo fallisce con probabilità 0.5, allora il
tempo di attesa per l’esecuzione di find_successor nella rete con la presenza di un
fallimento, sarà O(log N).
31
4 Open Chord
4.1 Introduzione
Dopo la versione in C di Chord del MIT [19], nel 2007 è stata pubblicata una versione
sempre open source di Chord realizzata in java da Sven Kafille e Karsten Loesing,
ingegneri della Otto-Friedrich-Universität Bamberg (Germania) [20].
La versione di Open Chord è nata proprio dall’esigenza di un numero sempre più elevato
di programmatori, di lavorare con java, linguaggio di programmazione che giorno dopo
giorno si è rivestito un’importante fetta di mercato nel mondo della programmazione ad
oggetti.
4.2 Caratteristiche di Open-Chord
Le principali caratteristiche di Open Chord riportate in [20] sono:
¾ Facilità nell’uso sia di una versione sincrona che asincrona delle DHT di Chord;
¾ Possibilità di memorizzare un qualsiasi oggetto Serializable sottoforma di risorsa;
¾ Trasparente mantenimento delle tabelle di routing dei nodi dell’anello;
¾ Trasparente replicazione dei dati memorizzati in ogni nodo;
¾ Presenza di una versione basata su comunicazione remota basata su socket java e per
questo simile alla versione originale di Chord.
¾ Presenza di una versione funzionante in locale e utilizzabile come simulatore di una
rete per l’effettuazione di test;
Per quanto riguarda il funzionamento dell’applicazione, esso si rifà in tutto e per tutto alla
versione in C, per questo l’attenzione si baserà soprattutto nella descrizione delle
particolarità del codice java e su come le varie azioni che un nodo può eseguire (join,
leave, etc.) vengono gestite in OpenChord.
32
4.3 Configurazione
Prima di andare a vedere il funzionamento dell’applicazione bisogna tenere in
considerazione la possibilità di Open Chord di poter essere configurato mediante un file di
proprietà il quale contiene una serie di campi che verranno a breve descritti. Le proprietà
possono essere divise in 3 principali categorie: proprietà per configurare il logging di Open
Chord 3 , proprietà per configurare il mantenimento e la replica delle finger table e proprietà
per configurare gli ascoltatori per iniziare le richieste.
Proprietà di configurazione logging:
de.uniba.wiai.lspi.util.logging.logger.class:
questa proprietà consente di specificare il nome completo (FQN) dell’implementazione di
logger utilizzata per messaggi i di log di Open Chord. Attualmente sono disponibili un tipo
di log su console (de.uniba.wiai.lspi.util.logging.SystemOutPrintlnLogger), e un logger
della libreria di log4j (de.uniba.wiai.lspi.util.logging.Log4jLogger). Quest'ultimo è quello
di default qualora non sia specificato uno. Se Log4j non può essere trovato nel classpath
allora verrà sempre utilizzato quello standard.
de.uniba.wiai.lspi.util.logging.off:
Se questa proprietà è impostato a true, il logging non viene effettuato. Questo è
automaticamente impostata a true se il livello di log o il logger non sono stati trovati nel
path.
log4j.properties.file:
Questa proprietà definisce il nome del file di proprietà, che viene utilizzata per configurare
log4j. Se il file è disponibile nel classpath può essere specificato solo il nome del file. Se il
file si trova in una directory non sul classpath, allora dovrà essere specificato il percorso
completo del file in un formato adatto per il suo funzionamento.
de.uniba.wiai.lspi.Chord.data.ID.number.of.displayed.bytes:
Questa proprietà definisce il numero di byte che dovranno essere visualizzati per un peer o
per una risorsa da memorizzare nei nodi.
3
per il test delle classi e il debug è stata utilizzata la libreria Log4j per la quale alcune proprietà vengono
settate dal file di proprietà.
33
Le proprietà per configurare il mantenimento e le copie dei nodi nelle finger di Open
Chord sono:
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.successors:
Questa proprietà deve essere impostata su un valore intero che rappresenta il numero di
repliche che sono create per ogni dato.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.StabilizeTask.start:
Questa proprietà deve essere impostata su un valore intero, che specifica il numero di
secondi da attendere fino a quando inizia il processo di stabilizzazione della rete.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.StabilizeTask.interval:
Questa proprietà specifica (con l'aiuto di un valore intero) il periodo di tempo in secondi
tra successive esecuzioni del task di stabilizzare.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.FixFingerTask.start:
Questa proprietà deve essere impostato su un valore intero, che specifica il numero di
secondi di attesa dalla creazione della rete, per avviare il processo di creazione della tabella
di routing di un peer.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.FixFingerTask.interval:
Questa proprietà specifica (con l'aiuto di un valore intero) il periodo di tempo in secondi
tra successive esecuzioni di aggiornamento delle finger tables dei vari nodi.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.CheckPredecessorTask.start:
Questa proprietà deve essere impostato su un valore intero, che specifica il numero di
secondi di attesa, dopo la creazione della rete, prima di determinare per ciascun nodo il
proprio predecessore.
de.uniba.wiai.lspi.chord.service.impl.ChordImpl.CheckPredecessorTask.interval:
Questa proprietà specifica (con l'aiuto di un valore intero) ogni quanto viene controllato e
aggiornato per ogni nodo il proprio predecessore.
34
Le proprietà di configurazione riguardanti il numero di richieste funzionanti in
contemporanea tra vari peer sono:
de.uniba.wiai.lspi.chord.com.socket.InvocationThread.corepoolsize:
Questa proprietà deve essere impostato su un valore intero, che specifica il numero di
threads che sono sempre a disposizione per servire le richieste provenienti da altri nodi.
de.uniba.wiai.lspi.chord.com.socket.InvocationThread.maxpoolsize:
Questa proprietà deve essere impostato su un valore intero, che specifica il numero
massimo di threads che possono essere pronti a servire le richieste provenienti da altri
nodi.
de.uniba.wiai.lspi.chord.com.socket.InvocationThread.keepalivetime:
Questa proprietà definisce, in secondi, per quanto tempo i threads possono ancora rimanere
in vita dopo essere diventati inattivi.
35
4.4 Architettura di Open Chord
L’architettura di Open Chord è suddivisa in tre strati (come da figura)
Figura 9: architettura di Open Chord
A livello più basso risiede il livello di comunicazione basato su Java Socket. I Sockets
java, permettono di far comunicare due entità mediante l’invio di pacchetti TCP 4 . In cima
a questo livello di comunicazione vi è un livello astratto di comunicazione che fornisce
interfacce astraendosi dal tipo protocollo di comunicazione utilizzato.
A questo scopo due classi astratte sono state sviluppate, che rappresentano lo strato astratto
di comunicazione e forniscono dei metodi di factory (di costruzione), per la creazione
d’istanze di se stessi per uno specifico protocollo di comunicazione. Ciò vuol dire che
quando sarà creato un oggetto di tipo Proxy o Endpoint, verrà restituito un oggetto
concreto che potrà essere di tre tipi in base al protocollo di comunicazione che si desidera
utilizzare .
4
Protocollo del livello di trasporto nello stack protocollare ISO-OSI. Di solito è usato con il protocollo di
levello rete IP
36
Proxy è un’interfaccia che serve a far conoscere ad un nodo tutti gli altri nodi per
permettere la comunicazione TCP.
Endpoint, altra interfaccia, invece, fornisce un punto di connessione remota tra peers con
l'aiuto di Proxy per un determinato protocollo di comunicazione. Ogni nodo dovrà quindi
disporre di un Endpoint mediante il quale potrà comunicare con gli altri nodi. Per
inizializzare l’Endpoint di un nodo sarà necessario conoscere l’URL del nodo stesso e la
creazione avverrà invocando il metodo della class Endpoint createEndpoint (Node, URL).
Questo metodo cerca di determinare l’Endpoint concreto e quindi basato su un particolare
protocollo con l'aiuto dell'url in cui è specificato il protocollo utilizzato.
Un Endpoint può essere in tre stati:
¾ STARTED
¾ LISTENING
¾ ACCEPT_ENTRIES
Nello stato STARTED l'Endpoint è stato inizializzato, ma non ascolta (eventualmente) i
messaggi in arrivo dalla rete. Un Endpoint si trova in questo stato se è creato con l'aiuto del
suo costruttore.
Nello stato di LISTENING, l’Endpoint accetta messaggi ricevuti dalla rete di Chord e li
utilizza per l’aggiornamento dei dati del nodo e quindi della lista dei successori e delle
finger tables. Il passaggio a questo stato è fatto mediante l’invocazione del metodo listen();
Nello stato ACCEPT_ENTRIES, l’Endpoint accetta messaggi dalla rete di Chord e sono
messaggi relativi all’archiviazione o la rimozione di voci dal DHT. Il passaggio a questo
stato si ha invocando il metodo acceptEntries ().
Il terzo livello, quello più in alto, si può dire preveda due interfacce che servono per la
gestione di tutta l’applicazione.
La prima interfaccia Chord, prevede l’utilizzo di metodi sincroni per recuperare,
conservare, e rimuovere i valori all'interno di DHT. L'altra interfaccia AsynChord, invece,
come dice il nome stesso, può essere utilizzato per il recupero, la memorizzazione e la
rimozione dei dati dal DHT in maniera asincrona. Lo strato logico di Chord è anche
responsabile per la replica dei dati e la manutenzione delle proprietà che sono necessarie
per mantenere il DHT in esecuzione, come descritto precedentemente.
37
4.5 Protocolli di comunicazione utilizzati
Attualmente sono disponibili nell’applicazione due implementazioni del protocollo di
comunicazione:
1. Locale (oclocal): questo protocollo è stato sviluppato in modo da favorire la
creazione di una rete di Open Chord all'interno di un unica JVM al fine di effettuare
prove.
2. Protocollo basato su Socket (ocsocket): questo protocollo affidabile basato su
socket e quindi sul protocollo di tipo TCP/IP facilita la comunicazione tra nodi
situati su macchine diverse.
Il protocollo, che è utilizzato da un nodo è determinato dal suo URL. L’implementazione
di URL è situata nel package de.uniba.wiai.lspi.chord.data. I nomi dei due protocolli sono
contenuti in un array di oggetti di tipo String e le costanti intere URL.LOCAL_PROTOCOL
e
URL.SOCKET_PROTOCOL
rappresentano
gli
indici
corrispondenti
per
il
riconoscimento del protocollo.
4.6 Struttura delle classi
Come già detto in precedenza Open Chord prevede due interfacce, che permettono di
recuperare, rimuovere, e memorizzare i dati da e per le DHT.
Queste interfacce (Chord, AsynChord) forniscono alcuni metodi comuni che sono
importanti per creare una rete e per effettuare join o leave in una rete già esistente. Questi
metodi sono riportati qui di seguito:
Lista 1: metodi comuni
public interface ... {
public abstract URL getURL();
public abstract void setURL(URL nodeURL)
throws IllegalStateException;
public abstract ID getID();
public abstract void setID(ID nodeID)
throws IllegalStateException;
public abstract void create()
throws ServiceException;
public abstract void create(URL localURL)
throws ServiceException;
38
public abstract void create(URL localURL, ID localID)
throws ServiceException;
public abstract void join(URL bootstrapURL)
throws ServiceException;
public abstract void join(URL localURL, URL bootstrapURL)
throws ServiceException;
public abstract void join(URL localURL, ID localID, URL bootstrapURL)
throws ServiceException;
public abstract void leave()
throws ServiceException;
public abstract void insert(Key key, Serializable object)
throws ServiceException;
public abstract Set<Serializable> retrieve(Key key)
throws ServiceException;
public abstract void remove(Key key, Serializable object)
throws ServiceException;
}
Ogni peer all’interno della rete sarà rappresentato da un identificatore univoco come
proposto in [19].
L’ID del nodo viene creato con l'aiuto dell'URL sfruttando la funzione SHA-1 [18] ed è
generato appena un nodo viene creato dopo aver settato l’URL invocando metodo
setURL(URL nodeURL). Un identificatore di un peer è rappresentata dalla classe
de.uniba.wiai.lspi.chord.data.ID. Impostare l'URL o l’ID di un peer è consentito solo
prima che un nodo crei la rete di Open Chord o che si unisca ad una rete già esistente.
E’ possibile creare una nuova rete con l'ausilio dei metodi create (), create (URL
localURL), e create (URL localURL, ID localID). Il primo metodo può essere invocato
solo se il nodo che deve creare la rete ha già settati URL e ID, il secondo se l’ID del nodo
non è ancora stato determinato il terzo invece è invocato quando del nodo si conoscono sia
URL che ID.
I metodi di join permettono ad un nodo di unirsi ad una rete Chord già esistente. Essi sono
simili a quelli che un peer deve invocare per poter creare una rete specificando quindi URL
e ID, ma c’è un ulteriore parametro che è l’URL cosiddetto di bootstrap che può essere
usato dal peer, che vuole accedere ad una rete, per unirsi all’anello. Normalmente come
URL di bootstrap viene utilizzato l’indirizzo del nodo che ha creato l’anello e a cui tutti gli
altri peers si uniranno.
Anche se Open Chord riesce a controllare trasparentemente eventuali crash e fallimenti di
un nodo, è bene che ogni peer, nel momento in cui decide di abbandonare la rete, notifichi
39
tale decisione agli altri nodi mediante il metodo leave(). Invocando tale metodo, si fa in
modo che le tabelle di routing dei nodi vicini possano essere subito aggiornate ed adeguate
ai cambiamenti della rete.
Oltre ai metodi sopra descritti, vi sono altri che vengono riportati di seguito.
Lista 2: metodi relativi ad azioni su oggetti di tipo Key
...
public abstract void insert(Key key, Serializable object)
throws ServiceException;
public abstract Set<Serializable> retrieve(Key key)
throws ServiceException;
public abstract void remove(Key key, Serializable object)
throws ServiceException;
}
Possono essere utilizzati per recuperare, inserire e rimuovere dati. Ad ogni risorsa, gestita
nella
rete,
è
associata
una
chiave 5
istanza
dell’interfaccia
de.uniba.wiai.lspi.chord.service.Key mediante la quale è possibile recuperare la risorsa in
fase di lookup o associarla ad un nodo come descritto in [19].
Per quanto riguarda la risorsa, essa potrà essere un qualsiasi oggetto java Serializable e in
fase di prova si è pensato di utilizzare delle Stringhe. Un’implementazione dell’oggetto
Key deve solo essere in grado di creare una rappresentazione in byte di sé, che può essere
ottenuta mediante il metodo getBytes(). Questi byte sono utilizzati da Open Chord per
creare un valore hash per un determinato dato con l'aiuto di una funzione hash. Per
garantire che Chord possa gestire e memorizzare in maniera corretta le chiavi, è necessario
che nell’implementazione della classe Key siano sovrascritti i metodi equals(Object) e
hashCode().
Il metodo insert(Key key, Serializable entry) inserisce la risorsa nella DHT associata alla
chiave data. Questa chiave può essere in seguito utilizzata per recuperare o rimuovere la
risorsa dalla DHT. Al fine di rimuovere un’entry da un nodo è necessario conoscere la
chiave della risorsa, come indicato dal metodo remove(Key key, Serializable entry). I
metodi dell’interfaccia Chord sono sincroni e bloccano il thread, che li invoca, fino a
quando l'operazione non è stata eseguita e il risultato ottenuto. Questo può non essere
5
Per chiave qui s’intende l’ID della risorsa e non un oggetto della classe Key
40
desiderato in tutti i casi, in quanto l’invocazione dei metodi descritti sopra può richiedere
un certo tempo più o meno lungo. Per cui, mentre viene recuperato un dato, ad esempio, si
vorrebbe che fosse possibile che lo stesso thread, possa essere usato per inserire, rimuovere
o recuperare alcuni altri dati in parallelo. Per questo scopo Open Chord fornisce
un'interfaccia con metodi che consentono la gestione asincrona dei dati permettendo così di
poter effettuare più operazioni in contemporanea. Questi metodi possono essere suddivisi
in due tipi. Il primo tipo di metodi sono riportati in Lista 3 i quali oltre ad avere specificata
la chiave della risorsa su cui effettuare l’operazione in input vogliono anche un oggetto di
tipo ChordCallback descritto nella Lista 4
Lista 3: AsynChord
package de.uniba.wiai.lspi.chord.service;
public interface AsynChord {
...
public void retrieve(Key key, ChordCallback callback);
public void insert(Key key, Serializable entry, ChordCallback callback);
public void remove(Key key, Serializable entry, ChordCallback callback);
public ChordRetrievalFuture retrieveAsync(Key key);
public ChordFuture insertAsync(Key key, Serializable entry);
public ChordFuture removeAsync(Key key, Serializable entry);
}
Lista 4: ChordCallback
package de.uniba.wiai.lspi.chord.service;
public interface ChordCallback {
public void retrieved(Key key, Set<Serializable> entries, Throwable t);
public void inserted(Key key, Serializable entry, Throwable t);
public void removed(Key key, Serializable entry, Throwable t);
}
41
Gli altri metodi ritornano un oggetto di tipo ChordFuture il quale successivamente può
essere usato per determinare se un’invocazione asincrona ha completato le sue operazioni
in modo da ottenere il risultato.
I metodi del primo tipo, si è già detto, richiedono un'istanza di ChordCallback, che è
notificato, quando le operazioni asincrone di estrazione, rimozione o memorizzazione sono
state completate. Se l’operazione di estrazione, rimozione, o memorizzazione dei dati ha
fallito, viene restituito un oggetto di tipo Throwable che rappresenta la causa del
fallimento. Quando l’operazione si conclude senza errori Throwable rimane settato a null.
Il secondo tipo di metodi, non necessitano di un oggetto di Callback come parametro, ma
invece restituiscono immediatamente un'istanza di ChordFuture la quale che può essere
usata per verificare se la corrispondente invocazione d’insertAsync(…) o removeAsync
(...) è stato effettuata mediante l'aiuto del metodo isDone (). Tale metodo restituisce true
quando l'invocazione è stata effettuata con successo. Quando l'invocazione è ancora in fase
di esecuzione viene restituito false. Quando l'invocazione è stata effettuata, ma non è
riuscita con successo, isDone() lancia un’eccezione del tipo ServiceException. Se un
thread vuole aspettare fino a quando l'operazione associata a un ChordFuture finisca, può
invocare waitForBeingDone(), che blocca il thread chiamante fino alla fine
dell'operazione. Questo metodo lancia una ServiceException se l'operazione non va a buon
fine e un InterruptedException se il thread per un qualche motivo s’interrompe.
Il metodo per recuperare i dati dal DHT restituisce un oggetto del tipo
ChordRetrievalFuture, interfaccia che estende ChordFuture.
Per lo scambio di messaggi tra i vari peer, è presente una classe Message, oggetto
Serializable e i vari tipi di messaggi che un nodo può scambiare con gli altri nodi della rete
è presente una classe MethodConstants in cui sono specificati dei valori interi i quali
servono appunto a distinguere i vari messaggi. I tipi dei messaggi possono essere:
¾
CONNECT = -1;
¾
FIND_SUCCESSOR = 0;
¾
GET_NODE_ID = 1;
¾
INSERT_ENTRY = 2;
¾
INSERT_REPLICAS = 3;
¾
LEAVES_NETWORK = 4;
¾
NOTIFY = 5;
¾
NOTIFY_AND_COPY = 6;
42
¾
PING = 7;
¾
REMOVE_ENTRY = 8;
¾
REMOVE_REPLICAS = 9;
¾
RETRIEVE_ENTRIES = 10;
¾
SHUTDOWN = 11;
Un nodo, mediante un Proxy crea una connessione Socket (nel caso in cui si operi in
remoto) col nodo interessato il quale riceverà il messaggio mediante Endpoint e quindi
potrà effettuare l’operazione richiesta restituendo se necessario il risultato.
Ogni nodo, per rimanere in ascolto di eventuali messaggi in arrivo ha un thread
RequestHandler il quale funge proprio da ascoltatore.
Al fine di capire meglio come avviene la comunicazione tra peers, di seguito viene
riportato un diagramma UML delle classi in cui viene messo in evidenzia il legame che c’è
tra Proxy ed Endpoint nel caso di comunicazione basata su Socket.
Figura 10: Diagramma UML delle classi di Open Chord per la comunicazione Socket
43
4.7 Prestazioni di Open Chord
Proprio per la sua affinità con la versione originale, Open Chord continua a mantenere le
stesse prestazioni di Chord (cap. 3).
Anche Open Chord permette infatti ricerche logaritmiche rispetto alla dimensione della
rete. Inoltre, con alta probabilità anche il numero di messaggi è dell’ordine O(log N), dove
N è il numero di nodi nel sistema.
Le altre caratteristiche di Open Chord da ricordare sono:
¾ Tabelle di routing nell’ordine log N
¾ Bilancia il carico dei dati
¾ È totalmente distribuito
¾ È scalabile
¾ Si adatta ai cambiamenti della rete
¾ Non pone vincoli sulla struttura delle chiavi di ricerca
¾ Si adatta velocemente alla dinamica delle join e dei leave
¾ Robusto rispetto ai fallimenti dei nodi
4.8 Limiti di Open Chord
La corrente implementazione di Open Chord non consente il caricamento remoto delle
classi, il che rende necessario, che tutte le implementazioni di Key e di tutti gli altri oggetti
che implementano un qualche tipo di risorsa che dovrebbe essere memorizzata all'interno
della DHT, debbano essere disponibili a livello locale su ogni peer. Esistono due
possibilità per ovviare a questo problema.
Una classe che si suppone essere usata come una Key o dato all'interno del DHT può
essere salvata all'interno del DHT con il suo nome completo fungendo da chiave. Questo
classi possono poi essere caricate da un ClassLoader personalizzato che carica le classi del
DHT. In questo modo su ogni peer dovrà avere presente soltanto l’implementazione di
Key.
Un'altra possibilità è quella di memorizzare nel DHT gli oggetti (risorse) utilizzando la
loro rappresentazione in byte. Per convertire gli oggetti nella loro rappresentazione in byte
44
e viceversa possono essere usate le classi del package java.io ByteArrayInputStream e
ByteArrayOutputStream insieme con le classi ObjectOutputStream e ObjectInputStream.
Anche in questo secondo caso c deve essere un’implementazione comune di Key.
Non è inoltre possibile cambiare facilmente il protocollo di comunicazione (ocsocket o
oclocal) di Open Chord, in quanto i protocolli sono strettamente legati all’URL specificato
all’inizio.
Se inoltre si vuole aggiungere un nuovo protocollo non devono essere create solo le classi
che implementano il protocollo ma è necessario modificare anche il factory per gli oggetti
Proxy ed Endpoint il quale deve essere quindi ricompilato.
Nell’attuale implementazione di Open Chord si presume che ogni nodo sia affidabile.
Pertanto ogni nodo può rimuovere dati arbitrari. Questo può essere impedito mediante
l’aggiunta di un Security Manager, che verifica le richieste in entrata. Così si fa in modo
che la replica e la memorizzazione di risorse avvengano solo quando si è sicuri del
contenuto di quanto si sta per memorizzare.
Per lo stesso motivo i peers possono pure rimuovere arbitrariamente dei dati e anche in
questo caso mancano controlli che garantiscono la sicurezza.
45
5 Self – Chord
5.1 Introduzione
Lavoro finale del lavoro di tesi è stata la realizzazione in java di Self-Chord. un sistema di
p2p che eredita da Chord, la capacità dei sistemi strutturati di costruire e mantenere un
insieme di peer, ma che ne migliora le prestazioni e le caratteristiche grazie all’attività di
agenti mobili bio-ispirati i quali, mediante l’auto-organizzazione già sperimentata in [1] e
[2], permettono al sistema di adattarsi ai vari cambiamenti a cui esso è sottoposto.
Gli agenti, grazie a semplici operazioni tutte spinte da calcoli probabilistici riescono ad
organizzare i peer nella rete e a suddividere in maniera efficiente le risorse tra i vari peer in
modo da facilitare il lookup delle risorse migliorandone i tempi di risposta.
Al contrario di Chord, il posizionamento delle chiavi è indipendente dagli indici dei peer e
si adatta alle varie condizioni del sistema.
I tre maggiori benefici che ha apportato Self-Chord sono:
1. In Chord, sia gli indici dei peers che le chiavi delle risorse sono definite dallo stesso
numero di bit applicando la funzione hash. In Self-Chord non esiste più la relazione
tra indici dei nodi e chiavi delle risorse. Questo permette di assegnare alle chiavi
delle risorse un significato semantico. Infatti, le varie risorse saranno organizzate in
tipi ossia suddivise in classi. Ciò dovrebbe migliorare la ricerca di un particolare
oggetto caratterizzato da un determinato tipo in quanto oggetti con lo stesso tipo
saranno situati sullo stesso peer o su peer comunque vicini.
2. I sistemi strutturati potrebbero soffrire di problemi di squilibrio per quanto riguarda
il posizionamento delle chiavi: in Chord, infatti, la chiave di una risorsa è
posizionata nel peer con ID pari o maggiore al valore della chiave. Tuttavia,
qualora tra un peer e il suo predecessore vi sia un’elevata distanza, sul peer saranno
memorizzate un elevato numero di chiavi con la conseguenza di alcuni nodi molto
“carichi” e altri anche vuoti. In Self-Chord ciò si è evitato permettendo di avere un
carico del bilancio: il numero delle chiavi memorizzate su un nodo, infatti, non
dipende né dalla sua distanza dal suo predecessore né dal numero delle chiavi. E’
infatti possibili, anche, settare il numero massimo di chiavi che un peer può tenere.
46
3. In Chord, quando un nodo accede o lascia la rete, deve eseguire una serie di
operazioni (capitolo 3.4) per spostare le risorse sugli altri peer. In particolare, se
molti peers accedono insieme alla rete e iniziano contemporaneamente a disporre le
risorse, il carico computazionale del sistema sarà molto elevato. In Self-Chord ciò
non è necessario in quanto saranno gli agenti stessi a riorganizzare le risorse,
mentre i peers si limiteranno a pubblicare le risorse senza occuparsi di altro.
5.2 Funzionamento di Self-Chord
Anche in Self-Chord, come in Chord, le risorse sono organizzate su di un anello. Ad ogni
peer è associato un indice che è ottenuto con una funzione hash uniforme, uguale a quella
usata in Chord, e può avere valori che vanno da 0 a 2
Bp
− 1 , dove BP è il numero di bits per
gli indici dei peers.
L'anello è costruito e mantenuto esattamente come in Chord (vedi capitolo 3) sia per la
gestione degli ingressi che delle uscite di un nodo.
A ogni risorsa, che deve essere pubblicata da un nodo, viene associata una chiave
anch’essa calcolata mediante l’applicazione della funzione di consistent hashing (SHA-1).
Tale chiave servirà ai nodi per ricercare la chiave nella rete e potrà avere un valore che va
da 0 a 2 Br − 1 dove Br è il numero di bit utilizzati per rappresentare le risorse o meglio per
distinguere i vari tipi di risorse. Grazie alla funzione hash di locality persevering si è fatto
in modo che chiavi simili siano associate a risorse simili. Il numero di possibili valori che
una chiave può assumere, Nc= 2 Br rappresenta quindi il numero di classi nelle quali le
risorse saranno divise.
Una classe di risorse, ossia un tipo per una risorsa, rappresenta una caratterizzazione che
distingue quella risorsa e definisce quindi un insieme di risorse aventi una particolarità in
comune.
Ad esempio, se le risorse sono dei libri, esse potranno essere divise in vari tipi ognuno dei
quali definisce un particolare genere letterale.
Come già affermato in precedenza, in Self-Chord non vi è alcun rapporto tra i valori di Bp
e Br per cui questo spiega anche perché non è una priorità assegnare una chiave ad un peer
avente stesso indice.
47
Tuttavia, per ereditare l'efficienza delle scoperta delle risorse che in Chord si effettua con
un numero di passi logaritmico nel numero dei nodi, le risorse dovranno comunque essere
ordinate sull’anello. A questo punto, per proseguire il discorso, è necessario introdurre gli
agenti bio-ispirati i quali hanno proprio il compito di organizzare le risorse tra i vari peer
dell’anello.
5.2.1 Gli agenti
Protagonista dell’intero sistema Self-Chord, sono gli agenti bio-ispirati. Il concetto di
agente infatti può essere associato a quello di formica o di ape cioè quegli animali
cosiddetti “operai” organizzati in “colonie” in cui sebbene il singolo sembri agire in modo
del tutto autonomo, l'attività dell'intera colonia appare altamente organizzata e denota un
comportamento intelligente.
Ogni peer dell'anello, al momento della sua connessione alla rete, può generare un agente
mobile mediante una data probabilità Pgen. che si potrà muovere in senso orario o in senso
antiorario sull’anello. Ogni agente sarà anche caratterizzato da un tempo di vita, frutto di
un calcolo probabilistico basato sulla durata media di connessione dei peers alla rete. Se la
media del tempo di connessione dei peers è Tpeer, e la media durata di vita degli agenti è
Tagent, può essere dimostrato che il numero di agenti che circolano sulla rete in un
determinato momento in cui sulla rete sono presenti Np peers, sarà dato da:
N a ≅ N p ⋅ Pgen ⋅
T peer
Tagent
(1)
Quindi gli agenti, spostano e ordinano le risorse in base alle loro chiavi permettendo così,
anche grazie all’aiuto delle finger tables di ciascun nodo, tempi logaritmici in fase di
lookup.
48
5.2.2 Centroide
L’ordinamento delle risorse avviene sfruttando il concetto di centroide di un peer.
Questi è definito come il valore reale, tra 0 e NC, che minimizza la distanza media tra se
stesso e le chiavi memorizzati nel nodo stesso e nei due peer adiacenti sull’anello.
Per esempio, con NC = 64, un peer che memorizza tre chiavi con valori {4,6,8} (per
semplicità, si presume che i due nodi adiacenti non memorizzino alcun valore) avrà un
centroide pari a 6. D'altro canto, un peer che memorizza due chiavi con valori {63, 0} ha
un centroide pari a 63,5. Come si può ben capire dagli esempi, il centroide risulta quindi un
indicatore delle chiavi memorizzate su un determinato peer e in questo modo sarà possibile
ordinare le chiavi.
5.2.3 Ordinamento delle chiavi
Operazione principale degli agenti è quello di ordinare le chiavi. Per semplicità si suppone
che un nodo possa trasportare una chiave alla volta ma, si potrebbe anche fare
diversamente. Quando un agente arriva su un nodo, a secondo che esso trasporti o meno
una risorsa si comporterà in maniera diversa. Se ha una risorsa, esso infatti, proverà ad
effettuare una DROP ossia cercherà di depositare la risorsa su un nodo altrimenti effettuerà
una PICK andando a prendere dal nodo in cui si trova una risorsa il cui tipo “si allontana”
dal valore del centroide.
Per capire meglio come vengono ordinate le chiavi, si faccia riferimento alla Figura 11.
49
Figura 11: esempio di rete Self-Chord. All’interno l’id del nodo. Tra parentesi graffe le risorse sul
noto e tra parentesi quadre il centroide per il nodo
In questo scenario di esempio, i valori di BP e Br sono rispettivamente pari a 4 e 3.
All’interno dell’anello accanto ad ogni puntino (nodo della rete) vi è un indirizzo in bits
che rappresenta l’ID del nodo. Esternamente invece sono riportate, tra parentesi graffe, le
risorse memorizzate su ciascun peer.
In Chord, se un nodo avesse pubblicato una risorsa con chiave 8 questa sarebbe stata
situata, sul secondo nodo in quanto il primo ha id=0 e il secondo ha id=15.
Nel caso di Self-Chord invece questo modo di operare viene meno e, come scritto in
precedenza, viene preso in considerazione il valore del centroide del nodo. Per cui la
risorsa 8 sarà inserita sul nodo che l'ha generata e in seguito su un nodo con centroide 8 o
comunque vicino ad esempio quello con ID=101110 o quello successivo.
E’ anche possibile che risorse con stesso tipo( rappresentato in bits) siano situati su nodi
diversi ma comunque vicini mantenendo il bilanciamento della rete e l’ordinamento dei
centroidi. Non necessariamente il nodo con ID minore sarà quello con centroide minore ma
come gli ID anche i centroidi tenderanno ad essere ordinati in ordine crescente in senso
orario.
50
Gli agenti, hanno la possibilità di muoversi in senso orario o in senso antiorario sull’anello.
Ciò viene deciso applicando una funzione che ritorna un valore casuale compreso tra 0 e 1:
se il valore sarà inferiore a 0,5 l’agente si muoverà verso sinistra altrimenti verso destra.
Inoltre, essi hanno la possibilità di muoversi sui vari nodi in modo lineare o logaritmico.
Un agente per prima cosa andrà alla ricerca delle chiavi che avranno valore r più alto del
valore del centroide c del nodo, infatti spostando tali chiavi si migliorano le condizioni di
“ordine” su un nodo. Se l’agente si muove verso destra e quindi in senso orario, la
condizione da verificare sarà
c < r < (c +
Nc
) mod( N c )
2
(2)
Al contrario, nel caso di movimento antiorario, saranno sposate le chiavi che soddisfano la
relazione
(c −
Nc
) mod( N c ) < r < c
2
(3)
Per cui si può affermare che la probabilità di prendere o meno una risorsa con chiave r sarà
inversamente proporzionale alla somiglianza tre il valore della chiave e il valore del
centroide c del nodo su cui la chiave si trova.
E’ quindi possibile scrivere la funzione di similarità f(r,c) e la probabilità di pick.
f (r , c) = 1 −
Ptake =
kp
k p + f (r , c)
d (r , c)
Nc
2
con 0 ≤ k p ≤ 1
(4)
(5)
dove d(r,c) è la distanza in termini numerici tra il valore della chiave della risorsa e il
centroide del nodo.
Una volta che la risorsa sarà presa dall’agente, esso si sposterà tra i vari nodi in base alla
direzione del suo spostamento. A questo punto, arrivato su un nuovo nodo, eseguirà il
controllo per decidere se depositare o meno la risorsa. Anche in questo caso tutto
dipenderà dalla distanza tra la chiave della risorsa r e il centroide c del nuovo nodo. La
probabilità che l’agente lasci sul nuovo nodo la risorsa sarà:
51
Pleave =
f (r , c)
con 0 ≤ k d ≤ 1
k d + f (r , c)
(6)
Nel caso di ordinamento logaritmico, il movimento lineare rimane solo quando l’agente
non trasporta chiavi.
Quando l’agente sarà in possesso di una risorsa, infatti, esso sarà “inviato” mediante l’aiuto
delle finger tables nella zona in cui i nodi dovrebbero avere centroide vicino al valore della
risorsa e per decidere verso quale nodo spostarlo, sarà necessario fare una proporzione tra
la distanza tra il valore della risorsa e i centroidi e la distanza tra il nodo in cui si trova
l’agente Ps e la possibile destinazione Pd calcolato nello spazio degli indici dei peers.
(r − c) Pd − Ps
=
Nc
Np
(7)
da qui è possibile determinare Pd come
Pd = Ps +
Np
Nc
(r − c)
(8)
Oltre all’utilizzo della normale finger table, ogni nodo, in Self-Chord avrà un’ulteriore
finger in cui saranno memorizzati i nodi appartenenti al semicerchio di sinistra: la logica di
riempimento è la stessa di quella utilizzata per la parte destra solo che in ogni entry ci sarà
il riferimento ad un nodo predecessore in posizione (ID-2i-1 +N)%N dove ID è l’ID del
nodo in considerazione e i è un valore compreso tra 1 e il logaritmo del numero dei nodi
(N) della rete. Ciò è dovuto al fatto che è necessario far muovere in maniera logaritmica
anche gli agenti sinistroidi.
Intuitivamente, l’approccio basato su ordinamento logaritmico è molto più veloce di quello
lineare, in quanto una chiave viene spostata in tempi più brevi senza la necessità di dover
attraversare tutti i nodi della rete.
D'altro canto, però, l’approccio lineare consente un più equo bilanciamento del carico.
Un’ultima considerazione è che l'approccio logaritmico assume implicitamente che i valori
delle chiavi delle risorsa siano distribuiti in modo uniforme. Nel caso di una distribuzione
non uniforme (alcune chiavi sono presenti in numero superiore ad altre) il calcolo del “peer
52
destinazione” per una risorsa potrebbe non essere corretto. Tuttavia, la natura logaritmica
del processo è in grado di compensare questi errori in maniera molto efficace e rapida.
5.3 Ordinamento lineare e logaritmo: confronto
La scelta se far muovere gli agenti linearmente o in modo logaritmo in base alla finger di
ogni nodo, risulta di fondamentale importanza nelle prestazioni globali del protocollo. Si
ottengono infatti, facendo alcuni test, risultati diversi in base al tipo di movimento scelto.
Per maggiori dettagli a riguardo si rimanda al capitolo successivo in cui verranno messe in
luce le prestazioni del protocollo sia con movimento lineare che logaritmico degli agenti.
5.4 Implementazione
La realizzazione di tale protocollo ha preso spunto dalla versione java di Open Chord
presentata al capitolo precedente. Di essa infatti è rimasta gran parte della struttura
comprese le diverse azioni che i vari nodi possono eseguire se pur con alcune differenze
che hanno portato una serie di benefici all’applicazione.
L’architettura e quindi lo scheletro dell’applicazione è rimasto lo stesso di quello
analizzato in Open Chord (capitolo 4).
Al contrario di prima però, in Self-Chord ogni nodo non ha più una sola finger table ma
due, in quanto è necessario memorizzare sia le entry per i nodi successivi sia quelle per i
nodi predecessori al fine di permettere agli agenti uno spostamento in senso antiorario.
Considerando, come esempio, la figura 12, per il nodo con id=00000 avremo due fingers
che saranno le due tabelle riportate in Figura 13.
I valori in tabella sono il risultato della funzione, già utilizzata in Chord, per determinare i
valori delle entries di una finger.
Nel caso della finger di destra, per un nodo ID, le entries faranno riferimento ai nodi in
posizione ID+2i-1 con i compreso tra 1 e log(N) (N è il numero dei nodi dell’anello); nel
caso di sinistra invece saranno considerati i nodi in posizione (ID-2i-1+N)%N con i che
assume lo stesso significato dato nel caso della tabella di destra.
53
Figura 12: anello con 16 peers con id ordinati
Finger Table SINISTRA
Finger Table DESTRA
Position of next node Next node
Position of next node
Next node
(0-20+16)%16
111100
0-20
001001
(0-21+16)%16
111011
0+21
001111
(0-22+16)%16
110111
0+22
011010
(0-23+16)%16
101110
0+23
101110
Figura 13: finger tables per spostamenti logaritmici a destra e sinistra di un agente
54
Nell’implementare Self-Chord oltre a predisporre un funzionamento in remoto e quindi
facendo in modo che ogni peer avvii l’applicazione potendosi connettere agli altri peers,
l’idea è stata di fare in modo che l’applicazione potesse funzionare anche in locale
simulando su una stessa macchina un numero variabile di peers in modo da poter effettuare
tests al fine di raccogliere risultati concreti sul funzionamento dell’applicazione.
Anche nel caso di Self-Chord è stato utilizzato lo stesso file di configurazione presente in
Open Chord e in aggiunta ne sono stati creati altri due: uno riguarda l’applicazione vera e
propria (“selfchord.properties") mentre l’altro (“simulator.properties”) serve in fase di
simulazione.
In selfchord.properties è possibile trovare i seguenti campi:
¾ costant: se messo a true significa che tutti i peers avranno lo stesso numero di risorse
¾ type_resources: numero dei tipi di risorse. Da qui si può determinare il numero di bit
utilizzati per rappresentare una risorsa come log(tipi_risorse)
¾ kp: costante per il calcolo della probabilità di pick
¾ kd: costante per il calcolo della probabilità di drop
¾
linear: se settato a true indica che l’ordinamento è lineare altrimenti è logaritmico.
Simulation.properties invece sarà costituito da:
¾ N_peer: numero di peer che costituiscono la rete
¾ end: specifica dopo quanti secondi far terminare la simulazione
¾
risorse: indica il numero di risorse da inserire in ogni nodo se costant è settato a true o
il numero medio di risorse per ogni peer se costant è uguale a false;
E’ bene adesso osservare alcune delle principali parti di codice che garantiscono il buon
funzionamento dell’applicazione.
Innanzi tutto, l’attenzione viene posta sull’Agente il quale non è altro che un oggetto
caratterizzato da un ID pari all’id del nodo che lo ha generato e che ha una lista in cui
memorizzare le varie risorse che devono essere trasportate sulla rete. Quando viene
inizializzato un nuovo agente per prima cosa è necessario determinare il verso di
spostamento e per fare ciò si determina un valore random tra 0 e 1 e se questi è minore di
0.5 allora la direzione viene settata a SINISTRA altrimenti DESTRA.
55
double x = Math.random();
int dir = 0;
if (x < 0.5)
dir = Agente.SINISTRA;
else
dir = Agente.DESTRA;
Oltre ciò, ogni agente, nel passare da un nodo ad un altro sarà caratterizzato da un tempo di
pausa che in media è di 10 secondi. Tale accorgimento è stato preso al fine di non
sovraccaricare il sistema.
Per la memorizzazione delle risorse ogni nodo ha un oggetto di tipo DataStore, una lista
in cui vengono memorizzate tutte le risorse le risorse in possesso di un nodo.
E’ necessario quindi porre l’attenzione su alcune classi già presenti in Open Chord ma
leggermente modificate e per questo non menzionate nel capitolo precedente.
Le classi, in particolare sono Risorsa, Key e ID.
La classe Risorsa rappresenta una qualsiasi risorsa memorizzabile su un nodo. Risorsa
estende inoltre Serializable per cui tutti gli oggetti serializzabili potranno essere considerati
potenziali risorse per un nodo. Una risorsa sarà caratterizzata da un ID 6 e da un tipo, valore
double che viene recuperato dall’ID del nodo espresso in binario. Per determinare il tipo di
una risorsa è necessario prima calcolare la chiave sotto forma di ID applicando la funzione
hash e quindi, in base al numero dei tipi di risorse settato sul file di proprietà, stabilire
quanti sono i bit che andranno a determinare il tipo della risorsa. Il tipo di una risorsa sarà
infatti del tutto casuale e sarà determinato dal valore dei primi m bit meno significativi
della chiave della risorsa. Al più quindi, con m bit si potranno avere 2m classi di risorse.
Per determinare il tipo è necessario invocare sull’ID della risorsa il metodo getTipo
passando in input il numero di bit che lo rappresenteranno.
public void tipi(int i){
bit_risorse=(int)(Math.ceil((Math.log(i)/Math.log(2))));
tipi_risorse=i;
int d=this.getId().getTipo(bit_risorse);
if(d>tipi_risorse) tipo=tipi_risorse;
6
il concetto di ID sta qui a rappresentare quello di chiave di una risorsa menzionato nei capitoli precedenti.
Esso non deve essere confuso con quello di Key che rappresenta il nome della risorsa dal quale, mediante
funzione hash, viene calcolata la chiave.
56
tipo=(double) d;
}
public final class ID implements Comparable<ID>, Serializable {
...
public int getTipo(int bit){
String s=bit(bit);
double ris=0;
for(int i=0;i<=bit;i++){
if(s.charAt(i)=='1'){
ris+=Math.pow(2, bit-1-i);}
}
return (int)ris;
}
public final String bit(int bit) {
String s=toBinaryString();
String nuova="";
int i=0;
while(i<=bit){
nuova=""+s.charAt(s.length()-1-i)+nuova;
i++;
}
return nuova;
}
...
}
Il metodo getTipo() quindi invocando il metodo bit(..) ottiene la Stringa che rappresenta il
valore binario del tipo e quindi si passa alla rappresentazione decimale.
La classe Key rappresenta invece un ulteriore attributo per una risorsa e insieme al valore
viene passato in input per l’inserimento della risorsa in un nodo. Tale valore sarà utilizzato,
applicando la funzione di consistent hashing, per determinare la chiave vera e propria della
risorsa. Ad esempio, se si considera come risorsa un file, essa sarà caratterizzato dal valore
che è il contenuto del file e dalla key che rappresenta il nome del file dal quale si determina
la chiave per la memorizzazione del file sui nodi. Per il calcolo della funzione hash è
utilizzata la classe HashFunction con il metodo getHashKey per calcolare il valore hash
57
per una risorsa e createUniqueNodeID per determinare l’ID di un peer in base al valore
dell’URL.
La classe URL rimane simile a quella vista in Open Chord con la sola differenza che
adesso è possibile specificare un solo tipo di protocollo ossia quello basato su Socket java
(ocsocket). Per la fase di testing viene utilizzato questo protocollo simulando però tutti i
nodi su una stessa macchina. Per differire in locale i vari nodi, i quali avranno lo stesso
indirizzo IP, saranno fatte variare per ogni nodo le porte di ascolto.
final ID getHashKey(Key entry) {
if (entry == null) {
throw new IllegalArgumentException(
"Parameter entry must not be null!");
}
if (entry.getBytes() == null || entry.getBytes().length == 0) {
throw new IllegalArgumentException(
"Byte representation of Parameter must not be
null or have length 0!");
}
byte[] testBytes = entry.getBytes();
return this.createID(testBytes);
}
...
final ID createUniqueNodeID(URL incomingURL) {
if (incomingURL == null) {
throw new IllegalArgumentException("URL must not be null!");
}
String id = incomingURL.toString();
ID resultKey = this.createID(id.getBytes());
return resultKey;
}
...
private final ID createID(byte[] testBytes) {
synchronized (this.messageDigest) {
this.messageDigest.reset();
this.messageDigest.update(testBytes);
return new ID(this.messageDigest.digest());
}
}
58
Ritornando alla struttura di Self-Chord, essa riprende, come già detto, quella di open
Chord.
Per quanto riguarda la creazione dell’ID di un nodo, si è ereditata da Open Chord la classe
ID e quindi anche il modo con cui rappresentare un nodo. In particolare, ogni ID sarà
caratterizzato da 8 cifre esadecimali suddivisi in 4 gruppi ognuno rappresentabile da 8 bit.
Per cui per ogni coppia di cifre sarà possibile avere un numero che va da 0 a 256.
Ad esempio un nodo potrebbe avere ID ED 2A 3A A2. Per facilitare la comprensione
dell’ID esso, in fase di testing (capitolo 6) sarà espresso con i numeri naturali. In caso di
numero di peer maggiore di 256 sarà comunque possibile rappresentare l’ID tenendo in
considerazione le altre cifre; ciò serve pure nel caso di 2 peers con il primo blocco uguale.
Per cui considerando l’ID precedente e un altro ED 2F 8B 2A il primo in decimale sarà
237 42 mentre il secondo 237 47 così da poter distinguere i due valori.
Adesso, per maggiori chiarimenti, si descriveranno più attentamente le classi che
interessano la struttura e la comunicazione tra peers dedicando ad ogni classe un breve
commento che spiega il funzionamento delle stesse.
5.4.1
Node
Classe astratta che rappresenta un nodo. Ogni nodo è quindi caratterizzato da un id e da un
URL. Nella classe sono presenti una serie di metodi che verranno invocati sul nodo da altri
nodi dopo l’arrivo di un messaggio di richiesta. I vari messaggi servono ad eseguire quelle
che sono le principali operazioni di un nodo quali la ricerca dei successori o la
memorizzazione di una risorsa. I metodi principali sono:
¾ findSuccessor(ID i): cerca e restituisce il successor(i) 7
¾ findSuccessor(): restituisce il diretto successore di un nodo
¾ findPredecessor(): restituisce il predecessore del nodo
¾ insertEntry(Risorsa entryToInsert): memorizza una risorsa sul nodo
¾ removeEntry(Risorsa entryToRemove): rimuove una risorsa dal nodo
¾ retrieveEntries(ID id): ricerca una particolare risorsa
7
vedi parte teorica su Chord al paragrafo 3.3
59
¾ passaAgente(Agente s): passa l’agente al nodo successivo
¾ richiediEntries() : restituisce le risorse memorizzate sul nodo
¾ getCentroide () : restituisce il valore del centroide del nodo
¾ disconnect(): disconnette il nodo dalla rete
¾ lookup(int tipo, int i): ricerca una data classe di risorse
¾ spostaRisorse(LinkedList<Risorsa>lista): sposta sul nodo la lista di risorse
5.4.3
InvocationThread
Classe che implementa thread e che serve per invocare i metodi su un nodo.
In particolare, vi è un thread in ascolto ( RequestHandler) che ascolta le richieste e in base
al tipo di richiesta effettua la dovuta invocazione servendosi di questo secondo thread.
public void run() {
int requestType = this.request.getRequestType();
String methodName = MethodConstants.getMethodName(requestType);
try {
Serializable result = this.handler.invokeMethod(requestType,
this.request.getParameters());
Response response = new Response(Response.REQUEST_SUCCESSFUL,
requestType, this.request.getReplyWith());
response.setResult(result);
...
}
5.4.4
RequestHandler
Ogni nodo sarà in possesso di un ascoltatore il quale riceverà le richieste provenienti dagli
altri nodi. Ricevuta la richiesta, in base al tipo di quest’ultima, verrà invocata la richiesta
sul nodo mediante il metodo invokeMethod e in seguito all’esecuzione della richiesta ne
sarà restituito l’esito.
60
5.4.5
Request e Response
Request modella un messaggio di richiesta che i peers possono scambiarsi al fine di
ottenere una particolare azione su un nodo. Ogni richiesta è caratterizzata da un valore
intero type il quale serve a distinguere il tipo di richiesta e da un vettore di parametri.
Response, come dice la parola stessa è la risposta che si restituisce in seguito ad una
richiesta. Essa potrà contenere un risultato e l’esito dell’operazione indicato da un valore
intero.
5.4.6
MethodConstants
Oltre alle costanti già menzionate nel capitolo precedente 8 in Self-Chord ne sono state
aggiunte di altre che vengono di seguito elencate:
¾ AGENTS: per il passaggio degli agenti da un nodo ad un altro
¾ KEEP_ENTRIES: per recuperare le risorse presenti su un nodo
¾ CENTROIDE: per recuperare il valore del centroide di un nodo
¾ SPOSTA_RISORSE: per spostare le risorse su un altro nodo
5.4.7
SocketProxy
Classe che estende Proxy e che rappresenta un collegamento tra due nodi. Ogni nodo avrà
una lista di Proxy uno per ogni altro nodo presente nell’anello.
Inoltre, mediante un SocketProxy un nodo può inviare una richiesta ad un nodo. Nel codice
riportato di seguito viene mostrato come si effettua il passaggio di un agente da un nodo ad
un altro.
public void passaAgente(Agente s) throws CommunicationException {
this.makeSocketAvailable();
Request request = this.createRequest(MethodConstants.AGENTS,
new Serializable[] { s});
try {
8
paragrafo 4.5 pagina 36
61
this.send(request);
} catch (CommunicationException ce) {
logger.debug("Connection failed!");
throw ce;
}
Per prima cosa si invoca il metodo makeSocketAvailable il quale serve per stabilire la
connessione tra i due nodi aprendo così “il canale” per la comunicazione TCP. Quindi
viene preparata la richiesta indicando il tipo e inserendo se necessario i parametri e come
ultimo passo vi è l’invio della richiesta. Contemporaneamente si rimane in attesa della
risposta che può avere esito positivo o negativo.
5.4.8
ChordImpl
E’ questa la classe principale, in cui vengono implementate tutte le operazioni che l’utente
può richiedere all’applicazione (creazione della rete, join di un nodo, inserimento di una
risorsa, …).
5.4.9
NodeImpl
La classe, che implementa Node, modella un singolo nodo e specifica tutti i metodi
necessari per eseguire le varie operazioni su un nodo. In particolare qui è da notare come
viene determinato il centroide e come vengono eseguire le operazioni di pick e drop di una
variabile da parte di un agente.
Di seguito si riporta il codice del metodo agente(…) in cui sono riportate le operazioni che
un agente deve fare per determinare se acquisire o rilasciare una risorsa.
public synchronized void agente(Agente s) {
synchronized (s) {
try{
mtx.acquire();
if (s.lista.size() > 0) {
// Si prova a posare la risorsa
boolean dropped = false;
Iterator<Risorsa> it = s.lista.listIterator(0);
62
while (it.hasNext() && !dropped) {
Risorsa entry = (Risorsa) it.next();
double f = calcolaF(entry);
double Pd=Math.pow(f / (kd + f), 2);
double valore = getRandomValue();
if (valore < Pd) {
if(!costant||dataStore.getNumberOfStoredEntries()<RISORSE_MAX){
dataStore.add(entry);
s.lista.remove(entry);
dropped = true;
}
else {
if(dataStore.getNumberOfStoredEntries()>RISORSE_MAX-1){
Iterator itt2 = s.lista.listIterator(0);
while (itt2.hasNext() && !dropped) {
Risorsa rc2 = (Risorsa) itt2.next();
int c2 = (int) centroide;
if (((s.getDirezione() == Agente.DESTRA) &&
(((rc2.getTipo()- c2 + tipiRisorse) %
tipiRisorse) < tipiRisorse / 2)) ||
((s.getDirezione() == Agente.SINISTRA) && (((c2rc2.getTipo() + tipiRisorse) % tipiRisorse) <
tipiRisorse / 2))) {
double f2 = calcolaF(rc2);
double Pp2=Math.pow(f2 / (kd + f2), 2);
double valore2 = getRandomValue();
if (valore2 < Pp2) {
dataStore.add(entry);
s.lista.remove(entry);
s.lista.add(rc2);
dataStore.remove(rc2);
dropped = true;
}// if
}
}// while
}
}// else
}//if(valore<Pd)
}// while
if (dropped)
calcolaCentroide();
}// end DROP
else if (s.lista.size() == 0) {
if (!costant||dataStore.getNumberOfStoredEntries() > RISORSE_MAX - 1) {
63
// si prova a prendere una risorsa
boolean picked = false;
Iterator itt = dataStore.getEntries().listIterator(0);
while (itt.hasNext() && !picked) {
Risorsa entry = (Risorsa) itt.next();
int c = (int) centroide;
if (((s.getDirezione() == Agente.DESTRA) && (((entry.getTipo()- c +
tipiRisorse) % tipiRisorse) < tipiRisorse / 2))|| ((s.getDirezione()
== Agente.SINISTRA) && (((c- entry.getTipo() + tipiRisorse) %
tipiRisorse) < tipiRisorse / 2))) {
double f = calcolaF(entry);
double Pp=Math.pow(kp/ (kp + f), 2);
double valore = getRandomValue();
if (valore < Pp) {
s.lista.add(entry);
dataStore.remove(entry);
calcolaCentroide();
picked = true;
}// end if(valore<Pd)
}
}// end while
}
}
A questo punto l’agente deve essere inviato al nodo successivo. Ciò dipende dal tipo di
spostamento che hanno gli agenti: se è lineare allo basterà inviarlo al nodo successivo; se
invece è logaritmico, allora bisognerà decidere dove inviare l’agente in base alla risorsa
che lo stesso trasporta. Si cercherà infatti, di inviare l’agente, sul nodo avente centroide
uguale o comunque molto vicino al centroide della risorsa. Inoltre, al contrario delle altre
versioni di Chord, come già detto in precedenza, le finger tables saranno due in quanto gli
agenti avranno la possibilità di muoversi a destra o a sinistra.
Nel momento in cui un agente dovrà essere inoltrato al prossimo nodo, si controllerà se la
risorsa dovrà essere portata nella metà destra o in quella sinistra dell’anello: tale scelta
dipende dal nodo su cui si trova l’agente e dal centroide del nodo stesso applicando il
metodo staDestra(..). Determinato ciò sarà necessario utilizzare la finger di destra o di
sinistra in modo da inviare l’agente al nodo più vicino possibile a quello con centroide
interessato. In questo tipo di movimento quindi non viene considerata la direzione di
64
spostamento dell’agente la quale sarà comunque presa in considerazione nel momento in
cui l’agente dovrà muoversi linearmente sull’anello.
Node responsibleNode=null;
if(s.lista.size()==0 || s.isLineare()==true){
if (s.getDirezione() == Agente.DESTRA) {
responsibleNode = this.findSuccessor();
} else{
responsibleNode = this.findPredecessor();
}
}
else if(s.isLineare()==false){
responsibleNode=prossimoNodo(s.lista.get(0),s.getDirezione());
}
else System.out.println("errore");
try {
mtx.release();
responsibleNode.passaAgente(s);
} catch (CommunicationException e) {
// TODO Blocco catch generato automaticamente
this.agente(s);
}
}catch (Exception e) {
mtx.release();
}
}
}
5.5 Principali Operazioni
Adesso, brevemente, sarà descritto come avvengono le principali operazioni che
l’applicazione permette di eseguire
Creazione di una nuova rete
Come in Open Chord per creare una nuova rete è necessario invocare il comando create()
su un’istanza di ChordImpl. Invocando il metodo di creazione della rete sarà possibile
specificare o meno ID e URL del nodo. Quindi viene invocato automaticamente, il metodo
createHelp() il quale si occuperà di creare un’istanza di NodeImpl la quale rappresenterà il
nodo vero e proprio.
65
Join
La join da parte di un nodo rimane la stessa vista in OpenChord. Quando un nodo entrerà a
far parte del nodo, per prima cosa, viene calcolato l’ID e in base a questo viene sistemato
nell’anello. Inizialmente, esso non conterrà risorse ma ben presto sarà popolato grazie
all’azione degli agenti.
Inserimento di una risorsa
Condizione per l’inserimento di una risorsa è che essa sia una risorsa Serializable. In fase
di test è stata utilizzata una stringa. Si calcola quindi la “Key” calcolata sulla stringa
ricavata applicando il metodo toString() di un qualsiasi oggetto java. Quindi si invoca il
metodo insert(…) sull’oggetto ChordImpl passando come parametri la chiave e il valore
della risorsa. La key passata in input, come già scritto in precedenza, sarà necessaria per il
calcolo del valore della chiave della risorsa mediante la funzione di consistent hashing,
public final Risorsa insert(Key key, Serializable s) {
if (key == null || s == null) {
throw new NullPointerException(
"Neither parameter may have value null!");
}
ID id = this.hashFunction.getHashKey(key);
Risorsa entryToInsert = new Risorsa(id, s);
entryToInsert.tipi(tipiRisorse);
localNode.dataStore.add(entryToInsert);
if(!agenteInviato){
agenteInviato=true;
localNode.calcolaCentroide();
preparaAgente();
}
return entryToInsert;
}
Determinata la risorsa, per il calcolo del tipi è necessario indicare quanti sono i tipi totali.
In questo modo infatti si determinano quante cifre binarie dell’id dovranno essere prese per
il calcolo del tipo (vedi pag. 49).
La prima volta che viene inserita una risorsa in automatico viene creato un agente e viene
inviato al nodo successivo.
66
Ricerca di una classe di risorse
Per quanto riguarda la ricerca di una determinata classe di risorse, è sufficiente indicare il
valore del tipo della risorsa. Quindi, muovendosi in modo logaritmico (si sfrutta il metodo
prossimoNodo(…)) verso destra o verso sinistra, si tenta di trovare la risorsa nei nodi
presenti nella finger garantendo un numero di passi logaritmici rispetto al numero dei nodi.
Se la risorsa non è presente viene restituito null.
5.6 L’applicazione
Come è stato già scritto in precedenza, obiettivo finale del lavoro di tesi è stato
l’implementazione in java del protocollo Self-Chord e quindi la realizzazione
dell’applicazione. Verrà quindi spiegata in breve l’interfaccia grafica e quindi le operazioni
che un utente può eseguire una volta avviata l’applicazione.
L’interfaccia si presenta molto semplice e sobria suddivisa principalmente in due aree: una
per il controllo dello stato del peer e una per la gestione delle risorse.
Prima cosa da fare una volta avviata l’applicazione è quella di indicare i dati per l’ingresso
alla rete. Se si desidera creare una nuova rete basterà inserire l’indirizzo e la porta di
ascolto dell’applicazione e quindi premere il pulsante “Create”. Nel caso invece si volesse
entrare a far parte di una rete già preesistente allora si dovranno indicare indirizzo (di
bootstrap) e porta del nodo che ha creato la rete seguito dalla pressione del tasto “Join”.
Per lasciare la rete invece basterà premere il bottone “Leave”.
Quando si è connessi ad una rete, in ogni istante sarà possibile, mediante il bottone “Data
Update”, entrare a conoscenza di alcuni dati del peer in corrispondenza dei campi:
¾ URL: url del nodo;
¾ ID: ID in esadecimale del nodo;
¾ Pre: nodo predecessore nell’anello;
¾ Suc: nodo successore nell’anello;
¾ Centroid: valore temporaneo del centroide;
Una volta entrati a far parte di una rete sarà possibile inserire risorse specificando il nome
della risorsa e premendo il tasto “Insert Resources” e mediante il pulsante “Update
67
Resources” l’utente potrà conoscere in ogni momento le risorse presenti sul nodo le quali
compariranno all’interno della lista sulla destra.
Per cercare una classe di risorse, quindi risorse aventi stesso tipo, basterà indicare il tipo e
premere il bottone “Search”.
Come è possibile notare, le funzioni sono poche e semplici consentendo anche ad utenti
inesperti l’utilizzo.
Figura 14: interfaccia grafica dell’applicazione Self-Chord
68
6 Simulazioni e Risultati
Atto finale dopo la realizzazione dell’applicazione è la fase di testing. In particolare,
operando sulla stessa macchina, si simulano un numero variabile di nodi e di tipi di risorse
per cercare di capire i tempi di risposta del sistema sia per quanto riguarda l’ordinamento
delle risorse sui vari peer sia la ricerca di una risorsa.
E’ doveroso sottolineare che per quanto riguarda la fase di test, sono state utilizzate
ulteriori classi per adempiere ad alcune operazioni che sarebbe necessario compiere
singolarmente su ogni peer, ad esempio per l’inserimento delle risorse. Durante le
simulazioni infatti, automaticamente, sono state inserite su ogni peer un certo numero di
risorse e si è partiti da un numero di peer connessi già stabilito a priori in modo da
effettuare l’analisi su particolari situazioni della rete.
Inoltre, per ogni caso di studio preso in esame, è stata effettuata sia una simulazione
considerando il movimento degli agenti lineare sia una con movimento logaritmico basato
sulle fingers dei vari nodi.
Ad intervalli di tempo determinati, in media ogni 10 secondi, un agente dopo aver provato
a fare pick o drop a secondo se trasporti o meno una risorsa viene spostato sul nodo
successivo che può essere il diretto successore (movimento lineare) o il nodo con centroide
più vicino al tipo di risorsa trasportata dall’agente 9 .
In fase di simulazione, per accelerare i tempi, gli intervalli si sono diminuiti dell’ordine dei
millesimi si secondo ma i risultati fanno riferimento ai tempi originali.
Per valutare quindi l’efficacia con cui gli agenti hanno operato, si prende in considerazione
il valore medio delle distanze dei centroidi tra due peers consecutivi che formano l’anello.
A titolo d’esempio si consideri l’immagine in Figura 12. Ogni peer (in rosso) ha un
centroide (valore all’interno del cerchio blu). Nel caso in esame si suppongono come
parametri Np=8 ed Nc=16 dove Np è il numero di peers ed Nc il numero dei tipi delle
risorse.
9
L’agente infatti può saltare su un altro nodo sfruttando la finger del nodo in cui si trova solo se sta
trasportando una risorsa; in caso contrario il movimento sarà in ogni caso lineare.
69
Figura 15: esempio di anello con centroidi ordinati
Per calcolare la distanza media quindi si calcolano.
Δ (C0,C1) = 2
Δ (C1,C2) = 2
Δ (C2,C3) = 1
Δ (C3,C4) = 3
Δ (C4,C5) = 3
Δ (C5,C6) = 1
Δ (C6,C7) = 3
Δ (C7,C0) = 1
A questo punto la media sarà
∑ ΔC
Np
=
16
=2
8
Infatti, nel caso in cui le chiavi sono correttamente ordinate e distribuite gradualmente su
tutto l’anello, il valore della distanza media dei centroidi dovrebbe tendere a
Nc
con una
Np
deviazione standard molto bassa 10 .
Tale fenomeno è stato constatato in tutte le simulazioni effettuate per cui è possibile
affermare che tale dato può essere molto importante nel momento in cui si vanno a
considerare le performance e l’efficienza del sistema.
10
Si ricorda che Nc è il numero delle classi delle risorse e Np il numero di peers che costituiscono l’anello
70
Ai fini dell’analisi dei risultati, nelle varie simulazioni sarà presente una tabella con 3
campi:
¾ ID: l’id del nodo in cifre decimali 11 . Esso è espresso in numero decimale considerando
i primi due byte dell’ID in esadecimale. Per l’ordinamento si ricorda che viene
considerato il primo byte. A parità di valore si passa a considerare il secondo e quindi
al terzo e infine al quarto.
¾ C: centroide del nodo.
¾ Risorse: insieme delle risorse presenti nel nodo all’istante in cui viene “fotografata” la
situazione della rete.
Nelle varie simulazioni effettuate si è cercato di mettere prima di tutto in risalto la
differenza di prestazioni che intercorrono tra i due approcci in particolare osservando i
tempi che nei due casi gli agenti impiegano per ordinare e organizzare le risorse.
A tal fine si è considerato un particolare caso di studio iniziale: una rete costituita da 32
peers e 1024 tipi in cui suddividere le 30 risorse inserite su ogni nodo.
Schematizzando, i vari parametri sono fissati a:
Np = 32
Nc=1024
kp = 0.3 (vedi equazione 5 )
kd = 0.9 (vedi equazione 6 )
Inoltre, ad ogni peer è stato assegnato un numero costante di risorse pari a 30.
Nel caso lineare inizialmente ci si trova in questa situazione.
ID
Centroid
3 43
304,0
9 87
488,01
13 30
588,01
15 147
936,01
31 17
1023,99
34 116
78,0
38 58
104,5
11
Risorse
222
27, 162, 167, 204, 204, 218, 271, 290, 315, 358, 369, 413, 425, 427, 442, 486,
488, 513, 577, 581, 583, 588, 601, 626, 648, 688, 693, 706, 758, 801, 843, 862,
865, 868, 920, 964, 995, 1013
61, 112, 128, 149, 224, 229, 268, 294, 417, 441, 455, 467, 534, 554, 609, 651,
676, 774, 904, 922, 937, 940, 950, 1022
0, 10, 26, 66, 85, 130, 137, 157, 159, 201, 207, 223, 315, 350, 358, 374, 377, 381,
431, 443, 470, 477, 482, 518, 593, 595, 659, 682, 700, 705, 756, 761, 795, 799,
837, 859, 872, 877, 919, 935, 936, 944, 962, 971, 1020
89, 394, 417, 714, 775, 778, 784, 874
75, 79, 167, 216, 243, 289, 294, 296, 314, 351, 448, 540, 577, 670, 691, 760, 768,
872, 981
15, 16, 47, 53, 56, 60, 61, 74, 79, 104, 105, 143, 153, 184, 244, 271, 316, 344,
385, 435, 437, 457, 462, 467, 487, 518, 583, 592, 638, 691, 695, 732, 754, 790,
805, 892, 919, 923, 954, 955, 981
per capire la rappresentazione dell’ID si rimanda al paragrafo 2 del capitolo 5.
71
54 63
613,015
54 10
74,015
54 199
642,0
56 114
511,98
74 91
308,01
87 56
332,0
90 43
775,99
94 126
596,01
120 4
624,01
145 205
553,5
148 186
192,00
149 83
302,0
166 163
651,0
172 160
876,0
174 2
942,01
176 236
117,01
207 204
127,99
208 161
64,01
221 160
1005,01
225 188
735,992
234 64
237 42
818,015
892,0
237 47
13,01
237 231
44,0
239 108
47,01
43, 63, 128, 150, 171, 176, 321, 324, 331, 356, 389, 429, 584, 676, 679, 681, 833,
885, 913, 987, 987, 1013
74, 84, 134, 231, 241, 261, 282, 321, 405, 424, 481, 521, 524, 535, 574, 580, 608,
662, 689, 720, 764, 858, 877, 901, 916, 917, 931, 960
17, 59, 155, 186, 190, 191, 234, 276, 288, 308, 315, 320, 339, 344, 352, 383, 396,
411, 440, 483, 541, 548, 550, 575, 586, 613, 628, 634, 639, 645, 651, 672, 678,
704, 706, 707, 715, 721, 778, 798, 801, 819, 836, 870, 889, 921, 941, 957
188, 360, 717, 837, 850, 884, 887
6, 88, 89, 93, 121, 124, 161, 164, 166, 166, 197, 242, 248, 266, 273, 282, 294,
308, 310, 311, 318, 328, 371, 379, 380, 400, 431, 454, 478, 501, 514, 552, 617,
645, 655, 655, 658, 661, 690, 745, 749, 783, 802, 802, 840, 856, 880, 895, 910
16, 294, 646, 715, 823
3, 55, 101, 133, 142, 154, 193, 223, 282, 316, 332, 344, 366, 418, 444, 524, 525,
557, 559, 604, 669, 695, 714, 716, 777, 778, 778, 806, 832, 885, 899, 937, 950,
982, 1015
52, 89, 130, 157, 327, 448, 452, 575, 596, 669, 695, 706, 776, 821, 888, 956
200, 243, 316, 356, 365, 397, 451, 472, 494, 519, 548, 580, 611, 621, 627, 647,
659, 683, 762, 904, 916, 918
135, 165, 205, 213, 306, 311, 318, 383, 444, 473, 522, 624, 647, 653, 721, 754,
767, 775, 781, 785, 921, 935, 949, 951, 991, 995, 1016
23, 44, 75, 107, 141, 155, 165, 172, 180, 180, 185, 189, 199, 212, 223, 230, 234,
239, 303, 328, 333, 336, 357, 398, 399, 450, 473, 485, 504, 517, 553, 554, 585,
624, 631, 652, 654, 672, 692, 726, 727, 757, 758, 783, 825, 832, 862, 915, 941,
949, 950, 950, 954, 956, 957
21, 187, 314, 335, 445, 538
2, 32, 43, 91, 230, 242, 273, 276, 299, 355, 403, 403, 422, 430, 452, 452, 465,
471, 516, 632, 637, 676, 719, 739, 757, 786, 810, 817, 839, 872, 908, 913, 941,
986, 1014
5, 17, 49, 101, 105, 122, 124, 151, 170, 332, 413, 461, 525, 529, 567, 605, 610,
615, 616, 639, 641, 650, 652, 663, 707, 816, 857, 867, 879, 883, 946, 958, 964
14, 45, 62, 69, 92, 117, 290, 307, 355, 408, 523, 591, 638, 789, 841, 896, 932,
942, 948, 988
44, 294, 596, 683
51, 61, 73, 129, 143, 155, 170, 175, 186, 195, 246, 246, 255, 273, 290, 294, 328,
368, 386, 390, 417, 441, 481, 483, 498, 544, 555, 570, 597, 708, 708, 726, 740,
768, 779, 791, 808, 821, 848, 869, 882, 918, 927, 953, 971
10, 12, 14, 27, 36, 55, 63, 97, 111, 130, 200, 219, 226, 231, 248, 252, 266, 268,
273, 288, 292, 298, 342, 342, 348, 401, 419, 429, 473, 512, 613, 613, 659, 667,
691, 705, 713, 728, 768, 770, 778, 836, 842, 884, 903, 909, 932, 972, 978, 1014,
1015
5, 15, 15, 88, 103, 245, 263, 271, 291, 304, 327, 328, 346, 378, 386, 399, 401,
404, 420, 439, 528, 574, 585, 650, 667, 714, 716, 723, 741, 748, 750, 767, 791,
796, 798, 887, 937, 953, 965, 996, 1005
42, 60, 91, 117, 137, 137, 156, 169, 188, 216, 235, 245, 289, 290, 336, 396, 464,
470, 475, 508, 513, 519, 583, 583, 590, 595, 615, 642, 645, 680, 691, 703, 703,
738, 744, 744, 804, 809, 816, 818, 825, 835, 865, 872, 895, 926, 928, 965, 1000
118, 130, 295, 361, 474, 741, 742, 780, 786, 815, 827
67, 133, 184, 343, 379, 657, 925, 955, 980, 991, 1021
54, 56, 90, 129, 130, 208, 212, 233, 234, 254, 264, 273, 280, 304, 313, 356, 357,
411, 435, 440, 458, 490, 503, 506, 515, 515, 544, 566, 594, 596, 599, 630, 641,
644, 668, 669, 723, 732, 756, 789, 818, 854, 855, 867, 877, 888, 891, 895, 910,
930, 960, 972, 991, 1004
13, 36, 39, 47, 59, 95, 96, 150, 154, 176, 179, 193, 201, 203, 249, 278, 300, 308,
311, 371, 397, 427, 495, 500, 520, 591, 671, 698, 712, 724, 740, 745, 789, 790,
800, 862, 894, 905, 906, 913, 932, 937, 950, 978, 981, 989
347, 427
Tabella 1: situazione iniziale della rete.
72
Patendo dalla situazione in Tabella 1, dopo aver avviato gli agenti, dopo 410000 secondi,
nei due casi si otterrà un sistema in cui i centroidi sono ordinati in senso orario e le risorse
distribuite tra i vari nodi in base ai loro centroidi; a titolo d’esempio, in Tabella 2, si mostra
la situazione a fine simulazione della rete nel caso di movimento logaritmico degli agenti.
Come fatto anche nella tabella precedente, per facilità si mostra nell’Id solo i primi 2 valori
dei 4 che costituiscono l’identificativo di un nodo.
ID
Centroid
3 43
262,0
9 87
282,015
13 30
296,0
15 147
327,99
31 17
361,05
34 116
413,01
38 58
437,015
54 63
508,01
54 10
460,0
54 199
525,01
56 114
575,99
74 91
618,0
87 56
649,0
90 43
672,007
94 126
703,99
120 4
721,01
145 205
759,98
148 186
789,98
149 83
822,0
166 163
858,5
172 160
901,01
Risorse
45, 186, 197, 204, 219, 234, 235, 239, 245, 246, 273, 308, 318, 348, 355, 358, 368
155, 170, 216, 223, 226, 231, 248, 248, 254, 261, 263, 264, 266, 273, 280, 282,
290, 306, 356, 378, 439
231, 242, 245, 271, 276, 282, 290, 290, 294, 296, 299, 311, 315, 315, 316, 321,
336, 350, 366, 385, 429, 431, 431, 443, 516, 528
241, 288, 292, 294, 294, 294, 294, 304, 308, 310, 311, 321, 343, 347, 358, 379,
381, 389, 401, 427, 429, 487, 495, 991
85, 129, 135, 230, 271, 304, 307, 314, 316, 316, 318, 328, 328, 328, 328, 332,
333, 351, 352, 357, 361, 371, 383, 397, 408, 435, 441, 450, 525, 566, 567, 574,
638, 791
74, 300, 342, 344, 360, 365, 374, 379, 386, 390, 394, 399, 399, 403, 430, 437,
440, 444, 452, 455, 472, 486, 490, 494, 514, 624, 645
291, 308, 313, 356, 380, 386, 397, 401, 411, 413, 417, 417, 418, 419, 424, 425,
427, 435, 454, 457, 458, 470, 473, 518, 524, 548, 550, 552, 575, 584, 590, 658,
681, 1013
405, 440, 448, 451, 462, 464, 471, 473, 478, 506, 512, 513, 517, 520, 544, 574,
613
303, 344, 355, 371, 383, 404, 417, 427, 444, 445, 452, 452, 465, 513, 519, 534,
535, 541, 554, 555, 557, 601, 609, 616
289, 398, 400, 420, 441, 470, 473, 481, 481, 482, 483, 485, 488, 504, 508, 515,
515, 525, 529, 540, 554, 581, 583, 585, 599, 615, 615, 624, 627, 652, 667, 704,
728, 744
474, 475, 501, 518, 519, 553, 559, 575, 577, 580, 583, 591, 596, 597, 605, 621,
645, 646, 648, 691, 691, 726, 776
369, 396, 461, 522, 523, 577, 580, 583, 586, 594, 595, 596, 596, 608, 613, 637,
672, 676, 703, 837
23, 195, 538, 544, 604, 610, 611, 613, 626, 628, 632, 638, 642, 645, 647, 650,
651, 659, 659, 661, 669, 691, 695, 700, 714, 745, 749, 775, 775, 932
521, 548, 630, 631, 647, 653, 654, 655, 657, 667, 671, 672, 678, 705, 719, 786,
816, 887, 921
483, 503, 639, 644, 650, 652, 669, 676, 676, 680, 683, 688, 691, 706, 708, 714,
716, 720, 721, 723, 741, 783, 818, 832, 868, 887, 942
583, 592, 634, 641, 651, 662, 668, 670, 682, 683, 689, 693, 698, 703, 705, 706,
707, 708, 712, 713, 717, 727, 732, 739, 742, 745, 750, 764, 778, 785, 796, 815,
819, 827, 836, 840, 879, 922, 941
524, 593, 641, 655, 659, 690, 706, 714, 715, 715, 721, 724, 726, 740, 754, 756,
760, 762, 768, 770, 777, 778, 784, 799, 802, 805, 808, 810, 843, 885, 908, 941,
944
669, 716, 723, 732, 741, 748, 761, 767, 767, 768, 781, 786, 789, 790, 791, 804,
809, 818, 880, 884, 885, 899, 915, 940, 954, 965, 986
679, 692, 695, 738, 740, 778, 783, 789, 789, 790, 801, 802, 821, 825, 825, 836,
841, 842, 848, 850, 855, 858, 872, 877, 888, 895, 920, 937, 956
2, 53, 695, 707, 756, 757, 757, 758, 758, 774, 780, 795, 798, 800, 801, 835, 837,
854, 857, 862, 862, 872, 891, 895, 906, 918, 921, 935, 941, 951, 1014
3, 59, 96, 778, 798, 816, 821, 832, 856, 859, 862, 874, 882, 883, 889, 896, 903,
73
174 2
932,01
176 236
963,0
207 204
982,01
208 161
11,0
221 160
51,015
225 188
107,015
234 64
128,0075
237 42
154,5
237 47
179,93
237 231
231,0
239 108
256,39
909, 913, 913, 918, 928, 932, 935, 950, 955, 956, 965, 1022
10, 17, 55, 744, 865, 869, 877, 884, 892, 894, 901, 916, 917, 919, 925, 926, 930,
937, 937, 948, 950, 953, 972, 1004, 1016
5, 6, 15, 75, 95, 122, 639, 865, 867, 872, 895, 904, 923, 949, 950, 950, 964, 971,
971, 980, 981, 982, 986, 991, 996, 1021
5, 10, 15, 42, 44, 49, 61, 67, 89, 103, 155, 166, 264, 833, 867, 870, 888, 910, 910,
916, 937, 946, 950, 953, 962, 964, 972, 978, 981, 989, 1000, 1013
13, 14, 16, 21, 56, 59, 66, 69, 79, 117, 159, 164, 165, 188, 778, 779, 913, 919,
927, 954, 955, 957, 958, 960, 978, 981, 995, 1014, 1015, 1015
12, 14, 26, 27, 36, 43, 47, 51, 52, 61, 62, 73, 84, 89, 107, 157, 167, 234, 276, 467,
904, 960, 987, 987, 988, 1005, 1020
15, 17, 27, 60, 61, 63, 105, 111, 112, 124, 133, 137, 153, 169, 189, 193, 200, 212,
213, 216, 243, 877, 932, 995
0, 56, 88, 91, 92, 93, 101, 124, 130, 130, 143, 154, 165, 172, 184, 186, 190, 218,
244, 268, 282, 289, 332
44, 47, 55, 60, 63, 74, 75, 90, 97, 101, 104, 117, 118, 121, 128, 130, 137, 156,
167, 175, 176, 187, 191, 223, 246, 342, 949
128, 130, 130, 133, 137, 141, 143, 155, 157, 171, 180, 180, 193, 205, 223, 224,
233, 234, 249, 290, 295, 296, 339, 356, 377, 403, 500, 931
36, 105, 155, 161, 162, 170, 176, 184, 185, 188, 199, 201, 201, 204, 212, 222,
246, 255, 298, 314, 315, 417, 422, 477, 570, 957
16, 54, 134, 142, 149, 150, 151, 179, 200, 203, 207, 208, 229, 230, 234, 242, 268,
271, 273, 273, 278, 288, 294, 311, 324, 327, 327, 331, 335, 344, 346, 357, 357,
396, 411, 413, 417, 448, 498, 585, 591, 936
Tabella 2: situazione finale della rete con Np=32 e Nc=1024 con approccio logaritmico
In questo modo si è cercato di capire le prestazioni in termini di organizzazione delle
risorse nei due casi e utilizzando come parametri la media delle distanze tra i centroidi e la
deviazione standard della stessa.
Osservando il Grafico 1 è possibile notare come i due approcci sebbene arrivino allo stesso
risultato (dimostrabile dal fatto che entrambi tendono allo stesso valore pari ad
Nc
) lo
Np
fanno con tempi diversi quasi con 3000 secondi di differenza. Ciò, come detto più volte è
dovuto al fatto che un agente, grazie alle finger arriverà prima sul nodo “target” e non
dovrà attraversare tutta la rete.
Al contrario, la deviazione standard, inizialmente, nel caso logaritmico tende ad aumentare
per poi scendere verso valori relativamente bassi. La deviazione infatti è indice di quanto
un valore si discosta da quello medio. Il fatto che siano valori bassi indica che i centroidi
tra i vari peers tendono ad organizzarsi e a mantenere, col passare del tempo, una stessa
distanza.
74
Grafico 1: Confronto tra le medie delle distanze tra centroidi di due peer adiacenti con
approccio lineare e logaritmico. Np=32 ed Nc=1024
Grafico 2: confronto tra deviazioni standard delle distanze tra centroidi di due peers
adiacenti con approccio logaritmico e lineare. Np=32 ed Nc=1024.
75
Altro fattore da considerare è la deviazione standard del numero di chiavi memorizzate su
un nodo. Il grafico 3 mostra l’andamento della deviazione: dopo la stabilità iniziale, gli
agenti portano scompenso alla rete muovendo un certo numero di risorse ma man mano
che il sistema si auto-organizza, anche il numero delle risorse sui vari peers tenderà a
stabilizzarsi.
Purtroppo, come si nota da quest’ultimo grafico, questo è un aspetto negativo
dell’approccio logaritmico. Nell’approccio lineare, infatti, ogni peer riceve gli agenti dai
due nodi adiacenti (gli agenti posso giungere sia da destra che da sinistra) cosicché tutti i
peers tendono a memorizzare lo stesso numero di chiavi. Al contrario, nel caso logaritmico
un agente che trasporta una chiave non si muoverà su un nodo adiacente ma sul nodo con
centroide più vicino al tipo della risorsa trasportata dall’agente stesso che è inserito nella
finger del nodo su cui si trova l’agente. Proprio per la definizione di finger, il numero di
finger che puntano ad un dato nodo non è costante ma dipende dagli indici dei peers e dai
suoi vicini sull’anello. Tale sbilanciamento si riflette quindi sullo sbilanciamento del
numero di agenti che arrivano sui vari peers e quindi sulla diversa distribuzione delle
risorse.
Number of Keys on a Peer - St.
16
14
12
10
8
Lin
6
Log
4
2
0
0
50000
100000
150000
200000
250000
300000
350000
Time(s)
Grafico 3: confronto tra deviazioni standard del numero di chiavi memorizzate su ciascun
peer con approccio logaritmico e lineare. Np=32 ed Nc=1024.
76
Dopo aver osservato i risultati ottenuti con approccio lineare e logaritmico, si è pensato che
una soluzione potrebbe essere un approccio che coniuga la rapidità dell’ordinamento
logaritmico con l'equità assicurata da quello lineare. Questo obiettivo può essere raggiunto
partendo inizialmente con il processo di ordinamento nella "modalità logaritmica", al fine
di riordinare rapidamente le chiavi delle risorse chiave e quindi passare alla "Modalità
lineare", per distribuire meglio le chiavi tra i vari peers della rete.
Un’ ulteriore prova effettuata è stata quella di modificare i valori di kp e kd in modo da
andare a modificare le probabilità di pick (presa di una risorsa) e quella di drop (posa di
una risorsa). In effetti, ricordando l’equazione (6), a parità degli altri fattori, aumentando
kp, il quale varia tra 0 e 1, la probabilità di effettuare un’operazione di pick aumenta. Al
contrario, aumentando kd, la probabilità di drop diminuisce.
Sono state effettuate alcune prove settando in maniera diversa i valori di kp e kd settati
rispettivamente a 0.3 e 0.9 per poi, una volta aumentarli tutte e due rispettivamente a 0.5 e
1 e una seconda volta a diminuirli a 0.1 e 0.5. Secondo quanto appena detto, nel primo caso
la probabilità di drop diminuisce e quella di pick aumenta cosicché, nel primo caso gli
agenti dovrebbero essere più propensi a prendere una risorsa piuttosto che a posarla,
mentre nel secondo caso essi si comporteranno in maniera contraria.
Centroid Distance - Mean
1000
kp=0.3
kd=0.9
kp=0.1
kd=0.5
100
kp=0.5
kd=1
10
0
50000
100000
150000
200000
250000
Time(s)
Grafico 4: andamento delle medie delle distanze tra centroidi adiacenti al variare dei
fattori kp e kd. Scala logaritmica.
77
Come mostrato nel Grafico 4, prendendo come andamento di riferimento la curva in blu
ossia la rete in cui kp e kd sono settati a 0,3 e 0,9, se si aumentano i due valori (curva
verde) il tempo per raggiungere il valore limite aumenterà mentre diminuendo i due fattori
si arriverà prima ad una situazione di organizzazione della rete in quanto la media delle
differenze tra i centroidi adiacenti tenderà prima al valore “limite”. Il protocollo
comunque, a regime, non dipenderà da tali valori in quanto questi sono utili sono nella fase
di organizzazione della rete. Con un qualsiasi valore di kp e kd, alla fine si otterrà lo stesso
risultato: l’ordinamento delle risorse con la conseguente organizzazione dell’intera rete.
Nei grafici 5 e 6 sono invece mostrati gli andamenti delle medie delle differenze tra
centroidi adiacenti una volta tenendo costante il numero delle risorse (1024) e variando il
numero di peers della rete, e un’altra volta mantenendo variando in numero delle classi di
risorse con Np fisso a 32 peers.
In entrambi i grafici, agli agenti è stato fatto adottare il movimento logaritmico in quanto,
da quanto detto nelle pagine precedenti risulta essere più efficiente ed efficace di quello
lineare.
Si noti come, seppur con tempi diversi, si raggiunge il valore
Nc
a
Np
dimostrazione che l’ordinamento delle risorse e l’auto-organizzazione dell’anello si ottiene
anche con un numero di classi di risorse o di peers diversi ma con tempi che possono
diminuire (valori inferiori di Np ed Nc) o tendere a valori molto elevati.
Centroid Distance - Mean
1000
128
100
Np=8
64
32
Np=16
Np=32
10
0
10000
20000
30000
40000
50000
60000
70000
80000
Time(s)
Grafico 5: confronto dell’andamento delle medie delle distanze tra centroidi di due peer
adiacenti con 1024 classi di risorse al variare del numero di peers. Scala logaritmica
78
Centroid Distance - Mean
1000
100
32
Nc=128
16
Nc=512
10
8
Nc=1024
1
0
100000
200000
300000
400000
500000
600000
700000
Time(s)
Grafico 6: confronto dell’andamento delle medie delle distanze tra centroidi di due peer
adiacenti con Np pari a 32 al variare delle classi di risorse. Scala logaritmica.
Ultimo indicatore da tenere in considerazione è la distribuzione delle chiavi in un nodo
rispetto al valore del centroide del nodo stesso. Quando le chiavi sono riordinati
correttamente, ogni peer contiene chiavi il cui valore è sempre molto vicino al valore del
centroide del peer. La distribuzione, infatti, se il lavoro degli agenti è fatto bene e quindi
riesce ad ordinare le risorse sui nodi con centroide vicino al tipo di ogni risorsa sarà
rappresentata da una gaussiana che tenderà a stringersi verso il valore 0.
Le Figure successive mostrato l’andamento della distribuzione delle chiavi rispetto al
centroide del nodo in una rete con 32 nodi e 1024 tipi di risorse.
Questo dato conferma che il valore della grande maggioranza delle chiavi è molto vicino
al centroide del peer (Figura 17): il numero di valori di chiave consentiti è di 1024 per cui
le differenze possono oscillare da -512 a 512, ma la differenza tra il valore di una chiave e
quella del rispettivo centroide è molto raramente più grandi di 50 che, in una rete ancora
non a regime ma comunque già in fase di auto-organizzazione, risulta essere un risultato
soddisfacente. Questo vuol dire che, nel momento in cui la rete raggiungerà la completa
organizzazione e tutte le risorse saranno ordinate correttamente, la ricerca di una classe di
79
risorse sarà effettuata sul nodo con centroide più vicino al valore della classe ricercata in
quanto, come confermano le figure, le risorse con il tipo ricercato si troveranno sul quel
peer o al più su i due peers adiacenti.
Figura 16: distribuzione delle chiavi rispetto al centroide del nodo all’inizio della simulazione.
Figura 17: distribuzione delle chiavi rispetto al centroide del nodo dopo 100000 secondi.
80
Figura 18: distribuzione delle chiavi rispetto al centroide del nodo dopo 200000 secondi
Figura 19: distribuzione delle chiavi rispetto al centroide del nodo dopo 400000 secondi
81
Dopo aver fatto l’analisi delle prestazione in termini di ordinamento delle risorse e quindi
organizzazione dell’anello, è necessario capire l’efficienza della ricerca di una determinata
classe di risorse. Come indice di efficienza si tiene in considerazione il numero di passi che
è necessario fare prima di trovare una determinata classe ossia quanti nodi è necessario
interrogare prima di arrivare al nodo con centroide più vicino al valore del tipo ricercato. In
Chord, grazie all’uso delle finger tables la ricerca viene effettuata in un numero di passi
logaritmico rispetto al numero di nodi che costituiscono la rete.
Nel caso di Self-Chord invece, grazie all’uso della doppia finger su ogni nodo, si riesce ad
avere una maggiore conoscenza di nodi e quindi, in media, saranno necessari un numero
⎡ log(N ) ⎤
minore di passi pari a ⎢
⎥ dove N è sempre il numero dei nodi dell’anello.
⎢ 3 ⎥
Al fine di comprendere tale concetto è possibile osservare la Figura 20.
Figura 20: esempio di passi per la ricerca di una classe di risorse su un anello con 32 peers
e 1024 classi di risorse
82
La rete è costituita da 32 nodi e su ognuno di essi è calcolato il valore del centroide.
Nell’esempio riportato, consideriamo il nodo con ID 7 e cerchiamo la classe di risorse 590.
In questo caso, com’è possibile osservare dall’immagine, grazie alla doppia finger è
possibile arrivare sul nodo con centroide più vicino con 3 passi: dal nodo 7 si passa al nodo
23 quindi si torna indietro con la finger sinistra al nodo 19 e da qui si effettua l’ultimo
passo verso il nodo 20.
Come prova di quanto in precedenza affermato, si sono effettuate, su una rete con 32 peers
e 1024 risorse, 100 ricerche di una classe scelta casualmente partendo da un qualsiasi nodo
della rete.
Il risultato delle varie ricerche è riportato nel grafico sottostante. La media finale del
numero dei passi è di 1,62 a dimostrazione che, con l’uso delle doppie finger, il numero di
⎡ log(N ) ⎤
⎛ log(N ) ⎞
mentre
il
numero
di
passi
medio
è
passi massimo è al più pari a ⎢
O
⎟
⎜
⎥
⎢ 2 ⎥
⎝ 3 ⎠
con N pari al numero dei nodi della rete.
3,5
3
Hops
2,5
2
1,5
1
0,5
0
Grafico 7: numero di hops per la ricerca di una risorsa, raggruppati per valori crescenti.
83
7 Conclusioni
Il lavoro svolto ha riguardato la realizzazione di un’applicazione p2p basata su agenti
mobili bio-ispirati i quali grazie a semplici operazioni guidate da calcoli probabilistici
riescono a riorganizzare e ordinare le risorse tra i vari peers della rete. In particolare, gli
agenti sono stati utilizzati per modificare il protocollo strutturato Chord al fine di
migliorarne le prestazione. Con Self-Chord infatti non è più necessario che chiavi delle
risorse e id dei nodi condividano lo stesso spazio degli identificatori né un nodo dovrà
preoccuparsi in fase di ingresso nella rete di dover trovare il nodo “responsabile” a cui
assegnare la risorsa che intende pubblicare: tutto sarà effettuato in maniera automatica
dagli agenti i quali saranno guidati da calcoli probabilistici. Ecco quindi che si può capire il
perché dell’accostamento degli agenti alle formiche: animali che hanno da sempre
affascinato gli studiosi poiché, sebbene il singolo insetto sembri agire in modo del tutto
autonomo, l'attività dell'intera colonia appare altamente organizzata e denota un
comportamento intelligente.
Per cui l’auto-organizzazione e l’elevata scalabilità sono le due caratteristiche
fondamentali di Self-Chord che lo contraddistinguono da Chord.
La possibilità di far muovere gli agenti in maniera logaritmica tra i vari nodi, e la presenza
di una doppia finger table su ogni nodo, garantisce tempi prestazionali migliori rispetto ad
un movimento lineare degli agenti, permettendo alla rete di raggiungere l’organizzazione e
l’ordinamento delle risorse in tempi minori.
Dopo aver osservato i risultati ottenuti con approccio lineare e logaritmico, una soluzione
futura si potrà basare su un approccio che coniuga la rapidità dell’ordinamento logaritmico
con l'equità assicurata da quello lineare al fine di ottimizzare ancora di più le prestazioni
del sistema.
Inoltre, grazie alla doppia finger table di cui è costituito ogni peer, si potranno effettuare
⎛ log( N ) ⎞
ricerche di una classe di risorse con un numero di passi che in media è O⎜
⎟ rispetto
⎝ 3 ⎠
al numero dei nodi che costituiscono la rete stessa.
Nonostante Self-Chord si basi su una topologia di rete ad anello, tale approccio basato su
agenti, in futuro potrà essere applicato anche su altri tipi di sistemi p2p
non
necessariamente basati su una struttura di rete circolare, come nelle griglie multi84
dimensionali o alberi. Anche in questi tipi di sistemi infatti, attraverso l’introduzione di
piccole modifiche basate su algoritmi bio-ispirati si potranno ottenere le stesse prestazioni
e caratteristiche ottenute in Self-Chord.
In questi capitoli, è stato quindi dimostrato quanto sperato e si sono ottenuti i risultati
prefissati inizialmente i quali sono stati confermati dalle prove effettuate e delle quali si
riporta nel capitolo 6.
85
Ringraziamenti
A conclusione di questo lavoro, segno del raggiungimento di un importante traguardo è doveroso
ringraziare alcune persone.
Si ringraziano l’Ing. Carlo Mastroianni e l’Ing. Agostino Forestiero che mi hanno pazientemente
seguito durante questi mesi di lavoro.
Ringrazio tutti gli amici con cui ho condiviso tante difficoltà ma anche tanti momenti felici della vita
universitaria ed in particolare Stefano e Carlo.
Ringrazio inoltre l’amico, “compare” e fratello Stefano il quale mi è stato vicino nei momenti più
importanti e difficili di questi anni.
Infine, rivolgo gli ultimi ringraziamenti alle persone più importanti, le quali hanno permesso che questo
momento della mia vita non rimanesse solo un sogno: Ines e i miei genitori i quali mi hanno sempre
supportato in tutte scelte di questi anni di università. A loro dedico il frutto di questi tre anni di studi.
86
Indice Figure
Figura 1: scalabilità del sistema; rapporto tra dimensione tabelle di routing e messaggi
da scambiare per la ricerca di una chiave .......................................................................... 16
Figura 2: un esempio di rete CAN con 5 nodi in uno spazio 2-d ........................................ 17
Figura 3: Esempio di albero binario in Kademlia .............................................................. 18
Figura 4: Anello di peers (in rosso) .................................................................................... 22
Figura 5: esempio di finger table per il nodo con ID 1 e relativa finger table................... 24
Figura 7: passi dell’algoritmo per la ricerca nella finger del nodo responsabile per una
chiave................................................................................................................................... 25
Figura 8 : esempio di passi logaritmici per la ricerca di un nodo successore ................... 26
Figura 9: architettura di Open Chord................................................................................. 36
Figura 10: Diagramma UML delle classi di Open Chord per la comunicazione Socket .. 43
Figura 11: esempio di rete Self-Chord. All’interno l’id del nodo. Tra parentesi graffe le
risorse sul noto e tra parentesi quadre il centroide per il nodo.......................................... 50
Figura 12: anello con 16 peers con id ordinati .................................................................. 54
Figura 13: finger tables per spostamenti logaritmici a destra e sinistra di un agente....... 54
Figura 14: interfaccia grafica dell’applicazione Self-Chord ............................................. 68
Figura 15: esempio di anello con centroidi ordinati .......................................................... 70
Figura 16: distribuzione delle chiavi rispetto al centroide del nodo all’inizio della
simulazione. ......................................................................................................................... 80
Figura 17: distribuzione delle chiavi rispetto al centroide del nodo dopo 100000 secondi.
............................................................................................................................................. 80
Figura 18: distribuzione delle chiavi rispetto al centroide del nodo dopo 200000 secondi81
Figura 19: distribuzione delle chiavi rispetto al centroide del nodo dopo 400000 secondi81
Figura 20: esempio di passi per la ricerca di una classe di risorse su un anello con 32
peers e 1024 classi di risorse .............................................................................................. 82
87
Bibliografia
[1]
A. Forestiero, C. Mastroianni, G. Spezzano, "Reorganization and discovery of grid
information with epidemic tuning". Future Generation Computer Systems, vol. 24,
n. 8, pp. 788--797, Elsevier Science, October 2008.
http://grid.deis.unical.it/index.html?page=publications.html%3Fauth%3Dcarlomast
roianni
[2]
A. Forestiero, C. Mastroianni, G. Spezzano, "So-Grid: A Self-organizing Grid
Featuring Bio-inspired Algorithms". ACM Transactions on Autonomous and
Adaptive Systems, vol. 3, n. 2, May 2008.
[3] Rowstron and P. Druschel, "Pastry: Scalable, decentralized object location and
routing for large-scale peer-to-peer systems". November, 2001.
[4] B. Y. Zhao, J. D. Kubiatowicz, and A. D. Joseph. Tapestry: An infrastructure for
fault-resilient wide-area location and routing. April 2001.
[5] Barkai David, Peer-to-peer computing : technologies for sharing and collaborating
on the net, Hillsboro, Intel Press.
[6] David Liben-nowell, Hari Balakrishnan, David Karger. Analysis of the evolution of
peer-to-peer systems (2002). In http://pdos.lcs.mit.edu/chord/papers/podc2002.ps
[7] FIPS 180-1. Secure Hash Standard. U.S. DoC/NIST, April 17, 1995.
In http://cr.yp.to/bib/2002/-sha.pdf.
[8] G. Coulouris, J. Dollimore, T. Kindberg, Distributed Systems: Concepts and
Design, Addison Wesley (Quarta Edizione) 2005
[9] Grid Computing, in http://it.wikipedia.org/wiki/Grid_computing
[10] Gurmeet Singh Manku. Dipsea: A Modular Distributed Hash Table. Ph. D. Thesis
(Stanford University), August 2004.
In http://infolab.stanford.edu/~manku/phd/index.html
[11] Hp Palo Alto Laboratories , Dejan S. Milojicic, Vana Kalogeraki, Rajan Lukose,
Kiran Nagaraja, Jim Pruyne, Bruno Richard, Sami Rollins, Zhichen Xu. “Peer-toPeer Computing”, in http://www.hpl.hp.com/techreports/2002/HPL-2002-57.pdf,
2002
[12] Karger D., Lehman E., Leighton F., Levine M., Lewin D., And Panigrahy R.
Consistent hashing and random trees: Distributed caching protocols for elieving
88
hot spots on the World Wide Web. In Proceedings of the 29th Annual ACM
mposium on Theory of Computing (El Paso, TX, May 1997), pp. 654–663.
[13] Minar Nelson, “Distributed Systems Topologies: Part 1”, 2002.
In www.openp2p.com
[14] Minar Nelson, “Distributed Systems Topologies: Part 2”, 2002.
In www.openp2p.com
[15] Moni Naor and Udi Wieder. Novel Architectures for p2p Applications: the
Continuous-Discrete Approach, 2003.
In http://portal.acm.org/citation.cfm?id=1273350&jmp=cit&coll=&dl=
[16] Peer-to-Peer Systems and Applications, Lecture Notes in Computer Science,
Volume 3485 Editori: Ralf Steinmetz, Klaus Wehrle, Settembre 2005
[17] Petar Maymounkov, David Mazieres. Kademlia: A peer-to-peer information system
based on the xor metric. 2002
[18] SHA-1, in http://it.wikipedia.org/wiki/Secure_Hash_Algorithm
[19] Stoica, R. Morris, D. Karger, M. F. Kaashoek, and H. Balakrishnan. Chord: A
scalable peer-to-peer lookup service for internet applications. In Proc. of the
Conference on Applications, technologies, architectures, and protocols for
computer communications SIGCOMM’01, 2001.
In http://pdos.csail.mit.edu/papers/chord:sigcomm01/
[20] Sven Kaffille, Karsten Loesing. Open Chord Version 1.0.4: User’s manual. OttoFriedrich-Universität Bamberg.
http://www.opus-bayern.de/uni-bamberg/volltexte/2007/122/pdf/Dokument_01.pdf
[21] Sylvia Ratnasamy, Paul Francis, Mark Handley, Richard Karp and Scott Schenker.
A scalable content-addressable network. 2001
89