Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict

Pagina creata da Emanuele Colella
 
CONTINUA A LEGGERE
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Distributed Tracing
 con OpenTracing & Jaeger
      Andrea Fornaia, Ph.D.
   Department of Mathematics and Computer Science

                 University of Catania

         Viale A.Doria, 6 - 95125 Catania Italy

                 fornaia@dmi.unict.it

          https://www.dmi.unict.it/fornaia/

                   A.A. 2020/2021
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Microservizi e Complessità
• Le aziende decidono di adottare un’architettura a
  microservizi per ottenerne i benefici:
   – maggior scalabilità dei componenti
   – maggiore produttività degli sviluppatori
   – rilasci più frequenti

• L’uso dei microservizi porta con sé nuovi problemi da
  risolvere e la gestione della complessità tipica dei
  sistemi (altamente) distribuiti

• Aziende come Twitter, Netflix e Uber hanno adottato
  con successo questo stile perché hanno trovato
  soluzioni efficienti per gestirne la complessità
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Osservabilità
•   Nella teoria dei controlli, un sistema si dice osservabile se il suo stato interno, e di conseguenza il
    suo comportamento, può essere determinato solo guardandone gli input e gli output

•   Questa definizione non è sufficientemente pratica per i sistemi software: a causa della loro
    complessità, è difficile spesso determinarne completamente lo stato interno partendo solo dagli
    input e dagli output

•   Una definizione più appropriata nel nostro contesto: l’osservabilità di un sistema software è
    direttamente proporzionale a “quanto è possibile per un operatore umano fare domande e
    ottenere risposte sul sistema stesso”

•   Simile al concetto di “monitoraggio”: tipicamente con questo si intende la raccolta e
    aggregazione di metriche e la segnalazione automatica di allarmi (numero di errori, tempi di
    latenza…)

•   Per “osservabilità” si intende più la capacità per un operatore di comprendere manualmente,
    tramite appositi strumenti, se il sistema si sta comportando come dovrebbe e investigare
    (debugging) in caso di anomalie

•   Parafrasando Brian Cantrill: L’osservabilità si poggia su “tre pilastri”: metriche, log, tracce, ma le
    sue “fondamenta” sono certamente l’operatore umano
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Tracing Distribuito

Obiettivi:
• Ricostruzione ordinata temporalmente del percorso intrapreso da
  una singola richiesta
• Risalire alla causa di eventuali problemi dell’applicazione (bug)
• Analisi della performance dell’applicazione (colli di bottiglia)
• Ricostruire il grafo delle dipendenze tra i servizi
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Metriche, Log, Tracce
•   Metriche: facili da raccogliere, aggregare, e
    utili per definire allarmi automatici, ma proprio
    l’aggregazione elimina le informazioni sul
    contesto necessarie per osservare la
    gestione della singola richiesta

•   Log: ci permettono di tener traccia del
    comportamento di una singola istanza di un
    servizio; sono “orientati al processo”. Fare
    trubleshooting usando solo i log dei singoli
    processi è come fare debugging senza avere
    uno stack trace: abbiamo indizi sparsi
    ovunque, ma ci manca una visione d’insieme

•   Traccia: raccoglie in maniera strutturata tutto
    il flusso di attività svolte per la gestione di
    una singola richiesta (request-centric) con
    relazioni di causalità. Le tracce possono
    contenere internamente anche log e metriche
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Log e Osservabilità in caso di Concorrenza

Supponiamo di voler osservare il comportamento del sistema nella gestione di una richiesta
proveniente da un utente:

•    (No Concurrency) Se ogni richiesta viene gestita in un processo separato (fork) i log possono
     essere sufficienti (i log sono già separati per ciascuna richiesta)

•    (Basic Concurrency) Se ogni richiesta viene gestita in un thread separato, dobbiamo almeno
     aggiungere ad ogni riga di log il nome del thread che l’ha generata

