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