UNIVERSITÀ DEGLI STUDI DI BOLOGNA
FACOLTÀ DI INGEGNERIA
Corso di Laurea Specialistica in Ingegneria Informatica
Reti di Calcolatori LS
Infrastruttura per la gestione
distribuita di un sistema di
prenotazione
Fabio Fabbri
Matricola: 169678
Introduzione
Il progetto realizzato tratta un’infrastruttura per la gestione dei nodi e delle entità presenti in un
sistema di prenotazione distribuito.
Le entità a cui l’applicazione fa riferimento sono di due tipi: prenotazioni e risorse. Queste ultime
sono gli elementi necessari alla prenotazione (ad esempio: “aule”, “ore”, “richiedenti”,.. ) e possono
essere personalizzate a seconda dell’applicazione che si vuole realizzare.
Gli attori a cui si fa riferimento sono semplicemente clienti e servitori.
I primi potranno effettuare richieste sulle entità, come la modifica di una risorsa (ad esempio
“aggiungo una nuova aula”), oppure la acquisizione di una prenotazione (ad esempio “prenoto aula
6.2 dalle 12 alle 13 il 30/03/05”).
I secondi invece dovranno fornire, in modo coordinato, il supporto necessario affinché le richieste
vengano soddisfatte.
Poiché lo sviluppo di un sistema di prenotazione distribuito sarebbe alquanto complesso, si è
preferito concentrare l’attenzione sulle tematiche di rete.
In particolare il progetto tratterà:
 un servizio di multicast per la rilevazione dei server al momento disponibili.
 un protocollo per l’acquisizione di oggetti replicati tramite uso di token
 le marche temporali per la sincronizzazione degli oggetti
 il bilanciamento del carico
Gli obiettivi implementativi che si intende approfondire sono:
 socket java non bloccanti (java.nio) che consentano ai
