OpenTelemetry OpenTelemetry

Maîtriser les coûts avec l'échantillonnage : configuration du SDK et du Collector

Décidez quand échantillonner dans le SDK et quand utiliser le tail sampling dans un Collector à deux niveaux.

15 min Avancé

Conserver chaque trace devient vite coûteux, et la plupart sont des requêtes routinières que personne n’ouvrira jamais. L’échantillonnage conserve un sous-ensemble représentatif ou intéressant. OpenTelemetry offre deux endroits pour décider : dans le SDK au démarrage d’une trace (head sampling) et dans le Collector une fois la trace terminée (tail sampling). Les deux relèvent de la pure configuration. Ce guide présente chacun et comment choisir.

Head sampling dans le SDK

La décision est prise lors de la création du span racine ; elle est donc peu coûteuse et ne nécessite aucune infrastructure supplémentaire. Elle ne peut pas savoir si la requête échouera ou sera lente, car cela ne s’est pas encore produit.

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

Les noms de samplers définis par la spécification incluent always_on, always_off, traceidratio, parentbased_always_on (la valeur par défaut), parentbased_always_off et parentbased_traceidratio. Utilisez les variantes parentbased_ : elles suivent la décision d’échantillonnage du parent de la requête entrante, de sorte qu’une trace est conservée ou abandonnée en entier à travers les services. Un simple traceidratio dans chaque service prendrait des décisions indépendantes et produirait des traces brisées.

Dans le fichier de configuration déclarative, la même idée se présente ainsi :

tracer_provider:
  sampler:
    parent_based:
      root:
        trace_id_ratio_based:
          ratio: 0.1

Consultez la documentation de votre SDK pour connaître les noms de samplers qu’il prend en charge dans le format de fichier.

Tail sampling dans le Collector

Le tail sampling attend qu’une trace soit complète, puis applique des politiques. Il vous permet de conserver toutes les erreurs et toutes les traces lentes plus une petite part du reste, ce que le head sampling ne peut pas faire. Le coût est plus d’infrastructure et de mémoire.

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 100
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 2000
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

Comment cela fonctionne :

  • decision_wait est le temps d’attente du processor après avoir vu le premier span d’une trace avant de décider. Il doit être supérieur à la durée de votre trace la plus lente, sinon vous déciderez sur des traces partielles.
  • num_traces est le nombre de traces conservées en mémoire pendant l’attente. Dimensionnez-le à peu près sur votre taux d’arrivée de traces multiplié par decision_wait, avec de la marge.
  • Les policies sont évaluées pour chaque trace. Une trace est conservée si n’importe quelle politique dit de la conserver ; les trois ci-dessus signifient donc « conserver les erreurs, conserver les lentes, et conserver aussi 5 pour cent de tout ».
  • D’autres types de politiques incluent string_attribute (correspondre à une valeur comme un niveau de client), numeric_attribute, always_sample, and (combiner des politiques) et composite (allouer des taux entre politiques).

tail_sampling se trouve dans la distribution contrib, et ses noms d’options évoluent ; consultez donc son README pour votre version.

Le piège : une trace doit atteindre un seul Collector

Le tail sampling ne fonctionne que si tous les spans d’une trace arrivent à la même instance de Collector. Avec plusieurs réplicas derrière un simple répartiteur de charge, les spans d’une trace se dispersent et les décisions sont prises sur des fragments. La solution standard est de procéder en deux niveaux :

  1. Le niveau 1 (couche de répartition de charge) utilise l’exporter loadbalancing pour router les spans par ID de trace vers une instance cohérente du niveau 2.
  2. Le niveau 2 (couche d’échantillonnage) exécute tail_sampling et exporte les traces conservées.

Niveau 1 :

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      dns:
        hostname: otel-sampler-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [loadbalancing]

Niveau 2 :

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
  batch:

exporters:
  otlp/backend:
    endpoint: tempo:4317

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

Utilisez un Service headless afin que le DNS liste les pods du niveau 2, et notez que lorsque des pods sont ajoutés ou retirés, le routage se déplace et quelques traces en cours peuvent être décidées sur des données incomplètes. Garder les couches séparées isole aussi les pannes : un problème dans l’échantillonnage ne bloque pas l’ingestion.

Combiner head et tail

Une configuration courante et sensée :

  • Un head sampler modeste dans le SDK (par exemple 50 pour cent) pour réduire le volume à la source pour les services à très fort trafic.
  • Du tail sampling dans le Collector pour garantir que les erreurs et les requêtes lentes survivent.

Soyez prudent : tout ce qui est abandonné en tête ne peut pas être récupéré en queue. Si vous devez conserver toutes les erreurs, n’échantillonnez pas ces services de manière agressive en tête.

Les métriques survivent à l’échantillonnage si vous les dérivez d’abord

Abandonner des traces abandonne aussi les chiffres que vous pourriez en calculer, comme les taux de requêtes et les percentiles de latence. Générez les métriques avant l’étape d’échantillonnage avec le connector spanmetrics, afin que les tableaux de bord restent exacts :

connectors:
  spanmetrics:

service:
  pipelines:
    traces/all:
      receivers: [otlp]
      exporters: [spanmetrics, loadbalancing]
    metrics/derived:
      receivers: [spanmetrics]
      processors: [batch]
      exporters: [otlphttp/prometheus]

Ici, chaque span alimente le connector, tandis que seul l’ensemble échantillonné continue vers le stockage.

Choisir

ObjectifApproche
Réduire le coût simplement, sans nouvelle infrastructureHead sampling avec parentbased_traceidratio
Conserver toutes les erreurs et requêtes lentesTail sampling dans un Collector à deux niveaux
Conserver des taux et percentiles exactsspanmetrics avant l’échantillonnage
Peu de services, faible volumePas d’échantillonnage ; y revenir quand le coût compte

Vérifier

  • Au niveau 2, comparez le nombre de spans en entrée et en sortie à l’aide des métriques propres du Collector (port 8888) pour confirmer que le ratio correspond à vos politiques.
  • Recherchez une requête échouée connue et assurez-vous que toute sa trace est présente.
  • Surveillez la mémoire au niveau 2, car il conserve les traces pendant decision_wait. Si elle augmente, réduisez num_traces ou ajoutez des réplicas, et gardez memory_limiter devant.

Guides populaires