Tracciamento dei servizi, OpenTracing e Jaeger

Tracciamento dei servizi, OpenTracing e Jaeger

Nei nostri progetti utilizziamo un'architettura a microservizi. Quando si verificano colli di bottiglia nelle prestazioni, viene dedicato molto tempo al monitoraggio e all'analisi dei log. Quando si registrano i tempi di esecuzione delle singole operazioni in un file di log, di solito è difficile capire cosa ha portato alla chiamata di queste operazioni, tracciare la sequenza delle azioni o il ritardo temporale di un'operazione rispetto a un'altra in diversi servizi.

Per ridurre il lavoro manuale, abbiamo deciso di utilizzare uno degli strumenti di tracciamento. Di cosa si tratta e come lo abbiamo utilizzato verrà trattato in questo articolo.

Quali problemi possono essere risolti con il tracciamento

  1. Identificare i colli di bottiglia nelle prestazioni sia all'interno di un singolo servizio che nell'intero albero di esecuzione tra tutti i servizi coinvolti. Ad esempio:
    • Molti brevi chiamate sequenziali tra servizi, ad esempio, per geocodifica o accesso al database.
    • Lunghe attese di input/output, ad esempio, trasferimento dati tramite rete o lettura da disco.
    • Lunga analisi dei dati.
    • Operazioni lunghe che richiedono CPU.
    • Sezioni di codice che non sono necessarie per ottenere il risultato finale e possono essere rimosse o eseguite in modo posticipato.
  2. Comprendere chiaramente in quale sequenza vengono effettuate le chiamate e cosa succede quando si esegue l'operazione.
    Tracciamento dei servizi, OpenTracing e Jaeger
    È evidente che, ad esempio, una richiesta è arrivata al servizio WS -> il servizio WS ha completato i dati tramite il servizio R -> quindi ha inviato una richiesta al servizio V -> il servizio V ha caricato molti dati dal servizio R -> si è recato al servizio P -> il servizio P è andato di nuovo al servizio R -> il servizio V ha ignorato il risultato e si è diretto verso il servizio J -> e solo dopo ha restituito la risposta al servizio WS, continuando comunque a calcolare qualcos'altro in background.
    Senza tale tracciamento o documentazione dettagliata per l'intero processo, è molto difficile capire cosa sta accadendo a un primo sguardo sul codice, e il codice è sparso tra diversi servizi ed è nascosto dietro un mucchio di bean e interfacce.
  3. Raccolta di informazioni sull'albero di esecuzione per un'analisi successiva. In ogni fase dell'esecuzione, nel tracciamento è possibile aggiungere informazioni disponibili in quel momento e successivamente capire quali dati di input hanno portato a tale scenario. Ad esempio:
    • ID utente
    • Autorizzazioni
    • Tipo del metodo selezionato
    • Log o errore di esecuzione
  4. La trasformazione dei trace in un sottoinsieme di metriche e l'analisi successiva già in forma di metriche.

Cosa può registrare il tracing. Span

Nel tracing esiste il concetto di span, che è l'equivalente di un log, nella console. Ogni span ha:

  • Un nome, di solito il nome del metodo che è stato eseguito
  • Il nome del servizio in cui è stato generato lo span
  • Un ID unico proprio
  • Qualche informazione meta in forma key/value che è stata registrata. Ad esempio, i parametri del metodo o se il metodo ha restituito un errore o meno
  • Il tempo di inizio e fine dell'esecuzione di questo span
  • ID dello span genitore

Ogni span viene inviato a un collector di span per essere salvato nel database per una successiva visualizzazione non appena ha completato la sua esecuzione. In seguito, è possibile costruire un albero di tutti gli span collegandoli per id del genitore. Durante l'analisi, è possibile trovare, ad esempio, tutti gli span in un determinato servizio che hanno impiegato più di un certo tempo. Poi, passando a uno span specifico, si può vedere l'intero albero sopra e sotto questo span.

Tracciamento dei servizi, OpenTracing e Jaeger

Opentrace, Jagger e come abbiamo implementato questo per i nostri progetti

Esiste uno standard comune Opentrace, che descrive come e cosa deve essere raccolto, senza legarsi il tracing a una specifica implementazione in un linguaggio. Ad esempio, in Java tutto il lavoro con i trace viene fatto tramite il comune API Opentrace, sotto il quale può nascondersi, ad esempio, Jaeger o un'implementazione predefinita vuota che non fa nulla.
Noi utilizziamo Jaeger come implementazione di Opentrace. È composto da diversi componenti:

Tracciamento dei servizi, OpenTracing e Jaeger

  • Jaeger-agent — un agente locale, che di solito si trova su ogni macchina e in esso registrano i servizi sulla porta predefinita locale. Se l'agente non è presente, i trace di tutti i servizi su questa macchina sono di solito disattivati.
  • Jaeger-collector — a cui tutti gli agenti inviano i trace raccolti, e lui li inserisce nel database scelto.
  • Il database — preferibilmente cassandra, ma noi utilizziamo elasticsearch, ci sono implementazioni anche per un paio di altri database e un'implementazione in memoria, che non salva nulla su disco.
  • Jaeger-query — è il servizio che si connette al database e restituisce i trace già raccolti per l'analisi.
  • Jaeger-ui — è l'interfaccia web per la ricerca e visualizzazione dei trace, si connette a jaeger-query.

Tracciamento dei servizi, OpenTracing e Jaeger

Un componente separato può essere considerata l'implementazione di opentrace jaeger per specifici linguaggi, attraverso cui gli span vengono inviati a jaeger-agent.
Integrazione di Jagger in Java si riduce all'implementazione dell'interfaccia io.opentracing.Tracer, dopo di che tutte le tracce attraverso di essa verranno inviate all'agente reale.

Tracciamento dei servizi, OpenTracing e Jaeger

Inoltre, è possibile collegare opentracing-spring-cloud-starter e l'implementazione di Jaeger opentracing-spring-jaeger-cloud-starter che configurerà automaticamente il tracciamento di tutto ciò che passa attraverso questi componenti, ad esempio le richieste http ai controller, le richieste al database tramite jdbc, ecc.

Registrazione delle tracce in Java

Da qualche parte al livello più alto deve essere creato il primo Span, questo può essere fatto automaticamente ad esempio dal controller di Spring quando riceve una richiesta, oppure manualmente se non esiste. Successivamente, viene passato attraverso lo Scope sottostante. Se qualche metodo sottostante desidera aggiungere un Span, prende dallo Scope l'attuale activeSpan, crea un nuovo Span e dichiara che il suo parent è l'activeSpan ottenuto, e rende il nuovo Span attivo. Quando chiama servizi esterni, gli viene passato l'attuale span attivo, e quei servizi creano nuovi span legati a questo span.
Tutto il lavoro avviene tramite un'istanza di Tracer, che può essere ottenuta tramite il meccanismo DI o GlobalTracer.get() come variabile globale, se il meccanismo DI non funziona. Per impostazione predefinita, se il tracer non è stato inizializzato, verrà restituito il NoopTracer che non fa nulla.
Successivamente, dal tracer, attraverso il ScopeManager, si recupera l'attuale scope, si crea un nuovo scope dall'attuale legato al nuovo span, e poi si chiude lo Scope creato, che chiude lo span creato e riporta nello stato attivo lo Scope precedente. Lo Scope è legato al thread, quindi nella programmazione multithreading non si deve dimenticare di passare lo span attivo a un altro thread, per attivare ulteriormente lo Scope di un altro thread legato a questo span.

io.opentracing.Tracer tracer = ...; // GlobalTracer.get()

void DoSmth () {
   try (Scope scope = tracer.buildSpan("DoSmth").startActive(true)) {
      ...
   }
}
void DoOther () {
    Span span = tracer.buildSpan("someWork").start();
    try (Scope scope = tracer.scopeManager().activate(span, false)) {
        // Fai le cose.
    } catch(Exception ex) {
        Tags.ERROR.set(span, true);
        span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex, Fields.MESSAGE, ex.getMessage()));
    } finally {
        span.finish();
    }
}

void DoAsync () {
    try (Scope scope = tracer.buildSpan("ServiceHandlerSpan").startActive(false)) {
        ...
        final Span span = scope.span();
        doAsyncWork(() -> {
            // FASE 2 SOPRA: riattivare lo Span nel callback, passando true a
            // startActive() se/quando lo Span deve essere terminato.
            try (Scope scope = tracer.scopeManager().activate(span, false)) {
                ...
            }
        });
    }
}

