Modulo PeerServices - Analisi Funzionale

Instrada i messaggi (ricevuti da altri nodi o generati dal nodo stesso) che sono destinati ad un dato hash-node.

Mantiene le informazioni necessarie.

Idea generale

Il funzionamento dei servizi peer-to-peer si basa sulla definizione di una funzione che associa ad una chiave k univocamente un nodo esistente nella rete al quale indirizzare delle richieste. Si può opzionalmente aggiungere il requisito che il nodo sia "partecipante" al servizio.

Sia S lo spazio di indirizzi validi per i nodi della rete.

Sia Vt il set di nodi nella rete al tempo t.

Sia αt : S → Vt la funzione suriettiva che al tempo t associa ad un indirizzo il nodo che lo detiene. E' suriettiva in quanto ogni nodo ha almeno un indirizzo. E' una funzione non completamente definita in S poiché un indirizzo potrebbe non essere stato assegnato ad alcun nodo.

Definiamo una funzione che al tempo t assegni ad ogni indirizzo in S un indirizzo nel dominio di αt. Cioè dato un indirizzo valido che potrebbe non essere stato assegnato tale funzione ritorna un indirizzo assegnato.

DHT: Distributed Hash Table

Sia p un servizio, sia K lo spazio delle chiavi definito da questo servizio. Il servizio p definisce una funzione di hash che mappa lo spazio delle chiavi sullo spazio degli indirizzi.

Quando un nodo, al tempo t, vuole scrivere la coppia chiave-valore (k, v) nel database distribuito il nodo calcola:

Contatta quindi il nodo hash_node(k) e chiede di memorizzare la coppia (k, v).

Analogamente il nodo che vuole reperire il dato associato alla chiave k, calcola hash_node(k) e chiede di leggere il dato associato a k.

Questo procedimento realizza un database distribuito, perché ogni nodo mantiene solo una porzione delle associazioni chiave-valore.

Fondamentale è la funzione Ht. Definiamo la funzione Ht(x) come l'indirizzo x’ associato ad un nodo esistente (x’ ∈ dom(αt)) che minimizza la distanza x - x’, in modo più rigoroso minargx’∈dom(αt)dist(x,x’). La funzione dist rappresenta in modo intuitivo la distanza tra due indirizzi, ma è definita in modo che la funzione Ht "cerchi" il primo indirizzo valido "procedendo verso destra" fino al gsize per poi ripartire da 0. Questo comportamento ci ritornerà utile in seguito. Precisamente la funzione dist(x,y) si calcola così:

HDHT: Hierarchical DHT

In una struttura gerarchica come Netsukuku un nodo non ha la conoscenza di tutti i nodi esistenti nella rete, quindi non può da solo computare la funzione Ht in quanto non conosce per intero dom(αt). L'implementazione avviene in modo distribuito.

Sia n un nodo che vuole inviare un messaggio m all'hash-node della chiave k per il servizio p. Il nodo n usa la funzione hp definita da p per calcolare dalla chiave k l'indirizzo x. A questo punto dovrebbe calcolare x’=Ht(x). Procede così:

Il messaggio m’ viene così inoltrato:

Il messaggio m’ raggiunge un nodo dentro il gnodo che mirava a raggiungere.

Il messaggio m’ raggiunge la destinazione finale:

Fault tolerance

Siano n ed m due nodi che conoscono una certa chiave k per un servizio p. Entrambi sono in grado di calcolare x = Ht ( hp ( k ) ) e di contattare x. Sia j il livello massimo tale che x ∉ nj, x ∉ mj. Cioè l'interno del g-nodo xj di livello j a cui appartiene il nodo x è sconosciuto per n e per m. Supponiamo ora che per qualche motivo i messaggi instradati dal modulo PeerServices si perdano all'interno del g-nodo xε (con ε piccolo a piacere), ε < j.

Pur essendo questa anomalia circoscritta ad un g-nodo piccolo a piacere, questo impedirebbe ai nodi n ed m di scrivere e leggere dati con chiave k nel servizio p.

Dopo che n vede fallire il suo tentativo di contattare x per salvare un record con chiave k, n può cercare di isolare il g-nodo malfunzionante; cioè n calcola x’ = Ht ( hp ( k ), exclude_list=[x] ) TODO

