Immergersi a capofitto nei log

Quando sono entrato a fare parte dal team di Amazon dopo il college, uno dei miei primi esercizi di inserimento è stato quello di rendere operativo il server Web amazon.com sul mio desktop di sviluppatore. Non ci sono riuscito al primo tentativo, e non ero sicuro di cosa avevo fatto di sbagliato. Un collega disponibile mi ha suggerito di guardare i log per vedere cosa non funzionava. Per fare ciò, ha detto che avrei dovuto "cat the log file" (la parola inglese "cat" significa "gatto" in italiano, mentre in questo contesto è un modo utilizzato nel linguaggio informatico per dire "estrarre il file dei log"). Ero convinto che mi stessero giocando una specie di scherzo o che facessero una battuta sui gatti che non capivo. Ho adoperato Linux al college solo per compilare e usare il controllo del codice sorgente e per usare un editor di testo. Quindi non sapevo che "cat" è in realtà un comando per stampare un file sul terminale che potevo inserire in un altro programma per cercare schemi.

I miei colleghi mi hanno indicato strumenti come cat, grep, sed e awk. Dotato di questo nuovo set di strumenti, mi sono immerso nei log del server Web amazon.com sul mio desktop di sviluppatore. L'applicazione per server Web era già stata strumentata per emettere tutti i tipi di informazioni utili nei suoi log. Questo mi ha permesso di vedere quale configurazione mancava e che impediva l'avvio del server Web, che mostrava dove poteva essersi verificato un arresto anomalo o che indicava dove non riusciva a comunicare con un servizio a valle. Il sito Web è composto da molti pezzi in movimento ed era essenzialmente una black box per me all'inizio. Tuttavia, dopo essermi immerso a capofitto nel sistema, ho imparato a capire come funzionava il server e come interagire con le sue dipendenze semplicemente osservando l'output della strumentazione.

Perché strumentazione

Mentre mi sono spostato da un team all'altro negli anni in Amazon, ho scoperto che la strumentazione sia una lente inestimabile tramite la quale io e altri in Amazon guardiamo per scoprire come funziona un sistema. Tuttavia, la strumentazione è utile per qualcosa di più della semplice conoscenza dei sistemi. È il nucleo della cultura operativa di Amazon. La grande strumentazione ci aiuta a vedere l'esperienza che stiamo offrendo ai nostri clienti.
 
Questa attenzione alle prestazioni operative si espande in tutta l'azienda. All'interno dei servizi associati ad amazon.com, una maggiore latenza si traduce in una scarsa esperienza di acquisto e quindi in una riduzione dei tassi di conversione. Per i clienti che utilizzano AWS, essi dipendono dall'elevata disponibilità e dalla bassa latenza dei servizi AWS.
 
Ad Amazon, non consideriamo solo la latenza media. Ci concentriamo ancora più da vicino sugli outlier di latenza, come il 99,9° e il 99,99° percentile. Questo perché se una richiesta su 1.000 o 10.000 è lenta, è comunque un'esperienza negativa. Scopriamo che quando riduciamo l'elevata latenza percentile in un sistema, i nostri sforzi hanno l'effetto collaterale di ridurre la latenza mediana. Al contrario, scopriamo che quando riduciamo la latenza mediana, ciò riduce meno frequentemente l'elevata latenza percentile.
 
L'altro motivo per cui ci concentriamo sull'elevata latenza percentile è che un'elevata latenza in un servizio può avere un effetto moltiplicatore su altri servizi. Amazon si basa su un'architettura orientata ai servizi. Molti servizi collaborano tra loro per ottenere risultati, ad esempio il rendering di una pagina Web su amazon.com. Di conseguenza, un aumento della latenza di un servizio in profondità nella catena delle chiamate, anche se l'aumento è in un percentile elevato, ha un grande effetto a catena sulla latenza sperimentata dall'utente finale.
 
I grandi sistemi di Amazon sono costituiti da numerosi servizi cooperanti. Ogni servizio è sviluppato e gestito da un singolo team (i "servizi" grandi sono composti da più servizi o componenti dietro le quinte). Il team al quale appartiene un servizio è noto come proprietario del servizio. Ogni membro di quel team ragiona come un proprietario e un operatore del servizio, indipendentemente dal fatto che quel membro sia uno sviluppatore di software, un ingegnere di rete, un manager o che svolga qualsiasi altro ruolo. In quanto proprietari, i team fissano obiettivi sulle prestazioni operative di tutti i servizi associati. Ci assicuriamo inoltre di avere la visibilità delle operazioni del servizio per garantire il raggiungimento di tali obiettivi, la gestione di eventuali problemi e il mantenimento di obiettivi ancora più elevati l'anno successivo. Per fissare obiettivi e ottenere tale visibilità, i team devono strumentare i sistemi.
La strumentazione ci consente inoltre di rilevare e rispondere tatticamente agli eventi operativi.
 
La strumentazione inserisce i dati in pannelli di controllo operativi, in modo che gli operatori possano visualizzare le metriche in tempo reale. Fornisce inoltre i dati agli allarmi, che attivano e coinvolgono gli operatori quando il sistema si comporta in modo imprevisto. Gli operatori utilizzano l'output dettagliato della strumentazione per diagnosticare rapidamente perché le cose sono andate male. Da lì, possiamo mitigare il problema e tornare più tardi per evitare che il problema si ripeta. Senza una buona strumentazione in tutto il codice, dedichiamo tempo prezioso alla diagnosi dei problemi.

