La progettazione dei servizi può prevedere l’integrazione di ogni genere di affidabilità e resilienza ma, per essere affidabili nella pratica, i servizi devono anche essere in grado di risolvere guasti prevedibili quando si verificano. In Amazon, costruiamo servizi caratterizzati da scalabilità e ridondanza orizzontale, perché l’hardware è progettato, in ultima analisi, per guastarsi. Qualsiasi disco rigido ha una durata massima prevista e qualsiasi software è soggetto prima o poi a bloccarsi. Può sembrare che la salute di un server sia binaria: o funziona o non funziona affatto e si toglie di mezzo. Purtroppo non è così. Riteniamo che piuttosto che limitarsi a spegnersi, il server che ha subito il guasto possa causare danni imprevedibili e talvolta sproporzionati a un sistema. I controlli dello stato rilevano e rispondono automaticamente a questo tipo di problemi.

Questo articolo descrive come utilizziamo i controlli dello stato per rilevare e risolvere i guasti di un unico server, illustra cosa accade quando non si utilizzano i controlli dello stato e spiega come i sistemi che reagiscono eccessivamente ai guasti dei controlli dello stato possono trasformare piccoli problemi in interruzioni assolute. Forniamo anche informazioni sulla base della nostra esperienza in Amazon per bilanciare i compromessi tra i vari tipi di implementazioni dei controlli dello stato.

Piccoli guasti con impatto fuori misura

Quando ero un giovane sviluppatore software presso Amazon, ho lavorato sulla flotta di rendering del sito web che sta dietro ad Amazon.com. Mentre lavoravo su una modifica per aggiungere un po’ di strumentazione e per verificare quando fosse accurata l’esecuzione del software, ho purtroppo scritto un bug. Il bug si avviava raramente, ma quando lo faceva tutte le pagine di errore di qualsiasi richiesta di un determinato server diventavano vuote. Solo riavviando il processo del server web è stato risolto il problema. Abbiamo rilevato l’errore e ripristinato rapidamente la condizione precedente alla modifica, abbiamo aggiunto un sacco di test e abbiamo migliorato i processi per cogliere condizioni come questa in futuro. Ma mentre il bug era in produzione, alcuni server in una grande flotta sono stati danneggiati in questo modo.
 
Una cosa che rendeva il bug particolarmente difficile da individuare era che il server non si rendeva conto che non fosse “non integro”. Inoltre, il server aveva perso la possibilità di segnalare il proprio stato di salute nei sistemi di monitoraggio, quindi non era stato messo automaticamente fuori servizio e non aveva fatto scattare i suoi soliti allarmi. A peggiorare le cose, il server era diventato molto veloce e aveva iniziato a produrre pagine di errore vuote molto più velocemente di quanto i suoi omologhi “server sani” effettuassero il rendering delle pagine corrette. La tecnologia di bilanciamento del carico che utilizzavamo all’epoca favoriva i server veloci rispetto a quelli lenti e aveva quindi indirizzato una quantità sproporzionata di traffico verso i server non sani, aumentando ulteriormente l’impatto.

Sono scattati altri allarmi, poiché il monitoraggio comporta la misurazione dei tassi di errore e della latenza da più punti del sistema. Mentre questi tipi di sistemi di monitoraggio e processi operativi possono fungere da rete di protezione per contenere il problema, i giusti controlli dello stato possono ridurre sensibilmente l’impatto di tutta questa classe di errori se si rilevano e si risolvono rapidamente i guasti.

Compromessi dei controlli dello stato

I controlli dello stato sono un modo per chiedere a un servizio su un particolare server se è in grado o meno di eseguire correttamente il lavoro. I sistemi di bilanciamento del carico rivolgono periodicamente questa domanda ad ogni server per determinare a quali server sia sicuro dirigere il traffico. Un servizio che interroga i messaggi di una coda potrebbe chiedersi se sia integro prima di decidere di interrogare più lavoro dalla coda. Gli agenti di monitoraggio, in funzione su ogni server o su una flotta di monitoraggio esterno, potrebbero chiedere ai server se questi siano integri in modo da poter dare l’allarme o risolvere automaticamente la situazione dei server che stanno evidenziando un guasto.