•    (Async Concurrency) Se più thread sono coinvolti nella gestione di una richiesta, come nel caso
     di chiamate asincrone intra-processo, il nome del thread non basta, ma serve creare un
     identificativo della richiesta da scambiare tra un thread e l’altro

•    (Distributed Concurrency) Se la richiesta viene gestita da più processi, l’identificativo della
     richiesta deve essere scambiato anche nelle comunicazioni inter-processo
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Aggregazione dei Log

•   Esistono varie soluzioni per aggregare i log, come l’uso dello stack ELK
    (ElastickSearch, LogStash, Kibana)
•   Pur avendo tutti i log del servizi distribuiti in un unico punto, per tracciare una
    singola richiesta è comunque necessario:
     – Condividere tra i processi l’id della richiesta da inserire nei log (propagazione del
       contesto)
     – Registrare esplicitamente le relazioni di causalità; il timestamp nei log di processi
       distribuiti può non essere affidabile per correlare l’ordine degli eventi, per via di
       problemi di disallineamento (clock-skew)

                       https://github.com/dmi-lab-isd/spring-elk
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Costruire la traccia di una richiesta

•   Una traccia è una sequenza ordinata temporalmente di attività scaturite da una richiesta
•   Rappresentabile come una sequenza temporale di span annidati
•   Uno span rappresenta un’unità di lavoro all’interno di un singolo servizio
•   Lo span di più alto livello viene detto root span e identifica l’intera gestione della richiesta
•   Una traccia può essere visualizzata con un diagramma di Gantt
•   L’idea è di inserire nel codice dei servizi (instrumentare) dei trace point, ovvero dei punti in cui
    decidere l’inizio e la fine di uno span, assieme ad altre informazioni di contesto
•   È buona prassi iniziare uno span prima di ogni chiamata ad un servizio esterno (es.
    microservizio o DB) e chiuderlo dopo aver ricevuto la risposta (inter-process)
•   Altri span più granulari possono essere aggiunti per tener traccia di chiamate a metodo
    all’interno del servizio (intra-process), migliorando l’osservabilità nella gestione della richiesta
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Propagazione del contesto

•   Gli span dei singoli servizi devono essere correlati tra loro per formare un’unica traccia

•   Vengono scambiati dei metadati di contesto, come un ID della richiesta, assieme alle
    chiamate o scambio di messaggi tra i servizi in rete (RPC, HTTP, RabbitMQ) es. tramite
    header in una richiesta HTTP o metadati in un messaggio su coda

•   Viene scambiato anche un riferimento allo span padre (span reference) (es. sia “Catalog”
    che “Promos” riceveranno l’ID della richiesta e il nome dello span “Web Server”) in modo da
    tener esplicitamente traccia della relazione di causalità

•   Tutti gli span vengono inviati (a intervalli regolari) ad un servizio centrale, detto back-end di
    tracing, o semplicemente tracer che si occupa di ricostruire le tracce e di renderle fruibili
    agli operatori tramite UI
Distributed Tracing con OpenTracing & Jaeger - Andrea Fornaia, Ph.D - DMI Unict
Tipi di Span Reference
• “child-of”:                                 A
                                               B
   –   A accade-prima-di B
   –   A dipende-dal-risultato-di B
   –   Astart – Bstart – Bend – Aend
   –   Es. RPC/REST, query su DB, chiamate a metodo sincrone

• “follows-from”:                      A
   –   A accade-prima-di B                          B
   –   A non-dipende-dal-risultato-di B
   –   Astart – Aend – Bstart – Bend
   –   Es. message queue, chiamate asincrone, fire and forget,
       producer-consumer
Clock Skew

•   Per stimare i tempi di latenza tra due richieste è importante che i clock dei servizi siano quanto più
    allineati possibile
•   Anche l’uso di un protocollo NTP può comunque causare dei disallineamenti (seppur minimi)
•   Le relazioni di causalità permettono di correggere lo slittamento dei clock tra i servizi (clock skew)
•   Se il server ha una relazione del tipo “chilf-of” sicuramente l’intero span deve essere contenuto
    all’interno dello span padre (client)
