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.