Distribuert sporing: Generell introduksjon
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:
- opentelemetry og
otlp
-protokollen, - http-headerne W3C Trace Context og W3C Baggage.
- Instrumenteringsbiblioteker for forskjellige språk
Og så trenger man en eller annen tjeneste som kan håndtere data:
- Opentelemetrys referanseimplementasjon
- Grafana Alloy for å samle data, og Grafana Tempo for å ta dem imot
- Jæger v2
- Vector har issues for opentelemetry-støtte
- Zipkin eksisterer, men jeg vet fryktelig lite om det bortsett fra at B3-headerne eksisterer.
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.