paint-brush
Observabilité du backend Java avec les traces OpenTelemetry et le code minimalpar@apanasevich
292 lectures

Observabilité du backend Java avec les traces OpenTelemetry et le code minimal

par Dmitriy Apanasevich10m2024/11/15
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

Comment nous avons intégré le framework OpenTelemetry dans notre backend Java, en obtenant un traçage avec un minimum de codage.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Observabilité du backend Java avec les traces OpenTelemetry et le code minimal
Dmitriy Apanasevich HackerNoon profile picture

Bonjour à tous ! Je suis Dmitriy Apanasevich, développeur Java chez MY.GAMES, je travaille sur le jeu Rush Royale et j'aimerais partager notre expérience d'intégration du framework OpenTelemetry dans notre backend Java. Il y a beaucoup à dire ici : nous aborderons les modifications de code nécessaires à sa mise en œuvre, ainsi que les nouveaux composants que nous avons dû installer et configurer – et, bien sûr, nous partagerons certains de nos résultats.

Notre objectif : parvenir à l'observabilité du système

Donnons un peu plus de contexte à notre cas. En tant que développeurs, nous voulons créer un logiciel facile à surveiller, à évaluer et à comprendre (et c'est précisément le but de la mise en œuvre d'OpenTelemetry - pour maximiser la sécurité du système). observabilité ).


Les méthodes traditionnelles de collecte d'informations sur les performances des applications impliquent souvent la journalisation manuelle des événements, des mesures et des erreurs :



Bien sûr, il existe de nombreux frameworks qui nous permettent de travailler avec des logs, et je suis sûr que tous ceux qui lisent cet article disposent d'un système configuré pour collecter, stocker et analyser les logs.


La journalisation a également été entièrement configurée pour nous, nous n'avons donc pas utilisé les fonctionnalités fournies par OpenTelemetry pour travailler avec les journaux.


Une autre façon courante de surveiller le système consiste à exploiter des mesures :


Nous disposions également d’un système entièrement configuré pour la collecte et la visualisation des métriques, nous avons donc ici aussi ignoré les capacités d’OpenTelemetry en termes de travail avec les métriques.


Mais un outil moins courant pour obtenir et analyser ce type de données système est traces .


Une trace représente le chemin emprunté par une requête dans notre système au cours de sa durée de vie. Elle commence généralement lorsque le système reçoit une requête et se termine par la réponse. Les traces se composent de plusieurs travées , chacun représentant une unité de travail spécifique déterminée par le développeur ou la bibliothèque de son choix. Ces étendues forment une structure hiérarchique qui permet de visualiser la manière dont le système traite la demande.


Pour cette discussion, nous nous concentrerons sur l'aspect traçage d'OpenTelemetry.

Quelques informations supplémentaires sur OpenTelemetry

Jetons également un peu de lumière sur le projet OpenTelemetry, né de la fusion de OpenTracing et Recensement ouvert projets.


OpenTelemetry fournit désormais une gamme complète de composants basés sur une norme qui définit un ensemble d'API, de SDK et d'outils pour divers langages de programmation, et l'objectif principal du projet est de générer, collecter, gérer et exporter des données.


Cela dit, OpenTelemetry n'offre pas de backend pour le stockage de données ou les outils de visualisation.


Comme nous nous intéressions uniquement au traçage, nous avons exploré les solutions open source les plus populaires pour stocker et visualiser les traces :

  • Jaeger
  • Zipkin
  • Grafana Tempo


En fin de compte, nous avons choisi Grafana Tempo en raison de ses capacités de visualisation impressionnantes, de son rythme de développement rapide et de son intégration avec notre configuration Grafana existante pour la visualisation des métriques. Le fait de disposer d'un outil unique et unifié constituait également un avantage considérable.

Composants OpenTelemetry

Décortiquons également un peu les composants d’OpenTelemetry.


La spécification:

  • API — types de données, opérations, énumérations

  • SDK — implémentation de spécifications, API sur différents langages de programmation. Un langage différent signifie un état SDK différent, d'alpha à stable.

  • Protocole de données (OTLP) et conventions sémantiques


L'API Java et le SDK :

  • Bibliothèques d'instrumentation de code
  • Exportateurs — outils permettant d'exporter les traces générées vers le backend
  • Cross Service Propagators — un outil permettant de transférer le contexte d'exécution en dehors du processus (JVM)


Le collecteur OpenTelemetry est un composant important, un proxy qui reçoit les données, les traite et les transmet. Regardons cela de plus près.

Collecteur OpenTelemetry