Che cosa misurare

Per gestire i servizi secondo i nostri elevati standard di disponibilità e latenza, noi, in quanto proprietari dei servizi, dobbiamo misurare il comportamento dei nostri sistemi.

Per ottenere i dati di telemetria necessari, i proprietari dei servizi misurano le prestazioni operative da più punti per ottenere più prospettive su come le cose si comportano end-to-end. Questo è complicato anche in un'architettura semplice. Considera un servizio che i clienti chiamano tramite un sistema di bilanciamento del carico: il servizio comunica con una cache remota e un database remoto. Vogliamo che ogni componente emetta metriche sul suo comportamento. Vogliamo anche metriche su come ogni componente percepisce il comportamento di altri componenti. Quando le metriche di tutte queste prospettive vengono riunite, il proprietario di un servizio può rintracciare rapidamente l'origine dei problemi e investigare per trovare la causa.

Molti servizi AWS forniscono automaticamente informazioni operative sulle tue risorse. Ad esempio, Amazon DynamoDB fornisce ad Amazon CloudWatch le metriche sui tassi di successo e di errore e sulla latenza, così come vengono misurati dal servizio. Tuttavia, quando costruiamo sistemi che utilizzano questi servizi, abbiamo bisogno di molta più visibilità sul comportamento dei nostri sistemi. La strumentazione richiede un codice esplicito che registra la durata delle attività, la frequenza con cui vengono esercitati determinati percorsi del codice, i metadati su ciò su cui l'attività stava lavorando e su quali parti delle attività hanno avuto esito positivo o negativo. Se un team non aggiungesse una strumentazione esplicita, sarebbe costretto a gestire il proprio servizio come una black box.

Ad esempio, se abbiamo implementato un'operazione API del servizio che ha recuperato le informazioni sul prodotto in base all'ID prodotto, il codice potrebbe apparire come nell'esempio seguente. Questo codice cerca le informazioni sul prodotto in una cache locale, seguita da una cache remota, seguita da un database:

public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {

  // check our local cache
  ProductInfo info = localCache.get(request.getProductId());
  
  // check the remote cache if we didn't find it in the local cache
  if (info == null) {
    info = remoteCache.get(request.getProductId());
	
	localCache.put(info);
  }
  
  // finally check the database if we didn't have it in either cache
  if (info == null) {
    info = db.query(request.getProductId());
	
	localCache.put(info);
	remoteCache.put(info);
  }
  
  return info;
}

Se gestissi questo servizio, avrei bisogno di molta strumentazione in questo codice per essere in grado di comprenderne il comportamento in produzione. Avrei bisogno della capacità di risolvere le richieste non riuscite o lente e di monitorare le tendenze e i segni che le diverse dipendenze siano sottovalutate o che si comportino male. Ecco lo stesso codice, annotato con alcune delle domande a cui dovrei essere in grado di rispondere sul sistema di produzione nel suo insieme o per una particolare richiesta:

public GetProductInfoResponse getProductInfo(GetProductInfoRequest request) {

  // Which product are we looking up?
  // Who called the API? What product category is this in?

  // Did we find the item in the local cache?
  ProductInfo info = localCache.get(request.getProductId());
  
  if (info == null) {
    // Was the item in the remote cache?
    // How long did it take to read from the remote cache?
    // How long did it take to deserialize the object from the cache?
    info = remoteCache.get(request.getProductId());
	
    // How full is the local cache?
    localCache.put(info);
  }
  
  // finally check the database if we didn't have it in either cache
  if (info == null) {
    // How long did the database query take?
    // Did the query succeed? 
    // If it failed, is it because it timed out? Or was it an invalid query? Did we lose our database connection?
    // If it timed out, was our connection pool full? Did we fail to connect to the database? Or was it just slow to respond?
    info = db.query(request.getProductId());
	
    // How long did populating the caches take? 
    // Were they full and did they evict other items? 
    localCache.put(info);
    remoteCache.put(info);
  }
  
  // How big was this product info object? 
  return info;
}

Il codice per rispondere a tutte queste domande (e ad altre ancora) è un po' più lungo dell'attuale logica di business. Alcune librerie possono aiutare a ridurre la quantità di codice della strumentazione, ma lo sviluppatore deve ancora porre le domande sulla visibilità di cui le librerie avranno bisogno, e quindi lo sviluppatore deve essere intenzionale sul cablaggio nella strumentazione.

Quando si risolve un problema di una richiesta che scorre attraverso un sistema distribuito, può essere difficile capire cosa sia successo se si guarda a quella richiesta solo in base a una interazione. Per mettere insieme il puzzle, troviamo utile riunire in un unico punto tutte le misurazioni su tutti questi sistemi. Prima di poterlo fare, ogni servizio deve essere strumentato per registrare un ID traccia per ogni attività e per propagare tale ID traccia a ciascun altro servizio che collabora a quell'attività. La raccolta della strumentazione tra i sistemi per un determinato ID traccia può essere eseguita sia dopo il fatto, secondo necessità, sia quasi in tempo reale utilizzando un servizio come AWS X-Ray.

Eseguire il drill-down

La strumentazione consente la risoluzione dei problemi a più livelli, dallo sguardo alle metriche per vedere se ci sono anomalie troppo sottili per innescare allarmi, fino all'esecuzione di un'indagine per scoprire la causa di tali anomalie.