Come abbiamo visto nell’esempio del mio bug nel sito web, quando un server non integro rimane in servizio, può ridurre in modo sproporzionato la disponibilità del servizio nel suo complesso. Con una flotta di dieci server, un server difettoso significa che la disponibilità della flotta non supererebbe il 90%. A peggiorare le cose, alcuni algoritmi di bilanciamento del carico, come “least requests”, danno più lavoro al server più veloce. Quando un server subisce un guasto, spesso inizia rapidamente a non essere in grado di rispondere alle richieste, creando un “buco nero” nella flotta di servizi che attirano più richieste dei server integri. In alcuni casi, aggiungiamo una protezione extra per prevenire i buchi neri rallentando le richieste non riuscite in modo da stabilire una corrispondenza con la latenza media delle richieste riuscite. Tuttavia esistono altri scenari, come nel caso di chi interroga le code, in cui il problema è più difficile da risolvere. Ad esempio, se chi interroga una coda sta interrogando i messaggi alla velocità con cui li riceve, anche un server che ha subito un guasto diventerà un buco nero. Con un insieme così vario di ambienti per la distribuzione del lavoro, il modo in cui pensiamo di proteggere un server parzialmente guasto varia da sistema a sistema.

Troviamo che i server subiscono guasti in modo indipendente per una serie di motivi, tra cui dischi che diventano non scrivibili e causano l’immediato errore delle richieste, clock che subiscono bruscamente effetti di asimmetria tanto che le chiamate alle dipendenze non superano l’autenticazione, server che non riescono a recuperare materiale crittografato aggiornato e causano un errore di decrittografia e crittografia, processi di supporto critici che si bloccano a causa dei propri bug, perdite di memoria e blocchi critici che congelano l’elaborazione.

I server si bloccano anche per motivi correlati che causano il guasto congiunto di molti o di tutti i server di una flotta. Le ragioni correlate includono l’interruzione dell’attività di una dipendenza condivisa e problemi di rete su larga scala. Il controllo dello stato ideale verifica ogni aspetto dell’integrità del server e dell’applicazione, persino accertandosi anche che i processi di supporto non critici siano in esecuzione. Tuttavia, quando il controllo dello stato non va a buon fine per un motivo non critico e quando tale guasto è correlato tra server, si verificano invariabilmente problemi. Se l’automazione rimuove i server dal servizio quando potrebbero ancora eseguire lavoro utile, l’automazione fa più male che bene.

La difficoltà dei controlli dello stato è questa tensione tra, da un lato, i benefici di controlli dello stato approfonditi e la rapida attenuazione dei guasti di un solo server e, dall’altro, il danno causato da un guasto falso positivo in tutta la flotta. Pertanto, una delle sfide della creazione di un buon controllo dello stato consiste nel vigilare attentamente contro i falsi positivi. In generale, questo significa che l’automazione che circonda i controlli dello stato dovrebbe smettere di dirigere il traffico verso un singolo server difettoso, ma continuare a consentire il traffico se l’intera flotta sembra avere problemi.

Modi per misurare l’integrità

Ci sono molte cose che possono rompersi su un server, e ci sono molti punti nei nostri sistemi dove misuriamo l’integrità del server. Alcuni controlli dello stato possono segnalare definitivamente che un particolare server è indipendentemente guasto, mentre altri sono meno specifici e segnalano falsi positivi in caso di guasti correlati. Alcuni controlli dello stato sono difficili da implementare. Altri sono implementati in fase di configurazione con servizi come Amazon Elastic Compute Cloud (Amazon EC2) ed Elastic Load Balancing. Ogni tipo di controllo dello stato ha i suoi punti di forza.

Controlli di liveness

I controlli di liveness verificano la connettività di base ad un servizio e la presenza di un processo server. Sono spesso eseguite da un sistema di bilanciamento del carico o da un agente di monitoraggio esterno e non sono a conoscenza dei dettagli sul funzionamento di un’applicazione. I controlli di liveness tendono ad essere inclusi nel servizio e non richiedono che l’autore dell’applicazione implementi alcunché. Alcuni esempi di controlli di liveness che usiamo in Amazon includono:

• Test che confermano che un server è in ascolto della porta prevista e accetta nuove connessioni TCP.
• Test che eseguono una richiesta HTTP di base e si assicurano che il server risponda con un codice di stato di 200.
• Controllo dello stato per Amazon EC2 che verificano gli elementi base necessari al funzionamento di qualsiasi sistema, come la raggiungibilità della rete.

Controlli dello stato locali

I controlli dello stato locali vanno oltre i controlli di liveness per verificare che l’applicazione sia probabilmente in grado di funzionare. Questi controlli verificano le risorse che non sono condivise con i peer del server. Pertanto, è improbabile che si blocchino contemporaneamente su molti server della flotta. Questi controlli dello stato verificano quanto segue:

