Traçage des services, OpenTracing et Jaeger

Traçage des services, OpenTracing et Jaeger

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

  1. 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.
  2. Comprendre clairement dans quel ordre les appels sont effectués et ce qui se passe lors de l'exécution d'une opération.
    Traçage des services, OpenTracing et Jaeger
    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.
  3. 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
  4. 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.

Traçage des services, OpenTracing et Jaeger

Opentrace, Jaeger et comment nous l'avons mis en Ɠuvre pour nos projets

Il existe une norme gĂ©nĂ©rale Opentrace, 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 Jaeger comme implémentation d'Opentrace. Il se compose de plusieurs composants :

Traçage des services, OpenTracing et Jaeger

  • 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.

Traçage des services, OpenTracing et Jaeger

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.
Intégration de Jaeger en Java 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.

Traçage des services, OpenTracing et Jaeger

Il est Ă©galement possible de connecter des composants Spring opentracing-spring-cloud-starter et l'implĂ©mentation de Jaeger opentracing-spring-jaeger-cloud-starter 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 TracingHttpClient

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 Autowired 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 github.com/opentracing-contrib/java-spring-jaeger/blob/master/opentracing-spring-jaeger-starter/src/main/java/io/opentracing/contrib/java/spring/jaeger/starter/JaegerAutoConfiguration.java
  • 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 d'échantillonnage des traces:

  1. Const qui envoie et enregistre toutes les traces.
  2. Probabilistic qui filtre les traces avec une certaine probabilité définie.
  3. 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 elasticsearch 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.
    Traçage des services, OpenTracing et Jaeger
  • AccĂ©dez Ă  l'une des traces et vĂ©rifiez ce qui a causĂ© des ralentissements.
    Traçage des services, OpenTracing et Jaeger

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

Articles

Vidéo

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster