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.
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).
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
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
Pour cette discussion, nous nous concentrerons sur l'aspect traçage d'OpenTelemetry.
Jetons également un peu de lumière sur le projet OpenTelemetry, né de la fusion de
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 :
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.
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
L'API Java et le SDK :
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.
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 :
Voici les deux principales méthodes d’échantillonnage utilisées pour déterminer les traces à conserver et celles à supprimer :
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.
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
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry prend en charge un grand nombre de
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
@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.
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
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.
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]
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
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.
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
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 !