Pour les systèmes à forte charge qui traitent des milliers de requêtes par seconde, la gestion du volume de données est cruciale. Les données de trace dépassent souvent les données métier en termes de volume, ce qui rend essentiel de hiérarchiser les données à collecter et à stocker. C'est là qu'intervient notre outil de traitement et de filtrage des données, qui vous permet de déterminer quelles données méritent d'être stockées. En règle générale, les équipes souhaitent stocker des traces qui répondent à des critères spécifiques, tels que :


  • Traces avec des temps de réponse dépassant un certain seuil.
  • Traces qui ont rencontré des erreurs lors du traitement.
  • Traces contenant des attributs spécifiques, tels que celles qui ont transité par un certain microservice ou qui ont été signalées comme suspectes dans le code.
  • Une sélection aléatoire de traces régulières qui fournissent un instantané statistique des opérations normales du système, vous aidant à comprendre le comportement typique et à identifier les tendances.

Voici les deux principales méthodes d’échantillonnage utilisées pour déterminer les traces à conserver et celles à supprimer :

  • Échantillonnage de la tête — décide au début d'une trace s'il faut la conserver ou non
  • Échantillonnage de queue : ne prend une décision qu'une fois que la trace complète est disponible. Cela est nécessaire lorsque la décision dépend de données qui apparaissent plus loin dans la trace. Par exemple, des données comprenant des plages d'erreur. Ces cas ne peuvent pas être traités par échantillonnage de tête, car ils nécessitent d'analyser d'abord la trace entière


Le collecteur OpenTelemetry permet de configurer le système de collecte de données afin qu'il n'enregistre que les données nécessaires. Nous discuterons de sa configuration plus tard, mais pour l'instant, passons à la question de ce qui doit être modifié dans le code pour qu'il commence à générer des traces.

Instrumentation sans code

Obtenir la génération de traces nécessitait vraiment un codage minimal – il suffisait simplement de lancer nos applications avec un agent Java, en spécifiant le configuration :


-javaagent:/opentelemetry-javaagent-1.29.0.jar

-Dotel.javaagent.configuration-file=/otel-config.properties


OpenTelemetry prend en charge un grand nombre de bibliothèques et frameworks , donc après avoir lancé l'application avec l'agent, nous avons immédiatement reçu des traces avec des données sur les étapes de traitement des requêtes entre les services, dans le SGBD, etc.


Dans notre configuration d'agent, nous avons désactivé les bibliothèques que nous utilisons dont nous ne voulions pas voir les étendues dans les traces, et pour obtenir des données sur le fonctionnement de notre code, nous l'avons marqué avec annotations :


 @WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); }


Dans cet exemple, l'annotation @WithSpan est utilisée pour la méthode, ce qui signale la nécessité de créer une nouvelle étendue nommée « acquire locks », et l'attribut « locks » est ajouté à l'étendue créée dans le corps de la méthode.


Lorsque la méthode a fini de fonctionner, le span est fermé et il est important de prêter attention à ce détail pour le code asynchrone. Si vous devez obtenir des données liées au travail du code asynchrone dans les fonctions lambda appelées à partir d'une méthode annotée, vous devez séparer ces lambdas en méthodes distinctes et les marquer avec une annotation supplémentaire.

Notre configuration de collecte de traces

Voyons maintenant comment configurer l'ensemble du système de collecte de traces. Toutes nos applications JVM sont lancées avec un agent Java qui envoie des données au collecteur OpenTelemetry.


Cependant, un seul collecteur ne peut pas gérer un flux de données important et cette partie du système doit être mise à l'échelle. Si vous lancez un collecteur distinct pour chaque application JVM, l'échantillonnage de queue sera interrompu, car l'analyse de trace doit se produire sur un seul collecteur, et si la requête passe par plusieurs JVM, les étendues d'une trace se retrouveront sur différents collecteurs et leur analyse sera impossible.


Ici, un collecteur configuré comme un équilibreur vient à la rescousse.


En conséquence, nous obtenons le système suivant : chaque application JVM envoie des données au même collecteur d'équilibrage, dont la seule tâche est de distribuer les données reçues de différentes applications, mais liées à une trace donnée, au même collecteur-processeur. Ensuite, le collecteur-processeur envoie les données à Grafana Tempo.



Examinons de plus près la configuration des composants de ce système.

Collecteur d'équilibrage de charge