•   Possiamo quindi shiftare a sinistra lo span del server, correggendone i timestamp, e di
    conseguenza anche tutti gli altri span del server verranno shiftati a sinistra
•   All’interno dello span padre rimane comunque dell’incertezza: all’inizio viene semplicemente
    centrato (d1 e d2 uguali)
•   Più relazioni di causalità sono note, più abbiamo possibilità di correggere lo slittamento
Itra e inter-process
                  context propagation

•   La propagazione del contesto è necessaria sia tra gli span di servizi diversi che
    tra gli span dello stesso servizio

•   Possono essere passati come parametri al metodo chiamato

•   Ma vedremo come librerie di supporto permettano uno scambio più semplice e
    trasparente del contesto, senza richiedere la modifica delle API (signature dei
    metodi) del nostro sistema
Tag, Log, Baggage
•   Oltre all’ID della richiesta (inserito automaticamente) e il riferimento allo
    span padre (da specificare), il programmatore può aggiungere diverse
    informazioni ad uno span:
     – Tag: coppie chiave-valore che valgono per tutto lo span

     – Log: identificano eventi, caratterizzati da un timestamp nello span e una
       struttura gerarchica di coppie chiave-valore (quindi differiscono dai log
       tradizionali)

     – Baggage: metadati che vengono trasferiti con le chiamate tra un servizio
       ed un altro; mentre tag e log sono pensati per essere solo scritti (e letti solo
       dal tracer centrale) i baggage possono essere letti dal codice in fase di
       instrumentazione, fornendo maggiori dettagli di contesto utili per migliorare
       i log di uno span figlio (diretto o indiretto)

     – Nota sui baggage: vengono scambiati in maniera trasparente,
       permettendo ai servizi intermedi di ignorarli se non necessari. Devono
       essere usati solo al fine del tracing e dell’osservabilità, non devono quindi
       essere usati come supporto alla logica di business
OpenTracing

•   API standard per la generazione di informazioni di tracing
•   Definisce il modello dati delle tracce, span, ecc
•   Vendor-neutral: adottato da diversi back-end di tracing (es. Zipkin, Jaeger)
•   Esistono implementazioni (librerie) per diversi linguaggi
•   Permette l’instrumentazione del codice per l’inserimento dei trace point
Instrumentazione

•   I trace point possono essere inseriti manualmente dallo sviluppatore nel codice
    del servizio (Service Instrumentation)

•   La programmazione ad aspetti permette di separare la logica di
    instrumentazione da quella funzionale

•   Diversi framework come Spring, forniscono delle librerie già instrumentate
    (Library Instrumentation), generando automaticamente degli span ogni volta
    che viene ricevuta una richiesta da parte di un controller HTTP, ogni volta che
    viene usato il restTemplate per fare una chiamata HTTP, o quando vengono
    effettuate delle query a DB (redis, MySql, MongoDB…)
Jaeger

•   Creato da Uber, e Ispirato a Zipkin (Twitter)
•   Adotta lo standard (data-model) di OpenTracing
•   Può appoggiarsi a Cassandra o ElasticSearch per lo storage delle tracce
•   Un client (passando per l’agent) invia le tracce al tracer centrale (buffering per ridurre i
    costi di rete)

•   Strategie di sampling per ridurre il costo del tracing (overhead, rete e storage) es. 1 traccia
    completa (tutti i tag, log, baggage) ogni 1000 richieste è la configurazione di default in
    produzione
•   Anche se non completa, le informazioni di base della traccia vengono comunque
    propagate (id richiesta e altri metadati di contesto per la causalità)
Jaeger All-in-one (Docker)
                               docker run -d --name jaeger \
                                 -p 6831:6831/udp \
                                 -p 16686:16686 \
                                 -p 14268:14268 \
                                 jaegertracing/all-in-one:1.6