contemporaneamente richieste di tipo diverso su un unico thread.
server
di
attendere
Scopo del progetto
L’idea iniziale era quella di rendere distribuito un progetto per la prenotazione sviluppato in
precedenza. Successivamente, invece, si è scelto di privilegiare la parte di reti di calcolatori, che
altrimenti sarebbe stata marginale, sviluppando l’infrastruttura di rete e limitando il livello
applicativo ad un semplice strumento di debugging.
Attualmente l’obiettivo principale del progetto è quello di sviluppare alcuni dei temi visti nel corso
di Reti di Calcolatori. Il “cuore” di questo progetto è una infrastruttura per la comunicazione e la
sincronizzazione di risorse nel distribuito. Uno degli obiettivi del progetto è quello di costituire una
base su cui sviluppare in futuro un’applicazione per la prenotazione distribuita.
Attori
Le tipologie di attori presenti nel sistema sono clienti e servitori.
I servitori gestiscono gli oggetti sui quali si possono effettuare le richieste. Ciascun servitore ne
conterrà una copia che deve essere aggiornata ad ogni modifica. Una marca temporale su ogni
oggetto ne indica la versione e, quando un servitore entra nel sistema, sincronizza le proprie risorse
in modo da possedere le versioni più aggiornate.
Un cliente, dopo essersi connesso al servitore “migliore” (che verrà trattato in seguito), richiede
effettua una o più operazioni di modifica di oggetti del sistema, infine si disconnette.
Gestione dei nodi servitori: multicast protocol
Figura1: invio informazioni server
Una delle caratteristiche del sistema è che di volta in volta possano essere presenti servitori diversi,
situati in locazioni diverse. È quindi necessario un servizio che consenta di accedere a quelli
presenti, senza conoscerne l’indirizzo a priori.
A tale scopo si utilizza un servizio di multicast: tutti i servitori inviano le proprie informazioni ad
un gruppo di multicast prefissato ad intervalli di tempo costanti (“periodi di multicast”) e restano in
attesa di tutti i messaggi che vengono inviati a tale gruppo.
Questo ha la duplice funzione di rilevare i nuovi entranti, che vengono connessi ai nodi già presenti,
e di verificare che i servitori già presenti non siano caduti. A questo scopo ognuno di essi dispone di
contatori (“time to live”) che indicano da quanti periodi di multicast non si ricevono informazioni
da ciascuno degli altri servitori. Quando il contatore si azzera, il corrispondente nodo si considera
caduto e si rimuovono le informazioni di stato e le connessioni corrispondenti.
L’identificazione dei servitori avviene per indirizzo IP della macchina, quindi all’interno di una rete
non se ne possono avere due sulla stessa macchina. Ad esempio potrebbero essere tutti sulla stessa
rete locale o tutti connessi ad Internet, ma con indirizzi diversi.
Quando un cliente si connette al sistema riceve dal gruppo multicast le informazioni su un servitore
a cui si lega inizialmente. Dopodichè viene ridiretto al server momentaneamente più scarico, che gli
assegna un identificatore. In questo modo possono essere presenti contemporaneamente anche più
clienti allo stesso indirizzo.
Bilanciamento del carico
Figura2: bilanciamento del carico
Un’informazione rilevante per il bilanciamento del sistema è il numero di clienti attualmente
connessi ad un servitore. Infatti si ipotizza che un cliente si connetta al sistema soltanto quando è
effettivamente interessato ad effettuare operazioni di modifica risorse o di prenotazione.
Sotto le ipotesi di frequenti connessioni e disconnessioni e di numero di clienti connessi come
indicatore del carico, è possibile definire una politica di bilanciamento molto semplice, che
comunque può essere efficace: la connessione avviene sul servitore “migliore”, cioè in cui è attivo
il minore numero di clienti in quell’istante.
Rilevazione della caduta di un cliente
Quando un servitore si disconnette dal sistema, gli altri servitori possono accorgersene utilizzando il
protocollo di multicast e il “time to live”.
Per rilevare la caduta di un cliente a causa di un malfunzionamento, invece, è necessario un
ulteriore scambio di messaggi: infatti se il cliente uscisse senza preavviso dopo aver cominciato il
servizio, bloccherebbe indefinitamente il sistema, che continuerebbe ad attendere una risposta. Per
ovviare a questo problema è stato scelto un semplice protocollo di “ping” che periodicamente invia
richieste ai client i quali, se non rispondono, vengono eliminati dal sistema. È stato trascurato il
caso in cui il cliente rimanga connesso indefinitamente senza terminare la propria operazione. Se si
volesse evitare questa eventualità sarebbe necessario introdurre un ulteriore timeout.
Acquisizione degli oggetti
Figura3: acquisizione esclusiva di un oggetto
L’utente che desidera effettuare richieste, deve acquisire in modo mutuamente esclusivo gli oggetti
che intende modificare. Ad esempio mentre sta effettuando una prenotazione su un combinazione di
risorse, dovrà essere l’unico in quell’istante a poter accedervi per evitare conflitti difficili da
risolvere.
La soluzione adottata è quella di associare un token ad ogni oggetto per il quale si vuole garantire la
mutua esclusione. Questi possono avere granularità diversa, dalla quale dipendono le caratteristiche
delle politiche di prenotazione.
Si consideri l’esempio delle prenotazioni relative alla facoltà di Ingegneria, in cui si prenotano aule
un giorno ad un certo orario. Un sistema a granularità “grossa” potrebbe concedere al client la
possibilità di avere accesso esclusivo al giorno selezionato. Al contrario si potrebbe offrire la
possibilità di acquisire ogni singolo elemento del prodotto cartesiano tra gli insiemi giorni, aule e
ore.
Il passaggio del token viene deciso dai servitori attraverso messaggi di richiesta e consegna.
Ciascuno di essi dovrà inoltre gestire la mutua esclusione tra i propri clienti che richiedono la stessa
risorsa. Ogni token conterrà una coda con le informazioni relative ai servitori che hanno richieste
pendenti sul relativo oggetto, nell’ordine in cui tali richieste sono pervenute.
Creazione ed acquisizione di un token
Un token può essere acquisito tramite l’invio di un messaggio di richiesta da un servitore a tutti gli
altri. Se questo, dopo un’attesa, si accorge di essere l’unico servitore nel sistema (“starter”), può
procedere alla creazione del token, che verrà descritta in seguito. Se il token è presente in un altro
nodo, non appena sarà liberato, verrà consegnato con un opportuno messaggio. Infine nel caso in
cui, a causa di una caduta di un nodo o per la mancanza di un vero e proprio “starter” (cioè all’avvio
i primi due nodi si sono riconosciuti a vicenda simultaneamente e nessuno si è incaricato della
creazione), il token non è stato creato si passa alla creazione.
Per evitare che due servitori decidano contemporaneamente di creare un token, si è adottata una
politica a priorità statica. Dal momento che sono già noti gli indirizzi IP, che sono univoci, si è
scelto di utilizzare confronto letterale tra questi per determinare il più prioritario, che quindi sarà
l’unico a poter gestire la creazione su richiesta di un token.
Rilascio di un token
Figura 4: acquisizione e rilascio di un token
È necessario definire una politica di rilascio del token da parte del servitore che abbia effettuato una
acquisizione.
Per ora si ipotizzi che i nodi funzionino correttamente e che le richieste terminino in un tempo
ragionevole; successivamente verranno visti in dettaglio anche i casi di caduta di un nodo.
Innanzitutto si imposta un quanto di tempo per il possesso del token. Al termine di ogni singolo
servizio si controlla se il timeout sia scaduto e, in caso affermativo, il token viene rilasciato ed
inviato al primo servitore presente nella relativa coda. Qualora il proprietario attuale non abbia
ancora svolto tutti i servizi che richiedevano l’oggetto associato, prima di rilasciare il token si
rimette in coda. Se la coda è vuota, quando il servizio viene completato si imposta il token allo stato
di “libero”.
Prima di inviare un messaggio di token ad un servitore che si era messo in coda precedentemente, si
controlla che questo nel frattempo non si sia disconnesso o sia caduto.
Scambio di messaggi
La comunicazione tra i nodi del sistema avviene tramite messaggi di tipo diverso.
I clienti durante la loro vita seguono un iter prefissato di invio e ricezione messaggi; invece i
servitori tengono traccia soltanto dello stato dei token e decidono quali azioni eseguire a seconda
del tipo di messaggio.

clientConnectionRequestMessage: il cliente si connette, specificando un valore booleano
“best” che indica se si tratta della connessione al primo server che viene rilevato col
protocollo di multicast o se invece si tratta di un messaggio al “best server”.

hostUpdateMessage: risposta alla richiesta di connessione del cliente, contenente le
informazioni relative ad un host. Il primo servitore risponde con le informazioni del “best
server” a cui inviare i messaggi successivi; il “best server” invece restituisce le
informazioni relative al client stesso, complete di identificativo univoco all’interno del
server.

clientRequestMessage: messaggio di richiesta di modifica di un oggetto, contenente
l’identificativo dell’oggetto desiderato.

clientReponseMessage: replica alla richiesta di acquisire un oggetto. Se la risorsa non esiste
nel sistema comunica un errore, altrimenti indica al cliente il segnale di “ready” per
l’esecuzione della richiesta.

tokenRequestMessage: un servitore, al quale viene richiesto un oggetto di cui non possiede
il token, invia un messaggio di questo tipo ad ognuno degli altri servitori.

tokenGrantMessage: ogni servitore interrogato da un tokenRequestMessage deve
rispondere con questo messaggio indicando se possiede il token e, in tal caso, deve mettere
in coda al token il richiedente. Anche nel caso di token assente si deve comunque dare una
risposta, in modo che, una volta ricevuti tutti i messaggi, si capisca che il token non è
proprio presente e sia necessario crearlo.

tokenCreationMessage: è il messaggio che indica al servitore con priorità statica più alta di
creare un token perché non presente nel sistema.