• Incapacità di scrivere su un disco o di leggere da un disco: può essere allettante ritenere che un servizio stateless non richieda un disco scrivibile. Tuttavia, in Amazon i nostri servizi tendono ad utilizzare i loro dischi per operazioni quali monitoraggio, registrazione e pubblicazione di dati di misurazione asincrona.
• Processi critici che si bloccano o si interrompono: alcuni servizi prendono le richieste utilizzando un proxy sul server (simile a NGINX) ed eseguono la loro logica di business in un altro processo server. Un controllo di liveness potrebbe verificare soltanto se il processo proxy è in esecuzione. Un processo di controllo dello stato locale potrebbe passare dal proxy all’applicazione per verificare che entrambi siano in esecuzione e che rispondano correttamente alle richieste. È interessante notare che, nell’esempio del sito web citato all’inizio dell’articolo, il controllo dello stato esistente era sufficientemente approfondito da garantire che il processo di rendering fosse in esecuzione e rispondente, ma non approfondito abbastanza da garantire che rispondesse correttamente.
• Processi di supporto mancanti: gli host a cui mancano i daemon di monitoraggio potrebbero lasciare che gli operatori si muovano “alla cieca” e ignari dello stato di salute dei loro servizi. Altri processi di supporto eseguono il push dei record sull’uso di misurazione e fatturazione o ricevono aggiornamenti delle credenziali. I server con processi di supporto interrotti mettono a rischio le funzionalità in modi discreti e difficili da individuare.

Controlli dello stato delle dipendenze

I controlli dello stato delle dipendenze sono un’ispezione approfondita della capacità di un’applicazione di interagire con i sistemi adiacenti. In teoria, questi controlli colgono i problemi locali del server, come le credenziali scadute, che gli impediscono di interagire con una dipendenza. Ma possono anche avere falsi positivi quando ci sono problemi con la dipendenza stessa. A causa di questi falsi positivi, occorre fare attenzione a come reagiamo agli errori dei controlli dello stato delle dipendenze. I controlli dello stato delle dipendenze possono verificare quanto segue:

• Configurazione errata o metadati obsoleti: se un processo cerca in modo asincrono aggiornamenti dei metadati o della configurazione ma il meccanismo di aggiornamento su un server è guasto, il server può diventare significativamente fuori sincrono con i suoi peer e comportarsi in modo imprevedibile e non testato. Tuttavia, quando un server non vede un aggiornamento per un po’ di tempo, non sa se il meccanismo di aggiornamento è guasto o se il sistema centrale di aggiornamento ha smesso di pubblicare aggiornamenti per tutti i server.
• Incapacità di comunicare con server peer o dipendenze: è noto che un comportamento di rete insolito influisce sulla capacità di un sottoinsieme di server di una flotta di comunicare con le dipendenze senza influire sulla capacità del traffico da inviare a quel server. Anche problemi di software, come i blocchi critici o i bug nei pool di connessione, possono ostacolare la comunicazione di rete.
• Altri bug software insoliti che richiedono un mancato recapito del processo: blocchi critici, perdite di memoria o bug di danneggiamento dello stato possono far sgorgare errori da un server. 

Rilevamento di anomalie

Il rilevamento di anomalie interessa tutti i server di una flotta per determinare se un server si comporti in modo strano rispetto ai suoi colleghi. Aggregando i dati di monitoraggio per server, possiamo continuamente confrontare i tassi di errore, i dati di latenza o altri attributi per trovare i server anomali e rimuoverli automaticamente dal servizio. Il rilevamento delle anomalie può trovare divergenza nella flotta che un server non può rilevare su se stesso, come ad esempio:

• Clock skew: soprattutto quando i server subiscono un carico elevato, è noto che i loro clock subiscono effetti di asimmetria anche significativa. Le misure di sicurezza, come quelle utilizzate per valutare le richieste firmate ad AWS, richiedono che l’ora indicata dal clock di un client sia entro cinque minuti dall’ora effettiva. Se non lo è, le richieste ai servizi AWS non vanno a buon fine.
• Codice obsoleto: se un server viene disconnesso dalla rete o spento per un periodo prolungato e poi torna in linea, è possibile che esegua codice pericolosamente obsoleto e incompatibile con il resto della flotta.
• Qualsiasi modalità di guasto imprevisto: talvolta i guasti dei server sono tali da restituire errori che vengono identificati come errori del client anziché i propri (HTTP 400 anziché 500). I server possono rallentare invece di bloccarsi, o possono rispondere più velocemente dei loro peer, il che è un segno che stanno restituendo risposte false ai loro intermediari. Il rilevamento delle anomalie è un concetto onnicomprensivo incredibile per le modalità di guasto impreviste.

Poche cose che devono valere affinché il rilevamento delle anomalie funzioni nella pratica:

• I server dovrebbero fare all’incirca la stessa cosa: nei casi in cui instradiamo esplicitamente tipi diversi di traffico verso tipi diversi di server, i server potrebbero non comportarsi in modo abbastanza simile da rilevare gli outlier. Tuttavia, nei casi in cui utilizziamo i sistemi di bilanciamento del carico per dirigere il traffico verso i server, è probabile che rispondano in modo simile.
• Le flotte devono essere relativamente omogenee: nelle flotte che includono diversi tipi di istanza, alcune istanze potrebbero essere più lente di altre, il che può falsamente innescare il rilevamento passivo dei server difettosi. Per aggirare questo scenario, raccogliamo i parametri per tipo di istanza.
• Gli errori o le differenze di comportamento devono essere segnalati: perché ci affidiamo ai server stessi per segnalare gli errori, cosa succede quando anche i loro sistemi di monitoraggio si bloccano? Fortunatamente il client di un servizio è un ottimo punto in cui aggiungere strumentazione. I sistemi di bilanciamento del carico come Application Load Balancer pubblicano log di accesso che mostrano quale server di backend è stato contattato ad ogni richiesta, il tempo di risposta e se la richiesta ha avuto successo o meno. 

Reagire in modo sicuro agli errori dei controlli dello stato

Quando un server determina di non essere integro, può intraprendere due tipi di azioni. Nel caso più estremo, può decidere a livello locale che non deve ricevere lavoro e mettersi fuori servizio facendo in modo che un controllo dello stato del sistema di bilanciamento del carico non vada a buon fine o interrompendo il sondaggio di una coda. Un altro modo in cui il server potrebbe reagire è informare un’autorità centrale che ha un problema e lasciare che sia il sistema centrale a decidere come gestire il problema. Il sistema centrale può affrontare il problema in tutta sicurezza senza che l’automazione distrugga l’intera flotta.

Esistono diversi modi per implementare e rispondere ai controlli dello stato. Questa sezione descrive alcuni modelli che usiamo in Amazon.

Apertura in mancanza di segnale

Alcuni sistemi di bilanciamento del carico possono agire come un’autorità centrale smart. Quando un unico server non supera un controllo dello stato, il sistema di bilanciamento del carico smette di inviargli traffico. Ma quando sono tutti i server a non superare il controllo dello stato contemporaneamente, il sistema di bilanciamento del carico si apre per mancanza di segnale, consentendo il traffico a tutti i server. Possiamo utilizzare i sistemi di bilanciamento del carico per supportare l’implementazione sicura di un controllo dello stato delle dipendenze, magari includendone uno che interroga il suo database e controlla che i suoi processi di supporto non critici siano in esecuzione.

Ad esempio, Network Load Balancer di AWS si apre per mancanze di segnale se nessun server risulta integro. Inoltre, se tutti i server di una zona di disponibilità non sono integri, Network Load Balancer chiude il traffico per quella zona. (Per ulteriori informazioni sull’uso dei Network Load Balancer per i controlli dello stato, vedere la documentazione su Elastic Load Balancing.) Il nostro Application Load Balancer supporta anche l’apertura in mancanza di segnale, così come Amazon Route 53. (Per ulteriori informazioni sulla configurazione dei controlli dello stato con Route 53, vedere la documentazione su Route 53.)

Quando ci affidiamo al comportamento apertura in mancanza di segnale, ci assicuriamo di testare le modalità di guasto del controllo dello stato delle dipendenze. Ad esempio, consideriamo un servizio in cui i server si connettono ad un archivio dati condiviso. Se l’archivio dati diventa lento o risponde con un basso tasso di errore, i server potrebbero occasionalmente non superare i controlli della stato delle dipendenze. Questa condizione fa sì che i server entrino e escano dal servizio ma non fa scattare la soglia di apertura in mancanza di segnale. Ragionare e verificare gli errori parziali delle dipendenze con questi controlli dello stato è importante per evitare una situazione in cui un guasto potrebbe indurre i controlli dello stato approfonditi a peggiorare le cose.

Mentre l’apertura in mancanza di segnale è un comportamento utile, in Amazon tendiamo a essere scettici su ciò su cui non possiamo ragionare o testare completamente in tutte le situazioni. Non siamo ancora riusciti a trovare dimostrazioni generali che l’apertura in mancanza di segnale si innesca come previsto per tutti i tipi di sovraccarico, blocchi parziali o errori grigi in un sistema o nelle dipendenze di quel sistema. A causa di questa limitazione, i team di Amazon tendono a limitare i controlli dello stato dei sistemi di bilanciamento del carico ad azione rapida ai controlli dello stato locali e si affidano a sistemi centralizzati per reagire con attenzione a controlli dello stato delle dipendenze più approfonditi. Ciò non significa che non usiamo il comportamento di apertura in mancanza di segnale o dimostriamo che funziona in casi particolari. Ma quando la logica può agire rapidamente su un gran numero di server, siamo estremamente cauti nei confronti di questa logica.

Controlli dello stato senza interruttore

Permettere ai server di reagire ai propri problemi può sembrare il percorso di recupero più rapido e semplice per il ripristino. Tuttavia, è anche il percorso più rischioso se il server ha torto in merito al proprio stato o non vede il quadro completo di ciò che sta accadendo in tutta la flotta. Quando tutti i server della flotta prendono simultaneamente la stessa decisione sbagliata, ciò può causare guasti a cascata in tutti i servizi adiacenti. Questo rischio ci presenta tuttavia un compromesso. Se esiste una lacuna nel controllo e nel monitoraggio dello stato, un server potrebbe ridurre la disponibilità di un servizio fino a quando il problema non viene rilevato. Tuttavia, questo scenario evita un’interruzione completa del servizio a causa di un comportamento inaspettato di controllo dello stato sull’intera flotta.

Di seguito indichiamo le best practice che seguiamo per l’implementazione dei controlli dello stato quando non è presente un interruttore incorporato:

• Configurare il produttore del lavoro (sistema di bilanciamento del carico, thread di sondaggio della coda) per eseguire controlli di liveness e dello stato locale. I server vengono messi fuori servizio automaticamente dal sistema di bilanciamento del carico solo se hanno qualche problema che è decisamente locale per quel server, come ad esempio un disco difettoso.
• Configurare altri sistemi di monitoraggio esterni per eseguire i controlli dello stato delle dipendenze e il rilevamento delle anomalie. Questi sistemi potrebbero tentare di arrestare automaticamente le istanze o di segnalare un allarme o coinvolgere un operatore.

Quando creiamo sistemi che reagiscono automaticamente ai guasti del controllo dello stato delle dipendenze, dobbiamo costruire la giusta quantità di soglia per evitare che il sistema automatico intraprenda inaspettatamente azioni drastiche. I team di Amazon che gestiscono i server stateful come Amazon DynamoDB, Amazon S3 e Amazon Relational Database Service (Amazon RDS) hanno importanti requisiti di durata per la sostituzione del server. Hanno anche costruito cauti limitatori di frequenza e loop di feedback di controllo affinché l’automazione si fermi e coinvolga le persone quando vengono superate le soglie. Quando costruiamo tale automazione, dobbiamo essere sicuri di notare quando un server non supera un controllo dello stato delle dipendenze. Per alcuni parametri, ci affidiamo ai server affinché autosegnalino il proprio stato a un sistema di monitoraggio centrale. Per compensare i casi in cui il server è così danneggiato che non è in grado di segnalare la sua integrità, ci rivolgiamo attivamente anche a loro per controllare lo stato. 

Dare priorità all’integrità

Soprattutto in condizioni di sovraccarico, è importante che i server assegnino la priorità ai loro controlli dello stato rispetto al lavoro regolare. In questa situazione, non superare o rispondere lentamente ai controlli dello stato può peggiorare ulteriormente una pessima situazione di calo di tensione. 

Quando un server non supera un controllo dello stato del sistema di bilanciamento del carico, chiede di fatto al sistema di bilanciamento del carico di disattivarlo immediatamente e per un periodo di tempo non banale. Quando un unico server si blocca non è un problema, ma se sopravviene un aumento del traffico verso il servizio, l’ultima cosa da fare è ridurre le dimensioni del servizio. La messa fuori servizio dei server durante un sovraccarico può causare una spirale discendente. Forzando i server restanti a prendere ancora più traffico è probabile che questi si sovraccarichino ulteriormente, o anche che non superino un controllo dello stato, e che la flotta diminuisca ulteriormente.

Il problema non è che i server sovraccarichi restituiscono errori quando sono sovraccaricati. È che i server non rispondono in tempo alla richiesta di ping del sistema di bilanciamento del carico. Dopo tutto, i controlli dello stato del sistema di bilanciamento del carico sono configurati con timeout, proprio come qualsiasi altra chiamata di assistenza remota. I server con cali di tensione sono lenti a rispondere per una serie di ragioni, tra cui elevati conflitti per l’uso della CPU, lunghi cicli di raccolta dell’immondizia o semplicemente l’esaurimento dei thread dei dipendenti. I servizi devono essere configurati in modo da accantonare risorse per rispondere tempestivamente ai controlli dello stato anziché accettare troppe richieste aggiuntive.

Fortunatamente, esistono alcune semplici best practice di configurazione che seguiamo per contribuire a prevenire questo tipo di spirale discendente. Strumenti come iptables, e anche alcuni sistemi di bilanciamento del carico, supportano il concetto di “connessioni massime”. In questo caso, il sistema operativo (o sistema di bilanciamento del carico) limita il numero di connessioni al server affinché il processo del server non venga inondato da richieste simultanee che lo rallenterebbero.

