Démystifier le tracing distribué dans une architecture de microservices


 

Dans une architecture de microservices, plus on a de dépendances entre les différents services et plus il devient difficile de diagnostiquer un problème en particulier. Ça devient compliqué pour l’administrateur de détecter les latences des différents appels entre les microservices, ou encore de connaître quel microservice est saturé dans le Mesh, et bien d’autres problèmes. Grâce à un système de tracing distribué, l’administrateur peut visualiser les appels entre les différents microservices d’un mesh, déterminer la durée de chacun de ces appels, et surtout vérifier s’il y a eu une latence et combien de temps elle a duré.

On remarque sur cette représentation qu’il n’est pas du tout évident de se retrouver avec ce nombre important d’appels entre les microservices dans un mesh. De plus, même avec des logs très bien détaillés le diagnostic des incidents reste très compliqué.

Photo credit :https://epsagon.com/wp-content/uploads/2019/04/Illustration_What-happend-1.png

Grâce à un système de tracing distribué, un administrateur peut aussi observer le comportement d’une requête qui transite dans un service mesh, pour analyser les dépendances entre les services et pouvoir facilement détecter les points de dysfonctionnement dans ce mesh. Il permet aussi d’analyser et d’optimiser les performances dans ce système de service distribué. Tout en ayant un graphe représentatif de toutes les dépendances entre les services du mesh sous forme d’une carte topologique de services.

Jaeger, Zipkin et d’autres sont des solutions de tracing distribué open source que nous pouvons intégrer dans ISTIO. Ils aident à monitorer et à diagnostiquer les incidents dans un système de microservices. Ils permettent aussi le stockage, l’agrégation et la présentation des traces, et tout cela dans le bût de :

  • Surveiller des transactions distribuées
  • Analyser la root cause des incidents
  • Déterminer les dépendances entre les services
  • Analyser des performances et optimiser les latences

Fonctionnement du tracing distribué sur ISTIO:

Avant de commencer à expliquer le principe de fonctionnement du tracing distribué,  il faut d’abord définir quelques terminologies:

Span: Est la plus petite unité dans un système de tracing distribué qui contient des informations concernant l’horodatage de début du span et la durée de l’opération. Ayant une relation de parent-enfant entre eux, l’ensemble des spans générés de la même requête forment une trace.

Trace: Constitue l’historique des flux et des communications qui transitent dans un système distribué. Autrement  dit, c’est l’ensemble des sauts effectués par une requête entre les différents microservices. Elle est construite à partir de plusieurs spans qui partagent le même TraceID, et représentée sous forme d’un arbre généalogique de dépendances aussi appelé directed acyclic graph (DAG) .

Comment les traces sont générées

Envoy, le service proxy d’ISTIO, garantit la création de la première entête d’une trace. Cette entête x-request-id, est utilisée par Envoy pour identifier chaque requête.  Cette entête est par la suite propagée vers tous les microservices qui interagissent entre eux. 

Cet identifiant unique de requête est aussi attaché à chaque log généré par le service. Donc, si on filtre sur le x-request-id, on pourra retrouver tous les logs de ce service liés à chaque requête.

Sur ISTIO, les sidecar proxies Envoy constituent les éléments les plus importants du tracing distribué. En effet, ce sont les sidecar proxies Envoy qui se chargent des tâches de création des spans qui vont un à un former une trace complète. Seule condition: Activer le Sidecar Injection !!! sur le namespace dans lequel tournera l’application concernée.

Une trace est construite suite à une itération de tâches décrite dans les événements suivants :

A l’entrée du service: A ce moment quand le service reçoit une requête, il vérifie  s’il existe des informations de tracing dans l’entête. Quand la requête vient d’être initiée, elle ne contient alors aucune information de tracing dans son entête, et donc le tout premier span sera créé et sera défini comme étant le span parent. Sinon, si le span parent est déjà créé auparavant, un simple span enfant sera créé.

A la sortie du service: C’est à ce moment précis que le span enfant est créé, c’est à dire au moment où la requête quitte un service vers un autre. Il sera alors rattaché à la liste de spans déjà présents pour continuer à former la trace. Afin que le prochain service qui recevra la requête puisse refaire les mêmes étapes citées avant, à savoir créer un span pour continuer à former la trace.

Une trace est représentée comme suit, avec une succession de spans enfant tous liés au span parent. Tous les spans partagent le même TraceID :

Et puisqu’un span contient un horodatage de début ainsi qu’une durée, on peut voir dans le schéma suivant l’étalement des spans de la trace dans un contexte temporel et hiérarchique, avec les différents appels entre les différents microservices :

Propagation des entêtes des traces:

Une fois générée, l’entête est présente sur le premier service qui initie la requête, et doit par la suite être propagée vers tous les autres services. 

Cette propagation est nécessaire pour s’assurer que les entêtes sont collectées et corrélées, à chaque interaction avec les services, dans le but de garantir qu’elles proviennent toutes de la même source.

Istio utilise la méthode de propagation B3, nommée ainsi car les entêtes HTTP qui commencent par ”x-b3-” vont être collectées par un service depuis la requête entrante, et propagées vers un autre service à travers la requête sortante. Ces entêtes sont les suivantes :

  • x-b3-traceid : C’est l’ID de toute la trace, il est partagé entre les spans de la même trace
  • x-b3-spanid : C’est l’indicateur de la position actuelle d’une opération dans une trace
  • x-b3-parentspanid : C’est l’indicateur de la position parent d’une opération dans une trace
  • x-b3-sampled : Indique la quantité d’information qui sera concernée par le tracing
  • x-b3-flags : On peut utiliser la valeur 1 pour demander le mode debug

Le schéma de communication suivant,  représente une communication entre deux services, où le service A envoie une requête à un autre service B.  Durant cet appel HTTP sortant, le service A va injecter à la requête les entêtes X-B3-TraceId, X-B3-SpanId, X-B3-Sampled, et X-B3-ParentSpanId, et le service B va extraire ces entêtes et continuer de former la trace.

 


 

Rédigé le 19/02/ 2020, par : Ali Hassan AYOUJIL
Rédigé le 19/02/ 2020, par : Ali Hassan AYOUJILIngénieur Cloud Devops chez BEOpenIT