•   -d esecuzione in modalità detached (in background)
•   --name il nome assegnato al container (servirà per renderlo accessibile da altri container)
•   -p per esporre (mappare) una porta del container sull’host (localhost)
•   jaegertracing/all-in-one:1.6 immagine di container pensata per lo sviluppo
•   Tutti i microservizi di cui è composto risiedono nello stesso container
•   Tracce solo in memoria (può essere configurato Cassandra o ElasticSearch)
•   http://localhost:16686 per la web UI
•   6831 per l’invio degli span (UDP)
•   14268 soluzione di backup HTTP/TCP per l’invio degli span in caso di problemi con UDP
•   I servizi di Jaeger hanno il self-tracing attivato, facendo quinidi il
    refresh della pagina più volte possiamo vedere che sono state
    tracciate delle richieste al servizio jaeger-query (il servizio della UI)

•   Nome span: nome-servizio: nome-operazione
•   Nome traccia (mostrato): nome root-span (in celeste la durata)
Tutorial: Hot R.O.D.

•   Mock-app di un servizio per la prenotazione di corse stile Uber (Hot Ride
    On Demand)
•   Fornito con Jaeger per mostrarne le funzionalità
•   Simula un’applicazione a microservizi che si appoggiano su redis e mysql
•   In realtà è implementato in un unico processo e l’accesso ai database è
    simulato
•   Questo non influisce con la validità delle informazioni di tracing
Hot R.O.D. (Docker)
                        docker run --rm -it \
                          --link jaeger \
                          -p8080-8083:8080-8083 \
                          jaegertracing/example-hotrod:1.6 \
                          all \
                          --jaeger-agent.host-port=jaeger:6831

•   --rm il container verrà cancellato dopo aver terminato
•   -it modalità interattiva (output/log visibili su stdout/terminale)
•   --link per permettere al container di risolvere il nome di rete jeager con il
    riferimento al container (con nome) jeager
•   -p8080-8083:8080-8083 esposto il range di porte del container su localhost
•   jaegertracing/example-hotrod:1.6 Hot R.O.D.
•   all il comando eseguito all’avvio del container (per lanciare tutti le parti in un
    unico processo, quindi volendo si potrebbe creare un container parte)
•   --jaeger-agent.host-port=jaeger:6831 il riferimento che il client jaeger userà
    per inviare le tracce al back-end
Richiediamo una corsa

•   Ogni bottone corrisponde ad una destinazione
•   Il sistema restituisce la targa del veicolo che ha preso in carico la richiesta
    della corsa, assieme al tempo di arrivo stimato
•   Vengono inoltre fornite alcune informazioni di debug, ovvero l’ID della
    richiesta (4734) e i tempo di latenza (quanto tempo ha impiegato il back-
    end del servizio Hot R.O.D. a fornire la risposta al front-end)
Scoprire l’architettura interna di
                Hot R.O.D.

•   Già con una sola richiesta jaeger è stato in grado di ricostruire empiricamente l’architettura
    (fino a quel momento esplorata) dell’applicazione (Dependencies/DAG)
•   Ogni nodo corrisponde ad un servizio
•   I pesi sugli archi corrispondono al numero di chiamate avvenute tra due servizi
Ricerca di una traccia

•   Service: frontend -> Find Traces

•   Sono presenti due tracce: la prima get fatta sull’endpoint “/” quando abbiamo aperto la
    pagina e la seconda sull’endpoint “/dispatch” quando abbiamo richiesto la corsa

•   Dall’anteprima della tracia possiamo vedere la durata (768.56 ms) quali servizi sono
    coinvolti, di quanti span è composta la traccia (51), quanti span ha ogni servizio e
    l’eventuale presenza di errori (3)
Data Flow della traccia (Gantt)

•   Sulla sinistra vediamo le gerarchia di chiamate ai servizi (servizio: operazione)
•   Sulla destra i corrispettivi span nel tempo
•   Anche solo da questa visione di alto livello (senza ispezionare le informazioni degli span)
    possiamo capire molto su come è stata gestita la richiesta da parte dei servizi
•   Notiamo anche alcuni problemi (errori) e possibilità di ottimizzazione…
Informazioni su una chiamata
                  HTTP