Quando un servizio è preceduto da un proxy o da un sistema di bilanciamento del carico che supporta il numero massimo di connessioni, sembra logico che il numero di thread dei dipendenti sul server HTTP corrisponda al numero massimo di connessioni massime nel proxy. Tuttavia, questa configurazione imposterebbe il servizio per una spirale discendente durante un calo di tensione. Anche i controlli dello stato dei proxy necessitano di connessioni, per cui è importante che il pool dei dipendenti di un server sia abbastanza grande da poter soddisfare ulteriori richieste di controlli dello stato. I dipendenti inattivi sono economici, quindi tendiamo a configurarne di più: da una manciata di dipendenti in più al doppio del numero massimo di connessioni proxy configurate.

Un’altra strategia che adottiamo per dare priorità ai controlli sullo stato è che i server implementino il loro numero massimo di richieste simultanee. In questo caso, i controlli dello stato del sistema di bilanciamento del carico sono sempre consentiti, ma le richieste normali vengono rifiutate se il server sta già lavorando su qualche soglia. Le implementazioni relative ad Amazon vanno da un semplice semaforo in Java alla più complessa analisi delle tendenze nell’utilizzo della CPU.

Un altro modo per garantire che i servizi rispondano in tempo a una richiesta di ping per il controllo dello stato è quello di eseguire la logica del controllo dello stato delle dipendenze in un thread in background e di aggiornare un flag isHealthy che la logica ping controlla. In questo caso, i server rispondono prontamente ai controlli dello stato e il controllo dello stato delle dipendenze produce un carico prevedibile sul sistema esterno con cui interagisce. Quando i team lo fanno, sono molto cauti nel rilevare un guasto del thread del controllo dello stato. Se il thread in background si chiude, il server non rileva un guasto futuro del server (o ripristino!).

Bilanciare i controlli dello stato delle dipendenze e l’ambito dell’impatto

I controlli dello stato delle dipendenze sono interessanti perché agiscono come un test approfondito dell’integrità di un server. Purtroppo possono essere pericolosi perché una dipendenza può causare un guasto a cascata in tutto il sistema.

Possiamo ricavare alcune informazioni sulla gestione delle dipendenze nel controllo dello stato esaminando la nostra architettura orientata ai servizi in Amazon. Ogni servizio di Amazon è progettato per fare un limitato numero di cose: non esiste un monolite che fa tutto. Vi sono molte ragioni per cui ci piace costruire servizi in questo modo, tra cui l’innovazione più rapida con piccoli team e la riduzione dell’ambito dell’impatto se c’è un problema relativo a un servizio. Questo progetto architettonico può essere applicato anche ai controlli dello stato.

Quando un servizio chiama un altro servizio, sta prendendo una dipendenza da quel servizio. Se un servizio richiama la dipendenza soltanto a volte, potremmo considerare la dipendenza come una “dipendenza soft” poiché il servizio può ancora fare alcuni tipi di lavoro anche se non può comunicare con la dipendenza. Senza protezione con apertura in mancanza di segnale, l’implementazione di un controllo dello stato che verifichi una dipendenza trasforma tale dipendenza in una “dipendenza hard”. Se la dipendenza è disattiva, anche il servizio si disattiva, creando un guasto a cascata con maggiore ambito d’impatto.

Anche se separiamo le funzionalità in servizi diversi, ogni servizio serve probabilmente più API. A volte, le API del servizio hanno le proprie dipendenze. Se un’API subisce un impatto, preferiamo che il servizio continui a servire le altre API. Ad esempio, un servizio può essere sia un piano di controllo (come le cosiddette API CRUD su risorse a lunga durata) sia un piano dei dati (API supercritiche a throughput elevato). Vorremmo che le API del piano dei dati continuassero a funzionare anche se le API del piano di controllo hanno problemi a comunicare con le proprie dipendenze.

Analogamente, anche una singola API può comportarsi diversamente a seconda dell’input o dello stato dei dati. Un modello comune è l’API di lettura che interroga un database ma colloca nella cache le risposte a livello locale per un certo lasso di tempo. Se il database è inattivo, il servizio può ancora servire letture collocate nella cache fino a quando il database è di nuovo in linea. Il mancato superamento dei controlli dello stato se solo un percorso di codifica è non integro aumenta l’ambito dell’impatto di un problema di comunicazione con una dipendenza.

Il dibattito su quale dipendenza sottoporre al controllo dello stato solleva un’importante questione relativa ai compromessi tra microservizi e servizi relativamente monolitici. Raramente c’è una regola ben definita per il numero di unità o endpoint distribuibili in cui suddividere un servizio, ma le domande di “quali dipendenze sottoporre al controllo dello stato” e “un guasto poi aumentare l’ambito dell’impatto” sono punti di vista interessanti da utilizzare per determinare in che misura di micro o macro realizzare un servizio. 