Al livello più alto, la strumentazione è aggregata in metriche che possono attivare allarmi ed essere visualizzate su pannelli di controllo. Questi metriche aggregate consentono agli operatori di monitorare il tasso complessivo delle richieste, la latenza delle chiamate del servizio e i tassi di errore. Questi allarmi e metriche ci informano di anomalie o modifiche che devono essere esaminate.

Dopo aver visto un'anomalia, dobbiamo capire perché quell'anomalia si sta verificando. Per rispondere a questa domanda, contiamo su metriche rese disponibili da ancora più strumenti. Instrumentando il tempo necessario per eseguire le varie parti per servire una richiesta, possiamo vedere quale parte dell'elaborazione è più lenta del normale o innesca errori più spesso.

Sebbene i timer e le metriche aggregati possano aiutarci a escludere le cause o a evidenziare un'area di indagine, non sempre forniscono una spiegazione completa. Ad esempio, potremmo essere in grado di capire dalle metriche che gli errori provengono da una particolare operazione API, ma le metriche potrebbero non rivelare abbastanza dettagli sul motivo per cui tale operazione fallisce. A questo punto, esaminiamo i dati di log non elaborati e dettagliati emessi dal servizio per quella finestra temporale. I log non elaborati mostrano quindi l'origine del problema, ovvero il particolare errore che si sta verificando o particolari aspetti della richiesta che stanno attivando un caso edge.

Come strumentiamo

La strumentazione richiede la codifica. Significa che quando stiamo implementando nuove funzionalità, dobbiamo dedicare del tempo per aggiungere un codice addizionale per indicare cosa è successo, se ha avuto successo o meno e quanto tempo ci è voluto. Poiché la strumentazione è un'attività di codifica così comune, nel corso degli anni sono emerse in Amazon pratiche per affrontare modelli comuni: la standardizzazione per librerie di strumentazione comuni e la standardizzazione per report metrici strutturati basati su log.

La standardizzazione delle librerie di strumentazione delle metriche aiuta gli autori delle librerie a dare ai relativi consumatori visibilità sul funzionamento della libreria. Ad esempio, i client HTTP comunemente utilizzati si integrano con queste librerie comuni, quindi, se un team di un servizio implementa una chiamata remota a un altro servizio, riceverà automaticamente la strumentazione su tale chiamata.

Quando un'applicazione strumentata viene eseguita e svolge un lavoro, i dati di telemetria risultanti vengono scritti in un file di log strutturato. Generalmente, viene emesso come una voce di log per "unità di lavoro", indipendentemente dal fatto che si tratti di una richiesta a un servizio HTTP o di un messaggio estratto da una coda.

Ad Amazon, le misurazioni nell'applicazione non sono aggregate e occasionalmente scaricate su un sistema di aggregazione delle metriche. Tutti i timer e i contatori per ogni lavoro sono scritti in un file di log. Da lì, i log vengono elaborati e le metriche aggregate vengono calcolate successivamente da qualche altro sistema. In questo modo, finiamo con tutto, dalle metriche operative aggregate di alto livello ai dati dettagliati sulla risoluzione dei problemi al livello della richiesta, il tutto con un unico approccio alla strumentazione del codice. Ad Amazon prima registriamo e poi produciamo metriche aggregate.

Strumentazione tramite registrazione

Più comunemente strumentiamo i nostri servizi per emettere due tipi di dati di log: dati richiesta e dati di debug. I dati di log della richiesta sono in genere rappresentati come una singola voce di log strutturata per ogni unità di lavoro. Questi dati contengono proprietà sulla richiesta e su chi ha effettuato la richiesta, a cosa serviva la richiesta, i contatori della frequenza con cui sono avvenute le cose e i timer della relativa durata. Il log delle richieste funge da log di controllo e da traccia per tutto ciò che è successo nel servizio. I dati di debug includono dati non strutturati o vagamente strutturati delle righe di debug emesse dall'applicazione. In genere, si tratta di voci di log non strutturate come errore Log4j o avvisi di righe di log. Ad Amazon questi due tipi di dati vengono generalmente emessi in file di log separati, in parte per motivi storici, ma anche perché può essere conveniente eseguire l'analisi dei log su un formato omogeneo delle voci di log.

Agenti come quello di CloudWatch Logs elaborano entrambi i tipi di dati di log in tempo reale e inviano i log a CloudWatch Logs. A sua volta, CloudWatch Logs produce metriche aggregate sul servizio in tempo quasi reale. Gli allarmi di Amazon CloudWatch leggono queste metriche aggregate e attivano allarmi.

Anche se può essere costoso registrare così tanti dettagli su ogni richiesta, ad Amazon riteniamo che sia incredibilmente importante farlo. Dopotutto, dobbiamo esaminare i blip di disponibilità, i picchi di latenza e i problemi segnalati dai clienti. Senza log dettagliati, non possiamo fornire risposte ai clienti e non saremo in grado di migliorare il loro servizio.  

Entrare nei dettagli

Il tema del monitoraggio e degli allarmi è vasto. In questo articolo non tratteremo argomenti come l'impostazione e l'ottimizzazione delle soglie di allarme, l'organizzazione di pannelli di controllo operativi, la misurazione delle prestazioni sia sul lato server che sul lato client, l'esecuzione continua di applicazioni "canary" e la scelta del sistema giusto da utilizzare per aggregare le metriche e analizzare i log.

Questo articolo si concentra sulla necessità di strumentare le nostre applicazioni per produrre i giusti dati di misurazione non elaborati. Descriveremo le cose che i team di Amazon si sforzano di includere (o evitare) nella strumentazione delle loro applicazioni.

Best practice per il log delle richieste

In questa sezione descriverò le buone abitudini che abbiamo imparato nel tempo ad Amazon sulla registrazione dei nostri dati strutturati "per unità di lavoro". Un log che soddisfa questi criteri contiene contatori che rappresentano la frequenza con cui accadono le cose, timer contenenti il tempo impiegato e proprietà che includono metadati su ciascuna unità di lavoro.

Come registriamo

Emetti una voce di log delle richieste per ogni unità di lavoro. Un'unità di lavoro è in genere una richiesta ricevuta dal nostro servizio o un messaggio che estrae da una coda. Scriviamo una voce di log del servizio per ogni richiesta ricevuta dal nostro servizio. Non uniamo più unità di lavoro insieme. In questo modo, quando risolviamo i problemi di una richiesta non riuscita, abbiamo una singola voce di log da esaminare. Questa voce contiene i parametri di input rilevanti sulla richiesta per vedere cosa stava tentando di fare, informazioni su chi era l'intermediario e tutte le informazioni sulla temporizzazione e sul contatore in un unico posto.
Non emettere più di una voce di log delle richieste per una determinata richiesta. Nell'implementazione di un servizio non bloccante, potrebbe sembrare conveniente emettere una voce di log separata per ogni fase di una pipeline di elaborazione. Invece, abbiamo più successo nella risoluzione dei problemi di questi sistemi eseguendo il plumbing di un handle su un singolo "oggetto metriche" tra le fasi della pipeline e quindi serializzando le metriche come unità dopo che tutte le fasi sono state completate. La presenza di più voci di log per unità di lavoro rende più difficile l'analisi dei log e aumenta di un moltiplicatore i costi generali di registrazione già alti. Se stiamo scrivendo un nuovo servizio non bloccante, proviamo a pianificare le metriche di registrazione del ciclo di vita in anticipo, perché diventa molto difficile eseguire il refactoring e correggerlo in seguito.
Suddividi le attività di lunga durata in più voci di log. Contrariamente alla precedente raccomandazione, se abbiamo un'attività di flusso di lavoro di lunga durata, di più minuti o di più ore, possiamo decidere di emettere periodicamente una voce di log separata in modo da poter determinare se sono stati fatti progressi o dove sta rallentando.
Registra i dettagli della richiesta prima di fare operazioni come la convalida. Riteniamo importante per la risoluzione dei problemi e per la registrazione di controllo registrare sufficienti informazioni sulla richiesta in modo da sapere cosa stava cercando di realizzare. Abbiamo anche scoperto che è importante registrare queste informazioni il prima possibile, prima che la richiesta abbia la possibilità di essere respinta dalla logica di convalida, autenticazione o limitazione delle richieste. Se stiamo registrando informazioni dalla richiesta in arrivo, ci assicuriamo di purificare l'input (codificare, escape e troncare) prima di registrarlo. Ad esempio, non vogliamo includere stringhe lunghe di 1 MB nella nostra voce di log del servizio se l'intermediario ne ha passata una. Ciò rischierebbe di riempire i nostri dischi e di costarci più del previsto per lo storage dei log. Un altro esempio di purificazione è quello di filtrare i caratteri di controllo ASCII o le sequenze di escape relative al formato del log. Potrebbe essere fonte di confusione se l'intermediario passasse da solo una voce di log del servizio e se fosse in grado di iniettarla nei nostri log! Vedi anche: https://xkcd.com/327/
Pianifica un modo per registrare con un livello maggiore di dettaglio. Per la risoluzione di alcuni tipi di problemi, il log non avrà abbastanza dettagli sulle richieste problematiche per capire perché non siano riuscite. Tali informazioni potrebbero essere disponibili nel servizio, ma il volume delle informazioni potrebbe essere troppo elevato per giustificare la registrazione per tutto il tempo. Può essere utile disporre di una manopola di configurazione che è possibile comporre per aumentare temporaneamente il livello di dettaglio del log mentre si esamina un problema. È possibile attivare la manopola su singoli host o per singoli clienti o a una frequenza di campionamento in tutta la flotta. È importante ricordarsi di riabbassare la manopola una volta terminata l'operazione.
Mantieni i nomi delle metriche brevi (ma non troppo brevi). Amazon ha utilizzato la stessa serializzazione del log del servizio per oltre 15 anni. In questa serializzazione, ogni nome di contatore e di timer viene ripetuto in testo semplice in ogni voce di log del servizio. Per ridurre al minimo i costi generali di registrazione, utilizziamo nomi di timer brevi ma descrittivi. Amazon sta iniziando ad adottare nuovi formati di serializzazione basati su un protocollo di serializzazione binario noto come Amazon Ion. In definitiva, è importante scegliere un formato che gli strumenti di analisi dei log possano comprendere e che sia anche il più efficiente possibile per serializzare, deserializzare e archiviare.
Assicurati che i volumi di log siano sufficientemente grandi per gestire la registrazione al throughput massimo. Eseguiamo il test di carico dei nostri servizi al massimo carico sostenuto (o persino sovraccarico) per ore. Quando il nostro servizio gestisce il traffico in eccesso, dobbiamo assicurarci che esso disponga ancora delle risorse per spedire i log off-box alla velocità con cui producono nuove voci di log, altrimenti i dischi si riempiranno. È inoltre possibile configurare la registrazione in modo che avvenga su una partizione del file system diversa dalla partizione radice, in modo che il sistema non si rompa di fronte a una registrazione eccessiva. Discuteremo di altre mitigazioni per questo in seguito, ad esempio l'utilizzo del campionamento dinamico proporzionale al throughput, ma indipendentemente dalla strategia, è fondamentale eseguire il test.
Considera il comportamento del sistema quando i dischi si riempiono. Quando il disco di un server si riempie, il server non può accedere al disco. Quando ciò accade, un servizio dovrebbe smettere di accettare richieste o eliminare i log e continuare a funzionare senza monitoraggio? Operare senza la registrazione è rischioso, quindi testiamo i sistemi per garantire che vengano rilevati server con dischi quasi completi.
Sincronizza gli orologi. La nozione di "tempo" nei sistemi distribuiti è notoriamente complicata. Non facciamo affidamento sulla sincronizzazione dell'orologio negli algoritmi distribuiti, ma è necessaria per dare un senso ai log. Eseguiamo daemon come Chrony o ntpd per la sincronizzazione dell'orologio e monitoriamo i server per la deviazione dell'orologio. Per rendere questo più facile, consulta l'Amazon Time Sync Service.
Emetti conteggi zero per le metriche di disponibilità. I conteggi degli errori sono utili, ma anche le percentuali di errore possono essere utili. Per instrumentare una metrica della "percentuale di disponibilità", una strategia che abbiamo trovato utile è quella di emettere un 1 quando la richiesta ha esito positivo e uno 0 quando la richiesta ha esito negativo. Quindi la statistica "media" della metrica risultante è la percentuale di disponibilità. L'emissione intenzionale di un punto dati 0 può essere utile anche in altre situazioni. Ad esempio, se un'applicazione esegue l'elezione del leader, emettere periodicamente un 1 quando un processo è il leader e uno 0 quando il processo non è il leader può essere utile per monitorare l'integrità dei follower. In questo modo, se un processo smette di emettere uno 0, è più facile sapere che qualcosa al suo interno si è rotto e non sarà in grado di subentrare se succede qualcosa al leader.

Che cosa registriamo

Registra la disponibilità e la latenza di tutte le dipendenze. Lo abbiamo trovato particolarmente utile nel rispondere alle domande su "perché la richiesta è stata lenta?" o "perché la richiesta non è riuscita?" Senza questo log, possiamo solo confrontare i grafici delle dipendenze con i grafici di un servizio e indovinare se un picco nella latenza di un servizio dipendente abbia portato al fallimento di una richiesta che stiamo esaminando. Molti framework dei servizi e dei client descrivono le metriche automaticamente, ma altri framework (come SDK AWS, ad esempio), richiedono strumenti manuali.
Dividi le metriche di dipendenza per chiamata, per risorsa, per codice di stato, ecc. Se interagiamo più volte con la stessa dipendenza nella stessa unità di lavoro, includiamo le metriche relative a ciascuna chiamata separatamente e chiariamo con quale risorsa interagiva ciascuna richiesta. Ad esempio, nel chiamare Amazon DynamoDB, alcuni team hanno trovato utile includere metriche di tempistica e di latenza per tabella, nonché per codice di errore e persino per il numero di tentativi. Ciò semplifica la risoluzione dei casi in cui il servizio era lento a causa di tentativi falliti di controllo condizionale. Queste metriche hanno anche rivelato casi in cui gli aumenti di latenza percepiti dal client erano in realtà dovuti alla limitazione dei tentativi o alla paginazione attraverso un set di risultati e non alla perdita di pacchetti o alla latenza di rete.
Registra le profondità della coda di memoria quando vi si accede. Se una richiesta interagisce con una coda e ne stiamo estraendo un oggetto o inserendo qualcosa, registriamo la profondità della coda corrente nell'oggetto metriche mentre ci siamo. Per le code in memoria, questa informazione è molto economica da ottenere. Per le code distribuite, questi metadati potrebbero essere disponibili gratuitamente in risposta alle chiamate API. Questa registrazione aiuterà a trovare backlog e fonti di latenza in futuro. Inoltre, quando estraiamo le cose da una coda, misuriamo quanto tempo sono rimaste nella coda. Ciò significa che dobbiamo aggiungere la nostra metrica "tempo di accodamento" al messaggio prima di accodarlo in primo luogo.
Aggiungi un contatore addizionale per ogni motivo di errore. Si consiglia l'aggiunta di codice che conteggi il motivo dell'errore specifico per ogni richiesta non riuscita. Il log dell'applicazione includerà informazioni che hanno portato all'errore e un messaggio di eccezione dettagliato. Tuttavia, abbiamo inoltre trovato utile vedere le tendenze nei motivi di errore nelle metriche nel tempo senza dover estrarre tali informazioni dal log dell'applicazione. È utile iniziare con una metrica separata per ogni classe di errore di eccezione.
Organizza gli errori per categoria di causa. Se tutti gli errori sono raggruppati nella stessa metrica, la metrica diventa rumorosa e inutile. Come minimo, abbiamo trovato importante separare gli errori che erano per "colpa del client" dagli errori che erano per "colpa del server". Oltre a ciò, un'ulteriore suddivisione potrebbe essere utile. Ad esempio, in DynamoDB i client possono effettuare richieste condizionate di scrittura che restituiscono un errore se l'elemento che stanno modificando non corrisponde alle condizioni preliminari della richiesta. Questi errori sono intenzionali e ci aspettiamo che accadano di tanto in tanto. Considerando che gli errori di "richiesta non valida" da parte dei client sono molto probabilmente bug che dobbiamo correggere.
Registra metadati importanti sull'unità di lavoro. In un log metrico strutturato, includiamo anche sufficienti metadati sulla richiesta in modo da poter determinare in seguito da chi proveniva la richiesta e che cosa stava tentando di fare. Ciò include i metadati che un cliente si aspetterebbe che avessimo nel log quando si presenta con problemi. Ad esempio, DynamoDB registra il nome della tabella con cui una richiesta interagisce e i metadati, quali se l'operazione di lettura sia stata una lettura coerente o meno. Tuttavia, non registra i dati archiviati o recuperati dal database.
Proteggi i log con controllo degli accessi e crittografia. Poiché i log contengono un certo livello di informazioni riservate, adottiamo misure per proteggere e garantire la sicurezza di tali dati. Queste misure includono la crittografia dei log, la limitazione dell'accesso agli operatori che si occupano della risoluzione dei problemi e il confronto regolare di tale accesso con una metrica storica o "baseline".
Evita di inserire informazioni eccessivamente sensibili nei log. I log devono contenere alcune informazioni sensibili per essere utili. In Amazon, riteniamo importante che i log includano informazioni sufficienti per sapere da chi proviene una determinata richiesta, ma tralasciamo informazioni eccessivamente sensibili, come parametri di richiesta che non influiscono sul routing o sul comportamento dell'elaborazione della richiesta. Ad esempio, se il codice analizza un messaggio del cliente e l'analisi non riesce, è importante non registrare il payload per proteggere la privacy dei clienti, per quanto difficile questo possa rendere la risoluzione dei problemi in un secondo momento. Utilizziamo gli strumenti per prendere decisioni su ciò che può essere registrato con consenso esplicito anziché con rifiuto esplicito, per impedire la registrazione di un nuovo parametro sensibile aggiunto in seguito. Servizi come Amazon API Gateway consentono di configurare quali dati includere nel loro log di accesso, che funge da buon meccanismo di consenso esplicito.
Registra un ID traccia e propagalo nelle chiamate back-end. Una determinata richiesta del cliente coinvolgerà probabilmente molti servizi che lavorano in collaborazione. Può trattarsi di un minimo di due o tre servizi per molte richieste AWS, fino a molti più servizi per le richieste amazon.com. Per dare senso a ciò che è accaduto durante la risoluzione dei problemi di un sistema distribuito, propaghiamo lo stesso ID traccia tra questi sistemi in modo da poter allineare i log dei vari sistemi per vedere dove si siano verificati gli errori. Un ID traccia è una sorta di ID richiesta meta che viene stampato dal servizio "front door" su un'unità di lavoro distribuita; il servizio "front door" rappresenta il punto di partenza per l'unità di lavoro. AWS X-Ray è un servizio che aiuta fornendo una parte di questa propagazione. Abbiamo ritenuto importante passare la traccia alla nostra dipendenza. In un ambiente multi-thread, è molto difficile e soggetto a errori che il framework esegua questa propagazione per nostro conto, quindi abbiamo preso l'abitudine di passare gli ID traccia e altri contenuti della richiesta (come un oggetto metriche!) nelle firme del metodo. Abbiamo anche trovato utile distribuire un oggetto di contesto nelle firme del metodo, in modo da non dover effettuare il refactoring quando troviamo un modello simile da passare in futuro. Per i team AWS, non si tratta solo di risolvere i problemi dei nostri sistemi, si tratta anche dei clienti che devono risolvere i loro problemi. I clienti si affidano alle tracce AWS X-Ray trasmesse tra i servizi AWS quando interagiscono tra loro per conto del cliente. Ciò richiede di propagare gli ID traccia di AWS X-Ray dei clienti tra i servizi in modo che possano ottenere dati di traccia completi.
Registra diverse metriche di latenza in base al codice dello stato e alla dimensione. Gli errori sono spesso rapidi, come l'accesso negato, la limitazione delle richieste e le risposte agli errori di convalida. Se le richieste dei client iniziano a essere limitate a un ritmo elevato, la latenza potrebbe apparire ingannevolmente buona. Per evitare questo inquinamento delle metriche, registriamo un timer separato per le risposte riuscite e ci concentriamo su quella metrica nei nostri pannelli di controllo e negli allarmi invece di utilizzare una metrica temporale generica. Allo stesso modo, se esiste un'operazione che può essere più lenta a seconda della dimensione dell'input o della risposta, consigliamo l'emissione di una metrica di latenza che è classificata, come SmallRequestLatency e LargeRequestLatency. Inoltre, garantiamo che la nostra richiesta e le nostre risposte siano adeguatamente limitate per evitare complesse modalità di brownout e di guasto, ma anche in un servizio attentamente progettato, questa tecnica per le metriche del bucket può isolare il comportamento del cliente e mantenere il rumore distraente fuori dai pannelli di controllo.

Best practice per il log dell'applicazione

Questa sezione descrive le buone abitudini che abbiamo appreso in Amazon sulla registrazione dei dati di log di debug non strutturati.

Mantieni il log dell'applicazione libero da spam. Anche se potremmo avere istruzioni INFO e DEBUG a livello di log sul percorso della richiesta per aiutare con lo sviluppo e il debug in ambienti di test, consigliamo di disabilitare questi livelli di log in produzione. Invece di fare affidamento sul log dell'applicazione per le informazioni di traccia delle richieste, pensiamo al log del servizio come a una posizione per le informazioni di traccia dove possiamo facilmente produrre metriche e vedere tendenze aggregate nel tempo. Tuttavia, non esiste una regola in bianco e nero qui. Il nostro approccio è quello di rivedere continuamente i nostri log per verificare se sono troppo rumorosi (o non abbastanza rumorosi) e regolare i livelli dei log nel tempo. Ad esempio, quando analizziamo i log troviamo spesso istruzioni di log troppo rumorose o metriche che vorremmo avere. Fortunatamente, questi miglioramenti sono spesso facili da apportare, quindi abbiamo preso l'abitudine di archiviare elementi di backlog di follow-up rapido per mantenere puliti i nostri log.
Includi l'ID richiesta corrispondente. Quando risolviamo un errore nel log dell'applicazione, spesso vogliamo vedere i dettagli sulla richiesta o sull'intermediario che ha causato l'errore. Se entrambi i log contengono lo stesso ID richiesta, possiamo facilmente passare da un log all'altro. Le librerie di registrazione dell'applicazione scriveranno l'ID richiesta corrispondente se configurato correttamente e l'ID richiesta è impostato come un ThreadLocal. Se un'applicazione è multithread, si consiglia di prestare particolare attenzione a impostare l'ID richiesta corretto quando un thread inizia a lavorare su una nuova richiesta.
Stabilisci limiti di velocità per lo spam di errori nel log dell'applicazione. In genere, un servizio non emette molto nel log dell'applicazione, ma se improvvisamente inizia a mostrare un grande volume di errori, potrebbe improvvisamente iniziare a scrivere un'alta percentuale di voci di log molto grandi con tracce di stack. Un modo che abbiamo scoperto per proteggerci è limitare la frequenza con cui un determinato logger effettuerà la registrazione.
Privilegia le stringhe di formato rispetto al formato di stringa # o alla concatenazione di stringhe. Le operazioni API precedenti del log dell'applicazione accettano un singolo messaggio di stringa anziché l'API stringa di formato varargs di log4j2. Se il codice è dotato di istruzioni DEBUG, ma la produzione è configurata a livello di ERRORE, è possibile sprecare il lavoro per la formattazione delle stringhe di messaggi DEBUG che vengono ignorate. Alcune operazioni dell'API di registrazione supportano il passaggio di oggetti arbitrari i cui metodi toString() verranno chiamati solo se la voce di log verrà scritta.
Registra gli ID richiesta da chiamate di servizio non riuscite. Se viene chiamato un servizio e restituisce un errore, è probabile che il servizio abbia restituito un ID richiesta. Abbiamo trovato utile includere l'ID richiesta nel nostro log in modo che, se avessimo bisogno di dare seguito a quel proprietario del servizio, avremmo modo di trovare facilmente le relative voci di log nel servizio corrispondente. Gli errori di timeout lo rendono difficile perché il servizio potrebbe non aver ancora restituito un ID richiesta o una libreria client potrebbe non averlo analizzato. Tuttavia, se abbiamo un ID richiesta di ritorno dal servizio, lo registriamo.

Best practice per servizi con throughput elevato

Per la stragrande maggioranza dei servizi su Amazon, la registrazione di ogni richiesta non impone un costo irragionevole. Servizi di throughput più elevati entrano in un'area più grigia, ma spesso registriamo comunque ogni richiesta. Ad esempio, è naturale presumere che DynamoDB, che serve al massimo oltre 20 milioni di richieste al secondo del solo traffico interno di Amazon, non registrerebbe molto, ma in effetti registra ogni richiesta per la risoluzione dei problemi e per motivi di audit e conformità. Ecco alcuni suggerimenti di alto livello che utilizziamo ad Amazon per rendere la registrazione più efficiente con un throughput per host più elevato:

Campionamento dei log. Invece di scrivere ogni voce, considera la possibilità di scrivere ogni N voci. Ogni voce include anche quante voci sono state ignorate in modo che i sistemi di aggregazione delle metriche possano stimare il volume di log reale nelle metriche che calcola. Altri algoritmi di campionamento come reservoir sampling forniscono campioni più rappresentativi. Altri algoritmi danno la priorità agli errori di registrazione o alle richieste lente rispetto alle richieste veloci e riuscite. Tuttavia, con il campionamento si perde la capacità di aiutare i clienti e risolvere guasti specifici. Alcuni requisiti di conformità lo vietano del tutto.
Serializzazione offload e log flush su un thread separato. Questa è una modifica facile ed è comunemente usata.
Rotazione frequente del log. La rotazione dei registri dei file di log ogni ora potrebbe essere conveniente, quindi hai meno file da gestire, ma ruotando ogni minuto, molte cose migliorano. Ad esempio, l'agente che legge e comprime il file di log leggerà il file dalla cache della pagina anziché dal disco e la CPU e l'IO dai log di compressione e spedizione verranno distribuiti nell'arco dell'ora anziché attivarsi sempre alla fine del ora.
Scrivi log precompressi. Se un agente di spedizione dei log comprime i log prima di inviarli a un servizio di archiviazione, la CPU e il disco del sistema aumenteranno periodicamente. È possibile ammortizzare questo costo e ridurre della metà l'IO del disco, eseguendo lo streaming dei log compressi sul disco. Questo comporta alcuni rischi. Abbiamo trovato utile utilizzare un algoritmo di compressione in grado di gestire file troncati in caso di crash dell'applicazione.
Scrivi su un ramdisk/tmpfs. Potrebbe essere più semplice per un servizio scrivere i log in memoria fino a quando non vengono spediti dal server invece di scrivere i log su disco. Nella nostra esperienza, questo funziona meglio con la rotazione del log ogni minuto rispetto alla rotazione del log ogni ora.
Aggregati in memoria. Se è necessario gestire centinaia di migliaia di transazioni al secondo su un singolo computer, potrebbe essere troppo costoso scrivere una singola voce di log per richiesta. Tuttavia, perdi molta osservabilità saltando questo, quindi abbiamo trovato utile non ottimizzare prematuramente.
Monitora l'utilizzo delle risorse. Prestiamo attenzione a quanto siamo vicini a raggiungere un limite di dimensionamento. Misuriamo il nostro IO e la CPU per server e quante di queste risorse vengono consumate dagli agenti di registrazione. Quando eseguiamo i test di carico, li eseguiamo abbastanza a lungo in modo da poter dimostrare che i nostri agenti di spedizione del log possono tenere passo con il nostro throughput.

