Kas yra Jaeger ir kodėl jis tapo tokiu svarbiu
Kai tavo mikroservisų architektūra pradeda augti kaip grybai po lietaus, greičiausiai susidursi su klasikine problema – kaip, po velnių, suprasti, kas vyksta su vienu užklausos keliu per dešimtis skirtingų servisų? Čia ir ateina į pagalbą Jaeger – atviro kodo distributed tracing sistema, kurią sukūrė Uber komanda ir vėliau perdavė Cloud Native Computing Foundation globai.
Jaeger leidžia sekti užklausas, keliaujančias per visą tavo mikroservisų ekosistemą. Įsivaizduok situaciją: klientas skundžiasi, kad užsakymo pateikimas užtrunka 8 sekundes vietoj įprastų dviejų. Be distributed tracing turėtum lakstyti po kiekvieną servisą, tikrinti logus, bandyti atkurti įvykių seką. Su Jaeger matai visą kelionę viename ekrane – nuo pradinio API gateway iki paskutinio duomenų bazės užklausos.
Sistema veikia pagal OpenTelemetry (anksčiau OpenTracing) standartą, o tai reiškia, kad nesi užrakintas tik prie Jaeger. Jei ateityje nuspręsi pereiti prie kitos tracing sistemos, tavo instrumentacija liks ta pati. Tai didelis privalumas, nes niekas nenori perašinėti pusės kodo bazės dėl monitoring įrankio keitimo.
Kaip veikia tracing mechanizmas
Distributed tracing koncepcija iš pirmo žvilgsnio gali atrodyti sudėtinga, bet iš tikrųjų principas gana paprastas. Kiekviena užklausa gauna unikalų trace ID – tai tarsi sekimo numeris siuntiniui. Kai ši užklausa keliauja per skirtingus servisus, kiekvienas servisas prideda savo „span” – tai tarsi laiko žymė su papildoma informacija apie tai, ką tas servisas darė.
Pavyzdžiui, kai vartotojas spaudžia „Pirkti” mygtuką e-parduotuvėje, gali įvykti maždaug toks scenarijus:
- Frontend servisas sukuria naują trace su unikaliu ID
- Užklausa eina į API Gateway – sukuriamas naujas span
- API Gateway kreipiasi į autentifikacijos servisą – dar vienas span
- Toliau eina į užsakymų servisą, kuris patikrina inventorių – du papildomi spans
- Galiausiai mokėjimo servisas apdoroja transakciją – paskutinis span
Visi šie spans yra susieti su tuo pačiu trace ID, todėl Jaeger UI gali juos sugrupuoti ir parodyti kaip vieną vizualią laiko juostą. Matai ne tik kiek laiko užtruko kiekvienas žingsnis, bet ir kokia buvo jų seka, ar vyko lygiagrečiai, ar nuosekliai.
Svarbu suprasti, kad Jaeger pats nieko neseka automatiškai. Tau reikia instrumentuoti savo aplikacijas – įdiegti bibliotekas, kurios generuos ir siųs šiuos spans. Laimei, daugeliui populiarių frameworkų jau egzistuoja paruošti integracijos paketai.
Architektūra ir komponentai
Jaeger sistema susideda iš kelių pagrindinių komponentų, ir suprasti jų vaidmenis padės teisingai ją įdiegti. Pirmiausia yra Jaeger Client – tai bibliotekos, kurias integruoji į savo aplikacijas. Jos generuoja spans ir siunčia juos toliau grandinėje.
Toliau eina Jaeger Agent – tai lengvas daemonas, kuris paprastai veikia tame pačiame host’e kaip ir tavo aplikacija. Jo darbas – priimti spans per UDP protokolą (kad nebūtų papildomo network overhead’o aplikacijai) ir siųsti juos batch’ais į Collector. Agent’as veikia kaip buferis, sumažinantis apkrovą tavo aplikacijoms.
Jaeger Collector yra centrinė komponentė, kuri priima visus spans, atlieka validaciją, indeksavimą ir išsaugo juos į storage backend’ą. Collector’ius gali horizontaliai skalinti, kai tavo tracing duomenų srautas auga.
Storage galimybės yra įvairios – galima naudoti Cassandra production aplinkoms su dideliais duomenų kiekiais, Elasticsearch jei nori galingesnių paieškos galimybių, arba net paprastą in-memory storage testavimui. Yra ir Kafka integracija, jei nori dar labiau atskirti duomenų priėmimą nuo saugojimo.
Galiausiai Jaeger Query servisas ir UI – tai tai, ką matai naršyklėje. Query servisas ištraukia duomenis iš storage ir pateikia juos per REST API, o UI suteikia grafinę sąsają trace’ų analizei.
Praktinis įdiegimas su Docker ir Kubernetes
Greičiausias būdas išbandyti Jaeger yra paleisti all-in-one Docker konteinerį. Tai puikiai tinka development aplinkai:
„`html
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:latest
„`
Po minutės jau gali atidaryti http://localhost:16686 ir matyti Jaeger UI. Žinoma, dar nieko nematysi, nes nėra instrumentuotų aplikacijų, kurios siųstų duomenis.
Production aplinkoje tikrai nenori naudoti all-in-one varianto. Čia reikia atskirų komponentų su tinkamu storage backend’u. Jei naudoji Kubernetes, rekomenduoju panaudoti Jaeger Operator – tai labai supaprastina deployment’ą ir valdymą.
Operatoriaus įdiegimas:
„`html
kubectl create namespace observability kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.51.0/jaeger-operator.yaml -n observability
„`
Tada gali sukurti custom resource Jaeger instance su Elasticsearch backend’u:
„`html
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: production-jaeger
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch:9200
index-prefix: jaeger
ingress:
enabled: true
„`
Operator’ius automatiškai sukurs visus reikalingus deployment’us, service’us ir konfigūracijas. Tai tikrai sutaupo laiko ir sumažina klaidų tikimybę.
Aplikacijų instrumentavimas
Dabar pats įdomiausias dalykas – kaip instrumentuoti savo aplikacijas. Parodysiu pavyzdžius su keliais populiariais frameworkais, nes teorija be praktikos nieko neverta.
Python su Flask aplikacija:
„`html
from flask import Flask
from jaeger_client import Config
from flask_opentracing import FlaskTracing
app = Flask(__name__)
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'logging': True,
'local_agent': {
'reporting_host': 'localhost',
'reporting_port': 6831,
}
},
service_name='my-flask-service'
)
jaeger_tracer = config.initialize_tracer()
tracing = FlaskTracing(jaeger_tracer, True, app)
@app.route('/api/users')
def get_users():
# Automatiškai sukuriamas span šiam endpoint'ui
return {'users': ['Alice', 'Bob']}
„`
Node.js su Express:
„`html
const express = require('express');
const initJaegerTracer = require('jaeger-client').initTracer;
const config = {
serviceName: 'my-node-service',
sampler: {
type: 'const',
param: 1,
},
reporter: {
logSpans: true,
agentHost: 'localhost',
agentPort: 6831,
},
};
const tracer = initJaegerTracer(config);
const app = express();
app.get('/api/products', (req, res) => {
const span = tracer.startSpan('get_products');
// Tavo logika čia
span.finish();
res.json({products: []});
});
„`
Java su Spring Boot yra dar paprasčiau, nes Spring Cloud Sleuth automatiškai integruojasi su Jaeger. Tiesiog pridedi dependency:
„`html
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-spring-jaeger-web-starter</artifactId>
</version>3.3.1</version>
</dependency>
„`
Ir konfigūruoji application.properties:
„`html
opentracing.jaeger.udp-sender.host=localhost opentracing.jaeger.udp-sender.port=6831 opentracing.jaeger.log-spans=true
„`
Svarbu suprasti sampling koncepciją. Jei trace’intum kiekvieną užklausą production aplinkoje su dideliu traffic’u, greičiausiai užsprogdintum savo storage ir pridėtum nemažą overhead’ą aplikacijoms. Todėl naudojamas sampling – pavyzdžiui, trace’ini tik 1% visų užklausų arba naudoji adaptive sampling, kuris automatiškai reguliuoja procentą pagal apkrovą.
Duomenų analizė ir debug’inimas
Kai jau turi veikiančią Jaeger sistemą su instrumentuotomis aplikacijomis, laikas išmokti efektyviai naudoti UI. Pagrindinis vaizdas rodo trace’ų sąrašą su filtravimo galimybėmis. Gali ieškoti pagal servisą, operaciją, tag’us, trukmę.
Įsivaizduok, kad gauni alert’ą apie padidėjusį latency. Atidarai Jaeger, filtruoji pagal problemišką servisą ir rūšiuoji pagal trukmę. Iš karto matai, kad kai kurie trace’ai užtrunka 10+ sekundžių, nors turėtų būti apie 200ms.
Spaudžiamas ant konkretaus trace’o, matai Gantt chart tipo vizualizaciją – kiekvienas span rodomas kaip horizontali juosta, kurios ilgis atitinka trukmę. Čia greitai pamatai, kad problema yra database query, kuris užtrunka 9 sekundes. Paspaudus ant to span’o, matai visą metadata – SQL užklausą, parametrus, error pranešimus jei yra.
Dar viena naudinga funkcija – dependency graph. Jaeger automatiškai sugeneruoja vizualų tavo mikroservisų tarpusavio ryšių grafiką. Tai neįkainojama, kai turi dešimtis servisų ir niekas tiksliai nežino, kas su kuo komunikuoja. Grafike matai ne tik ryšius, bet ir kiek užklausų vyksta, kokia jų success rate.
Tag’ai yra super galingas įrankis. Gali pridėti custom tag’us prie span’ų – pavyzdžiui, user_id, order_id, feature_flag būseną. Vėliau gali filtruoti trace’us pagal šiuos tag’us. Pavyzdžiui, jei konkretus klientas skundžiasi problema, gali surasti visus jo trace’us per paskutinę valandą.
Performance ir skalabilumo aspektai
Vienas dažniausių klausimų – kiek overhead’o prideda distributed tracing? Atsakymas: priklauso nuo implementacijos, bet tinkamai sukonfigūruotas Jaeger turėtų pridėti mažiau nei 1% latency ir CPU overhead’o.
Raktas yra teisingas sampling. Production aplinkoje dažniausiai naudojamas probabilistic sampling – pavyzdžiui, 0.1% arba 1% visų užklausų. Tai reiškia, kad iš 10000 užklausų trace’insi tik 10 arba 100. Skamba mažai, bet su dideliu traffic’u tai vis tiek suteikia pakankamai duomenų problemoms identifikuoti.
Yra ir adaptive sampling strategijos, kurios automatiškai didina sampling rate’ą, kai aptinka anomalijas – pavyzdžiui, error’us arba neįprastai lėtas užklausas. Tai geriausias iš abiejų pasaulių – mažas overhead’as normaliu metu, bet detalūs duomenys, kai kažkas ne taip.
Storage planning’as irgi svarbus. Span’ai gali greitai sukaupti daug duomenų. Cassandra backend’as gerai horizontaliai skaluojasi, bet reikia planuoti retention policy. Dažniausiai saugoma:
- Hot data (pastaros 7 dienos) – greitam access’ui
- Warm data (8-30 dienų) – retesniam analizei
- Cold data arba išvis ištrinama po 30 dienų
Elasticsearch backend’as suteikia geresnes paieškos galimybes, bet gali būti brangesnis dideliais mastais. Čia reikia svarstyti trade-off’us pagal savo poreikius.
Collector’ių skalabilumas paprastai nėra problema – jie stateless, todėl gali tiesiog pridėti daugiau instance’ų už load balancer’io. Agent’ai irgi lengvi, nes jie tik buffering’ą daro lokaliai.
Integracijos su kitu observability stack’u
Jaeger retai gyvena vienas – paprastai jis yra dalis platesnės observability strategijos kartu su metrics (Prometheus) ir logging (ELK stack arba Loki). Šių trijų kombinacija vadinama „three pillars of observability”.
Labai galingas dalykas yra koreliacijos tarp šių sistemų. Pavyzdžiui, gali pridėti trace_id į savo aplikacijos logus. Tada Jaeger UI gali rodyti nuorodas į susijusius log įrašus Kibana, o Grafana dashboarduose gali turėti nuorodas į atitinkamus trace’us.
Praktinis pavyzdys su Python logging:
„`html
import logging
from jaeger_client import Tracer
def log_with_trace(message, span):
trace_id = span.trace_id
span_id = span.span_id
logging.info(f"trace_id={trace_id} span_id={span_id} {message}")
„`
Prometheus metrics integracija leidžia matyti bendrą vaizdą. Pavyzdžiui, Prometheus rodo, kad error rate’as išaugo 5x per paskutines 10 minučių. Tada eini į Jaeger ir filtruoji trace’us su error tag’u, kad pamatytum konkrečias nesėkmingas užklausas ir kur tiksliai jos failina.
Grafana turi Jaeger data source plugin’ą, todėl gali įterpti trace query rezultatus tiesiai į savo dashboardus. Tai labai patogu, nes nereikia šokinėti tarp skirtingų UI.
Service mesh’ai kaip Istio automatiškai generuoja distributed tracing duomenis be papildomo kodo aplikacijose. Istio proxy (Envoy) automatiškai propagate’ina trace header’ius ir siunčia spans į Jaeger. Tai puikus variantas, jei nori tracing be aplikacijų kodo keitimo, nors negausi tokių detalių kaip su manual instrumentation.
Ką daryti, kai viskas jau veikia
Turėti veikiančią Jaeger sistemą yra tik pradžia. Tikroji vertė ateina iš nuolatinio naudojimo ir įtraukimo į development workflow. Štai keletas patarimų, kaip maksimaliai išnaudoti distributed tracing:
Pirma, įtrauk trace’ų peržiūrą į savo incident response procesą. Kai kyla production problema, pirmasis žingsnis turėtų būti Jaeger atidarymas ir problemiškų trace’ų analizė. Tai gerokai greičiau nei log’ų kasimas.
Antra, naudok Jaeger performance optimizacijai. Reguliariai peržiūrėk lėčiausius endpoint’us ir ieškok optimizavimo galimybių. Dažnai atrasi, kad problemos ne ten, kur tikėjaisi – pavyzdžiui, ne database query lėtas, o N+1 problema arba nereikalingi tarpiniai API call’ai.
Trečia, mokyk savo komandą. Distributed tracing yra galingas įrankis, bet tik jei žmonės moka jį naudoti. Organizuok training session’us, pasidalink best practices, sukurk dokumentaciją su dažniausiais use case’ais.
Ketvirta, automatizuok alerting’ą. Gali sukurti alert’us, kurie trigger’inasi, kai tam tikri trace’ai viršija threshold’us. Pavyzdžiui, jei 95th percentile latency viršija 2 sekundes, gauni notifikaciją su nuoroda į atitinkamus trace’us Jaeger.
Penkta, naudok trace’us kaip dokumentaciją. Naujiems komandos nariams gali parodyti realius trace’us, kad paaiškintum, kaip sistema veikia. Tai daug geriau nei architektūros diagramos, kurios dažnai būna pasenusios.
Distributed tracing su Jaeger nėra silver bullet, bet tai vienas iš svarbiausių įrankių šiuolaikinėje mikroservisų architektūroje. Investicija į tinkamą setup’ą ir instrumentaciją atsipirks šimteriopai, kai sutaupysi valandas debug’inimo laiko arba greitai identifikuosi performance bottleneck’us. Pradėk nuo mažo – instrumentuok vieną ar du servisus, išmok basic UI naudojimą, ir palaipsniui plėsk coverage’ą. Po kelių mėnesių negalėsi įsivaizduoti, kaip anksčiau gyvenai be šito.
