Debugowanie problemów w architekturze mikroserwisowej to jak szukanie igły w stogu siana. Jedno żądanie użytkownika może przechodzić przez dziesiątki serwisów. Gdy coś idzie nie tak, tradycyjne logi często nie wystarczą. Distributed tracing daje nam pełny obraz przepływu żądania przez system.
Dlaczego distributed tracing jest kluczowy?
W monolicie śledzenie żądania jest proste – wszystko dzieje się w jednym procesie. W mikrousługach żądanie skacze między serwisami, każdy ze swoimi logami i metrykami. Bez distributed tracing nie wiemy gdzie jest bottleneck ani dlaczego żądanie trwa 5 sekund zamiast 500ms.
Co się nauczysz:
- Czym jest distributed tracing i jak działa
- Architektura i komponenty Jaeger
- Implementacja tracing w Spring Boot z OpenTracing
- Instrumentacja kodu – manualna i automatyczna
- Analiza trace’ów i znajdowanie problemów wydajnościowych
- Best practices i strategie sampling
Wymagania wstępne:
- Doświadczenie z mikrousługami i Spring Boot
- Podstawowa znajomość Docker i docker-compose
- Zrozumienie komunikacji REST między serwisami
- Znajomość podstawowych metryk wydajnościowych
Podstawy distributed tracing
Distributed tracing opiera się na kilku kluczowych konceptach:
Span – pojedyncza operacja w ramach trace, np. wywołanie metody czy request HTTP
Context – metadane propagowane między serwisami (trace ID, span ID, baggage)
Jak to działa?
Każde żądanie dostaje unikalny trace ID. Gdy serwis A wywołuje serwis B, przekazuje ten ID w headerach HTTP. Każdy serwis tworzy własne span’y które są częścią większego trace.
// Przykład propagacji kontekstu GET /api/orders/123 HTTP/1.1 X-B3-TraceId: 463ac35c9f6413ad48485a3953bb6124 X-B3-SpanId: a2fb4a1d1a96d312 X-B3-ParentSpanId: 0020000000000001 X-B3-Sampled: 1
Architektura Jaeger
Jaeger składa się z kilku komponentów:
1. Jaeger Client
Biblioteka w aplikacji która instrumentuje kod i wysyła trace’y do agenta.
2. Jaeger Agent
Lokalny daemon który zbiera trace’y z aplikacji i przekazuje do collectora. Redukuje network overhead.
3. Jaeger Collector
Przyjmuje trace’y od agentów, waliduje i zapisuje w storage.
4. Storage Backend
Cassandra lub Elasticsearch do przechowywania trace’ów.
5. Jaeger UI
Web interface do przeglądania i analizy trace’ów.
Instalacja Jaeger
Najprostszy sposób to all-in-one image dla developmentu:
# docker-compose.yml version: '3' services: jaeger: image: jaegertracing/all-in-one:1.8 ports: - "5775:5775/udp" # agent - accept zipkin.thrift - "6831:6831/udp" # agent - accept jaeger.thrift - "6832:6832/udp" # agent - accept jaeger.thrift binary - "5778:5778" # agent - serve configs - "16686:16686" # query - serve UI - "14268:14268" # collector - accept jaeger.thrift - "9411:9411" # collector - accept zipkin environment: - COLLECTOR_ZIPKIN_HTTP_PORT=9411
Implementacja w Spring Boot
1. Dodanie zależności
io.opentracing.contrib opentracing-spring-jaeger-web-starter 1.0.1 io.jaegertracing jaeger-client 0.32.0
2. Konfiguracja
# application.yml opentracing: jaeger: enabled: true service-name: ${spring.application.name} udp-sender: host: localhost port: 6831 const-sampler: decision: true # sample wszystkie requesty (dev) log-spans: true # Produkcja - probabilistic sampler # probabilistic-sampler: # sampling-rate: 0.1 # 10% requestów
3. Automatyczna instrumentacja
Spring Boot starter automatycznie instrumentuje:
- Incoming HTTP requests (RestController)
- Outgoing HTTP requests (RestTemplate)
- JDBC queries
- Scheduled tasks
@RestController @RequestMapping("/api/orders") public class OrderController { @Autowired private OrderService orderService; @Autowired private RestTemplate restTemplate; // automatycznie instrumentowany @GetMapping("/{id}") public Order getOrder(@PathVariable String id) { // Ten endpoint automatycznie tworzy span return orderService.findOrder(id); } } @Service public class OrderService { @Autowired private OrderRepository repository; public Order findOrder(String id) { // JDBC query też będzie traced Order order = repository.findById(id); // Wywołanie do innego serwisu - kontekst propagowany Customer customer = restTemplate.getForObject( "http://customer-service/api/customers/" + order.getCustomerId(), Customer.class ); order.setCustomer(customer); return order; } }
Manualna instrumentacja
Czasami potrzebujemy więcej kontroli nad span’ami:
Tworzenie custom spans
@Component public class PaymentService { @Autowired private Tracer tracer; public PaymentResult processPayment(Order order) { // Tworzenie nowego span Span span = tracer.buildSpan("process-payment") .withTag("order.id", order.getId()) .withTag("payment.amount", order.getTotalAmount()) .start(); try (Scope scope = tracer.activateSpan(span)) { // Symulacja processing validatePaymentDetails(order); // Child span dla external call Span paymentGatewaySpan = tracer.buildSpan("payment-gateway-call") .asChildOf(span) .withTag("gateway", "stripe") .start(); try (Scope gwScope = tracer.activateSpan(paymentGatewaySpan)) { return callPaymentGateway(order); } finally { paymentGatewaySpan.finish(); } } catch (Exception e) { span.setTag("error", true); span.log(ImmutableMap.of( "event", "error", "message", e.getMessage() )); throw e; } finally { span.finish(); } } }
Dodawanie baggage
@Component public class SecurityFilter implements Filter { @Autowired private Tracer tracer; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String userId = extractUserId(httpRequest); // Dodanie user ID do baggage - będzie propagowane do wszystkich serwisów Span activeSpan = tracer.activeSpan(); if (activeSpan != null) { activeSpan.setBaggageItem("user.id", userId); activeSpan.setTag("user.id", userId); } chain.doFilter(request, response); } }
Analiza trace’ów w Jaeger UI
Jaeger UI (http://localhost:16686) oferuje kilka widoków:
1. Search traces
Wyszukiwanie po serwisie, operacji, tagach, czasie trwania.
2. Trace Timeline
Wizualizacja wszystkich span’ów w trace – widać dokładnie gdzie jest bottleneck.
3. Service Dependencies
Graf zależności między serwisami na podstawie trace’ów.
4. Trace Comparison
Porównanie dwóch trace’ów – świetne do debugowania regression.
Strategie sampling
W produkcji nie możemy trace’ować wszystkiego – to za dużo danych:
1. Const Sampler
// Wszystko (dev) lub nic (prod) Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() .withType(ConstSampler.TYPE) .withParam(1); // 1 = wszystko, 0 = nic
2. Probabilistic Sampler
// Losowy % requestów Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() .withType(ProbabilisticSampler.TYPE) .withParam(0.1); // 10% requestów
3. Rate Limiting Sampler
// Max N trace'ów na sekundę Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() .withType(RateLimitingSampler.TYPE) .withParam(2.0); // max 2 trace/sec
4. Adaptive Sampler
// Dynamiczny sampling based on traffic Configuration.SamplerConfiguration samplerConfig = Configuration.SamplerConfiguration.fromEnv() .withType(AdaptiveSampler.TYPE) .withParam(1.0); // target 1 trace/sec per endpoint
Best practices
1. Sensowne nazwy operacji
// ŹLE span = tracer.buildSpan(request.getRequestURI()).start(); // DOBRZE span = tracer.buildSpan("GET /api/orders/{id}").start();
2. Użyteczne tagi
span.setTag("http.method", "GET"); span.setTag("http.status_code", 200); span.setTag("user.id", userId); span.setTag("order.total", orderTotal); span.setTag("cache.hit", cacheHit); span.setTag("db.statement", "SELECT * FROM orders WHERE id = ?");
3. Correlation z logami
// Dodaj trace ID do MDC @Component public class TracingMDCFilter implements Filter { @Autowired private Tracer tracer; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Span span = tracer.activeSpan(); if (span != null) { MDC.put("traceId", span.context().toTraceId()); MDC.put("spanId", span.context().toSpanId()); } try { chain.doFilter(request, response); } finally { MDC.clear(); } } } // Logback pattern%d{ISO8601} [%thread] [%X{traceId}/%X{spanId}] %-5level %logger{36} - %msg%n
Troubleshooting
Brakujące trace’y
Broken traces
Gdy trace jest „połamany” (brak połączenia między span’ami):
- Sprawdź propagację kontekstu w HTTP headers
- Upewnij się że wszystkie serwisy używają tego samego formatu (B3, Jaeger, W3C)
- Async operations mogą gubić kontekst – użyj tracer.activeSpan()
Performance overhead
# Optymalizacje dla produkcji opentracing: jaeger: udp-sender: max-packet-size: 65000 # większe batche reporter: max-queue-size: 10000 # większy buffer flush-interval: 1000 # rzadsze wysyłanie
FAQ – Często zadawane pytania
OpenTracing to starszy standard (używany tutaj z Jaeger). OpenTelemetry to nowy, zunifikowany standard który łączy OpenTracing i OpenCensus. Jaeger wspiera oba, ale OpenTelemetry jest przyszłością – migruj gdy będziesz gotowy.
Nie! To uzupełnienie. Logi pokazują co się stało, metryki pokazują trendy, a trace’y pokazują jak żądanie przepływa przez system. Potrzebujesz wszystkich trzech dla pełnej observability.
Zależy od ruchu i miejsca. Typowo: 2-7 dni dla wszystkich trace’ów, 30 dni dla trace’ów z błędami, sampling historycznych trace’ów na dłużej. W Cassandra łatwo o retention policy.
Tak! Zipkin to popularna alternatywa. Jaeger ma lepsze wsparcie dla dużej skali, więcej features (adaptive sampling) i aktywniejszy development. Ale Zipkin jest prostszy w setup.
Musisz ręcznie propagować kontekst. Przed async operation pobierz span: Span span = tracer.activeSpan(), potem w async code aktywuj go: try (Scope scope = tracer.activateSpan(span)) {…}
Z sensownym sampling (0.1%) overhead jest minimalny – <1% CPU. Główny koszt to network (wysyłanie do Jaeger). Używaj local agent i batching. Unikaj za dużo tagów i logów w spans.
Włącz log-spans: true w konfiguracji – zobaczysz span’y w logach. Sprawdź metrics Jaeger agent (port 5778). Użyj tcpdump do sprawdzenia czy pakiety UDP docierają do agenta.
Przydatne zasoby:
- Jaeger Documentation
- OpenTracing Java Guide
- Jaeger GitHub Repository
- Jaeger Blog na Medium
- Distributed Tracing with Jaeger (video)
🚀 Zadanie dla Ciebie
Zaimplementuj distributed tracing w przykładowej aplikacji e-commerce:
- Stwórz 3 mikroserwisy: order-service, inventory-service, payment-service
- Dodaj Jaeger i OpenTracing do każdego serwisu
- Zaimplementuj flow: create order → check inventory → process payment
- Dodaj custom span dla każdej business operation
- Symuluj błąd w payment-service i znajdź go w Jaeger UI
- Bonus: Dodaj correlation z logami przez MDC
Cel: Zobacz jak trace pomaga znaleźć bottleneck gdy payment-service odpowiada wolno!
Distributed tracing z Jaeger to must-have w każdej poważnej architekturze mikroserwisowej. Jak radzisz sobie z debugowaniem w swoich systemach rozproszonych? Jakie inne narzędzia do observability polecasz? Podziel się doświadczeniami!