Repliche

Quando un nodo v riceve la richiesta di memorizzare (o aggiornare, o rinfrescare) un record con chiave k nella sua porzione del database distribuito si occupa di replicare questo dato su un numero q di nodi replica. L'obiettivo è fare sì che se il nodo muore o si sconnette dalla rete, alla prossima richiesta di lettura del dato venga comunque data la risposta corretta. Quindi v deve scegliere i nodi che saranno contattati per la chiave k quando lui non parteciperà più.

Scelta dei g-nodi di replica

Ovviamente il nodo v verifica di essere il risultato di Ht ( h ( k ) ).

Poi v calcola quale sarebbe stato il secondo miglior candidato, in sua assenza. Anche in questo caso quello che può computare il nodo v è un g-nodo u di livello j, con la funzione Ht ( h ( k ), exclude_list=[v] ) 1 . Riguardo al g-nodo u, v sa che al suo interno esiste almeno un nodo ("partecipante") ma non sa quali e quanti.

Quindi v invia al g-nodo u la richiesta di replicare il record in un massimo di q nodi. Vedremo di seguito come viene comunicato questo messaggio da v ad u, come u lo esegue, come u risponde a v indicando quante repliche ha potuto salvare. Per ora assumiamo che v riceve la risposta da parte del g-nodo u, che gli comunica di aver effettuato w repliche (w <= q) oppure che passato un certo tempo massimo (che dipende dalla dimensione di u, p. es. 2 secondi per il numero di nodi stimati nel g-nodo u) il nodo v consideri il g-nodo u non in grado di soddisfare alcuna replica.

Il nodo v sottrae da q il numero w (oppure 0 se u non ha risposto) e se q > 0 reitera le operazioni partendo dal calcolo di Ht ( h ( k ), exclude_list=[v, u] ). Le operazioni si considerano concluse quando q diventa 0 oppure quando Ht ( h ( k ), exclude_list) non è in grado di restituire altri g-nodi, vale a dire se nella rete ci sono meno di q partecipanti al servizio.

Note:

  1. Nella funzione che cerca i candidati per le repliche è possibile che la ricerca venga ristretta ad un certo g-nodo; questo è a discrezione del servizio: ad esempio il Coordinator di un g-nodo non sarà mai contattato all'esterno del g-nodo stesso, quindi sarebbe inutile replicare i record di sua pertinenza su nodi esterni.

Salvataggio delle repliche

Vediamo come fa il nodo generico v a richiedere il salvataggio di max. q repliche di un record r del servizio pid all'interno del g-nodo u.

Sia l'indice j tale che il g-nodo vj+1 è il minimo g-nodo comune tra v ed u. Similmente a quanto visto prima, la richiesta che viene inviata al g-nodo u fa un percorso interno a vj+1 ed è costituita da un messaggio m’ che contiene la tupla v: v0·v1·...·vj, le coordinate lvl e pos del g-nodo che si vuole raggiungere, un identificativo id_msg generato per il messaggio. Quando il messaggio viene ricevuto da un nodo u0 appartenente ad u questo realizza una connessione TCP con il nodo v tramite percorso interno al g-nodo vj+1.

Una volta realizzata la connessione TCP tra v e u0 il dialogo consiste in:

Per memorizzare le repliche dentro il g-nodo u, u0 procede così:

Popolamento della cache al bootstrap

Quando un nodo n entra nella rete (oppure quando inizia a partecipare ad un servizio) può venirgli assegnato un indirizzo prossimo a qualche chiave precedentemente salvata nel database distribuito. Deve svolgere quindi alcune operazioni per reperire e memorizzare i record di sua pertinenza.

Nell'analisi dell'algoritmo delineato di seguito si tenga conto che queste operazioni non sono strettamente necessarie al funzionamento del servizio. Se queste non vengono effettuate con precisione l'eventuale inconsistenza nel database distribuito sarà comunque temporanea. E' quindi di proposito che si è cercato un trade-off tra la rigorosità del risultato e la leggerezza dell'implementazione. Ad esempio l'inoltro dei messaggi verso i nodi viene svolto dagli hop intermedi senza garantire il successo né l'identificazione del punto esatto in cui si può essere perduto il messaggio.

Prerequisito di queste operazioni è che il nodo sia maturo, cioè abbia piena conoscenza dei nodi e g-nodi presenti nella rete che sono di sua visibilità. In caso di servizio opzionale deve anche avere piena conoscenza della mappa di nodi partecipanti al servizio.

Abbiamo visto che la funzione Ht cerca l'indirizzo che minimizza la funzione dist e che la funzione dist introduce una sorta di "direzione a destra" nella ricerca dell'indirizzo. Definiamo ora la funzione H_revt(x) = minargx’∈dom(αt)dist_rev(x,x’) dove la funzione dist_rev implementa la "direzione a sinistra" nella ricerca dell'indirizzo. Per farlo basta definire dist_rev(x,x’) = dist(x’,x).

Questo è l'algoritmo che esegue n per popolare la sua cache:

Note:

  1. Come il nodo n utilizzi la cache reperita dai vari singoli nodi dipende dal servizio. Di norma n memorizza tutti i record nella sua cache.

Con questo algoritmo il nodo n, dato il g-nodo gn_dx appartenente a nj+1, contatta il nodo n_dx:

Il messaggio m’ viene così inoltrato:

Il messaggio m’ raggiunge un nodo dentro il g-nodo gn_dx.

Il messaggio m’ raggiunge la destinazione finale:

Requisiti

Durante le operazioni del modulo è possibile aggiungere un servizio di tipo opzionale.

Deliverables

Classi e interfacce

La mappa delle rotte è un oggetto di cui il modulo conosce l'interfaccia IPeersMapPaths. Tramite essa il modulo può:


In alcuni casi sarà necessario realizzare una comunicazione TCP verso un nodo originante di un messaggio. In tali casi si vuole raggiungere il nodo attraverso un percorso interno al proprio g-nodo di un dato livello. L'oggetto fornito al modulo a questo scopo implementa l'interfaccia IPeersBackConnectionFactory, la quale permette di:


Le tuple che il modulo elabora per il calcolo della funzione Ht sono istanze di una classe nota al modulo. Anche se rappresentano degli indirizzi di nodi (all'interno della rete o all'interno di un g-nodo) non viene usata la stessa classe che rappresenta un Netsukuku address (Naddr) o una sua interfaccia.

La classe usata in questo modulo è PeerTuple. Si tratta di una classe serializzabile in quanto le tuple vanno comunicate nei messaggi da inoltrare.


La classe che implementa un servizio deriva da PeerService.

La classe base non sa come ottenere da una chiave k la tupla x, questo procedimento spetta alla classe derivata. Tuttavia molte operazioni saranno uguali nella maggior parte dei servizi. Quindi la classe base cerca di fornire i servizi comuni senza tuttavia essere di impedimento alla classe derivata se vuole usare altre modalità di calcolo. Per fare questo la classe base fornisce:

Quando le operazioni del modulo richiedono il calcolo di h ( x ) su un certo servizio p, il metodo 'perfect_tuple' viene richiamato sull'istanza di PeerService, quindi tale metodo deve essere pubblico.

Se tale metodo non viene ridefinito dalla classe derivata, il suo comportamento è il seguente. L'istanza conosce le dimensioni dei g-nodi ad ogni livello (gsizes) quindi calcola la dimensione dello spazio degli indirizzi validi. Poi richiama il metodo 'hash_from_key' passando oltre alla chiave k il numero massimo dell'hash (la dimensione dello spazio di indirizzi meno uno). In questo metodo la classe derivata deve occuparsi di associare alla chiave un valore di hash (di norma uniformemente distribuito) compreso tra 0 e il valore massimo (inclusi). Questo metodo è demandato alla classe derivata e quindi è definito astratto. Inoltre deve essere usato solo con la modalità sopra descritta, quindi può essere definito protetto.

Poi, nel metodo 'perfect_tuple', l'istanza usa il valore di hash per produrre una tupla sulla base della sua conoscenza di gsizes.

Se invece la classe derivata ridefinisce il metodo 'perfect_tuple' è libera di calcolare direttamente la tupla x a partire dalla chiave e dalle sue conoscenze.