objectMessage: messaggio contenente un oggetto di business logic.
Marche temporali
Le risorse contengono un valore che ne indica la versione. Un servitore sincronizza i propri oggetti
o in seguito ad una modifica da parte di un cliente o all’arrivo nel sistema di un altro servitore.
Nella fase di sincronizzazione ciascun servitore invia agli altri gli oggetti e ciascuno mantiene
quello con versione più recente.
I messaggi invece utilizzano il protocollo Vector Clock. Sull’attuale livello applicativo non sono
presenti funzionalità che utilizzino le marche temporali dei messaggi, ma si ritiene che possibili
estensioni del progetto potrebbero necessitarne. Realizzando vincoli non gestibili col meccanismo
dei token infatti si potrebbero utilizzare gli istanti di arrivo delle richieste per arbitrare possibili
collisioni tra prenotazioni.
Caduta di un servitore durante la fase di sincronizzazione delle risorse
Un momento particolarmente critico per la caduta di un servitore è la fase di sincronizzazione tra
risorse, cioè quando un cliente richiede la modifica di un oggetto e alcuni nodi vengono aggiornati
con la nuova versione, mentre altri no.
Il caso più rilevante è la caduta di servitore a cui il cliente è collegato. Il cliente non riceverà mai la
risposta alla sua richiesta quindi non potrà sapere se questa è stata accolta dal sistema o meno.
 Il nodo cade dopo aver cominciato a propagare la versione: il sistema, appena si accorge
della caduta, aggiornerà tutte le risorse sincronizzando tra loro i servitori rimasti.
 Non è ancora stata propagata la modifica: l’oggetto sarà ancora disponibile nella sua
versione precedente. Se però il servitore caduto si riattiverà prima di successive modifiche,
la fase di sincronizzazione sceglierà come aggiornata la sua versione. In caso di parità di
versioni ma oggetti diversi, il contenuto del nodo che si sta connettendo al sistema (si tratta
di un caso di modifica e caduta del servitore prima della propagazione) sarà ritenuto
obsoleto e verrà sostituito.
Il cliente comunque, riconnettendosi ad un altro servitore, potrà verificare se la modifica è
andata a buon fine.
Se cade un altro servitore, invece, questo verrà semplicemente aggiornato non appena tornerà
disponibile.
Implementazione con socket non bloccanti (java.nio)
I servitori si scambiano frequentemente messaggi tra loro, quindi devono disporre di una rete di
interconnessione molto fitta. Utilizzando socket standard sarebbe necessario mantenere una
connessione per ognuno degli altri servitori che rimanga in attesa di ricevere messaggi da quel
particolare mittente.
Nell’implementazione del progetto in linguaggio java, utilizzando le socket “standard” del package
java.net si sarebbe dovuto creare un thread diverso per ogni connessione. Questi thread
costituirebbero uno spreco di risorse in quanto per la maggior parte del tempo rimarrebbero bloccati
in attesa di ricevere un messaggio.
Una soluzione contenuta nel Framework Java per ovviare a questi problemi è contenuta nel package
java.nio.channels, che fornisce la possibilità di utilizzare canali che consentono di ricevere
messaggi da mittenti diversi su uno stesso thread.
Figura 5: Schema selettore java.nio
Si faccia riferimento alla figura 5, considerando che “server” e “client” si riferiscono alla socket, ma
in entrambi i casi ci si riferisce ai servitori del sistema. Sul lato server della socket viene registrato
un selettore, al quale faranno riferimento tutte le socket lato client. Ciascuna di esse eseguirà
richieste di accettazione e di lettura, che verranno messi in coda in modo trasparente dal selettore e
gestiti poi lato server attraverso l’operazione di select.
Questo consente una lettura non bloccante, evitando di aprire un thread per ogni connessione.
Applicazione attuale e sviluppi futuri
Nell’ implementazione attuale è possibile soltanto modificare il contenuto degli oggetti, simulando
il comportamento di un utente che intende effettuare prenotazioni o modificare risorse necessarie
alla prenotazione.
Questo progetto intende soltanto fornire le funzionalità per consentire poi di sviluppare
un’applicazione completa e personalizzata per la prenotazione in ambiente distribuito, senza doversi
occupare dei problemi di comunicazione.
Quello della prenotazione comunque è stato soltanto il “caso di studio” da cui il progetto è partito e
su cui sono state rivolte le scelte delle diverse politiche adottate. Probabilmente saranno possibili
anche ulteriori utilizzi di questa infrastruttura al di fuori di questo ambito, sempre riguardanti
comunque l’accesso a risorse distribuite.