ITSkillsCenter
Business Digital

Echantillonnage tail-based des traces pour maitriser les couts pas-a-pas

12 min de lecture

📍 Article principal : Observabilité applicative en 2026 : OpenTelemetry, traces distribuées et stack LGTM — pour le contexte conceptuel et l’architecture d’ensemble.

Pourquoi le tail-based sampling est la stratégie qui paie

Au-delà d’un certain volume, garder 100 % des traces devient économiquement inacceptable. Le réflexe naïf est de réduire le débit côté SDK avec un sampler probabiliste à 1 ou 5 %. Cette approche, dite head-based sampling, présente un défaut majeur : elle décide de garder ou non une trace dès le premier span, sans connaître la suite. On jette à l’aveugle 95 ou 99 % du trafic, y compris les traces qui auraient terminé en erreur ou avec une latence anormale — précisément celles qu’on aurait voulu garder pour investiguer.

Le tail-based sampling inverse la logique : on attend la fin de la trace, on examine ce qu’elle contient (durée totale, présence d’erreurs, attributs métier), et on décide alors de la garder ou non. Une politique typique conserve 100 % des traces en erreur, 100 % des traces lentes, et un échantillon de 5 % des autres. Le résultat est une réduction massive du volume sans perte d’information utile pour l’investigation.

Ce tutoriel met en place le tail-based sampling dans un OpenTelemetry Collector, d’abord en mode mono-instance pour comprendre les politiques, puis en mode distribué à deux couches qui passe à l’échelle.

Prérequis

  • Un Collector OpenTelemetry contrib fonctionnel — voir le tutoriel Configurer un OpenTelemetry Collector
  • Une instance Tempo qui ingère les traces — voir Envoyer les traces vers Tempo
  • Une application qui produit des traces avec une diversité de durées et de statuts
  • Notions de YAML et compréhension de l’architecture du Collector
  • Temps estimé : 40 à 55 minutes

Étape 1 — Comprendre la contrainte d’unicité de l’instance

Le processor tail_sampling a une contrainte structurelle simple à formuler mais essentielle à respecter : tous les spans d’une trace donnée doivent arriver sur la même instance de Collector. Sans cette garantie, l’instance ne voit qu’une partie de la trace et prend une décision incohérente, ou pire, plusieurs instances prennent des décisions différentes pour la même trace.

En mode mono-instance (un seul Collector), la contrainte est triviale : tout arrive au même endroit. En mode multi-instances pour la haute disponibilité ou la montée en charge, il faut une couche en amont qui route par trace_id vers une instance précise — c’est ce que fait l’exporter loadbalancing, qu’on configure à l’étape 6.

Étape 2 — Configuration du processor en mono-instance

On commence par le cas simple : un Collector unique qui reçoit les traces, les passe au processor tail_sampling, puis exporte vers Tempo. La configuration de base définit la fenêtre d’attente, la capacité, et les politiques.

# otelcol.yaml (extrait)
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow-traces-policy
        type: latency
        latency:
          threshold_ms: 500
      - name: sample-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resource, tail_sampling, batch]
      exporters: [otlp/tempo]

Trois paramètres clés gouvernent le comportement. decision_wait est le délai après lequel une trace est considérée comme terminée et soumise aux politiques — 10 secondes est un défaut raisonnable pour des requêtes web typiques. num_traces est la capacité maximale de traces simultanément en mémoire ; au-delà, les nouvelles traces sont rejetées. expected_new_traces_per_sec aide le Collector à pré-allouer les structures internes.

Important : l’ordre dans la liste de processors place tail_sampling avant batch. C’est cohérent — on prend la décision puis on bat, pas l’inverse. Mettre batch avant briserait la sémantique du sampling.

Étape 3 — Anatomie des politiques

Les politiques s’évaluent dans l’ordre. Dès qu’une politique vote sample, la trace est conservée — pas besoin d’évaluer les suivantes. Si toutes votent not_sample, la trace est rejetée. Cette logique permet d’organiser les politiques du plus prioritaire au plus permissif.

Dans la configuration ci-dessus :

  • errors-policy garde 100 % des traces dont au moins un span a un statut ERROR
  • slow-traces-policy garde 100 % des traces dont la durée totale dépasse 500 ms
  • sample-rest est la politique de fond qui garde 5 % des traces restantes par échantillonnage probabiliste sur le trace_id

Cette combinaison capture l’essentiel : tout ce qui erreure, tout ce qui est lent, et un échantillon représentatif du trafic normal pour les besoins de tendance et de comparaison. Elle réduit typiquement le volume de 90 % sans perdre l’utile.

Étape 4 — Politiques avancées par attribut

Les politiques ci-dessus couvrent les cas génériques. Pour des besoins métier précis (garder 100 % des traces d’un endpoint critique, exclure systématiquement le bruit d’un health check), on utilise des politiques par attribut.

policies:
  - name: keep-errors
    type: status_code
    status_code:
      status_codes: [ERROR]

  - name: keep-slow
    type: latency
    latency:
      threshold_ms: 500

  - name: keep-payment-route
    type: string_attribute
    string_attribute:
      key: http.route
      values: ["/api/payment", "/api/checkout"]

  - name: drop-healthcheck
    type: drop
    drop:
      drop_sub_policy:
        - name: is-healthcheck
          type: string_attribute
          string_attribute:
            key: http.route
            values: ["/health", "/ready"]

  - name: sample-rest
    type: probabilistic
    probabilistic:
      sampling_percentage: 5

La politique keep-payment-route garde 100 % des traces qui contiennent un span dont l’attribut http.route est /api/payment ou /api/checkout — utile pour les flux critiques où l’on veut une visibilité totale. La politique drop-healthcheck de type drop exclut explicitement les traces de health check : les politiques drop ont précédence sur les politiques sample, donc une trace qui matche un drop_sub_policy est rejetée même si une autre politique voterait pour la conserver. C’est le mécanisme officiel pour exclure des routes du sampling, et il diffère d’une approche naïve par and + probabilistic 0 qui ne fonctionne pas (les autres politiques top-level peuvent toujours voter sample).

Étape 5 — Surveiller le processor lui-même

Le Collector expose des métriques internes sur le tail sampling qu’il faut superviser. Les compteurs clés :

  • otelcol_processor_tail_sampling_global_count_traces_sampled — total trace décidées par politique
  • otelcol_processor_tail_sampling_count_traces_sampled — par décision (sampled/not_sampled) et par politique
  • otelcol_processor_tail_sampling_sampling_decision_timer_latency — temps de décision
  • otelcol_processor_tail_sampling_traces_on_memory — nombre de traces actuellement en mémoire

Ces métriques permettent de vérifier que les politiques se déclenchent comme prévu et que le processor n’est pas en saturation. Une saturation se manifeste par des traces rejetées (traces_on_memory qui plafonne à num_traces), ce qui demande soit d’augmenter la capacité, soit d’ajouter une instance et de passer à la couche distribuée.

Étape 6 — Architecture distribuée à deux couches

Quand le volume dépasse ce qu’une instance unique tient, on passe à une architecture en deux couches. La première couche (load balancer layer) reçoit toutes les traces et les redirige par trace_id vers la seconde couche (tail sampling layer) qui exécute les politiques. Cette topologie garantit que tous les spans d’une trace arrivent sur la même instance de la seconde couche, condition nécessaire pour des décisions cohérentes.

# Couche 1 : load balancer
# otelcol-lb.yaml
exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      static:
        hostnames:
          - tail-sampling-1:4317
          - tail-sampling-2:4317
          - tail-sampling-3:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [loadbalancing]

La clé est routing_key: traceID. L’exporter calcule un hash du trace_id et choisit l’instance de destination de manière déterministe. Tous les spans d’une trace finissent sur la même instance de la couche tail. Le resolver static liste les destinataires manuellement ; en Kubernetes, le resolver k8s ou dns détecte les pods de la couche tail à la volée et adapte la liste.

# Couche 2 : tail sampling
# otelcol-tail.yaml
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 200000
    expected_new_traces_per_sec: 5000
    policies:
      - name: errors-policy
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow-traces-policy
        type: latency
        latency:
          threshold_ms: 500
      - name: sample-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/tempo]

La couche 2 est identique au mode mono-instance, à un détail près : la capacité num_traces et expected_new_traces_per_sec sont dimensionnées pour la part du trafic qu’une instance reçoit, pas pour le total. Avec trois instances tail-sampling, chacune gère environ un tiers du trafic.

Étape 7 — Choisir le decision_wait

Le decision_wait est le compromis le plus délicat à régler. Trop court, on prend la décision avant la fin de certaines traces et on fragmente les arbres. Trop long, on retient des traces en mémoire et on consomme des ressources. La règle empirique est decision_wait = p99_durée_trace + marge.

Pour des requêtes web typiques (durée majoritairement sous 1 seconde), 10 secondes laissent une marge confortable. Pour des batchs ou des opérations longues (export de fichier, traitement asynchrone), il faut monter à 60 secondes voire plus. À chaque environnement de mesurer la distribution réelle des durées de trace et d’ajuster.

Étape 8 — Vérifier l’efficacité du sampling

