Traçage distribué
Mettez en œuvre le traçage distribué avec Jaeger et Tempo pour la visibilité du flux des requêtes sur les microservices.
Objectif
Suivre les requêtes dans les systèmes distribués pour comprendre la latence, les dépendances et les points de défaillance.
Quand l'utiliser
- Déboguer les problèmes de latence
- Comprendre les dépendances entre services
- Identifier les goulots d'étranglement
- Tracer la propagation des erreurs
- Analyser les chemins des requêtes
Concepts du traçage distribué
Structure d'une trace
Trace (Request ID: abc123)
↓
Span (frontend) [100ms]
↓
Span (api-gateway) [80ms]
├→ Span (auth-service) [10ms]
└→ Span (user-service) [60ms]
└→ Span (database) [40ms]
Composants clés
- Trace - Parcours de la requête d'un bout à l'autre
- Span - Opération unique au sein d'une trace
- Context - Métadonnées propagées entre les services
- Tags - Paires clé-valeur pour le filtrage
- Logs - Événements horodatés au sein d'un span
Configuration de Jaeger
Déploiement Kubernetes
# Déployer Jaeger Operator
kubectl create namespace observability
kubectl create -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.51.0/jaeger-operator.yaml -n observability
# Déployer une instance Jaeger
kubectl apply -f - <<EOF
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
namespace: observability
spec:
strategy: production
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch:9200
ingress:
enabled: true
EOF
Docker Compose
version: "3.8"
services:
jaeger:
image: jaegertracing/all-in-one:1.62
ports:
- "5775:5775/udp"
- "6831:6831/udp"
- "6832:6832/udp"
- "5778:5778"
- "16686:16686" # UI
- "14268:14268" # Collector
- "14250:14250" # gRPC
- "9411:9411" # Zipkin
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
Référence : Voir references/jaeger-setup.md
Instrumentation de l'application
OpenTelemetry (recommandé)
Python (Flask)
from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from flask import Flask
# Initialiser le tracer
resource = Resource(attributes={SERVICE_NAME: "my-service"})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(JaegerExporter(
agent_host_name="jaeger",
agent_port=6831,
))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# Instrumenter Flask
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
@app.route('/api/users')
def get_users():
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("get_users") as span:
span.set_attribute("user.count", 100)
# Logique métier
users = fetch_users_from_db()
return {"users": users}
def fetch_users_from_db():
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("database_query") as span:
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.statement", "SELECT * FROM users")
# Requête à la base de données
return query_database()
Node.js (Express)
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
const { JaegerExporter } = require("@opentelemetry/exporter-jaeger");
const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");
const { HttpInstrumentation } = require("@opentelemetry/instrumentation-http");
const {
ExpressInstrumentation,
} = require("@opentelemetry/instrumentation-express");
// Initialiser le tracer
const provider = new NodeTracerProvider({
resource: { attributes: { "service.name": "my-service" } },
});
const exporter = new JaegerExporter({
endpoint: "http://jaeger:14268/api/traces",
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
// Instrumenter les bibliothèques
registerInstrumentations({
instrumentations: [new HttpInstrumentation(), new ExpressInstrumentation()],
});
const express = require("express");
const app = express();
app.get("/api/users", async (req, res) => {
const tracer = trace.getTracer("my-service");
const span = tracer.startSpan("get_users");
try {
const users = await fetchUsers();
span.setAttributes({ "user.count": users.length });
res.json({ users });
} finally {
span.end();
}
});
Go
package main
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
func initTracer() (*sdktrace.TracerProvider, error) {
exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://jaeger:14268/api/traces"),
))
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("my-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
func getUsers(ctx context.Context) ([]User, error) {
tracer := otel.Tracer("my-service")
ctx, span := tracer.Start(ctx, "get_users")
defer span.End()
span.SetAttributes(attribute.String("user.filter", "active"))
users, err := fetchUsersFromDB(ctx)
if err != nil {
span.RecordError(err)
return nil, err
}
span.SetAttributes(attribute.Int("user.count", len(users)))
return users, nil
}
Référence : Voir references/instrumentation.md
Propagation de contexte
En-têtes HTTP
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE
Propagation dans les requêtes HTTP
Python
from opentelemetry.propagate import inject
headers = {}
inject(headers) # Injecte le contexte de trace
response = requests.get('http://downstream-service/api', headers=headers)
Node.js
const { propagation } = require("@opentelemetry/api");
const headers = {};
propagation.inject(context.active(), headers);
axios.get("http://downstream-service/api", { headers });
Configuration de Tempo (Grafana)
Déploiement Kubernetes
apiVersion: v1
kind: ConfigMap
metadata:
name: tempo-config
data:
tempo.yaml: |
server:
http_listen_port: 3200
distributor:
receivers:
jaeger:
protocols:
thrift_http:
grpc:
otlp:
protocols:
http:
grpc:
storage:
trace:
backend: s3
s3:
bucket: tempo-traces
endpoint: s3.amazonaws.com
querier:
frontend_worker:
frontend_address: tempo-query-frontend:9095
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tempo
spec:
replicas: 1
template:
spec:
containers:
- name: tempo
image: grafana/tempo:2.7
args:
- -config.file=/etc/tempo/tempo.yaml
volumeMounts:
- name: config
mountPath: /etc/tempo
volumes:
- name: config
configMap:
name: tempo-config
Référence : Voir assets/jaeger-config.yaml.template
Stratégies d'échantillonnage
Échantillonnage probabiliste
# Échantillonner 1 % des traces
sampler:
type: probabilistic
param: 0.01
Échantillonnage avec limitation de débit
# Échantillonner max 100 traces par seconde
sampler:
type: ratelimiting
param: 100
Échantillonnage adaptatif
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
# Échantillonner en fonction de l'ID de trace (déterministe)
sampler = ParentBased(root=TraceIdRatioBased(0.01))
Analyse des traces
Trouver les requêtes lentes
Requête Jaeger :
service=my-service
duration > 1s
Trouver les erreurs
Requête Jaeger :
service=my-service
error=true
tags.http.status_code >= 500
Graphique de dépendances entre services
Jaeger génère automatiquement des graphiques de dépendances entre services montrant :
- Les relations entre services
- Les taux de requêtes
- Les taux d'erreurs
- Les latences moyennes
Bonnes pratiques
- Échantillonner correctement (1-10 % en production)
- Ajouter des tags significatifs (user_id, request_id)
- Propager le contexte sur tous les limites de services
- Enregistrer les exceptions dans les spans
- Utiliser un nommage cohérent pour les opérations
- Surveiller la surcharge du traçage (<1 % d'impact CPU)
- Configurer des alertes pour les erreurs de trace
- Implémenter le contexte distribué (baggage)
- Utiliser les événements de span pour les jalons importants
- Documenter les normes d'instrumentation
Intégration avec la journalisation
Logs corrélés
import logging
from opentelemetry import trace
logger = logging.getLogger(__name__)
def process_request():
span = trace.get_current_span()
trace_id = span.get_span_context().trace_id
logger.info(
"Processing request",
extra={"trace_id": format(trace_id, '032x')}
)
Dépannage
Aucune trace n'apparaît :
- Vérifier l'endpoint du collecteur
- Vérifier la connectivité réseau
- Vérifier la configuration d'échantillonnage
- Examiner les logs de l'application
Surcharge de latence élevée :
- Réduire le taux d'échantillonnage
- Utiliser le processeur de span par lot
- Vérifier la configuration de l'exportateur
Compétences associées
prometheus-configuration- Pour les métriquesgrafana-dashboards- Pour la visualisationslo-implementation- Pour les SLO de latence