Quando 2 nodi si scambiano messaggi, si devono sincronizzare. Il ricevente deve avere un timestamp che sia maggiore o uguale di quello che riceve. In caso sia rimasto indietro, fa un balzo in avanti e si sincronizza.
Deadlock distribuiti
Non paghi di dover gestire il deadlock in mille modi diversi, ecco che dobbiamo gestirlo anche in sistemi distribuiti.
Al solito, quando ho un nodo che attende un altro che attende un altro che attende il primo, ho un deadlock.
Sistema semplice: metto un time-out. Se ci mette troppo, qualcosa va storto. È in effetti il metodo più usato perché più semplice da implementare.
Altrimenti, posso tentare di rilevare e risolvere i deadlock, attraverso un protocollo asincrono e distribuito, realizzato tramite RPC. C'è sempre il mio grafo di cui devo rilevare la ciclicità, ma in questo caso i nodi sono distribuiti, e devono quindi comunicare tra loro per realizzare un grafo.
Per evitare di inviare una pletora di messaggi, invio solamente le condizioni di attesa verso il nodo da cui attendo.
Una sequenza di attesa ha più o meno questa faccia:
Ein -> t1,1 -> t2,1 -> Eout
Ein = evento in ingresso
t1,1 = transazione 1 sul nodo 1
t2,1 = transazione 2 sul nodo 1
Eout = evento in uscita
I nodi inviano verso Eout, cioè il nodo che attendono, solo una condizione di attesa in cui una transazione vecchia sta attendendo una transazione nuova: in termini di id, devo avere che ti > tj.
Il nodo che riceve la sequenza la deve combinare con la sua, e poi inviarla al suo Eout. Nella combinazione, devo collassare tutte le transazioni e far rimanere solamente i nodi, così da poter rilevare il ciclo.
Urge spiegazione migliore e più completa...
Protocolli per il commit distribuito
Questa è un'altra gatta da pelare, perché tutti i nodi devono eseguire insieme il commit.
Il protocollo più diffuso è il two-phase commit, che NON c'entra con il 2PL, anche se ha lo stesso nome.
In questo protocollo, la decisione di fare commit o abort è presa da una terza parte, il cosiddetto transaction manager (TM). Il TM deve gestire i resource manager (RM), ovvero i nodi coinvolti nella transazione distribuita.
Il TM deve mantenere in proprio un log, con questi tre tipi di record:
- prepare = si sta preparando al commit, e salva la lista dei RM coinvolti
- global commit / global abort
- complete = è terminato il 2PC
RM parimenti mantiene un log:
- ready = l'RM ha tutti i lock che gli servono, ed è pronto
- begin, insert, delete, update = relativi alla transazione in atto
Se un RM si dice ready, in quel momento perde la sua autonomia. Deve attendere che TM gli dica che cosa fare. Entra in una fase di incertezza in cui deve attendere che gli altri RM coinvolti comunichino con il TM, e che quindi il TM possa prendere decisioni. Non può fare proprio niente, in questo momento.
Se invece, per qualche motivo, il RM stabilisce di essere non ready, invia comunicazione al TM, e abortisce per conto suo. Infatti, per far abortire una transazione distribuita è sufficiente che 1 RM dica NO. Se è lui stesso a dire NO, ha la certezza che la transazione abortirà, e quindi non attende istruzioni dal TM.
1a fase:
- il TM manda il PREPARE a tutti i RM, e attende, impostando un timeout
- ogni RM dice READY o NON-READY, e nel secondo caso fa tutto da solo, come abbiamo appena visto
- il TM raccoglie le risposte da tutti, e decide per un GLOBAL COMMIT o GLOBAL ABORT
2a fase: la decisione del TM deve essere comunicata a tutti
- il RM scrive nel suo log e ivnia un ACK al TM
- RM esegue la sua transazione
- il TM raccoglie gli ACK da tutti. Se un ACK tarda, attende con un nuovo timeout e rimanda la decisione
Siccome stiamo parlando di nodi sulla rete, possono accadere diversi spiacevoli accidenti: possono andare down sia i RM che il TM, perdere messaggi, o addirittura cadere la rete intera.
RM down
Un RM cade, e si presume che prima o poi si risveglierà (se non ci pensa da solo, ci pensa l'admin).
Deve guardare il suo log. Se ha un ready, chiede al TM che cosa fare. Tutte le transazioni dubbie vengono messe in READY, e viene chiesto al TM che cosa fare.
TM down
Anche qui, il TM si riavvia e guarda il log.
Se vede un PREPARE, può:
- mandare un GLOBAL ABORT a tutti i RM coinvolti (come dicevo sopra, nel record PREPARE è salvata la lista di tutti i RM coinvolti), e si prepara alla 2a fase
- ripetere la 1a fase
Se vede un GLOBAL COMMIT / ABORT, ripete la 2a fase.
Se vede un COMPLETE, non c'è problema, vuol dire che è morto dopo aver messo a posto tutti.
Perdita di messaggi
Se scade il timeout della fase 1, il TM non ha modo di determinare se i messaggi persi siano stati un PREPARE o un READY. Nel dubbio, invia un GLOBAL ABORT e si riparte.
Se invece viene perso un GLOBAL oppure un ACK, è il timeout della fase 2 che scade, e quindi viene ripetuta solo lei.
Cade la rete
Quando cade la rete, vuol dire che essa si partiziona in parti che non comunicano. Se TM e i suoi RM stanno nella stessa partizione, ok, vanno avanti lo stesso. Altrimenti, si abortisce tutto.
Ottimizzazioni
2PL è molto pesante, perché si scambia un mucchio di messaggi. Si può cercare di risparmiare un po' qua e là.
Abort presunto: il TM riceve richieste da RM incerti, e non ha nessuna informazione su di essi: invia un GLOBAL ABORT a tutti (che cosa sono i RM incerti?)
Read only: se ho un RM che riceve istruzioni read-only, lo ignoro nella 2a fase, che tanto non può dare contributo significativo (non interessa il commit di una lettura).
Varianti
C'è il lock distribuito a 4 fasi, in cui c'è un TM di backup. Il TM "normale" comunica tutte le sue decisioni PRIMA al TM di backup, così se va giù c'è il backup che comunque ne sa qualcosa. Ma è pesante...
Poi c'è quello a 3 fasi in cui un nodo fa il sostituto del TM, se quest'ultimo cade. Il problema è che la finestra di incertezza si allunga. Il problema con sto sistema è che se la rete si partiziona, ed ogni partizione elegge il suo TM, è l'anarchia. Ecco perché è poco usato.