Le test final consiste à valider que les politiques se comportent comme attendu sur un échantillon de trafic réel. On compare le nombre de traces ingérées à la couche LB (avant sampling) au nombre de traces qui atteignent Tempo (après sampling). Le ratio donne directement l’efficacité du sampling.

# Métriques typiques à comparer
# couche LB
sum(rate(otelcol_receiver_accepted_spans{job="otel-lb"}[5m]))

# couche tail (après sampling)
sum(rate(otelcol_exporter_sent_spans{job="otel-tail"}[5m]))

Avec une politique conservant 100 % erreurs + 100 % lentes + 5 % du reste, on s’attend à un ratio d’environ 5-15 % selon la prévalence d’erreurs et de traces lentes dans le trafic. Si le ratio est plus élevé, l’une des politiques garde plus que prévu — vérifier que threshold_ms n’est pas trop bas. Si le ratio est plus bas, c’est que le trafic est exceptionnellement propre, ou que sampling_percentage est trop conservateur pour les besoins d’analyse.

Erreurs fréquentes

Activer tail_sampling sans la couche load balancer

Avec plusieurs Collectors derrière un load balancer round-robin classique, les spans d’une même trace se répartissent au hasard. Chaque instance ne voit qu’une partie et prend des décisions incohérentes. Symptôme : traces fragmentées dans Tempo, des spans présents et d’autres absents pour la même trace_id. La fix est obligatoire : passer à la topologie à deux couches avec routing_key: traceID.

decision_wait trop court par rapport aux opérations longues

Si l’application contient des spans longs (uploads, batchs), un decision_wait de 10 secondes prend la décision pendant que la trace n’est pas terminée. Les spans postérieurs à la décision ne sont pas pris en compte par la politique. Augmenter decision_wait à 60 ou 120 secondes pour couvrir le pire cas raisonnable.

num_traces sous-dimensionné

Si la capacité maximale est dépassée, les nouvelles traces sont rejetées et le sampling devient incohérent. Surveiller traces_on_memory et l’ajuster en fonction du trafic réel : num_traces ≈ debit × decision_wait × 1.5 pour avoir une marge.

Politique probabiliste avant les politiques spécifiques

L’ordre des politiques compte : la première à voter sample emporte la décision. Mettre la politique probabiliste de 5 % avant errors-policy peut faire perdre des traces en erreur quand le tirage probabiliste tombe sur not_sample. Mettre toujours les politiques de garde absolue (erreurs, latence, routes critiques) en premier.

Confondre tail sampling et head sampling SDK

Activer un sampler 1 % côté SDK puis du tail sampling côté Collector revient à appliquer les deux taux successivement : on ne garde que 5 % du déjà-1 %, soit 0,05 %. Désactiver le head sampling côté SDK (OTEL_TRACES_SAMPLER=always_on) et laisser toute la décision au tail sampling.

Tutoriels associés

Ressources et références officielles

FAQ

Tail sampling vs head sampling, lequel choisir ?

Tail dans la majorité des cas, parce qu’il prend des décisions informées. Head est plus simple et n’exige pas de Collector centralisé, mais perd l’information sur le résultat de la trace. La combinaison la plus pragmatique est : pas de head sampling (100 % côté SDK), tail sampling dans le Collector quand le volume justifie l’effort.

Combien d’instances tail-sampling pour quel débit ?

Une instance tient confortablement 5 000 à 10 000 traces simultanées en mémoire avec 1 vCPU et 2 Go RAM. Pour 100 000 traces/s sur des durées moyennes de 1 seconde, prévoir 3 à 5 instances avec une légère marge. La supervision de traces_on_memory donne le signal de scale-out.

Le sampling fonctionne-t-il aussi pour les métriques ?

Pas au sens du tail sampling — les métriques sont par construction agrégées et il n’y a pas de « décision a posteriori » à prendre. Pour réduire le volume de métriques, on agit sur la cardinalité (filtrer ou réécrire les attributs côté Collector) et sur la fréquence d’export, pas par sampling.

Comment garder le contexte entre traces samplées et métriques ?

Activer le générateur de span metrics côté Tempo, qui produit des métriques RED à partir de toutes les traces ingérées avant le sampling, ou positionner le générateur dans la couche LB. De cette manière, les métriques reflètent le trafic complet, et le sampling n’affecte que le stockage des traces individuelles.

Peut-on avoir plusieurs politiques de probabilité ?

Oui. On peut par exemple définir 10 % pour les traces du service A et 1 % pour les traces du service B en combinant string_attribute et probabilistic via la composition and. Cette finesse est utile quand certains services génèrent un volume disproportionné par rapport à leur valeur diagnostique.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité