Parte IV — Sistemi distribuiti · Capitolo 16

Algoritmi distribuiti

~50 min di lettura17 widget interattivi

In questo capitolo

  1. Sfide degli algoritmi distribuiti
  2. Mutua esclusione distribuita: il problema
  3. Algoritmo centralizzato con coordinatore
  4. Pseudocodice: processo client e coordinatore
  5. Simulatore: algoritmo centralizzato
  6. Algoritmo decentralizzato: Ricart-Agrawala
  7. Pseudocodice Ricart-Agrawala
  8. Ricart-Agrawala in azione: due simulatori
  9. Leader election: Chang-Roberts
  10. Esplora: macchina a stati di Chang-Roberts
  11. Message ordering: ordinamento causale
  12. Algoritmo con matrice di clock
  13. Ordinamento totale e sincronizzatori
  14. Global state: consistent cut e snapshot
  15. Algoritmo di Chandy-Lamport
  16. Esplora: gli stati di Chandy-Lamport
  17. Consensus: definizione e proprieta
  18. FLP e algoritmo base con round
  19. Byzantine fault e BGA
  20. Paxos, Raft e replicated state machines
  21. Verifica le tue conoscenze

1. Sfide degli algoritmi distribuiti

Progettare algoritmi per sistemi distribuiti significa operare senza clock condiviso, senza memoria condivisa e in presenza di guasti. Questi tre vincoli cambiano radicalmente l'approccio: non possiamo piu usare semafori o mutex come si fa nel caso concorrente classico, perche non esiste una risorsa centrale di sincronizzazione.

Idea chiave

L'unico meccanismo di coordinazione disponibile in un sistema distribuito asincrono e lo scambio di messaggi. Ogni algoritmo distribuito si riduce a decidere cosa comunicare, quando comunicarlo e come interpretare cio che si riceve.

La maggior parte degli algoritmi discussi in questo capitolo assume un sistema asincrono senza guasti (tempo di consegna finito ma non noto a priori). Solo per il consenso si introduce l'ipotesi di sincronia con timeout per gestire i fallimenti.

SfidaConseguenzaSoluzione negli algoritmi
Nessun clock condivisoImpossibile ordinare eventi con tempo fisicoClock logici e relazione happened-before
Nessuna memoria condivisaStato globale non osservabile direttamenteSnapshot algoritmi come Chandy-Lamport
Nessun failure detection accuratoLento e guasto sono indistinguibiliTimeout e modelli a sincronia

Piu in dettaglio, i sistemi distribuiti presentano tre assenze fondamentali che li rendono radicalmente diversi dai sistemi concorrenti tradizionali (basati su memoria condivisa):

Non esiste un limite superiore noto al tempo di consegna dei messaggi. Il ritardo puo essere arbitrario ma finito. La maggior parte degli algoritmi distribuiti assume reti asincrone in assenza di guasti. Il nondeterminismo e massimo, la concorrenza e massima. Ideali per ambienti non critici dove la probabilita di guasto e bassa.

Esiste un limite superiore noto al tempo di consegna dei messaggi e alla durata delle azioni dei processi. Si possono usare timeout per rilevare guasti. La progettazione degli algoritmi e piu semplice. Necessari quando si devono gestire guasti, ad esempio negli algoritmi di consensus o nei protocolli bizantini.

Categorie principali di algoritmi distribuiti

Il professore elenca le cinque macro-categorie che gli algoritmi distribuiti devono affrontare, tutte basate sugli orologi logici e sulla relazione happened before gia introdotta nel modulo precedente:

  1. Ordinamento degli eventi (parziale): orologi logici di Lamport, vector clocks. Sono il fondamento usato da tutti gli altri algoritmi
  2. Osservazione dello stato: global snapshot per catturare lo stato globale del sistema da un singolo processo
  3. Coordinamento e accordo: mutua esclusione distribuita, elezione del leader, multicast communication con ordinamento, consensus
  4. Transazioni distribuite e controllo della concorrenza
  5. Replicazione e servizi fault-tolerant
Idea chiave

Tutti questi algoritmi si basano sugli orologi logici e sulla relazione happened before (→) per definire un ordinamento tra eventi correlati. Il concetto di tempo logico di Lamport (1978) e il fondamento su cui poggia l'intera architettura degli algoritmi distribuiti.

Modelli di computazione distribuita

Il professore richiama i tre modelli principali per descrivere il comportamento di un programma distribuito:

2. Mutua esclusione distribuita: il problema

Il problema e analogo a quello della sezione critica in ambito concorrente: un insieme di processi distribuiti ha nel proprio codice una porzione di programma che puo essere eseguita da un solo processo alla volta. Nel caso concorrente useremmo un semaforo o un mutex. Nel caso distribuito dobbiamo progettare un algoritmo basato esclusivamente sullo scambio di messaggi.

Per l'esame

Le tre proprieta della sezione critica distribuita sono: safety (due processi non possono mai essere contemporaneamente in CS), liveness (ogni richiesta viene prima o poi concessa) e fairness (le richieste vengono servite secondo l'ordine della relazione happened-before).

Il professore sottolinea due approcci principali. Il primo e centralizzato: si assume un coordinatore che gestisce i permessi, soluzione piu semplice ma con un single point of failure. Il secondo e decentralizzato (Ricart-Agrawala), dove non esiste un coordinatore unico e tutti i processi collaborano in modo simmetrico.

Un processo coordinatore gestisce un token. I processi inviano richieste al coordinatore, che le accoda e assegna il token rispettando l'ordinamento causale. Piu semplice da implementare, ma il coordinatore e un collo di bottiglia e un punto unico di fallimento.

Ogni processo che vuole entrare in CS invia una richiesta timestampata a tutti gli altri processi. Un processo concede il permesso (OK) se non e interessato alla CS o se la propria richiesta ha un timestamp maggiore. Simmetrico, senza coordinatori, ma richiede O(N-1) messaggi per richiesta.

3. Algoritmo centralizzato con coordinatore

Nell'algoritmo centralizzato esiste un processo coordinatore P0 che detiene il token. I processi clients Pi (con i = 1..N) cooperano trasportando il proprio vector clock v in ogni messaggio (piggybacking), cosi che il coordinatore possa ricostruire l'ordinamento causale delle richieste.

Il punto fondamentale e la fairness: se due richieste arrivano, la richiesta che e accaduta prima nella relazione happened-before deve essere servita prima, anche se arriva al coordinatore dopo. Per fare questo, ogni processo allega il proprio vector clock v alla richiesta: v[j] rappresenta il numero di richieste fatte da Pj che precedono causalmente lo stato corrente.

Idea chiave

Quando un processo invia una richiesta al coordinatore, include non solo la propria richiesta ma anche la conoscenza che ha delle richieste fatte dagli altri. Cosi il coordinatore puo ritardare una richiesta se sa che ce n'e un'altra in sospeso che la precede causalmente.

Strutture dati del coordinatore

Il coordinatore mantiene due strutture:

Una richiesta w in reqList e eligible se per ogni j, w.v[j] == reqDone[j] (e per j == w.p, w.v[j] == reqDone[j] + 1). Cio significa: non ci sono richieste accadute prima di w che non sono state ancora soddisfatte.

Vediamo un esempio concreto. P1 invia una richiesta con vettore [1,0]. P2 invia una richiesta con vettore [1,1] (perche sa che P1 ha gia fatto richiesta). Il coordinatore, quando riceve [1,0] da P1, nota che non ci sono richieste pendenti da altri, quindi puo servire subito P1. Quando riceve [1,1] da P2, vede che P2 stesso dichiara che P1 ha una richiesta in sospeso — ma il coordinatore ha gia servito P1, quindi il suo reqDone[1] ora vale 1, e la richiesta di P2 diventa eligible.

sequenceDiagram
    participant P1
    participant P0 as Coordinatore
    participant P2

    P1->>P0: req(v=[1,0])
    P0->>P0: reqDone=[0,0] serve P1
    P0->>P1: token
    P1->>P0: release(token)
    P2->>P0: req(v=[1,1])
    P0->>P0: reqDone=[1,0] w.v=[1,1] eligible
    P0->>P2: token

In termini piu formali: se s → t (lo stato s precede causalmente lo stato t), la richiesta fatta in s deve essere servita prima di quella fatta in t, anche se la richiesta t arriva al coordinatore prima. Sul lato coordinatore, una richiesta w e eleggibile se w.v ≤ reqDone: per ogni j ≠ w.p, w.v[j] == reqDone[j] (nessuna richiesta pendente da altri), e per j = w.p, w.v[j] == reqDone[j] + 1 (esattamente una richiesta pendente dal richiedente).

Per l'esame

La condizione di eleggibilità è il cuore dell'algoritmo: garantisce che le richieste siano servite secondo l'ordine causale, non secondo l'ordine di arrivo al coordinatore. È un esempio perfetto di come gli orologi vettoriali possano essere usati per implementare fairness in un sistema distribuito.

Idea chiave

I processi piggybackano il proprio vector clock su tutti i messaggi in uscita. Quando un processo riceve un messaggio, aggiorna il proprio vector clock con il component-wise max. Cosi il vettore nella richiesta contiene la conoscenza causale del richiedente.

Per l'esame

L'algoritmo centralizzato richiede 2 messaggi per richiesta (richiesta + token di risposta) piu i messaggi per propagare i vector clock. Il coordinatore e un single point of failure e puo diventare un collo di bottiglia.

Il flusso completo richiesta–token–rilascio, con la chiamata a checkReq sia alla ricezione di una richiesta sia al ritorno del token, e riassunto nel diagramma seguente.

sequenceDiagram
    participant P1 as Pi (client)
    participant C as P0 (coordinator)
    participant P2 as Pj (client)

    Note over P1: v[i]++, send(req, v)
    P1->>C: (req, v = [1,0,0,...])
    Note over C: append reqList
if haveToken: checkReq() Note over C: w = first(eligible)
send token, incr reqDone C->>P1: token Note over P1: inCS = true Note over P1: [sezione critica] P1->>C: token (release) Note over P1: inCS = false Note over C: haveToken = true
checkReq() again C->>P2: token (prossimo eligible)

Il processo client Pi, quando riceve un messaggio da un altro processo, aggiorna il proprio vettore v con il component-wise max del suo vecchio valore e del vettore ricevuto. Questo garantisce che la conoscenza causale si propaghi correttamente.

4. Pseudocodice: processo client e coordinatore

Il codice seguente mostra la struttura dell'algoritmo: ogni client tiene un vector clock v, incrementa la propria componente per ogni richiesta, e invia il vettore al coordinatore. Il coordinatore, ricevuta la richiesta, verifica l'eligibility prima di assegnare il token.

5. Simulatore: algoritmo centralizzato

Aziona il simulatore per vedere come il coordinatore gestisce le richieste concorrenti di due processi. Clicca "Passo P1" o "Passo P2" per far avanzare ogni processo, oppure "Reset" per ricominciare. Il coordinatore decide automaticamente se assegnare il token.

6. Algoritmo decentralizzato: Ricart-Agrawala

Presentato da Glenn Ricart e Ashok Agrawala nel 1981, questo algoritmo e completamente decentralizzato e non richiede un coordinatore. L'idea e semplice: ogni processo che vuole entrare in sezione critica invia un messaggio con timestamp a tutti gli altri processi. Un processo ricevente risponde con un OK se non e interessato a entrare in CS o se la propria richiesta ha un timestamp maggiore (cioe e avvenuta dopo secondo l'ordine logico). Altrimenti, accoda la richiesta in attesa.

Un processo ottiene il permesso di entrare in CS quando ha ricevuto OK da tutti gli N-1 processi. Quando esce dalla CS, invia OK a tutti i processi nella propria coda di attesa.

Il numero di messaggi per richiesta e 2(N-1): (N-1) richieste inviate + (N-1) OK ricevuti. L'algoritmo funziona anche con canali non FIFO.

Nota del redattore

Ricart-Agrawala e considerato ottimale per il numero di messaggi: ogni entrata in CS richiede esattamente 2(N-1) messaggi, e nessun messaggio superfluo viene generato.

flowchart TD
    A[Pi vuole entrare in CS] --> B[myts = logical_clock]
    B --> C[Invia req(myts) a tutti]
    C --> D{Attesa OK da tutti}
    D -->|Riceve OK da Pj| E[numOK++]
    E --> F{numOK == N-1?}
    F -->|Si| G[Entra in CS]
    F -->|No| D
    G --> H[myts = infinity]
    H --> I[Invia OK a pending queue]
    I --> J[Svuota pending queue]

Protocollo

  1. Richiesta: Pi imposta myts = orologio logico corrente, invia req(myts) a tutti gli altri processi, azzera numOK.
  2. Ricezione richiesta (Pj): se mytsmittente < mytslocale, invia OK immediatamente; altrimenti accoda il mittente in pendingQ.
  3. Ricezione OK: incrementa numOK. Quando numOK == N-1, Pi entra in CS.
  4. Rilascio: Pi imposta myts = infinito, invia OK a tutti i processi in pendingQ, svuota la coda.

È un esempio di come un timestamp logico possa fungere da «biglietto numerato» per determinare l'ordine di accesso a una risorsa condivisa senza un'autorità centrale.

Idea chiave

L'algoritmo di Ricart-Agrawala dimostra che la mutua esclusione distribuita può essere implementata senza un coordinatore centrale, a costo di O(N) messaggi per accesso. È un compromesso classico tra centralizzazione (un singolo punto di guasto, ma pochi messaggi) e decentralizzazione (nessun punto di guasto singolo, ma più messaggi).

7. Pseudocodice Ricart-Agrawala

Ogni processo Pi mantiene una coda di richieste pendenti (pendingQ), un timestamp myts (inizializzato a infinito, segnalando che non si e interessati alla CS), e un contatore numOK.

8. Ricart-Agrawala in azione: due simulatori

Il primo simulatore ripercorre l'algoritmo su 3 processi, evidenziando le condizioni di risposta:

  1. Per richiedere la CS, un processo Pi imposta myts al proprio clock logico e invia un messaggio (req, myts) a tutti gli altri processi.
  2. Alla ricezione di una richiesta da Pj, un processo Pi risponde con OK se:
    • non e interessato a entrare in CS (myts == infinity), oppure
    • la propria richiesta ha timestamp maggiore (quindi e successiva).
    Altrimenti, accoda Pj nella propria pendingQ.
  3. Un processo ottiene la CS quando ha ricevuto OK da tutti gli altri processi.
  4. Nel rilasciare la CS, invia OK a tutti i processi in pendingQ e resetta myts a infinity.
Premi "Passo successivo" per eseguire Ricart-Agrawala con 3 processi passo-passo.

La fairness e assicurata dalla relazione d'ordine sui timestamp (a parita di timestamp, vince il PID piu basso).

Il secondo simulatore, con avanzamento manuale dei singoli processi, mostra il caso in cui due processi competono per la sezione critica: chi ha il timestamp piu alto accoda l'altro in pendingQ e concede l'OK solo al rilascio.

9. Leader election: Chang-Roberts

Il problema della leader election (elezione del leader) sorge naturalmente quando si usa un approccio centralizzato: se il coordinatore fallisce, chi diventa il nuovo coordinatore? L'algoritmo di Chang-Roberts risolve questo problema superimponendo una topologia logica ad anello sulla rete fisica e usando un criterio semplice: viene eletto il processo con il PID piu alto.

Il funzionamento e il seguente:

Il numero di messaggi nel caso peggiore e 2N-1 election + N leader.

Idea chiave

L'anello logico non esiste nella rete fisica: e una sovrapposizione (superimposing) creata dall'algoritmo per organizzare il flusso dei messaggi. Questo e un pattern comune negli algoritmi distribuiti.

L'algoritmo di Chang-Roberts (1979) è elegante nella sua semplicità. Alla ricezione di (leader, j), un processo registra j come leader e inoltra il messaggio (se non è il leader stesso). È ampiamente usato per scegliere coordinatori in algoritmi centralizzati, sfruttando il fatto che la topologia ad anello è facile da mantenere e il protocollo è semplice da dimostrare corretto.

Un dettaglio importante riguarda i messaggi con ID minore del proprio: se il processo e gia awake, il messaggio viene semplicemente inghiottito (scartato), senza avviare una nuova candidatura.

flowchart LR
    subgraph Anello PID
        P1[P1] --> P2[P2]
        P2 --> P3[P3]
        P3 --> P4[P4]
        P4 --> P1
    end
    P1 -.->|"election(1)"| P2
    P2 -.->|"election(2)"| P3
    P3 -.->|"election(3)"| P4
    P4 -.->|"election(4) → leader(4)"| P1

Un esempio con PID non ordinati sull'anello: P2 avvia l'elezione, il suo messaggio viene rimpiazzato da quello di P7 (l'ID massimo), che fa il giro completo e torna al mittente.

graph LR
    A[P2
id=2] -->|election,2| B[P7
id=7] B -->|election,7| C[P3
id=3] C -->|election,7| D[P1
id=1] D -->|election,7| E[P5
id=5] E -->|election,7| F[P4
id=4] F -->|election,7| A A -->|leader,7| B B -->|leader,7| C C -->|leader,7| D D -->|leader,7| E E -->|leader,7| F F -->|leader,7| A
Idea chiave

L'algoritmo usa l'anello logico solo per il controllo dell'elezione, non per il flusso dei dati applicativi. E un esempio classico di superimposing: sovrapporre un protocollo di coordinamento a una topologia fisica arbitraria. Questo pattern si ritrova in molti algoritmi distribuiti.

10. Esplora: macchina a stati di Chang-Roberts

Esplora i possibili stati di un processo durante l'esecuzione dell'algoritmo di Chang-Roberts. Clicca sugli stati per vedere le transizioni possibili e la descrizione di ciascuno stato.

Lo pseudocodice dell'algoritmo e mostrato qui sotto. Ogni processo mantiene awake (se ha gia partecipato all'elezione) e leaderId (una volta eletto).

11. Message ordering: ordinamento causale

Nella computazione asincrona classica non ci sono restrizioni sull'ordine di ricezione dei messaggi. L'ordinamento causale e una restrizione utile: se send(m1) → send(m2) (l'invio di m1 precede causalmente l'invio di m2), allora vogliamo che rec(m1) → rec(m2) (la ricezione di m1 deve precedere la ricezione di m2). Nessun overtaking causale.

Per l'esame

L'ordinamento FIFO e piu debole: garantisce solo che se lo stesso processo invia m1 prima di m2, allora m1 viene ricevuto prima di m2. L'ordinamento causale e piu forte: considera anche la causalita che attraversa processi diversi.

Immaginiamo tre processi P1, P2, P3. P1 invia un messaggio a P3, poi P1 invia un messaggio a P2. P2, dopo aver ricevuto il messaggio da P1, invia un messaggio a P3. Senza ordinamento causale, P3 potrebbe ricevere prima il messaggio di P2 e poi quello di P1, violando la causalita: l'invio di P1 a P3 e avvenuto prima dell'invio di P2 a P3, quindi dovrebbe essere ricevuto prima.

flowchart LR
    subgraph P1
        a1[send m1 to P3] --> a2[send m2 to P2]
    end
    subgraph P2
        a2 --> b1[rec m2 from P1]
        b1 --> b2[send m3 to P3]
    end
    subgraph P3
        a1 -.-> c1[rec m1]
        b2 -.-> c2[rec m3]
    end
    a1 --> c1
    b2 --> c2
    a2 --> b1

Nell'esempio: send(m1) → send(m3) perche send(m1) → rec(m2) → send(m3). Quindi vogliamo rec(m1) → rec(m3).

In un sistema completamente asincrono non ci sono restrizioni sull'ordinamento dei messaggi. Questo massimizza il nondeterminismo e la concorrenza, ma a volte e utile ridurre il nondeterminismo imponendo vincoli sull'ordinamento per semplificare la programmazione. Dal punto di vista del middleware, la verifica di eligibility avviene nel sistema di comunicazione: un messaggio non eligible viene messo in una coda di buffer e consegnato al processo solo quando lo diventa (perche altri messaggi sono arrivati e hanno aggiornato la matrice).

sequenceDiagram
    participant A as P1
    participant B as P2
    participant C as P3

    Note over A: m[1,2]++, send to P2
    A->>B: msg1 (con matrice m)
    Note over B: check eligibility:
m'[1,2] == m[1,2]+1?
se si: update, receive Note over A: m[1,3]++, send to P3 A->>C: msg2 (con matrice m) Note over B: P2 invia a P3 B->>C: msg3 (con matrice m') Note over C: msg3 arriva prima di msg2?
Se send(msg2) -> send(msg3)
allora rec(msg2) -> rec(msg3)
Se msg3 arriva prima, buffer C->>C: buffer msg3
aspetta msg2 A->>C: msg2 arriva (dopo) Note over C: msg2 eligible? si
aggiorna matrice
controlla msg3 bufferizzato
ora msg3 eligible C->>C: deliver msg3

12. Algoritmo con matrice di clock

L'algoritmo per garantire l'ordinamento causale estende il vector clock in una matrice di clock m di dimensione N×N, dove m[j,k] rappresenta il numero di messaggi inviati da Pj a Pk conosciuti da Pi nello stato corrente.

Quando un processo Pi invia un messaggio a Pj:

Quando un processo Pi riceve un messaggio da Pj con matrice m':

Nota del redattore

Il professore osserva che, nonostante la complessita della matrice, il principio e semplice: ogni processo guarda solo la propria colonna nella matrice ricevuta per decidere se ci sono messaggi pendenti da altri processi che devono essere ricevuti prima.

Il professore nota anche che se non ci fosse stato lo scambio di messaggi tra P1 e P2, gli eventi sarebbero stati concorrenti e non si sarebbe potuta ricostruire la catena causale. Tutto dipende dagli scambi di messaggi effettivi tra i processi.

Riassumendo la condizione di eleggibilità per un messaggio ricevuto da Pi da Pj con matrice m':

Non è consentito il «sorpasso causale»: se un messaggio è stato inviato prima (causalmente) di un altro, deve essere ricevuto prima.

13. Ordinamento totale e sincronizzatori

Oltre all'ordinamento causale, esistono forme piu forti. L'ordinamento totale (sincrono) e piu restrittivo dell'ordinamento causale: richiede che tutti i messaggi possano essere trattati come logicamente istantanei. Nel caso di messaggi multicast, significa che tutti i messaggi vengono consegnati nello stesso ordine a tutti i processi. Per ogni coppia di messaggi x, y e per ogni coppia di processi P, Q: se x viene ricevuto da P prima di y, allora y non viene ricevuto da Q prima di x.

Sincronizzatori

Progettare algoritmi distribuiti e piu facile assumendo una rete sincrona. I sincronizzatori permettono di simulare una rete sincrona su una asincrona, usando il meccanismo dei pulse (colpi): ogni processo ha un contatore pulse, e in ogni round i invia esattamente un messaggio a tutti i vicini con pulse = i, e attende un messaggio da ogni vicino con lo stesso pulse prima di avanzare al round successivo.

Pj::
var
  pulse: integer := 0

round i:
  pulse := pulse + 1
  /* simula round i dell'algoritmo sincrono */
  send msg to all neighbors with pulse
  wait for exactly one msg from each neighbor with (pulse = i)

Il costo di un sincronizzatore si misura in termini di messaggi aggiuntivi (message complexity) e di tempo aggiuntivo (time complexity) necessari per simulare un singolo pulse.

Il quadro completo delle garanzie di ordinamento comprende tre livelli. L'ordinamento FIFO e la forma piu debole: i messaggi inviati da uno stesso mittente a uno stesso destinatario vengono ricevuti nell'ordine di invio; molti middleware di messaggistica lo garantiscono per default. L'ordine totale richiede un coordinamento aggiuntivo e si basa su algoritmi come ISIS o su un sequencer centralizzato: e la base per i sistemi di replicated state machine.

Garanzia: i messaggi tra coppia (mittente, destinatario) arrivano in ordine di invio.

Costo: basso, spesso fornito dal protocollo di trasporto (TCP).

Limitazione: non dice nulla su messaggi da mittenti diversi o a destinatari diversi.

Garanzia: se invio di m1 precede causalmente invio di m2, allora m1 viene ricevuto prima di m2 ovunque.

Costo: matrice NxN di interi per processo. Buffer per messaggi non ancora eligible.

Utile per: applicazioni dove l'ordine causale e importante (es. aggiornamenti a un documento condiviso).

Garanzia: tutti i processi vedono tutti i messaggi multicast nello stesso ordine globale.

Costo: richiede un sequencer o un consenso distribuito.

Utile per: replicated state machine, database distribuiti, sistemi di consensus.

14. Global state: consistent cut e snapshot

Catturare lo stato globale di un sistema distribuito e una sfida perche non esiste memoria condivisa. Uno stato globale e un insieme di stati locali che sono tutti concorrenti tra loro secondo la relazione happened-before. Per molte applicazioni e sufficiente catturare uno stato globale che e esistito nel passato, non necessariamente quello corrente.

Idea chiave

Uno stato globale consistente non e semplicemente il prodotto cartesiano degli stati locali. Serve garantire che se un evento e = receive(msg) e e' = send(msg), allora se e' → e, deve valere che e' appartiene allo stato globale. Questa e la definizione di consistent cut.

Un taglio inconsistente si verifica quando registriamo la ricezione di un messaggio senza aver registrato anche il suo invio. Per esempio, se P1 invia un messaggio a P2, e nella nostra istantanea vediamo P2 che lo ha ricevuto ma P1 che non lo ha ancora inviato, la istantanea e inconsistente.

flowchart LR
    subgraph P1
        a1[e1] --> a2[e2]
        a2 --> a3[e3]
    end
    subgraph P2
        b1[e4] --> b2[e5]
        b2 --> b3[e6]
    end
    a2 -. msg .-> b2

G1 (inconsistente): include e2, e3, e5, e6. G2 (consistente): include e1, e2, e4, e5. In G1, e5 (ricezione del messaggio) e incluso ma non e2 (invio). In G2, se il messaggio e inviato ma non ancora ricevuto, va considerato come "in transito" e incluso nello stato del canale.

15. Algoritmo di Chandy-Lamport

Proposto da K. Mani Chandy e Leslie Lamport nel 1985, questo algoritmo permette di catturare uno snapshot globale consistente senza bloccare i processi, utilizzando un meccanismo a marker.

Assunzioni: tutti i canali sono unidirezionali e FIFO. Ogni processo ha un colore (bianco/rosso).

L'algoritmo funziona cosi:

Grazie ai canali FIFO, nessun processo bianco riceve mai un messaggio inviato da un processo rosso. Questo garantisce che gli stati locali salvati siano mutuamente concorrenti (consistent cut).

Applicazioni degli snapshot includono: rilevamento deadlock, rilevamento terminazione, debug distribuito, checkpoint e recovery.

Un punto sottile: lo snapshot catturato corrisponde allo stato del sistema appena prima che i processi diventino rossi. Lo stato dei canali è dato dai messaggi inviati da processi bianchi e ricevuti da processi rossi: sono i messaggi in transito al momento dello snapshot.

flowchart LR
  subgraph P["Processo P"]
    p0["e0"] --> p1["e1"] --> p2["e2"]
    p2 --> p3["e3"]
  end
  subgraph Q["Processo Q"]
    q0["f0"] --> q1["f1"] --> q2["f2"]
  end
  p2 -.->|"messaggio m"| q1
  p1 -.->|"messaggio m'"| q2
  

L'algoritmo è usato per rilevamento di deadlock, terminazione e osservazione di predicati globali.

16. Esplora: gli stati di Chandy-Lamport

Chandy-Lamport e uno dei risultati piu importanti e influenti nel campo dei sistemi distribuiti, alla base di molte tecniche di debugging e checkpointing. Il primo esploratore segue il ciclo di vita del protocollo dal punto di vista dell'intero snapshot.

Idea chiave

I canali FIFO garantiscono che nessun messaggio inviato da un processo white possa arrivare dopo il marker a un processo red. Questo assicura che gli stati locali catturati siano mutuamente concorrenti.

Il risultato e un'istantanea globale che corrisponde allo stato del sistema "subito prima che i processi diventassero rossi". Questa istantanea e consistente e puo essere usata per rilevare deadlock, terminare computazioni, o come checkpoint per il recovery.

Attenzione

Uno stato globale non e semplicemente il prodotto degli stati locali. Un taglio inconsistente (inconsistent cut) si verifica quando viene registrata la ricezione di un messaggio ma non la sua spedizione. Formalmente, un global state S e consistente se per ogni evento e = receive(msg), l'evento e' = send(msg) (dove e' → e) appartiene a S.

Il secondo esploratore segue invece il singolo processo, canale per canale:

17. Consensus: definizione e proprieta

Il consenso e uno dei problemi fondamentali del distributed computing: trovare un accordo tra processi distribuiti sul valore di una proprieta o su un'azione da compiere. E pervasivo nei sistemi reali: transazioni in DB (commit/abort), leader election, replicazione di state machine, atomic broadcast, clock synchronization, blockchain.

La definizione formale del problema richiede:

Per l'esame

Le tre proprieta del consenso sono: termination (ogni processo corretto deve prima o poi settare la propria decision variable), agreement (tutti i processi corretti decidono lo stesso valore) e integrity (se tutti i processi corretti propongono lo stesso valore, quel valore deve essere la decisione; non si puo cambiare idea una volta deciso).

Idea chiave

Molti problemi distribuiti sono riconducibili al consenso. Perfino la mutua esclusione puo essere vista come un problema di consenso: dobbiamo trovare un accordo su chi entra in sezione critica. I criteri possono essere diversi (primo che ha fatto richiesta, PID piu alto), ma il meccanismo di accordo e lo stesso.

In forma piu formale, i tre requisiti si esprimono cosi:

Risultato fondamentale: FLP

FLP (Fischer, Lynch, Patterson, 1985): in una rete asincrona, anche con un singolo processo che puo morire (crash non annunciato), il problema del consensus e impossibile da risolvere. Questo teorema di impossibilita ha profonde implicazioni pratiche: tutti i protocolli di consensus reali devono rilassare qualche assunzione, tipicamente introducendo sincronia o randomizzazione.

Dal teorema alla pratica: consensus sotto sincronia

Per aggirare FLP, si assume un sistema sincrono (bound superiore sul ritardo dei messaggi e sulla durata delle azioni) e si definiscono modelli di guasto specifici. I modelli principali sono:

ModelloDescrizione
CrashUn processore si arresta (halt). Il guasto piu semplice da modellare
Crash+linkUn processore crasha oppure un link di rete cade e resta giu permanentemente
OmissionUn processo invia solo un sottoinsieme dei messaggi che dovrebbe inviare, o ne riceve solo un sottoinsieme
ByzantineUn processore ha comportamento arbitrario: puo inviare messaggi contraddittori, valori falsi, o agire in modo malevolo. Il caso piu generale e difficile

18. FLP e algoritmo base con round

Il celebre risultato FLP (Fischer, Lynch, Patterson, 1985) dimostra che in una rete asincrona, in presenza anche di un solo processo che crasha in modo non annunciato, il problema del consenso e impossibile da risolvere. Questo perche non e possibile distinguere un processo lento da uno crashato.

Per risolvere il consenso si assume quindi un sistema sincrono, con un bound superiore sul ritardo dei messaggi e sulla durata delle azioni. L'algoritmo base procede a round:

Numero di messaggi: O((f+1)N^2).

Attenzione

L'algoritmo base funziona solo in assenza di comportamenti byzantine. Se un processo puo inviare valori arbitrari o mentire, serve un algoritmo piu robusto.

L'impossibilità deriva dall'incertezza: non potendo distinguere un processo guasto da uno lento, un algoritmo che deve garantire terminazione non può aspettare indefinitamente, ma se decide troppo presto rischia di sbagliare. Le soluzioni pratiche introducono ipotesi aggiuntive: sincronia parziale, failure detector, o assunzioni probabilistiche. Il teorema non dice che il consenso è impossibile nella pratica: dice che è impossibile garantirne la soluzione in un modello puramente asincrono.

19. Byzantine fault e BGA

Per superare l'impossibilità FLP, la pratica introduce assunzioni di sincronia: un limite superiore noto al ritardo dei messaggi e alla durata delle azioni. Questo permette di usare timeout per rilevare guasti. Quando i guasti possono essere arbitrari (byzantine), serve N ≥ 3f+1 per tollerare f guasti bizantini.

Nel caso byzantine faults, i processi guasti possono avere comportamenti arbitrari: inviare valori sbagliati, contraddirsi, mentire deliberatamente. Il problema e noto come Byzantine General Agreement (BGA): generali leali (processi corretti) devono coordinarsi (decidere se attaccare o ritirarsi) nonostante la presenza di traditori (processi faulty).

Il teorema fondamentale: non esiste protocollo f-resiliente per BGA se N ≤ 3f, dove N = numero totale di processi, f = numero di processi faulty. In altre parole, serve almeno il 66.7% di processi corretti per tollerare guasti bizantini.

Algoritmo con rotating coordinator (king)

L'algoritmo opera in f + 1 round, con un coordinatore rotante (chiamato king) in ogni round. Almeno un round avra un king non faulty:

Questo garantisce che tutti i processi corretti convergano allo stesso valore finale nonostante la presenza di faulty processes.

Il diagramma seguente illustra la struttura a round dell'algoritmo base (crash-stop) con f = 1, quindi 2 round: dopo lo scambio completo, tutti i processi corretti hanno lo stesso insieme V e applicano la stessa funzione di decisione.

graph TD
    subgraph "Round 1"
        P1a[P1: V={v1}] -->|send v1| P2a[P2]
        P1a -->|send v1| P3a[P3]
        P2a[P2: V={v2}] -->|send v2| P1a
        P2a -->|send v2| P3a
        P3a[P3: V={v3}] -->|send v3| P1a
        P3a -->|send v3| P2a
    end
    subgraph "Round 2 (f=1, rounds=2)"
        P1b[P1: V={v1,v2,v3}] -->|send nuovi| P2b[P2]
        P1b -->|send nuovi| P3b[P3]
        P2b[P2: V={v1,v2,v3}] -->|send nuovi| P1b
        P2b -->|send nuovi| P3b
        P3b[P3: V={v1,v2,v3}] -->|send nuovi| P1b
        P3b -->|send nuovi| P2b
    end
    subgraph "Decisione"
        P1c[P1: decide(V)]
        P2c[P2: decide(V)]
        P3c[P3: decide(V)]
    end
    

Il problema dei generali bizantini (Lamport, Shostak, Pease, 1982) e un classico: i generali leali devono decidere all'unisono se attaccare o ritirarsi nonostante la presenza di traditori che possono inviare messaggi contraddittori a destinatari diversi, o non inviare nulla.

Per l'esame

Teorema BGA: non esiste un protocollo f-resilient per il Byzantine General Agreement se N ≤ 3f (N = numero totale di processi, f = guasti). Serve almeno 3f+1 processi per tollerare f guasti bizantini. Ad esempio, con 1 traditore servono almeno 4 generali; con 2 traditori almeno 7.

20. Paxos, Raft e replicated state machines

Il professore accenna a due famiglie di protocolli di consenso piu avanzati, che tipicamente vengono approfonditi in corsi successivi.

Paxos (Lamport, 1998) e una famiglia di protocolli per il consenso in reti con processori non affidabili, pensata per ambienti crash-stop (non bizantini). Offre vari trade-off tra numero di processori, ritardi di messaggio, livello di attivita dei partecipanti e tipi di fallimento tollerati. Ampiamente usato per la replica di file e database dove serve durabilita.

Raft (Ongaro e Ousterhout, 2013) e un algoritmo di consenso progettato per essere facile da capire rispetto a Paxos, pur essendo equivalente in termini di fault-tolerance e performance. Offre un modo generico per distribuire una state machine su un cluster di nodi. Il sito raft.github.io contiene implementazioni open-source in Go, C++, Java e Scala.

Il consenso emerge tipicamente nel contesto delle replicated state machine: ogni server ha una state machine e un log. La state machine e il componente che vogliamo rendere fault-tolerant (es. una hash table). L'algoritmo di consenso gestisce un log replicato contenente i comandi dei client. Le state machine processano sequenze identiche di comandi, producendo cosi le stesse uscite. Se una state machine applica il comando "set x to 3" come n-esimo comando, nessun'altra state machine applichera un n-esimo comando diverso.

flowchart LR
    Client -->|"set x=3"| S1[Server 1]
    Client -->|"set x=3"| S2[Server 2]
    Client -->|"set x=3"| S3[Server 3]
    subgraph Consensus
        S1 --> L1[Log: set x=3]
        S2 --> L2[Log: set x=3]
        S3 --> L3[Log: set x=3]
    end
    L1 --> M1[State Machine: x=3]
    L2 --> M2[State Machine: x=3]
    L3 --> M3[State Machine: x=3]

Paxos (Lamport, 1998)

Il nome deriva dall'articolo "The Part-Time Parliament" (Lamport, 1998), che presenta il consenso come un parlamento che legifera anche quando alcuni parlamentari sono assenti. Paxos bilancia diversi aspetti: numero di processori, ritardi di messaggio prima di apprendere il valore concordato, livello di attivita dei partecipanti, numero di messaggi scambiati, e tipi di guasto tollerati. E usato in tutti i contesti dove serve durabilita, come la replicazione di file o database.

Raft (Ongaro, Ousterhout, 2013)

Raft (Reliable, Replicated, Redundant, And Fault-Tolerant) e un algoritmo di consensus progettato per essere facile da capire. Equivalente a Paxos in fault-tolerance e performance, ma significativamente piu semplice da implementare. E utilizzato in progetti come etcd e Consul.

Proposto da Leslie Lamport nel 1998. Famiglia di protocolli, non un singolo algoritmo monolitico. Considerato storicamente il riferimento per il consensus in produzione, ma notoriamente difficile da comprendere e implementare correttamente. Il nome e la metafora del parlamento riflettono la complessita della presentazione originale.

Proposto da Diego Ongaro e John Ousterhout nel 2013. Progettato esplicitamente per l'insegnamento e la comprensione. Usa un leader forte, elezione del leader con timeout randomizzati, e una struttura a log piu chiara. E lo standard de facto per nuovi progetti che necessitano di consensus.

L'algoritmo di consensus coordina il log replicato: garantisce che se una state machine applica "set x to 3" come n-esimo comando, nessun'altra state machine nello stesso cluster applichera mai un comando diverso come n-esimo. Tutte eseguono la stessa identica sequenza di comandi, producendo gli stessi risultati e arrivando agli stessi stati.

graph LR
    Client -->|"set x=3, get y, ..."| LB[Load Balancer]
    LB --> SM1[Server 1
State Machine
Log: [set x=3,...]] LB --> SM2[Server 2
State Machine
Log: [set x=3,...]] LB --> SM3[Server 3
State Machine
Log: [set x=3,...]] Consensus[Consensus Module
Raft / Paxos] -.-> SM1 Consensus -.-> SM2 Consensus -.-> SM3 SM1 -.->|replica log| Consensus SM2 -.->|replica log| Consensus SM3 -.->|replica log| Consensus

Il sistema appare ai client come un'unica state machine affidabile, anche se una minoranza dei server nel cluster fallisce. Questo pattern e alla base di sistemi come Google File System (GFS), HDFS, RAMCloud, e innumerevoli database distribuiti.

Il risultato FLP

FLP dimostra che non esiste un algoritmo di consenso deterministico che sia contemporaneamente: tollerante a un singolo crash, funzionante in rete asincrona, e garantito terminare sempre. Almeno una di queste tre condizioni va rilassata — ed e esattamente cio che fanno Paxos e Raft con le loro assunzioni di sincronia parziale.

In Raft il flusso e guidato da un leader forte: il client invia il comando al leader, che lo replica ai follower tramite AppendEntries e conferma il commit una volta raggiunta la maggioranza.

flowchart TD
    Client -->|"set x=3"| Leader[Leader]
    subgraph Raft Cluster
        Leader -->|"AppendEntries"| F1[Follower]
        Leader -->|"AppendEntries"| F2[Follower]
        Leader -->|"AppendEntries"| F3[Follower]
    end
    F1 -->|"ACK"| Leader
    F2 -->|"ACK"| Leader
    F3 -->|"ACK"| Leader
    Leader -->|"commit x=3"| Client

Se un server fallisce, la macchina a stati continua a funzionare sugli altri. Questa architettura e alla base di molti sistemi fault-tolerant: GFS, HDFS, RAMCloud, Chubby, ZooKeeper, etcd.

Per l'esame

Il professore ha menzionato Paxos e Raft come "further explorations". Il risultato di FLP, le proprieta del consenso e la differenza tra crash-stop e byzantine sono argomenti chiave per l'esame.

La fama di Paxos come algoritmo difficile da comprendere ha portato allo sviluppo di Raft.

Per l'esame

La catena concettuale è: il consenso è il problema astratto, Paxos e Raft sono algoritmi concreti che lo risolvono sotto ipotesi di crash-stop, e le macchine a stati replicati sono l'architettura che usa il consenso per costruire servizi fault-tolerant. Questa gerarchia è fondamentale per comprendere i sistemi distribuiti moderni.

Verifica le tue conoscenze

Testa la tua comprensione degli algoritmi distribuiti con queste domande. Clicca su ogni domanda per vedere la risposta.

Quali sono le tre proprieta della sezione critica distribuita?

Safety: mai due processi contemporaneamente in CS. Liveness: ogni richiesta prima o poi viene concessa. Fairness: le richieste vengono servite nell'ordine della relazione happened-before.

Come fa il coordinatore nell'algoritmo centralizzato a garantire la fairness anche se le richieste arrivano in ordine diverso da quello causale?

Usando i vector clock trasportati dalle richieste (piggyback vector). Il coordinatore valuta l'eligibility di una richiesta w confrontando w.v con il proprio reqDone: una richiesta e eligible solo se non ci sono richieste accadute prima di w che non sono state ancora soddisfatte. Se una richiesta arriva in ritardo, il coordinatore la ritarda fintanto che non arrivano tutte quelle che la precedono causalmente.

Quanti messaggi servono per entrare in CS nell'algoritmo di Ricart-Agrawala?

2(N-1) messaggi: (N-1) richieste inviate a tutti gli altri processi + (N-1) OK ricevuti. Non ci sono messaggi superflui.

Nell'algoritmo di Chang-Roberts, come fa un processo a capire di essere il leader?

Quando riceve un messaggio election con ID uguale al proprio (j == myid), significa che il suo messaggio iniziale ha fatto tutto il giro dell'anello ed e tornato indietro perche nessun altro aveva un ID piu alto. A quel punto si autoproclama leader e propaga un messaggio leader.

Cosa dice il risultato FLP?

In una rete asincrona, anche con un singolo processo che crasha in modo non annunciato, il consenso e impossibile da risolvere. Questo perche non si puo distinguere un processo lento da uno crashato. Per risolvere il consenso serve un'ipotesi di sincronia (con timeout).

Quanti processi servono per tollerare f byzantine faults?

Il teorema BGA dice che N deve essere maggiore di 3f (quindi almeno 3f + 1). In altre parole, servono almeno il 66.7% di processi corretti per tollerare guasti bizantini.

Cos'e un consistent cut e perche e importante?

Un consistent cut e un insieme di stati locali dove per ogni evento receive(m) incluso, anche l'evento send(m) corrispondente e incluso (a meno che accada dopo il taglio e il messaggio sia considerato in transito). E importante perche garantisce che lo snapshot globale rappresenti uno stato che e effettivamente esistito nel sistema, senza paradossi temporali come messaggi ricevuti ma mai inviati.

Cosa garantiscono i canali FIFO nell'algoritmo di Chandy-Lamport?

Garantiscono che nessun processo bianco riceva mai un messaggio inviato da un processo rosso. Questo e fondamentale per assicurare che gli stati locali salvati siano mutuamente concorrenti (consistent cut): quando un processo diventa rosso dopo aver ricevuto un marker, tutti i messaggi successivi che ricevera saranno da mittenti rossi.

Differenza tra orologio logico e vector clock?

L'orologio logico (Lamport) assegna un singolo numero a ogni evento e soddisfa: se a → b allora C(a) < C(b), ma non viceversa. Il vector clock usa un array di N contatori e soddisfa: a → b se e solo se VC(a) < VC(b), permettendo cosi di determinare con precisione se due eventi sono in relazione causale o concorrenti.

Perche il problema del consenso e considerato fondamentale nei sistemi distribuiti?

Perche molti problemi si riconducono a un problema di consenso: mutua esclusione (chi entra in CS?), leader election (chi e il leader?), atomic broadcast, transazioni distribuite, blockchain, clock synchronization. Una volta risolto il consenso, si ha uno strumento generale per risolvere un'intera classe di problemi di coordinazione distribuita.

Quali sono le tre assenze fondamentali che caratterizzano i sistemi distribuiti?

Assenza di clock condiviso (non si possono sincronizzare orologi fisici), assenza di memoria condivisa (nessun processo conosce lo stato globale), assenza di rilevamento accurato dei guasti (in un sistema asincrono non si distingue tra processo lento e processo caduto).

Nell'algoritmo di mutua esclusione centralizzato, quando una richiesta w e considerata "eligible"?

w e eleggibile se w.v e "al massimo" uguale a reqDone: per ogni j diverso da w.p si ha w.v[j] == reqDone[j], e per j == w.p si ha w.v[j] == reqDone[j] + 1. Questo significa che non esistono richieste accadute prima di w che non sono ancora state soddisfatte.

Qual e la condizione per avere un protocollo BGA (Byzantine General Agreement) risolvibile?

N > 3f, dove N e il numero totale di processi e f e il numero di processi guasti con comportamento arbitrario. Se N ≤ 3f, non esiste alcun protocollo f-resilient. Ad esempio, per tollerare 1 guasto bizantino servono almeno 4 processi.

Cosa garantisce l'ordinamento causale dei messaggi rispetto al semplice FIFO?

L'ordinamento causale e piu forte del FIFO: dati due messaggi m1, m2 tali che send(m1) → send(m2), allora rec(m1) → rec(m2). Il FIFO garantisce solo l'ordine all'interno dello stesso processo mittente, mentre l'ordinamento causale vieta i sorpassi (overtaking) anche tra processi diversi.

Cosa dice il teorema CAP di Brewer?

Un sistema distribuito con dati condivisi puo garantire al massimo due delle tre proprieta: Consistency (tutti vedono gli stessi dati), Availability (ogni richiesta riceve risposta), Partition tolerance (funziona nonostante partizioni di rete). La scelta dipende dai requisiti applicativi.

Qual e la differenza fondamentale tra Java RMI e un message-oriented middleware (MOM)?

RMI e basato su distributed object computing: chiamate a metodo sincrone, proxy/stub, trasparenza oggettuale, invocazione di metodi su oggetti remoti. Un MOM (es. RabbitMQ) e basato su messaggi asincroni, code, pubblicazione/sottoscrizione, disaccoppiamento temporale e spaziale. RMI e orientato agli oggetti; i MOM sono orientati ai messaggi.

Descrivi il funzionamento dell'algoritmo di Chandy-Lamport per il global snapshot.

Ogni processo ha un colore (bianco/rosso). Tutti iniziano bianchi. Quando un processo decide di fare lo snapshot (o riceve un marker), salva lo stato locale, diventa rosso, e invia marker su tutti i canali in uscita. I messaggi inviati da processi bianchi e ricevuti da processi rossi costituiscono lo stato dei canali. Canali FIFO garantiscono che nessun processo bianco riceva messaggi da processi rossi, assicurando che gli stati locali siano mutuamente concorrenti (consistent cut).