•   Sono presenti dei tag che descrivono la chiamata (es. endpoint e
    parametri e il codice restituito, 200) e dei log che descrivono gli
    eventi interni allo span (notare il timestamp, a differenza del log)
Informazioni su una query a DB

•   Sono presenti dei tag con le informazioni sulla query, e dei log sull’evento
Span con errore

•   Uno span viene segnato come in errore se ha un tag error = true

•   L’errore risiede su una richiesta fatta a redis per ottenere le informazioni su un guidatore,
    data la targa

•   Possiamo vedere dai log sia la targa interessata che il messaggio di errore: “redis
    timeout”

•   Si tratta quindi di un problema di redis sicuramente da investigare nel codice

•   Ma dallo span successivo l’errore viene subito fatta una nuova richiesta per la stessa
    targa: le politiche di robustezza (pattern retry) funzionano correttamente!
Individuare le cause di latenza (1)

•   Notiamo un tipico pattern a scala: tutte le richieste sono sequenziali, ma
    in realtà sospettiamo non siano realmente dipendenti tra loro – visione
    macroscopica

•   Ognuna riguarda la richiesta di informazioni su di un numero di targa
    diverso (a parte i retry)… possiamo ora indagare meglio guardando il
    codice del servizio o i rispettivi log completi – visione microscopica

•   Questo suggerisce di inserire una gestione parallela delle richieste,
    risparmiando così tempo per soddisfare la richiesta
Individuare le cause di latenza (2)

•   Anche qui notiamo una situazione simile alla precedente, ma notiamo che
    un certo livello di parallelismo è stato introdotto, ma solo per 3 operazioni
    alla volta
•   Questo potrebbe essere dovuto ad una configurazione stringente di un
    fixed executor pool (pool di thread di dimensioni fisse, a 3 in questo caso)
•   Si potrebbe quindi aumentare la dimensione del pool
Proviamo ad aumentare il numero
           di richieste

• Lanciando alcune richieste in parallelo le
  prestazioni del sistema peggiorano notevolmente
  (3750 ms contro i 768.56 ms della prima)
Diagnosi del problema

•   Osservando la traccia (possiamo identificarla perché è la più lunga o tramite una ricerca di
    un tag, es. il numero di targa)
•   Tutti gli span sono indicizzati in base alle informazioni sui tag e sui log
•   Il problema è dato da una maggiore latenza sull’accesso al DB (servizio mysql)
•   Dai log notiamo che il problema è un lock di attesa, sicuramente da attenzionare
•   La traccia c’è servita ad avere una visione di insieme e capire meglio la radice del problema
    (es. è dovuto all’accesso a mysql e non a redis) – visione macroscopica
•   Possiamo ora investigare meglio il motivo del problema guardando il codice del servizio
    specifico o I relativi log – visione microscopica
Riferimenti
•   Yuri Shkuro: Mastering Distributed Tracing (Packt, 2019)
•   Austin Parker et al.: Distributed Tracing in Practice: Instrumenting, Analyzing, and Debugging
    Microservices (O’REILLY, 2020)
•   Steve Flanders: An intro to distributed tracing
    https://sflanders.net/2019/03/28/an-intro-to-distributed-tracing/
•    Adam Quan: Distributed Tracing, OpenTracing and Elastic APM
    https://www.elastic.co/blog/distributed-tracing-opentracing-and-elastic-apm
•   OpenTracing Specification: https://opentracing.io/specification/
•   Jaeger Architecture: https://www.jaegertracing.io/docs/1.13/architecture/
•   OpenTracing: Instrumenting your application:
    https://opentracing.io/docs/best-practices/instrumenting-your-application/
•   Yuri Shkuro: Take OpenTracing for a HotROD ride
     https://medium.com/opentracing/take-opentracing-for-a-hotrod-ride-f6e3141f7941
•   Spring OpenTracing Starter: https://github.com/opentracing-contrib/java-spring-jaeger
Puoi anche leggere