Per la programmazione multithreading ci sono anche TracedExecutorService e wrapper simili, che passano automaticamente lo span corrente al thread quando si avviano task asincroni:

private ExecutorService executor = new TracedExecutorService(
    Executors.newFixedThreadPool(10), GlobalTracer.get()
);

Per le richieste http esterne c'è TracingHttpClient

HttpClient httpClient = new TracingHttpClientBuilder().build();

Problemi con cui ci siamo imbattuti

  • I bean e il DI non sempre funzionano se il tracer non è utilizzato in un servizio o in un componente, allora Autowired Il tracer potrebbe non funzionare e sarà necessario usare GlobalTracer.get().
  • Le annotazioni non funzionano se non sono un componente o un servizio, o se la chiamata al metodo avviene da un metodo adiacente della stessa classe. È necessario prestare attenzione, verificare cosa funziona e utilizzare la creazione manuale del trace se @Traced non funziona. È possibile anche aggiungere un compilatore aggiuntivo per le annotazioni java, allora dovrebbero funzionare ovunque.
  • Nelle versioni più vecchie di spring e spring boot, l'autoconfigurazione opentracing spring cloud non funziona a causa di bug nel DI, quindi se si desidera che i trace funzionino automaticamente nei componenti di spring, si può fare analogamente a github.com/opentracing-contrib/java-spring-jaeger/blob/master/opentracing-spring-jaeger-starter/src/main/java/io/opentracing/contrib/java/spring/jaeger/starter/JaegerAutoConfiguration.java
  • In groovy, il try with resources non funziona, è necessario utilizzare obbligatoriamente try finally.
  • È necessario specificare il proprio spring.application.name per ogni servizio sotto il quale verranno registrati i trace. Inoltre, un nome separato per produzione e test, per non mescolarli insieme.
  • Se si utilizza GlobalTracer e tomcat, tutti i servizi avviati in questo tomcat hanno un unico GlobalTracer, quindi avranno tutti lo stesso nome di servizio.
  • Quando si aggiungono i trace al metodo, è necessario assicurarsi che non venga chiamato in un ciclo molte volte. Bisogna aggiungere un unico trace generale per tutte le chiamate, che registrerà il tempo totale di esecuzione. Altrimenti si creerà un carico eccessivo.
  • Una volta in jaeger-ui abbiamo effettuato richieste troppo grandi per un gran numero di trace e poiché non aspettavamo la risposta abbiamo fatto di nuovo. Di conseguenza, jaeger-query ha iniziato a utilizzare molta memoria e a rallentare elastic. Riavviare jaeger-query ha aiutato.

Campionamento, archiviazione e visualizzazione dei trace

Ci sono tre tipi di campionamento dei trace:

  1. Const che invia e salva tutti i trace.
  2. Probabilistico che filtra i trace con una certa probabilità stabilita.
  3. Il ratelimiting che limita il numero di trace al secondo. È possibile configurare questi parametri sul client, oppure su jaeger-agent o nel collector. Attualmente nel nostro stack di validatori viene utilizzato const 1 poiché le richieste non sono molte, ma occupano un tempo considerevole. In futuro, se questo dovesse causare un carico eccessivo sul sistema, sarà possibile limitare.

Se si utilizza Cassandra, di default conserva le trace solo per due giorni. Noi utilizziamo elasticsearch e le trace vengono conservate per tutto il tempo e non vengono eliminate. Viene creato un indice separato per ogni giorno, ad esempio jaeger-service-2019-03-04. In futuro sarà necessario impostare una pulizia automatica delle trace più vecchie.

Per visualizzare le trace è necessario:

  • Selezionare il servizio per il quale si desidera filtrare le trace, ad esempio tomcat7-default per un servizio che è in esecuzione su Tomcat e non può avere un proprio nome.
  • Successivamente, selezionare l'operazione, il periodo di tempo e il tempo minimo di operazione, ad esempio a partire da 10 secondi, per prendere solo le esecuzioni lunghe.
    Tracciamento dei servizi, OpenTracing e Jaeger
  • Accedere a una delle trace e verificare cosa stava rallentando.
    Tracciamento dei servizi, OpenTracing e Jaeger

Inoltre, se si conosce un id della richiesta, è possibile trovare la trace tramite questo id cercando nei tag, se questo id è registrato nello span della trace.

Documentazione

Articoli

Video

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster