Difficoltà dei sistemi distribuiti - Jacob Gabrielson - Difficoltà dei sistemi distribuiti - Awsstatic

Pagina creata da Simona Volpe
 
CONTINUA A LEGGERE
Difficoltà dei sistemi distribuiti
                                     Jacob Gabrielson

Difficoltà dei sistemi distribuiti
Copyright © 2019, Amazon Web Services, Inc. o società affiliate. Tutti i diritti riservati.
Difficoltà dei sistemi distribuiti

Nel momento in cui abbiamo aggiunto il nostro secondo server, i sistemi distribuiti sono diventati il modo
di vivere in Amazon. Quando ho iniziato ad Amazon nel 1999, avevamo così pochi server che potevamo
assegnare ad alcuni dei nomi riconoscibili come "fishy" o "online-01". Tuttavia, anche nel 1999, il calcolo
distribuito non è stato facile. Allora, come adesso, le sfide con i sistemi distribuiti riguardavano latenza,
ridimensionamento, comprensione delle API di rete, dati di marshalling e non-marshalling e complessità
di algoritmi come Paxos. Mano a mano che i sistemi diventavano rapidamente più grandi e più distribuiti,
quelli che erano stati casi teorici limite si sono trasformati in eventi regolari.
Lo sviluppo di servizi di calcolo distribuito delle utility, come reti telefoniche interurbane affidabili o servizi
Amazon Web Services (AWS), è difficile. Il calcolo distribuito è anche più strano e meno intuitivo rispetto
ad altre forme di calcolo a causa di due problemi correlati. Errori indipendenti e non determinismo
causano i problemi più rilevanti nei sistemi distribuiti. Oltre ai tipici errori informatici a cui la maggior
parte degli ingegneri sono abituati, gli errori nei sistemi distribuiti possono verificarsi in molti altri modi.
Quel che è peggio è che è sempre impossibile sapere se qualcosa non ha funzionato.
Nella Builders' Library di Amazon, ci occupiamo di come AWS gestisce i più complicate problemi di
sviluppo e operazioni derivanti dai sistemi distribuiti. Prima di approfondire queste tecniche nel dettaglio
in altri articoli, vale la pena rivedere i concetti che contribuiscono al motivo per cui il calcolo distribuito
sia così piuttosto strano. Innanzitutto, esaminiamo i tipi di sistemi distribuiti.

Tipi di sistemi distribuiti
I sistemi distribuiti variano effettivamente in termini di difficoltà dell'implementazione. A un'estremità
dello spettro, abbiamo i sistemi distribuiti offline. Questi includono sistemi di elaborazione in batch,
cluster di analisi di big data, aziende di rendering di scene di film, cluster di ripiegamento delle proteine e
simili. Sebbene siano ben lungi dall'avere un'implementazione banale, i sistemi distribuiti offline vantano
quasi tutti i vantaggi del calcolo distribuito (scalabilità e tolleranza agli errori) e quasi nessuno degli aspetti
negativi (modalità di errore complesse e non determinismo).
Nel mezzo dello spettro, troviamo i sistemi distribuiti soft real-time. Si tratta di sistemi critici che devono
continuamente produrre o aggiornare i risultati, ma che dispongono di una finestra temporale
relativamente generosa entro cui farlo. Esempi di questi sistemi includono alcuni costruttori di indici di
ricerca, sistemi che cercano server con problemi, ruoli per Amazon Elastic Compute Cloud (Amazon EC2)
e così via. Un'indicizzatore di ricerca potrebbe essere offline (a seconda dell'applicazione) da 10 minuti a
molte ore senza indebito impatto sul cliente. I ruoli per Amazon EC2 devono trasferire le credenziali
aggiornate (essenzialmente) ad ogni istanza EC2, ma devono disporre di ore per farlo poiché le vecchie
credenziali non scadono per un certo lasso di tempo.
All'estrema e più difficile estremità dello spettro, abbiamo sistemi distribuiti hard real-time. Spesso sono
chiamati servizi di richiesta/risposta. In Amazon, quando pensiamo alla costruzione di un sistema
distribuito, il sistema hard real-time è il primo tipo a cui pensiamo. Purtroppo, i sistemi distribuiti hard
real-time sono i più difficili da gestire bene. Ne determinano la difficoltà l'arrivo imprevedibile delle
richieste e la necessità di dover fornire le risposte rapidamente (ad esempio, il cliente attende attivamente
la risposta). Gli esempi includono server Web front-end, la pipeline degli ordini, le transazioni con carta di
credito, le singole API AWS, la telefonia e così via. I sistemi distribuiti hard real-time sono il principale
focus di questo articolo.

                                                          2
Difficoltà dei sistemi distribuiti

I sistemi hard real-time sono strani
In una trama dei fumetti di Superman, Superman incontra un alter ego di nome Bizarro che vive su un
pianeta (Mondo Bizarro) dove tutto è arretrato. Bizarro assomiglia un po' a Superman, ma in realtà è
cattivo. I sistemi distribuiti hard real-time sono uguali. Assomigliano un po' a un normale computer, ma
in realtà sono diversi e, francamente, parteggiano un po' dalla parte dei cattivi.
Lo sviluppo di sistemi distribuiti hard real-time è bizzarro per un motivo: la rete di richiesta/risposta.
Non intendiamo i dettagli nitidi di TCP/IP, DNS, socket o altri protocolli simili. Tali argomenti sono
potenzialmente difficili da comprendere, ma assomigliano ad altri problemi difficili in ambito informatico.
Ciò che rende difficili i sistemi distribuiti hard real-time è che la rete consente l'invio di messaggi da un
dominio di errore all'altro. L'invio di un messaggio potrebbe sembrare innocuo. In effetti, l'invio di
messaggi è dove tutto inizia a diventare più complicato del normale.
Per fare un esempio semplice, basta guardare il seguente frammento di codice da un'implementazione di
Pac-Man. Destinato a funzionare su un singolo computer, non invia alcun messaggio su nessuna rete.
board.move(pacman, user.joystickDirection())
ghosts = board.findAll(":ghost")
for (ghost in ghosts)
  if board.overlaps(pacman, ghost)
    user.slayBy(":ghost")
    board.remove(pacman)
    return
Ora, immaginiamo di sviluppare una versione in rete di questo codice, in cui lo stato dell'oggetto board
è mantenuto su un server separato. Ogni chiamata all'oggetto board, come findAll(), comporta l'invio e la
ricezione di messaggi tra due server.
Ogni volta che viene inviato un messaggio di richiesta/risposta tra due server, deve sempre verificarsi la
stessa serie di otto passaggi. Per comprendere il codice Pac-Man in rete, esaminiamo le basi della
messaggistica di richiesta/risposta.
Messaggistica di richiesta/risposta attraverso una rete
Un'azione di andata/ritorno di richiesta/risposta comporta sempre gli stessi passaggi. Come mostrato nel
diagramma seguente, il client del computer CLIENT invia una richiesta MESSAGE sulla rete NETWORK al
server SERVER del computer, che risponde con il messaggio REPLY, anche sulla rete NETWORK.

                                                           3
Difficoltà dei sistemi distribuiti

    CLIENT           NETWORK                SERVER

           MESSAGGIO

                              MESSAGGIO

                                RISPONDI

            RISPONDI

Nel caso felice in cui tutto funziona, si verificano i seguenti passaggi:

    1.   INVIA RICHIESTA: CLIENT inserisce MESSAGE della richiesta su NETWORK.
    2.   RICHIESTA DI CONSEGNA: NETWORK consegna MESSAGE a SERVER.
    3.   CONVALIDA RICHIESTA: SERVER convalida MESSAGE.
    4.   AGGIORNA STATO SERVER: SERVER aggiorna il proprio stato, se necessario in base a MESSAGE.
    5.   INVIA RISPOSTA: SERVER invia la risposta REPLY a NETWORK.
    6.   CONSEGNA RISPOSTA: NETWORK consegna REPLY a CLIENT.
    7.   CONVALIDA RIPOSTA: CLIENT convalida REPLY.
    8.   AGGIORNA STATO DEL CLIENT: CLIENT aggiorna il proprio stato, se necessario, in base a REPLY.

Questi sono molti passaggi per un misero viaggio di andata e ritorno! Inoltre, questi passaggi sono la
definizione della comunicazione di richiesta/risposta attraverso una rete; non c'è modo di saltarne
nessuno. Ad esempio, è impossibile saltare il passaggio 1. Il client deve inviare MESSAGE sulla rete
NETWORK in qualche modo. Fisicamente, questo significa inviare pacchetti tramite un adattatore di rete,
che fa sì che i segnali elettrici passino attraverso i fili attraverso una serie di router che comprendono la
rete tra CLIENT e SERVER. Tutto ciò è separato dal passaggio 2 perché il passaggio 2 potrebbe non riuscire
per motivi indipendenti, tra cui una perdita improvvisa di energia da parte del SERVER che non consente
di accettare i pacchetti in arrivo. La stessa logica può essere applicata ai passaggi rimanenti.
Pertanto, una singola richiesta/risposta sulla rete esplode una singola cosa (richiamando un metodo) in
otto cose. Peggio ancora, come menzionato in precedenza, CLIENT, SERVER e NETWORK possono entrare
in errore in maniera indipendente gli uni dagli altri. In caso di errore, il codice degli ingegneri deve gestire
ciascuno dei passaggi descritti in precedenza. Questo è raramente vero per l'ingegneria tipica. Per
comprendere il perché, esaminiamo la seguente espressione dalla versione del codice a singolo computer.
board.find("pacman")

Tecnicamente, ci sono alcuni strani modi in cui questo codice potrebbe fallire in fase di esecuzione, anche
se l'implementazione di board.find è di per sé priva di bug. Ad esempio, la CPU potrebbe surriscaldarsi
spontaneamente in fase di esecuzione. L'alimentazione del computer potrebbe interrompersi, anche
spontaneamente. Il kernel potrebbe andare nel panico. La memoria potrebbe riempirsi e alcuni oggetti

                                                         4
Difficoltà dei sistemi distribuiti

che board.find tenta di creare potrebbero non essere creati. Oppure, il disco sul computer su cui è in
esecuzione potrebbe riempirsi e board.find potrebbe non riuscire ad aggiornare alcuni file delle statistiche
e quindi restituire un errore, anche se probabilmente non dovrebbe. Un raggio gamma potrebbe colpire
il server e stravolgere qualcosa nella RAM. Ma, il più delle volte, gli ingegneri non si preoccupano di queste
cose. Ad esempio, i test unitari non prevedono mai lo scenario "Cosa succede se la CPU non funziona" e
prevedono solo raramente scenari di memoria insufficiente.
Nell'ingegneria tipica, questi tipi di errore si verificano su un singola computer; vale a dire un singolo
dominio di errore. Ad esempio, se il metodo board.find entra in errore perché la CPU si blocca
spontaneamente, è lecito ritenere che l'intero computer sia inattivo. Non è nemmeno concettualmente
possibile gestire questo errore. È possibile avanzare le stese ipotesi sugli altri tipi di errore elencati in
precedenza. Si potrebbe provare a scrivere dei test per alcuni di questi casi, ma non ha molto senso per
l'ingegneria tipica. Se si verificano questi errori, è lecito ritenere che anche tutto il resto sia in errore.
Tecnicamente, diciamo che condividono tutti lo stesso destino. La condivisione del destino riduce
immensamente le diverse modalità di errore che un ingegnere deve gestire.

Gestione delle modalità di errore in sistemi distribuiti hard
real-time
Gli ingegneri che lavorano su sistemi distribuiti hard real-time devono verificare tutti gli aspetti dell'errore
di rete poiché i server e la rete non condividono lo stesso destino. A differenza del caso del singolo
computer, se la rete non funziona, il computer client continuerà a funzionare. Se il computer remoto non
funziona, il computer client continuerà a funzionare e così via.
Per testare in modo esauriente i casi di errore delle fasi di richiesta/risposta descritte in precedenza,
gli ingegneri devono presumere che ogni fase possa entrare in errore. Inoltre, devono garantire che
il codice (sia sul client che sul server) si comporti sempre correttamente alla luce di tali errori.
Esaminiamo un'azione di richiesta/risposta di andata/ritorno in cui le cose non funzionano:

    1. INVIA RICHIESTA entra in errore: la rete NETWORK non è riuscita a recapitare il messaggio
       (ad esempio, il router intermedio si è arrestato in modo anomalo nel momento sbagliato) oppure
       SERVER lo ha rifiutato esplicitamente.
    2. CONSEGNA RICHIESTA entra in errore: NETWORK consegna correttamente MESSAGE a SERVER,
       ma SERVER si arresta in modo anomalo subito dopo aver ricevuto MESSAGE.
    3. CONVALIDA RICHIESTA entra in errore: SERVER decide che MESSAGE non è valido. La causa
       potrebbe essere dovuta a qualsiasi cosa. Ad esempio, pacchetti danneggiati, versioni di software
       incompatibili o bug sul client o sul server.
    4. AGGIORNA STATO DEL SERVER entra in errore: SERVER tenta di effettuare l'aggiornamento
       senza tuttavia riuscirci.
    5. INVIA RISPOSTA entra in errore: indipendentemente dal fatto che stia cercando di rispondere
       con esito positivo o negativo, SERVER potrebbe non riuscire a inviare la risposta. Ad esempio, la
       sua scheda di rete potrebbe arrestarsi in modo anomalo proprio nel momento sbagliato.
    6. CONSEGNA RISPOSTA entra in errore: NETWORK potrebbe non riuscire a consegnare REPLY
       a CLIENT come indicato in precedenza, anche se NETWORK funzionava in una fase precedente.
    7. CONVALIDA RISPOSTA entra in errore: CLIENT decide che REPLY non è valida.
    8. AGGIORNA STATO DEL CLIENT entra in errore: CLIENT potrebbe ricevere il messaggio REPLY ma
       non riesce ad aggiornare il proprio stato, non riesce a capire il messaggio (a causa
       di incompatibilità) o non riesce a completare l'azione per qualche altro motivo.

                                                         5
Difficoltà dei sistemi distribuiti

Queste modalità di errore sono ciò che rende così difficile il calcolo distribuito. È ciò che chiamo le otto
modalità di errore dell'apocalisse. Alla luce di queste modalità di errore, rivediamo nuovamente questa
espressione dal codice Pac-Man.
board.find("pacman")

Questa espressione si espande nelle seguenti attività sul lato client:

   1. Invia un messaggio, come {action: "find", name: "pacman", userId: "8765309"}, sulla rete,
      indirizzato al computer Board.
   2. Se la rete non è disponibile o la connessione al computer Board viene esplicitamente rifiutata, si
      genera un errore. Questo caso è in qualche modo speciale perché il client sa, deterministicamente,
      che la richiesta non avrebbe potuto essere ricevuta dal computer server.
   3. Attendere una risposta.
   4. Se non si riceve alcuna risposta, abbiamo un timeout. In questo passaggio, il timeout indica che il
      risultato della richiesta è SCONOSCIUTO. Potrebbe essere successo o meno. Il client deve gestire
      correttamente l’errore UNKNOWN.
   5. Se si riceve una risposta, determinare se si tratta di una risposta andata a buon fine, di una risposta
      di errore o di una risposta incomprensibile/corrotta.
   6. Se non si tratta di un errore, decomprimere la risposta e trasformarla in un oggetto comprensibile
      dal codice.
   7. Se si tratta di un errore o di una risposta incomprensibile, sollevare un'eccezione.
   8. Qualunque cosa gestisca l'eccezione deve determinare se deve ritentare la richiesta o rinunciare e
      interrompere il gioco.

L'espressione avvia anche le seguenti attività sul lato server:

   1. Riceve la richiesta (ciò potrebbe anche non accadere).
   2. Convalida la richiesta.
   3. Cerca l'utente per vedere se l'utente è ancora attivo. (Il server potrebbe aver rinunciato a
      contattare l'utente perché non ha ricevuto alcun messaggio da troppo tempo).
   4. Aggiorna la tabella keep-alive per l'utente in modo che il server sappia che si trova
      (probabilmente) ancora lì.
   5. Cerca la posizione dell'utente.
   6. Pubblica una risposta contenente qualcosa come {xPos: 23, yPos: 92, clock: 23481984134}.
   7. Qualsiasi ulteriore logica del server deve gestire correttamente gli effetti futuri del client. Ad
      esempio, non può ricevere il messaggio, riceve il messaggio ma non riesce a capirlo, riceve il
      messaggio e lo arresta in modo anomalo o lo gestisce correttamente.

In breve, una singola espressione nel codice normale si trasforma in quindici passaggi aggiuntivi nel codice
di sistemi distribuiti hard real-time. Questa espansione è dovuta agli otto diversi punti in cui ogni
comunicazione di andata/ritorno tra client e server può non riuscire. Qualsiasi espressione che rappresenti
un ciclo di andata e ritorno sulla rete, come board.find("pacman"), produce quanto segue.
(error, reply) = network.send(remote, actionData)
switch error
 case POST_FAILED:
   // handle case where you know server didn't get it
 case RETRYABLE:

                                                        6
Difficoltà dei sistemi distribuiti

  // handle case where server got it but reported transient failure
 case FATAL:
  // handle case where server got it and definitely doesn't like it
 case UNKNOWN: // i.e., time out
  // handle case where the *only* thing you know is that the server received
  // the message; it may have been trying to report SUCCESS, FATAL, or RETRYABLE
 case SUCCESS:
  if validate(reply)
    // do something with reply object
  else
    // handle case where reply is corrupt/incompatible

Questa complessità è inevitabile. Se il codice non gestisce correttamente tutti i casi, il servizio alla fine
entrerà in errore in modi bizzarri. Immagina di provare a scrivere test per tutte le modalità di errore in cui
potrebbe incorrere un sistema client/server come l'esempio Pac-Man!

Testare i sistemi distribuiti hard real-time
Il test della versione per singolo computer dello snippet del codice Pac-Man è più semplice. Crea alcuni
oggetti Board diversi, mettili in stati diversi, crea alcuni oggetti User in stati diversi e così via. Gli ingegneri
penserebbero in maniera più intensa alle condizioni limite e potrebbero usare test generativi o un fuzzer.
Nel codice Pac-Man, ci sono quattro punti in cui viene utilizzato l'oggetto Board. Nel Pac-Man distribuito,
ci sono quattro punti in quel codice che hanno cinque diversi risultati possibili, come illustrato in
precedenza (POST_FAILED, RETRYABLE, FATAL, UNKNOWN o SUCCESS). Questi moltiplicano
enormemente lo spazio degli stati dei test. Ad esempio, gli ingegneri di sistemi distribuiti hard real-time
devono gestire numerose permutazioni. Supponiamo che la chiamata a board.find() non vada a buon fine
e produca un errore POST_FAILED. Quindi, è necessario testare cosa succede quando non va a buon fine
producendo un errore RETRYABLE, quindi testare cosa succede se non va a buon fine producendo un
errore FATAL e così via.
Ma anche quel test è insufficiente. Nel codice tipico, gli ingegneri possono supporre che se board.find()
funziona, funzionerà anche la prossima chiamata a board, board.move(). Nell'ingegneria dei sistemi
distribuiti hard real-time, non esiste tale garanzia. Il computer server potrebbe non funzionare in modo
indipendente in qualsiasi momento. Di conseguenza, gli ingegneri devono scrivere test per tutti e cinque
i casi per ogni chiamata rivolta alla scheda. Supponiamo che un ingegnere abbia inventato 10 scenari da
testare nella versione per singolo computer di Pac-Man. Ma, nella versione dei sistemi distribuiti, deve
testare ciascuno di questi scenari 20 volte. Ciò significa che la matrice di test passa da 10 a 200!
Ma, non è tutto. L'ingegnere può anche possedere il codice del server. Qualunque combinazione di errori
del client, di rete e lato server si verifichi, deve eseguire dei test in modo che il client e il server non
finiscano in uno stato danneggiato. Il codice del server potrebbe essere simile al seguente.
handleFind(channel, message)
 if !validate(message)
   channel.send(INVALID_MESSAGE)
   return
 if !userThrottle.ok(message.user())
   channel.send(RETRYABLE_ERROR)

                                                          7
Difficoltà dei sistemi distribuiti

   return
 location = database.lookup(message.user())
 if location.error()
   channel.send(USER_NOT_FOUND)
   return
 else
   channel.send(SUCCESS, location)

handleMove(...)
 ...

handleFindAll(...)
 ...

handleRemove(...)
 ...

Ci sono quattro funzioni lato server da testare. Supponiamo che ogni funzione, su un singolo computer,
abbia cinque test ciascuno. Ecco 20 test proprio lì. Poiché i client inviano più messaggi allo stesso server,
i test dovrebbero simulare sequenze di richieste diverse per assicurarsi che il server rimanga solido. Esempi
di richieste includono find (trova), move (sposta), remove (rimuovi) e findAll (trova tutti).
Supponiamo che un costrutto abbia 10 scenari diversi con una media di tre chiamate in ogni scenario.
Ecco altri 30 test. Ma uno scenario deve anche testare i casi di errore. Per ciascuno di questi test, è
necessario simulare cosa succede se il client ha ricevuto uno dei quattro tipi di errore (POST_FAILED,
RETRYABLE, FATAL e UNKNOWN) e quindi chiama nuovamente il server con una richiesta non valida. Ad
esempio, un client potrebbe chiamare con successo find, ma a volte potrebbe tornare indietro UNKNOWN
quando chiama move. Potrebbe quindi chiamare di nuovo find per qualche motivo. Il server gestisce
correttamente questo caso? Probabilmente, ma non sarà possibile saperlo a meno che non lo si provi.
Così, come con il codice lato client, anche la matrice di test sul lato server esplode in complessità.

Gestione di incognite sconosciute
È sbalorditivo considerare tutte le permutazioni di errori che un sistema distribuito può incontrare,
specialmente su più richieste. Un modo che abbiamo scoperto per avvicinarci all'ingegneria distribuita è
diffidare di tutto. Ogni riga di codice, a meno che non possa dare luogo alla comunicazione di rete,
potrebbe non fare ciò che dovrebbe.
Forse la cosa più difficile da gestire è il tipo di errore UNKNOWN delineato nella sezione precedente.
Il cliente non sa sempre se la richiesta è andata a buon fine. Forse ha spostato Pac-Man (o, in un servizio
bancario, ha prelevato denaro dal conto bancario dell'utente), o forse no. In che modo gli ingegneri
dovrebbero gestire queste cose? È difficile perché gli ingegneri sono umani e gli umani tendono a lottare
con la vera incertezza. Gli umani sono abituati a guardare al codice come vedremo più avanti.
bool isEven(number)
 switch number % 2
  case 0
   return true
  case 1
   return false

                                                        8
Difficoltà dei sistemi distribuiti

Gli umani comprendono questo codice perché fa quello che sembra fare. Gli umani faticano con la versione
distribuita del codice, che distribuisce parte del lavoro a un servizio.
bool distributedIsEven(number)
 switch mathServer.mod(number, 2)
  case 0
   return true
  case 1
   return false
  case UNKNOWN
   return WHAT_THE_FARG?

È quasi impossibile per un essere umano capire come gestire correttamente un errore UNKNOWN. Cosa
significa realmente UNKNOWN? Il codice dovrebbe riprovare? Se sì, quante volte? Quanto dovrebbe
aspettare tra i tentativi? Peggio ancora quando il codice innesca effetti collaterali. All'interno di
un'applicazione di budget in esecuzione su un singolo computer, prelevare denaro da un conto è semplice,
come mostrato nell'esempio seguente.
class Teller
 bool doWithdraw(account, amount)
  switch account.withdraw(amount)
    case SUCCESS
     return true
    case INSUFFICIENT_FUNDS
     return false

Tuttavia, la versione distribuita di tale applicazione è strana a causa di un errore UNKNOWN.
class DistributedTeller
 bool doWithdraw(account, amount)
  switch this.accountService.withdraw(account, amount)
    case SUCCESS
     return true
    case INSUFFICIENT_FUNDS
     return false
    case UNKNOWN
     return WHAT_THE_FARG?

Capire come gestire il tipo di errore UNKNOWN è uno dei motivi per cui, nell'ingegneria distribuita,
le cose non sono sempre come sembrano.

                                                      9
Difficoltà dei sistemi distribuiti

Branchi di sistemi distribuiti hard real-time
Le otto modalità di errore dell'apocalisse possono verificarsi a qualsiasi livello di astrazione all'interno di
un sistema distribuito. L'esempio precedente era limitato a un singolo computer client, una rete e un
singolo server. Anche in quello scenario semplicistico, la matrice dello stato di errore è esplosa in
complessità. I sistemi distribuiti reali sono dotati di matrici dello stato di errore più complicate rispetto
all'esempio del singolo computer client. I sistemi distribuiti reali sono costituiti da più computer che
possono essere visualizzati a più livelli di astrazione:

    1.   Computer individuali
    2.   Gruppi di computer
    3.   Gruppi di gruppi di computer
    4.   E così via (potenzialmente)

Ad esempio, un servizio basato su AWS potrebbe raggruppare computer dedicati alla gestione delle risorse
che si trovano all'interno di una particolare zona di disponibilità. Potrebbero esserci anche altri due gruppi
di computer che gestiscono altre due zone di disponibilità. Quindi, tali gruppi potrebbero essere
raggruppati in un gruppo di regioni AWS. E quel gruppo di regioni potrebbe comunicare (a livello logico)
con altri gruppi di regioni. Purtroppo, anche a questo livello più alto e più logico, si applicano esattamente
tutti gli stessi problemi.
Supponiamo che un servizio abbia raggruppato alcuni server in un unico gruppo logico, GROUP1. Il gruppo
GROUP1 potrebbe talvolta inviare messaggi a un altro gruppo di server, GROUP2. Questo è un esempio
di ingegneria distribuita ricorsiva. Qui possono essere applicate tutte le stesse modalità di errore di rete
descritte in precedenza. Supponiamo che GROUP1 desideri inviare una richiesta a GROUP2. Come
mostrato nel diagramma seguente, l'interazione richiesta/risposta a due computer è simile a quella del
singolo computer discussa in precedenza.

    GROUP1             NETWORK               GROUP2

            MESSAGGIO

                                MESSAGGIO

                                  RISPONDI

              RISPONDI

In un modo o nell'altro, alcuni computer all'interno di GROUP1 devono inserire un messaggio sulla rete,
NETWORK, indirizzato (a livello logico) a GROUP2. Alcuni computer all'interno di GROUP2 devono
elaborare la richiesta e così via. Il fatto che GROUP1 e GROUP2 siano costituiti da gruppi di computer non
modifica i principi fondamentali. GROUP1, GROUP2 e NETWORK possono continuare ad entrare in errore
in maniera indipendente gli uni dagli altri.

                                                       10
Difficoltà dei sistemi distribuiti

Tuttavia, questa è solo una vista a livello di gruppo. Esiste anche un'interazione a livello computer-
computer all'interno di ciascun gruppo. Ad esempio, GROUP2 potrebbe essere strutturato come mostrato
nel diagramma seguente.

                                             GROUP2

                                       Sistema di bilanciamento
                                              del carico

                               S20               S25                S29

Inizialmente, viene inviato un messaggio a GROUP2 , tramite il sistema di bilanciamento del carico,
a un computer (possibilmente S20) all'interno del gruppo. I progettisti del sistema sanno che S20
potrebbe entrare in errore durante la fase di AGGIORNA STATO. Pertanto, potrebbe essere necessario
che S20 passi il messaggio ad almeno un altro computer, che sia uno dei suoi pari o un computer in un
gruppo diverso. Come fa effettivamente S20 a farlo? Inviando un messaggio di richiesta/risposta a,
diciamo, S25, come mostrato nel diagramma seguente.

                                                        GROUP1

                                                                Messaggio

                                               GROUP2

                                               Sistema di bilanciamento
                                                      del carico

                                              Messaggio

                                     Messaggio
                          S20                              S25            S29

                                                      11
Difficoltà dei sistemi distribuiti

Pertanto, S20 sta eseguendo reti in modo ricorsivo. Tutti e otto gli stessi errori possono verificarsi di
nuovo, indipendentemente. L'ingegneria distribuita sta avvenendo due volte, anziché una volta. Il
messaggio da GROUP1 a GROUP2, a livello logico, può entrare in errore in tutti e otto i modi. Quel
messaggio si traduce in un altro messaggio, che può entrare in errore esso stesso, indipendentemente, in
tutti gli otto modi discussi in precedenza. Testare questo scenario comporterebbe almeno quanto segue:

      Un test per tutti gli otto modi in cui la messaggistica a livello di gruppo da GROUP1 a GROUP2
       può entrare in errore.
      Un test per tutti gli otto modi in cui la messaggistica a livello di server da S20 a S25 può entrare
       in errore.

Questo esempio di messaggistica di richiesta/risposta mostra perché testare i sistemi distribuiti rimane
un problema particolarmente annoso, anche dopo aver maturato oltre 20 anni di esperienza. Il test è
impegnativo data la vastità dei casi limite, ma è particolarmente importante in questi sistemi. Una volta
implementati i sistemi, i bug possono impiegare molto tempo prima di esplodere. Inoltre, i bug possono
avere un impatto imprevedibilmente ampio su un sistema e sui suoi sistemi adiacenti.

I bug distribuiti sono spesso latenti
Se alla fine si verificherà un errore, la saggezza comune insegna che è meglio che accada prima piuttosto
che dopo. Ad esempio, è meglio scoprire un problema di ridimensionamento in un servizio, che richiederà
sei mesi per essere risolto, almeno sei mesi prima che quel servizio raggiunga tale scala. Allo stesso modo,
è meglio trovare i bug prima che colpiscano la produzione. Se i bug colpiscono la produzione, è meglio
trovarli rapidamente, prima che colpiscano molti clienti o abbiano altri effetti collaterali.
I bug distribuiti, ovvero quelli risultanti dalla mancata gestione di tutte le permutazioni delle otto
modalità di errore dell'apocalisse, sono spesso gravi. Esempi occorsi nel tempo abbondano nei grandi
sistemi distribuiti, dai sistemi di telecomunicazione ai principali sistemi Internet. Queste interruzioni non
solo sono diffuse e costose, ma possono essere causate da bug che sono stati distribuiti nella produzione
mesi prima. Ci vuole quindi un po' di tempo per innescare la combinazione di scenari che portano
effettivamente a questi bug (e nel frattempo si diffondono in tutto il sistema).

I bug distribuiti si diffondono epidemicamente
Vorrei descrivere un altro problema fondamentale per i bug distribuiti:

   1. I bug distribuiti implicano necessariamente l'uso della rete.
   2. Pertanto, è più probabile che i bug distribuiti si diffondano ad altri computer (o gruppi di
      computer), perché, per definizione, coinvolgono già l'unica cosa che collega i computer.

Anche Amazon ha riscontrato questi bug distribuiti. Un vecchio esempio, ma pertinente, è un errore a
livello di sito di www.amazon.com. L'errore è stato causato dall'errore di un singolo server all'interno del
servizio di catalogo remoto durante il riempimento del disco.
A causa della cattiva gestione di tale condizione di errore, il server di catalogo remoto ha iniziato a
restituire risposte vuote a ogni richiesta ricevuta. Ha inoltre iniziato a restituirle molto rapidamente,
perché è molto più veloce non restituire nulla che qualcosa (almeno così è stato in questo caso).

                                                       12
Difficoltà dei sistemi distribuiti

Nel frattempo, il sistema di bilanciamento del carico tra il sito Web e il servizio di catalogo remoto non ha
notato che tutte le risposte avevano una lunghezza pari a zero. Tuttavia, ha notato che erano
incredibilmente più veloci di tutti gli altri server di catalogo remoti. Quindi, ha inviato un'enorme quantità
di traffico da www.amazon.com al server di catalogo remoto il cui disco era pieno. In effetti, l'intero sito
Web è andato in crash perché un server remoto non è stato in grado di visualizzare alcuna informazione
sul prodotto.
Abbiamo trovato rapidamente il server danneggiato e lo abbiamo rimosso dal servizio per ripristinare il
sito Web. Quindi, abbiamo seguito la nostra consueta procedura di determinazione delle cause principali
e di identificazione dei problemi per evitare che la situazione si ripetesse. Abbiamo condiviso quelle lezioni
su Amazon per aiutare ad impedire che altri sistemi incorressero nello stesso problema. Oltre ad
apprendere le lezioni specifiche su questa modalità di errore, questo incidente è stato un ottimo esempio
di come le modalità di errore si propagano rapidamente e imprevedibilmente nei sistemi distribuiti.

Riepilogo dei problemi nei sistemi distribuiti
In breve, la progettazione di sistemi distribuiti è difficile perché:

       Gli ingegneri non possono combinare condizioni di errore. Al contrario, devono prendere in
        considerazione numerose permutazioni di errori. La maggior parte degli errori può verificarsi in
        qualsiasi momento, indipendentemente da (e quindi, potenzialmente, in combinazione con)
        qualsiasi altra condizione di errore.
       Il risultato di qualsiasi operazione di rete può essere UNKNOWN, nel qual caso la richiesta potrebbe
        essere stata completata correttamente, non essere andata a buon fine o essere ricevuta ma non
        elaborata.
       I problemi distribuiti si verificano a tutti i livelli logici di un sistema distribuito, non soltanto a
        livello di computer fisici di basso livello.
       I problemi distribuiti peggiorano ai livelli più alti del sistema, a causa della ricorsione.
       I bug distribuiti spesso compaiono molto tempo dopo essere stati distribuiti su un sistema.
       I bug distribuiti possono diffondersi su un intero sistema.
       Molti dei suddetti problemi derivano dalle leggi della fisica delle reti, che non possono essere
        modificate.

Solo perché il calcolo distribuito è difficile, e strano, non significa che non ci siano modi per affrontare
questi problemi. Attraverso la Builders' Library di Amazon, analizziamo come AWS gestisce i sistemi
distribuiti. Speriamo che troverai ciò che abbiamo appreso prezioso mentre costruisci per i tuoi clienti.

                                                        13
Puoi anche leggere