Sistema Sistema di di supporto supporto per per l'allocazione l'allocazione di di processi processi in in ambito ambito distribuito distribuito Simone Colombo Matricola: 0000231933 Verso la parallelizzazione Attualmente per incrementare le prestazioni e ridurre i tempi di esecuzione si va sempre più verso una parallelizzazione dell'esecuzione, come mostrano i recenti sviluppi nell'ambito dei processori, con l'introduzione dei dual core e dei quad core. Da qui nasce l'idea di utilizzare la parallelizzazione anche sfruttando più macchine assieme e di progettare quindi una applicazioni di supporto che la fornisca in automatico, in modo tale che il programmatore possa occuparsi solo della realizzazione del suo applicativo, come se dovesse girare in locale. Sarà poi sufficiente eseguire l'applicazione di supporto su tutte le macchine che si desidera partecipino alla rete e successivamente eseguire la propia. Requisiti I requisiti che una applicazione del genere deve rispettare sono: gestire l'ingresso e l'uscita di una macchina dalla rete, anche dinamicamente durante l'esecuzione gestire la distribuzione dei processi tra le varie macchine, bilanciando opportunamente il carico gestire eventuali oggetti condivisi utilizzati dai vari processi comunicare il risultato all'applicazione madre essere tollerante ai guasti, almeno nell'ipotesi di guasto singolo Architettura generale Il sistema è stato diviso in due parti: una che si occupa della gestione della rete una che si occupa della parte di deployment e argomenti correlati La prima parte, implementata dalla classe NetManager, ha nello specifico i seguenti compiti: Ingresso dei nodi nella rete e loro uscita, mantenendo sempre la stessa in uno stato integro gestione della riconnessione di un nodo nel caso in cui questo sia caduto gestione della comunicazione tramite messaggi tra i vari nodi (via TCP) La seconda parte, implementata dalla classe Server, si occupa invece di: gestione degli oggetti condivisi e delle code di prenotazione gestione dei processi: allocazione, riallocazione, rimozione La rete 1/2 L'ingresso di un nodo nella rete (realizzata ad anello) avviene nel seguente modo: 1 - Invio di messaggio su UDP a tutti i nodi della rete, su porta predefinita 2 - Ogni nodo che riceve il messaggio risponde (sempre su quella porta UDP) comunicando il suo IP e quello dei suoi vicini 3 - Il nodo entrante sceglie come primo vicino il primo nodo a rispondergli, come secondo il primo a rispondere fra i due vicini di quest'ultimo; se non riceve risposte si considera l'unico nodo della rete 4 - Vengono create le connessioni TCP tra il nodo entrante e i due vicini, e chiuse quella precedente tra i due vicini La rete 2/2 5 - Il nodo entrante manda un messaggio nella rete nel quale comunica quali processi avviati da altri su di lui ha in sospeso (in modo da sapere se riavviarli o meno) e quali lui si aspetta siano in esecuzione su altri nodi (per procedere eventualmente a una riallocazione) 6 - Gli altri nodi che ricevono questo messaggio aggiornano man mano le informazioni, in modo che quello entrante possa comportarsi di conseguenza Quando un nodo deve uscire dalla rete prima manda un messaggio indicando quali processi assegnati a lui da altri nodi ha in esecuzione su di se, e quali lui ha assegnato ad altri nodi; in questo modo i membri della rete sanno quali processi sospendere. Provvede poi a informare il nodo precedente e successivo che hanno un nuovo vicino, richiudendo così l'anello. La comunicazione nella rete La comunicazione avviene tramite messaggi fatti circolare nell'anello; ogni messaggio (implementato dalla classe Message) ha i seguenti campi: un identificatore del tipo di messaggio (EnterNetwork, ReurceAsking, ecc) IP del sender, ovvero del nodo che ha mandato in giro il messaggio una GUID usata come identificativo univoco 10 campi in cui inserire le informazioni Ogni nodo ha un buffer in cui tiene i messaggi inviati dall'ultimo ping effettuato sul vicino a quel momento, in modo da poterli rimandare, in caso di caduta del nodo successivo, una volta ripristinata la rete. Tiene inoltre in memoria i messaggi arrivati negli ultimi due intervalli di rilevamento, in modo da identificare eventuali ricezioni doppie. La distribuzione del carico I processi che si vogliono far eseguire dovranno ereditare da una specifica classe, che implementa i metodi base, oppure implementare l'interfaccia apposita. Il protocollo adottato per la distribuzione e l'allocazione è il seguente: 1 - L'applicazione client ottiene un riferimento al server, che prima aveva registrato il suo servizio. 2 - L'applicazione invoca apposita procedura sul server, passando il processo, e i coefficienti di uso di CPU e RAM. 3 - Viene mandato un messaggio nell'anello con i coefficienti, ogni nodo calcola il proprio punteggio e se superiore al migliore attuale, lo aggiorna, indicando anche il proprio IP. 4 - Quando il messaggio torna al nodo sender, questo provvede a passare il processo al nodo prescelto (segnandosi a chi lo ha consegnato). 5 - Il nodo prescelto si segna da chi lo ha ricevuto e lo avvia. La consegna dei risultati Quando un processo termina, provvede lui stesso, dopo aver richiesto un riferimento all'oggetto remoto server, a informarlo della sua terminazione. Quest'ultimo provvederà a informare il nodo che aveva assegnato a lui il processo, in caso di caduta di quest'ultimo terrà memorizzato il thread terminato per un certo tempo, per poi cancellarlo. Sarà l'applicazione che a polling dovrà chiedere al nodo su cui è in esecuzione se ci sono suoi thread terminati, indicando il relativo identificativo. Gli oggetti condivisi Per permettere interazione tra i thread, è stata data la possibilità all'applicazione di fornire oggetti condivisi fra i vari processi, che potranno utilizzarli in mutua esclusione. Una volta che il client ha fornito tali oggetti, la procedura per utilizzarli è la seguente: 1 - Il processo fa richiesta di utilizzo al nodo su cui sta eseguendo. 2 - La richiesta viene propagata al nodo che aveva richiesto l'esecuzione. 3 - Se l'oggetto è libero, viene consegnato al nodo richiedente, altrimenti la richiesta viene messa in coda. 4 - L'oggetto viene usato. 5 - Il thread chiede al nodo di restituire l'oggetto, la consegna viene propagata al nodo “main”. 6 - Se vi sono richieste in coda, l'oggetto viene consegnato, altrimenti viene segnato come disponibile. Rilevazione dei guasti Il sistema per offrire una minima resistenza ai guasti, dovrà essere in grado di identificare e rispondere in modo opportuno alla caduta di un nodo della rete. Per identificare tali cadute, ogni 20 secondi un nodo controlla che i due vicini siano ancora presenti, se non lo sono allora capisce di essere caduto lui, altrimenti, se è caduto il suo successore, avvia la procedura di ripristino. In caso di caduta, il nodo provvede a sospendere tutti i processi assegnati da altri a lui e a segnare l'istante di interruzione, sia per questi, sia per quelli assegnati da lui ad altri. Prova poi a riconnettersi 3 volte in modo automatico alla rete, a distanza di tempo, fallite le quali si potrà nuovamente accedere alla rete manualmente. Reazione ai guasti Se ci si accorge che il successore è caduto, si manda un messaggio nell'anello in senso contrario, indicando l'indirizzo del nodo caduto e il propio. Gli altri nodi propagheranno il messaggio, sospendendo i processi relativi al nodo caduto (e settandone il FallIstant), fino a che questo non giungerà al nodo successivo a quello guasto. Questo provvederà a richiudere l'anello e a mandare un messaggio per informare del termine della procedura di recovery. Comunicazione con nodi caduti Può capitare che un nodo cerchi di comunicare con uno caduto, il cui guasto non è ancora stato rilevato. Se la comunicazione avviene tramite messaggio tutto sembrerà andare correttamente, anche se ovviamente il messaggio non sarà ricevuto. Per ovviare a questo è stata implementata una politica Exactly Once, come discusso in precedenza. Se invece si comunica tramite invocazione di procedura remota, ci si accorgerà subito del problema; in tale caso si memorizzerà l'azione che si voleva effettuare per poi farla una volta che il nodo torna in rete. Se l'azione era l'assegnazione di un processo a quel nodo, viene ripetuto il protocollo di scelta. Gestione dei processi Come già discusso in precedenza, se il nodo che ha allocato un processo o quello su cui è stato allocato cadono, entrambi segneranno quel momento come FallIstant relativo a quel nodo. E' presente un demone attivo che periodicamente controlla quanto tempo è passato dal FallIstant, se è superiore al timeout rimuove il thread e, nel caso sa il demone in esecuzione sul nodo che ha allocato il processo, avvia la procedura di riallocazione. Quando un thread viene rimosso anche gli oggetti condivisi usati da lui vengono liberati, vengono cancellate sue eventuali richieste in coda, e se vi sono comunicazioni pendenti tra i due nodi inerenti quel processo, vengono rimosse anche loro. Test 1/2 I test sono stati effettuati su un numero di pc variabile da 1 a 4, con caratteristiche generalmente non troppo differenti. I tempi riportati sono quelli medi, ottenuto con l'istanza “fl417”, effettuando 8 esecuzioni contemporanee. Si noti come il comportamento, sia per quando riguarda lo speed up, sia per l'efficienza sia quello che è lecito aspettarsi. Test 2/2 Il grafico mostra come il rapporto tra numero di processori e tempo sia approssimabile con una iperbole e quindi con una proporzionalità inversa. Il grafico è differente da quello ideale a causa della non perfetta allocazione del carico, dei tempi di comunicazione (anche se trascurabili) e della presenza di parti che non possono essere parallelizzate (accesso a risorse comuni). Conclusioni e sviluppi futuri L'applicativo, come evidenziato dai test, risponde a tutti i requisiti richiesti, permette di collegare un numero arbitrario di macchine in rete e non risulta essere particolarmente pesante. I tempi vengono ridotti significativamente, anche se vanno dati in modo corretto i due coefficienti che vengono usati per decidere l'allocazione, e questo spesso non è facile. Nel caso si volesse procedere nello sviluppo, i punti principali su cui potrebbe vertere il miglioramento sono: Revisione della funzione che calcola il punteggio di ciascun nodo, considerando anche altri parametri oltre a CPU e RAM. Cambiamento del protocollo di join dei nodi, per permettere l'ingresso di più macchine assieme Estensione del funzionamento oltre la rete locale.