Distribuert sporing: Generell introduksjon

Emil Snorre Alnæs

Hva, hvorfor

I tillegg til metrikker og logger er det en datatype til vi vil ha: spor (traces). Spor kan bære logglinjer og metrikker, og automatisk pusle sammen hvordan et kall har beveget seg gjennom et distribuert system.

Dette betyr at i stedet for i tillegg til å lete i loggsystemer, sjekke grafer og ha alarmer for visse typer logglinjer eller metrikker som er utenfor tålegrensa, så kan vi gi en gitt forespørsel en sporingsid som datamaskinen kan bruke til å generere id-er for andre forespørsler som resulterer fra den første, se hva som bruker mest tid før vi kan levere et svar, og eventuelt hvor i suppa det gikk galt.

Sporing supplerer metrikker og logging som man vil ha uansett:

  • Logglinjer er sin egen kilde til informasjon man kan grave i eller arkivere;
  • Metrikker kan brukes til å bygge opp histogrammer, trender over tid og hvordan et system fungerer generelt;
  • En enkeltsporing gir en innsikt i en enkelthendelse som har beveget seg gjennom systemet, og kan fungere som en slags profiling light.

Så med f.eks et service mesh vil man få metrikker på hvor lang tid en app bruker på å håndtere en gitt path; med sporing kan man se i større detalj hvorfor den bruker så lang tid på et konkret kall; med distribuert sporing kan man se mer av hvorfor kall til andre apper bruker så lang tid som de gjør. Det er fortsatt ikke profiling, men man kan få en pekepinn på hvor i systemet man bør gjøre tiltak for å få ting til å gå raskere.

Mer konkret: Med service mesh-metrikker kan man lett se at når app a kaller b.svc/c tar det vanligvis x ms, mens når d gjør det samme tar det vanligvis y ms, og hvilke HTTP-koder de vanligvis får tilbake; med sporinger kan vi se forskjellen i kallene a og d gjør, og hva a, b og c gjør internt. Hvis de er instrumentert for det.

Eller i stedet for å finne en vilkårlig feilmelding i loggsystemet eller en uønsket høy 5xx-linje i grafene, og så leke detektiv for å finne ut av hvorfor det gikk galt, så kan man filtrere på sporinger i en feilsituasjon, og få sammenhengen servert.

Grafana har en fin oversikt på engelsk. Linkerd har også en post om noen myter.

Hvordan

I praksis vil dette lede til en hel haug teknologi-komponenter for å generere data:

Og så trenger man en eller annen tjeneste som kan håndtere data:

Man vil også trenge noe konfigurasjon; mye av dette kan gjøres via noen standard-environment-variable for SDK og OTLP exporter, som OTEL_SERVICE_NAME, OTEL_TRACES_SAMPLER, OTEL_EXPORTER_OTLP_ENDPOINT.

Sampleren er det verdt å ta en titt på: parentbased_always_on er sannsynligvis en god måte å drukne i data, og parentbased_traceidratio er sannsynligvis en greiere default.

Praktisk eksempel (i Rust)

Dette blir for mye styr for bloggposten egentlig tror jeg, og det finnes garantert bedre bloggposter og gitrepoer rundtom med eksempler, men, gitt

kan man noenlunde enkelt sette sammen sin egen eksempel-app, hvor man vil gjøre noe i retning av

// denne linja …
use init_tracing_opentelemetry::tracing_subscriber_ext::build_otel_layer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

let registry = tracing_subscriber::registry().with(log_filter);
let timer_utc = tracing_subscriber::fmt::time::ChronoUtc::rfc_3339();
let event_fmt = tracing_subscriber::fmt::format()
    .json()
    .with_span_list(true)
    .with_timer(timer_utc)
    .with_current_span(false);
let fmt = tracing_subscriber::fmt::layer()
    .event_format(event_fmt)
    .fmt_fields(tracing_subscriber::fmt::format::JsonFields::default());

// … og denne med `build_otel_layer` er egentlig det som skiller seg fra et mer
// generisk loggoppsett med tracing
registry.with(build_otel_layer()?).with(fmt).try_init()?;

for å få noe greit json-loggformat og opentelemetry i tracing-oppsettet, og så, med axum, noe i retning av

// includes her blir for mye styr
pub fn router() -> Router {
    Router::new()
        .route("/", get(root))
        // disse to linjene fikser header-mikkmakket for distributed tracing
        .layer(OtelInResponseLayer)
        .layer(OtelAxumLayer::default())

// denne annotationen sørger for at funksjonen traces
#[instrument]
async fn root(...) -> Result<Response, AppError> {
    todo!("Bygg ditt eget svar");
}

hvor altså #[instrument] er det du trenger for å få traces av funksjonskall, med logger og det hele inkludert, og axum-middlewaren tar seg av å fikse trace context med headere sånn at distribuert tracing kan fungere. build_otel_layer er ikke så vanskelig å skru sammen selv om man vil; axum-middlewaren er heller ikke allverdens hokuspokus.

Det initielle oppsettet er altså mer jobb enn å få metrikker uten å egentlig løfte en finger med et service mesh, men gir deg også mer detaljert informasjon ut.