
Dans nos projets, nous utilisons une architecture de microservices. Lorsqu'il y a des goulets d'étranglement en termes de performance, beaucoup de temps est consacré à la surveillance et à l'analyse des journaux. Lors de la journalisation des délais des opérations individuelles dans un fichier journal, il est souvent difficile de comprendre ce qui a conduit à l'appel de ces opérations, de suivre la séquence des actions ou de comprendre le décalage temporel d'une opération par rapport à une autre dans différents services.
Pour minimiser le travail manuel, nous avons décidé d'utiliser l'un des outils de traçage. Cet article parlera de la maniÚre dont et pour quelles raisons nous pouvons utiliser le traçage.
Quels problÚmes peut-on résoudre grùce au traçage
- Identifier les goulets d'étranglement en performance, tant au sein d'un service que dans l'ensemble de l'arbre d'exécution entre tous les services impliqués. Par exemple :
- De nombreux appels consécutifs courts entre les services, par exemple, pour la géocodification ou l'accÚs à la base de données.
- De longs temps d'attente pour les entrées/sorties, par exemple, le transfert de données sur le réseau ou la lecture sur disque.
- Un long parsing de données.
- Des opérations longues nécessitant une CPU.
- Des segments de code qui ne sont pas nĂ©cessaires pour obtenir le rĂ©sultat final et qui peuvent ĂȘtre supprimĂ©s ou exĂ©cutĂ©s de maniĂšre diffĂ©rĂ©e.
- Comprendre clairement dans quel ordre les appels sont effectués et ce qui se passe lors de l'exécution d'une opération.

On voit que, par exemple, la requĂȘte est arrivĂ©e au service WS -> le service WS a complĂ©tĂ© les donnĂ©es via le service R -> ensuite a envoyĂ© une requĂȘte au service V -> le service V a chargĂ© beaucoup de donnĂ©es depuis le service R -> a fait appel au service P -> le service P a encore une fois consultĂ© le service R -> le service V a ignorĂ© le rĂ©sultat et est allĂ© au service J -> et a finalement renvoyĂ© la rĂ©ponse au service WS, tout en continuant en arriĂšre-plan Ă calculer autre chose.
Sans ce traçage ou une documentation dĂ©taillĂ©e sur l'ensemble du processus, il est trĂšs difficile de comprendre ce qui se passe au premier coup d'Ćil sur le code, surtout lorsque le code est dispersĂ© dans diffĂ©rents services et cachĂ© derriĂšre de nombreux bins et interfaces. - Collecter des informations sur l'arbre d'exĂ©cution pour une analyse ultĂ©rieure diffĂ©rĂ©e. Ă chaque Ă©tape d'exĂ©cution, il est possible d'ajouter des informations disponibles Ă ce stade dans le traçage et de comprendre ensuite quelles donnĂ©es d'entrĂ©e ont conduit Ă un tel scĂ©nario. Par exemple :
- ID utilisateur
- Droits
- Type de méthode sélectionnée
- Journal ou erreur d'exécution
- La transformation des traces en un sous-ensemble de métriques et l'analyse ultérieure sous forme de métriques.
Ce que la traçabilité peut enregistrer. Span
Dans le traçage, il y a le concept de span, qui est l'équivalent d'un log dans la console. Un span a :
- Un nom, généralement le nom de la méthode exécutée
- Le nom du service dans lequel le span a été généré
- Un identifiant unique propre
- Une certaine méta-information sous forme de key/value, qui a été enregistrée dans ce span. Par exemple, les paramÚtres de la méthode ou si la méthode s'est terminée par une erreur ou non.
- Le temps de début et de fin de l'exécution de ce span
- L'identifiant du span parent
Chaque span est envoyĂ© au collector de spans pour ĂȘtre sauvegardĂ© dans la base de donnĂ©es pour consultation ultĂ©rieure dĂšs qu'il a terminĂ© son exĂ©cution. Par la suite, il est possible de construire un arbre de tous les spans en les reliant par l'ID du parent. Lors de l'analyse, on peut trouver, par exemple, tous les spans dans un service donnĂ© qui ont pris plus d'un certain temps. Ensuite, en consultant un span spĂ©cifique, on peut voir tout l'arbre au-dessus et en dessous de ce span.

Opentrace, Jaeger et comment nous l'avons mis en Ćuvre pour nos projets
Il existe une norme gĂ©nĂ©rale , qui dĂ©crit comment et quoi collecter, sans lier la traçabilitĂ© Ă une mise en Ćuvre spĂ©cifique dans un langage donnĂ©. Par exemple, en Java, tout le travail avec les traces se fait par le biais d'une API commune Opentrace, tandis qu'en dessous peut se cacher, par exemple, Jaeger ou une implĂ©mentation par dĂ©faut vide qui ne fait rien.
Nous utilisons comme implémentation d'Opentrace. Il se compose de plusieurs composants :

- Jaeger-agent â un agent local, qui est gĂ©nĂ©ralement installĂ© sur chaque machine et dans lequel les services enregistrent sur le port par dĂ©faut local. Si l'agent est absent, les traces de tous les services sur cette machine sont gĂ©nĂ©ralement dĂ©sactivĂ©es.
- Jaeger-collector â tous les agents y envoient les traces collectĂ©es, et il les place dans la base de donnĂ©es choisie.
- La base de donnĂ©es â leur prĂ©fĂ©rence est Cassandra, mais nous utilisons Elasticsearch, il existe des implementations pour quelques autres bases de donnĂ©es et une implementation en mĂ©moire qui ne sauvegarde rien sur disque.
- Jaeger-query â c'est un service qui interroge la base de donnĂ©es et restitue les traces collectĂ©es pour analyse.
- Jaeger-ui â c'est une interface web pour rechercher et visualiser les traces, elle interroge jaeger-query.

Un composant distinct peut ĂȘtre considĂ©rĂ© comme l'implĂ©mentation d'opentrace Jaeger pour des langages spĂ©cifiques, Ă travers laquelle les spans sont envoyĂ©s au jaeger-agent.
Il s'agit d'implémenter l'interface io.opentracing.Tracer, aprÚs quoi toutes les traces passeront par elle vers un véritable agent.

Il est Ă©galement possible de connecter des composants Spring et l'implĂ©mentation de Jaeger qui configurera automatiquement la traçabilitĂ© de tout ce qui passe par ces composants, comme les requĂȘtes HTTP dans les contrĂŽleurs, les requĂȘtes Ă la base de donnĂ©es via JDBC, etc.
Journalisation des traces en Java
Ă un niveau supĂ©rieur, le premier Span doit ĂȘtre créé. Cela peut ĂȘtre fait automatiquement par exemple par le contrĂŽleur Spring lors de la rĂ©ception d'une requĂȘte, ou manuellement s'il n'y a pas de contrĂŽleur. Ensuite, il est transmis par l'intermĂ©diaire du Scope infĂ©rieur. Si un quelconque mĂ©thode souhaite ajouter un Span, elle prend l'activeSpan actuel Ă partir du Scope, crĂ©e un nouveau Span et indique que son parent est l'activeSpan reçu, puis rend le nouveau Span actif. Lors de l'appel de services externes, l'actuel active span est transmis, et ces services crĂ©ent de nouveaux spans liĂ©s Ă ce span.
Tout fonctionne via une instance de Tracer, que l'on peut obtenir par le biais du mécanisme DI, ou via GlobalTracer.get() comme une variable globale, si le mécanisme DI ne fonctionne pas. Par défaut, si le tracer n'a pas été initialisé, un NoopTracer sera retourné, qui ne fait rien.
Ensuite, à partir du tracer, le ScopeManager récupÚre le Scope actuel, crée un nouveau Scope à partir de l'actuel, lié au nouveau span, puis le Scope créé est fermé, ce qui ferme le span créé et restaure l'état actif de l'ancien Scope. Le Scope est lié à un thread, donc en programmation multithread, il ne faut pas oublier de transmettre l'active span dans un autre thread, pour permettre l'activation du Scope de cet autre thread lié à ce span.
io.opentracing.Tracer tracer = ...; // GlobalTracer.get()
void DoSmth () {
try (Scope scope = tracer.buildSpan("DoSmth").startActive(true)) {
...
}
}
void DoOther () {
Span span = tracer.buildSpan("someWork").start();
try (Scope scope = tracer.scopeManager().activate(span, false)) {
// Effectuer des actions.
} catch(Exception ex) {
Tags.ERROR.set(span, true);
span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex, Fields.MESSAGE, ex.getMessage()));
} finally {
span.finish();
}
}
void DoAsync () {
try (Scope scope = tracer.buildSpan("ServiceHandlerSpan").startActive(false)) {
...
final Span span = scope.span();
doAsyncWork(() -> {
// ĂTAPE 2 CI-DESSUS : rĂ©activer le Span dans le rappel, en passant vrai Ă
// startActive() si/quan le Span doit ĂȘtre terminĂ©.
try (Scope scope = tracer.scopeManager().activate(span, false)) {
...
}
});
}
}Pour la programmation multithread, il existe également TracedExecutorService et des wrappers similaires qui transmettent automatiquement le span actuel dans le thread lors de l'exécution de tùches asynchrones :
private ExecutorService executor = new TracedExecutorService(
Executors.newFixedThreadPool(10), GlobalTracer.get()
);Pour les requĂȘtes http externes, il y a
HttpClient httpClient = new TracingHttpClientBuilder().build();Les problÚmes auxquels nous avons été confrontés
- Les beans et DI ne fonctionnent pas toujours si le tracer est utilisé en dehors d'un service ou d'un composant, alors Le Tracer peut ne pas fonctionner et il faudra utiliser GlobalTracer.get().
- Les annotations ne fonctionnent pas si ce n'est pas un composant ou un service, ou si l'appel de mĂ©thode se fait depuis une mĂ©thode adjacente de la mĂȘme classe. Il faut faire attention, vĂ©rifier ce qui fonctionne, et utiliser la crĂ©ation manuelle de trace si @Traced ne fonctionne pas. On peut Ă©galement ajouter un compilateur supplĂ©mentaire pour les annotations java, alors elles devraient fonctionner partout.
- Dans les anciennes versions de spring et spring boot, l'autoconfiguration opentracing spring cloud ne fonctionne pas Ă cause de bugs dans la DI, alors si l'on veut que les traces fonctionnent automatiquement dans les composants Spring, on peut le faire par analogie avec
- En groovy, le try avec ressources ne fonctionne pas, il faut absolument utiliser try finally.
- Chaque service doit spécifier son propre spring.application.name sous lequel les traces seront enregistrées. De plus, un nom distinct pour la production et le test, afin de ne pas les mélanger.
- Si l'on utilise GlobalTracer et tomcat, tous les services lancĂ©s dans ce tomcat partagent un seul GlobalTracer, donc ils auront tous le mĂȘme nom de service.
- Lors de l'ajout de traces à une méthode, il faut s'assurer qu'elle n'est pas appelée en boucle plusieurs fois. Il est nécessaire d'ajouter une trace commune pour tous les appels, qui enregistrera le temps total d'exécution. Sinon, une charge excessive sera générée.
- Une fois, dans jaeger-ui, nous avons effectuĂ© des requĂȘtes trop volumineuses sur un grand nombre de traces et, ne pouvant pas attendre la rĂ©ponse, nous avons rĂ©itĂ©rĂ©. En fin de compte, jaeger-query a utilisĂ© beaucoup de mĂ©moire et a ralenti elastik. Un redĂ©marrage de jaeger-query a rĂ©solu le problĂšme.
Ăchantillonnage, stockage et visualisation des traces
Il y a trois types :
- Const qui envoie et enregistre toutes les traces.
- Probabilistic qui filtre les traces avec une certaine probabilité définie.
- Le ratelimit qui limite le nombre de traces par seconde. Ces paramĂštres peuvent ĂȘtre configurĂ©s sur le client, soit sur jaeger-agent soit dans le collecteur. Actuellement, dans notre pile de validateurs, nous utilisons const 1 car le nombre de requĂȘtes n'est pas trĂšs Ă©levĂ© mais elles prennent beaucoup de temps. Ă l'avenir, si cela entraĂźne une charge excessive sur le systĂšme, il faudra limiter.
Si l'on utilise Cassandra, par défaut, elle ne conserve les traces que pendant deux jours. Nous utilisons et les traces sont conservées indéfiniment et ne sont pas supprimées. Un index distinct est créé pour chaque jour, par exemple jaeger-service-2019-03-04. à l'avenir, il faudra configurer un nettoyage automatique des anciennes traces.
Pour visualiser les traces, il faut :
- Choisir le service en fonction duquel vous souhaitez filtrer les traces, par exemple tomcat7-default pour un service qui fonctionne dans Tomcat et qui ne peut pas avoir son propre nom.
- Ensuite, sélectionnez l'opération, la période de temps et la durée minimale de l'opération, par exemple à partir de 10 secondes, afin de ne récupérer que les exécutions longues.

- Accédez à l'une des traces et vérifiez ce qui a causé des ralentissements.

De plus, si un identifiant de requĂȘte est connu, il est possible de trouver la trace par cet identifiant via la recherche par balises, si cet identifiant est consignĂ© dans le span de la trace.
Documentation
- Documentation opentracing
- Documentation jaeger
- Connexion jaeger java
- Connexion spring opentracing
Articles
- Jaeger Opentracing et Microservices dans un projet réel en PHP et Golang
- Evolving Distributed Tracing at Uber Engineering
- Exécution de l'agent Jaeger sur du matériel nu
Vidéo
- Comment nous avons utilisĂ© Jaeger et Prometheus pour offrir des requĂȘtes utilisateur ultra-rapides â Bryan Boreham
- Introduction : Jaeger â Yuri Shkuro, Uber & Pavol Loffay, Red Hat
- Serghei Iakovlev, "Une petite histoire d'un grand succĂšs : OpenTracing, AWS et Jaeger"
Source : habr.com