Predisponi gli strumenti di analisi dei log corretti

Ad Amazon, gestiamo i servizi che scriviamo, quindi dobbiamo tutti diventare esperti nella risoluzione dei relativi problemi. Ciò include la possibilità di eseguire analisi dei log senza sforzo. Ci sono molti strumenti a nostra disposizione, dall'analisi dei log locali per esaminare un numero relativamente piccolo di log, all'analisi dei log distribuiti per setacciare e aggregare i risultati di un enorme volume di log.

Abbiamo ritenuto importante investire negli strumenti e nei runbook dei team per l'analisi dei log. Se i log sono piccoli ora, ma un servizio prevede di crescere nel tempo, prestiamo attenzione a quando i nostri strumenti attuali smettono di ridimensionare, in modo da poter investire nell'adozione di una soluzione di analisi dei log distribuita.

Analisi dei log locali

Il processo di analisi dei log potrebbe richiedere esperienza in varie utilità della riga di comando di Linux. Ad esempio, un'operazione comune "trova gli indirizzi IP top talker nel log" è semplicemente:

cat log | grep -P "^RemoteIp=" | cut -d= -f2 | sort | uniq -c | sort -nr | head -n20

Tuttavia, ci sono un sacco di altri strumenti che sono utili per rispondere a domande più complesse con i nostri log, tra cui:

• jq: https://stedolan.github.io/jq/
• RecordStream: https://github.com/benbernard/RecordStream

Analisi dei log distribuiti

Qualsiasi servizio di analisi dei big data può essere utilizzato per eseguire analisi dei log distribuiti (ad esempio Amazon EMR, Amazon Athena, Amazon Aurora e Amazon Redshift). Tuttavia, alcuni servizi sono dotati di sistemi di registrazione, ad esempio Amazon CloudWatch Logs.

CloudWatch Logs Insights
• AWS X-Ray: https://aws.amazon.com/xray/
• Amazon Athena: https://aws.amazon.com/athena/

Conclusione

In quanto proprietario di servizi e sviluppatore di software, passo gran parte del mio tempo a guardare gli output della strumentazione, quali grafici su pannelli di controllo e singoli file di log, e a utilizzare strumenti di analisi dei log distribuiti come CloudWatch Logs Insights. Queste sono alcune delle mie cose preferite da fare. Quando ho bisogno di una pausa dopo aver terminato un compito impegnativo, ricarico le batterie e mi ricompenso con alcune analisi di log. Comincio con domande come "perché questa metrica è aumentata qui?" o "la latenza di questa operazione potrebbe essere inferiore?" Quando le mie domande portano a un vicolo cieco, spesso capisco alcune misurazioni che potrebbero essere utili nel codice, quindi aggiungo la strumentazione, i test e invio una revisione del codice ai miei compagni di squadra.

Nonostante il fatto che molte metriche vengano fornite con i servizi gestiti che utilizziamo, dobbiamo dedicare molta attenzione alla strumentazione dei nostri servizi in modo da avere la visibilità di cui abbiamo bisogno per gestirli in modo efficace. Durante gli eventi operativi, dobbiamo determinare rapidamente perché stiamo riscontrando un problema e cosa possiamo fare per mitigarlo. Avere le giuste metriche sui nostri pannelli di controllo è cruciale in modo che possiamo fare quella diagnosi rapidamente. Inoltre, poiché cambiamo sempre i nostri servizi, aggiungiamo nuove funzionalità e cambiamo il modo in cui interagiscono con le loro dipendenze, l'esercizio di aggiornamento e di aggiunta della giusta strumentazione è sempre in corso.

• "Look at your data" ("Guarda i tuoi dati"), dell'ex collega di Amazon John Rauser: https://www.youtube.com/watch?v=coNDCIMH8bk (incluso il minuto 13:22 dove stampa letteralmente i log per guardarli meglio)
• "Investigating anomalies" ("Indagare le anomalie") dell'ex collega di Amazon John Rauser: https://www.youtube.com/watch?v=-3dw09N5_Aw
• "How humans see data" ("Come gli umani vedono i dati") dell'ex collega di Amazon John Rauser: https://www.youtube.com/watch?v=fSgEeI2Xpdc
https://www.akamai.com/uk/en/about/news/press/2017-press/akamai-releases-spring-2017-state-of-online-retail-performance-report.jsp


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.

Utilizzo della riduzione del carico per evitare sovraccarichi Evitare insormontabili backlog di code