Dans la configuration du collecteur-équilibreur, nous avons configuré les parties principales suivantes :


 receivers:  otlp:    protocols:      grpc: exporters:  loadbalancing:    protocol:      otlp:        tls:          insecure: true    resolver:      static:        hostnames:          - collector-1.example.com:4317          - collector-2.example.com:4317          - collector-3.example.com:4317 service:  pipelines:    traces:      receivers: [otlp]      exporters: [loadbalancing]


  • Récepteurs — où les méthodes (par lesquelles les données peuvent être reçues par le collecteur) sont configurées. Nous avons configuré la réception des données uniquement au format OTLP. (Il est possible de configurer la réception des données via de nombreux autres protocoles , par exemple Zipkin, Jaeger.)
  • Exportateurs : partie de la configuration où l'équilibrage des données est configuré. Parmi les collecteurs-processeurs spécifiés dans cette section, les données sont distribuées en fonction du hachage calculé à partir de l'identifiant de trace.
  • La section Service précise la configuration du fonctionnement du service : uniquement avec des traces, en utilisant le récepteur OTLP configuré en haut et en transmettant les données en tant qu'équilibreur, c'est-à-dire sans traitement.

Le collecteur avec traitement de données

La configuration des collecteurs-processeurs est plus compliquée, alors regardons-y :


 receivers:  otlp:    protocols:      grpc:        endpoint: 0.0.0.0:14317 processors:  tail_sampling:    decision_wait: 10s    num_traces: 100    expected_new_traces_per_sec: 10    policies:      [          {            name: latency500-policy,            type: latency,            latency: {threshold_ms: 500}          },          {            name: error-policy,            type: string_attribute,            string_attribute: {key: error, values: [true, True]}          },          {            name: probabilistic10-policy,            type: probabilistic,            probabilistic: {sampling_percentage: 10}          }      ]  resource/delete:    attributes:      - key: process.command_line        action: delete      - key: process.executable.path        action: delete      - key: process.pid        action: delete      - key: process.runtime.description        action: delete      - key: process.runtime.name        action: delete      - key: process.runtime.version        action: delete exporters:  otlp:    endpoint: tempo:4317    tls:      insecure: true service:  pipelines:    traces:      receivers: [otlp]      exporters: [otlp]


Similairement à la configuration du collecteur-équilibreur, la configuration de traitement se compose des sections Récepteurs, Exportateurs et Service. Cependant, nous nous concentrerons sur la section Processeurs, qui explique comment les données sont traitées.


Tout d’abord, la section tail_sampling démontre une configuration qui permet de filtrer les données nécessaires au stockage et à l'analyse :


  • latency500-policy : cette règle sélectionne les traces avec une latence supérieure à 500 millisecondes.

  • error-policy : cette règle sélectionne les traces qui ont rencontré des erreurs lors du traitement. Elle recherche un attribut de chaîne nommé « error » avec les valeurs « true » ou « True » dans les étendues de trace.

  • probabilistic10-policy : cette règle sélectionne aléatoirement 10 % de toutes les traces pour fournir des informations sur le fonctionnement normal de l'application, les erreurs et le traitement des requêtes longues.


En plus de tail_sampling, cet exemple montre la section resource/delete pour supprimer les attributs inutiles non requis pour l'analyse et le stockage des données.

Résultats

La fenêtre de recherche de traces Grafana qui en résulte vous permet de filtrer les données selon différents critères. Dans cet exemple, nous affichons simplement une liste de traces reçues du service de lobby, qui traite les métadonnées du jeu. La configuration permet un filtrage ultérieur par attributs tels que la latence, les erreurs et l'échantillonnage aléatoire.


La fenêtre d'affichage des traces affiche la chronologie d'exécution du service de lobby, y compris les différentes périodes qui composent la demande.


Comme vous pouvez le voir sur l'image, la séquence des événements est la suivante : les verrous sont acquis, puis les objets sont récupérés du cache, suivi de l'exécution d'une transaction qui traite les requêtes, après quoi les objets sont à nouveau stockés dans le cache et les verrous sont libérés.


Les plages liées aux requêtes de base de données ont été générées automatiquement grâce à l'instrumentation des bibliothèques standard. En revanche, les plages liées à la gestion des verrous, aux opérations de cache et au lancement des transactions ont été ajoutées manuellement au code métier à l'aide des annotations susmentionnées.



Lorsque vous visualisez une plage, vous pouvez voir des attributs qui vous permettent de mieux comprendre ce qui s'est passé pendant le traitement, par exemple, voir une requête dans la base de données.



L'une des fonctionnalités intéressantes de Grafana Tempo est la graphique de service , qui affiche graphiquement tous les services exportant des traces, les connexions entre eux, le débit et la latence des requêtes :


Pour conclure

Comme nous l'avons vu, travailler avec le traçage OpenTelemetry a considérablement amélioré nos capacités d'observation. Avec des modifications de code minimales et une configuration de collecteur bien structurée, nous avons obtenu des informations approfondies. De plus, nous avons vu comment les capacités de visualisation de Grafana Tempo ont complété davantage notre configuration. Merci de votre lecture !