Cose reali che non sono andate a buon fine con i controlli dello stato

Tutto questo può avere un senso in teoria, ma cosa succede ai sistemi nella pratica quando non superano i controlli dello stato? Abbiamo cercato modelli nelle cronologie dei clienti AWS e di tutta Amazon per illustrare il quadro complessivo. Abbiamo anche esaminato i fattori di compensazione: i tipi di “cintura e bretelle” che i team implementano per evitare che un punto debole di un controllo dello stato possa causare un problema diffuso.

Distribuzioni

Un modello di problemi del controllo dello stato riguarda le distribuzioni. Sistemi di distribuzione come AWS CodeDeploy effettuano il push del nuovo codice in un sottoinsieme della flotta alla volta, in attesa che un’ondata di distribuzione venga completata prima di passare alla successiva. Questo processo si basa sul fatto che i server riferiscano al sistema di distribuzione una volta che sono operativi con il nuovo codice. Se non riferiscono, il sistema di distribuzione si accorge che c’è qualcosa che non va nel nuovo codice e ripristina la situazione precedente alla distribuzione.

Lo script di distribuzione di avvio del servizio di base si limiterebbe semplicemente ad eseguire il fork del processo del server e a rispondere immediatamente “distribuzione completata” al sistema di distribuzione. Tuttavia ciò è pericoloso perché molte cose possono andare storte con il nuovo codice: il nuovo codice potrebbe restituire un errore subito dopo l’avvio, bloccarsi e non riuscire ad avviare l’ascolto su un socket del server, non riuscire a caricare la configurazione necessaria per elaborare le richieste correttamente o riscontrare un bug. Quando un sistema di distribuzione non è configurato per la verifica a fronte del controllo dello stato delle dipendenze, non si rende conto che sta eseguendo il push di una distribuzione errata. Prosegue la sua marcia distruggendo un server dopo l’altro.

Fortunatamente, nella pratica i team di Amazon implementano molteplici sistemi di mitigazione per evitare che questo scenario metta KO l’intera flotta. Una di queste misure di mitigazione consiste nel configurare allarmi che si attivano ogni volta che le dimensioni complessive della flotta sono troppo piccole o sono in funzione con un carico elevato o se c’è un alto tasso di latenza o di errore. Se uno qualsiasi di questi allarmi scatta, il sistema di distribuzione si arresta e viene ripristinata la situazione precedente alla distribuzione.

Un altro tipo di mitigazione consiste nell’utilizzo di distribuzioni per fasi. Anziché distribuire l’intera flotta in un’unica distribuzione, il servizio può essere configurato per distribuire un sottoinsieme, magari una zona di disponibilità, prima di mettersi in pausa ed eseguire una suite completa di test di integrazione con quella zona. Questo allineamento di distribuzione per zona di disponibilità è pratico perché i servizi sono già progettati per essere in grado di continuare a funzionare in caso di problemi con un’unica zona di disponibilità.

E, naturalmente, prima della distribuzione in produzione, i team di Amazon eseguono il push i quei cambiamenti attraverso ambienti di test ed eseguono test di integrazione automatizzati in grado di rilevare questo tipo di guasto. Tuttavia, possono esistere sottili e inevitabili differenze tra la produzione e gli ambienti di prova, per cui è importante unire molti livelli di sicurezza della distribuzione per cogliere tutti i tipi di problemi prima di causare un impatto sulla produzione. Sebbene i controlli dello stato siano importanti per proteggere i servizi da distribuzioni errate, ci assicuriamo di non fermarci qui. Pensiamo agli approcci “cintura e bretelle” che servono da rete di protezione per tutelare le flotte da questi e altri errori.

Processori asincroni

Un altro modello di guasto riguarda l’elaborazione asincrona dei messaggi, come un servizio che ottiene il lavoro interrogando una coda SQS o un flusso di Amazon Kinesis. A differenza dei sistemi che ricevono le richieste da sistemi di bilanciamento del carico, non c’è nulla che esegua automaticamente controlli dello stato per rimuovere i server dal servizio.

Quando i servizi non hanno controlli dello stato abbastanza approfonditi, i singoli server dei dipendenti in coda possono avere problemi come l’esaurimento dello spazio sui dischi o dei descrittori dei file. Questo problema non impedirà al server di estrarre lavoro dalla coda, ma impedirà al server di essere in grado di elaborare correttamente i messaggi. Questo problema ha causato un ritardo nell’elaborazione dei messaggi, dove il server difettoso estrae rapidamente il lavoro dalla coda e non riesce a gestirlo.

In questo tipo di situazioni, ci sono spesso diversi fattori di compensazione che aiutano a contenere l’impatto. Ad esempio, se un server non riesce a elaborare il messaggio che estrae da SQS, SQS lo riconsegna a un altro server dopo un timeout visibilità del messaggio configurato. La latenza end-to-end aumenta, ma i messaggi non vengono lasciati cadere. Un altro fattore di compensazione è un allarme che scatta quando ci sono troppi errori nell’elaborazione dei messaggi, avvertendo un operatore di indagare.

Esaurimento dello spazio sui dischi

Un’altra classe di guasti che vediamo è quando lo spazio nei dischi sui server si esaurisce, causando un errore sia dell’elaborazione che della registrazione. Questo errore comporta una lacuna nella visibilità del monitoraggio, poiché il server potrebbe non essere in grado di segnalare i propri guasti al sistema di monitoraggio.

Anche in questo caso, diversi controlli di attenuazione impediscono ai servizi di “muoversi alla cieca” e mitigano rapidamente l’impatto. I sistemi preceduti da un proxy, come Application Load Balancer o API Gateway, avranno il tasso di errore e i parametri di latenza prodotti da tale proxy. In quel caso, gli allarmi si attivano anche se il server non li segnala. Per i sistemi basati su code, servizi come Amazon Simple Queue Service (Amazon SQS) segnalano i parametri che indicano che l’elaborazione è ritardata per alcuni messaggi.

Ciò che queste soluzioni hanno in comune è che esistono diversi livelli di monitoraggio. Il server stesso segnala gli errori, ma lo fa anche un sistema esterno. Lo stesso principio è importante per i controlli dello stato. Un sistema esterno può testare l’integrità di un dato sistema più accuratamente di quanto possa testare se stesso. Questo è il motivo per cui con AWS Auto Scaling, i team configurano un sistema di bilanciamento del carico per effettuare controlli esterni dello stato dei ping.

I team scrivono anche il proprio sistema personalizzato di controllo dello stato per chiedere periodicamente a ogni server se sia integro e segnalare ad AWS Auto Scaling quando un server non è integro. Un’implementazione comune di questo sistema prevede una funzione Lambda che viene eseguita ogni minuto e testa l’integrità di ogni server. Questi controlli dello stato anche salvare il proprio stato tra un’esecuzione e l’altra, ad esempio, in DynamoDB in modo da non contrassegnare inavvertitamente troppi server contemporaneamente come non integri.

Zombie

Un altro modello di problemi include i server zombie. I server possono venire scollegati dalla rete per brevi periodi di tempo ma rimanere in esecuzione, oppure possono disattivarsi per periodi prolungati ed essere successivamente riavviati.

Quando i server zombie tornano in vita possono essere significativamente fuori sincrono con il resto della flotta, il che può causare seri problemi. Per esempio, se un server zombie esegue una versione software molto precedente e incompatibile, può causare errori quando cerca di interagire con un database con uno schema diverso o può usare la configurazione sbagliata.

Per affrontare il problema degli zombie, i sistemi rispondono spesso ai controlli dello stato con la versione software attualmente in esecuzione. Quindi un agente di monitoraggio centrale confronta le risposte su tutta la flotta per cercare una versione inaspettatamente superata in esecuzione e impedisce a questi server di tornare in servizio.

Conclusioni

I server, e il software in esecuzione su di essi, si bloccano per una marea di ragioni strane. L’hardware finisce per rompersi fisicamente. Come sviluppatori di software, succede che scriviamo qualche bug come quello che ho descritto sopra, che mette fuori servizio il software. Sono necessari diversi livelli di controllo, dai leggeri controlli di liveness al monitoraggio passivo dei parametri per server, per cogliere tutti i tipi di modalità di guasto inattesi.

Quando si verificano questi guasti, è importante rilevarli e mettere rapidamente fuori servizio i server interessati. Tuttavia, come per qualsiasi automazione della flotta, aggiungiamo limitatori di frequenza, soglie e interruttori che disattivano l’automazione e comportano l’intervento umano in situazioni di incertezza o in situazioni estreme. L’apertura in mancanza di segnale e la creazione di attori centralizzati sono strategie destinate a raccogliere i benefici di un controllo dello stato approfondito con la sicurezza dell’automazione a frequenza limitata.

Attività pratiche

Prova alcuni dei principi che hai imparato con un'attività pratica.


Informazioni sull'autore

David Yanacek è un ingegnere capo che lavora su AWS Lambda. David è stato sviluppatore software presso Amazon dal 2006 e ha lavorato in precedenza su Amazon DynamoDB e AWS IoT, oltre che su framework di servizi web interni e sui sistemi di automazione delle operazioni di flotta. Una delle attività preferite di David al lavoro è l’analisi dei log e il vaglio dei parametri operativi per scoprire come rendere sempre più fluida l’esecuzione dei sistemi nel tempo.

Timeout, nuovi tentativi e backoff con jitter