home ausy
home ausy

Aujourd'hui les architectures logicielles tendent de plus en plus vers des approches distribuées orientées « micros services ». Finie l’ère de l’application monolithique ayant tous les droits, nous sommes maintenant confrontés à une multitude d'applications aux droits dédiés et isolés. Ces « micro services » permettent une plus grande évolutivité de par leur design de type "faible couplage", une meilleure disponibilité, ainsi qu’une meilleure sécurité.

La contrepartie ? Plus d'entités fonctionnant en parallèle dans nos systèmes, et de fait des systèmes de plus en plus complexes et difficiles à analyser. Un des challenges de l’ingénierie de demain : l’observabilité des systèmes embarqués complexes.

Ausy, au travers de son ” Center of Expertise Embedded “, se positionne comme un acteur clé prêt à attaquer ce challenge de taille, et propose une première vue de l’état de l’art des solutions linux embarquées.

 

1. Concrètement, l’observabilité c’est quoi ?

L’observabilité est la capacité à mesurer les états internes d’un système en examinant uniquement ce qu’il produit. Un système est considéré comme « observable » si son état actuel peut être estimé uniquement en utilisant les informations de sorties. L’observabilité est un vieux concept qui trouve son origine en automatisme dans la théorie du contrôle des systèmes autorégulés (voir Rudolf Kalman : https://en.wikipedia.org/wiki/Observability).

Ramené au domaine informatique, l’observabilité d’un système se fait au travers du triptyque : métriques, logs et traces.

  • Les métriques : elles permettent de détecter des anomalies de performances, quand les valeurs dévient trop de leurs moyennes.
  • Les logs : ils permettent de savoir QUAND une erreur a été produite. Néanmoins, les logs ne permettent généralement pas de comprendre facilement POURQUOI une erreur a été produite. Cela est encore plus vrai dans les systèmes distribués où il faut analyser les logs de plusieurs services, conteneurs, processus fonctionnant en parallèle et où les causes principales d’une erreur peuvent être multiples.
  • Les traces : elles permettent de comprendre comment une erreur est survenue. En mettant des traces aux différents endroits importants de notre système (point de passage), on peut suivre le cycle de vie des transactions dans notre système sans impacter trop fortement la dynamique de ce dernier. On peut alors plus aisément déterminer si nous avons affaire à une problématique de « race-condition » ou une problématique de latence liée à une charge trop élevée ou à d’autres problèmes.

Contrairement aux « logs » et « métriques » qui sont généralement bien connus et maîtrisés, les traces le sont souvent moins. Et cela par manque de connaissance et de maîtrise des solutions existantes.

Dans la suite de cet article nous apportons donc une attention particulière à celles-ci en détaillant les différentes solutions disponibles sous linux, à la fois côté noyau et côté espace utilisateur. Nous prendrons soin de détailler leurs implémentations et leurs optimisations. Ceci afin de démontrer en quoi ces solutions sont bien plus efficaces qu’une solution « maison ad-hoc » rajoutée au-dessus d’un mécanisme de log.

 

2. Les traces dans le noyau Linux

Le noyau linux offre plusieurs mécanismes qui permettent d’ajouter des traces:

  • Ftrace;
  • Trace points;
  • Kernel probes.

2.1 Ftrace (the Function tracer)

Ftrace est un mécanisme qui permet de tracer les différentes fonctions utilisées dans le noyau linux. Ceci permet donc de savoir quelles sont les fonctions exécutées et à quel moment elles le sont.

Historiquement, cette fonctionnalité du noyau vient des « patchs linux-realtime ». Plusieurs options d’affichage du traceur sont disponibles :

  • function : traceur par défaut qui affiche chaques fonctions appelées avec un timestamp.
  • function_graph : même fonctionnalité que la précédente mais avec un affichage qui montre le graphe d’appel des fonctions.
  • irqsoff, preempoff, preemptirqsoff, wakeup, wakeup_rt : traceur spécifique pour mesurer la latence de différentes parties du noyau linux.

Ftrace fonctionne sur le même principe que le mécanisme « mcount » utilisé par les options de profilage de GCC/Clang. Chaque appel de fonction et retour de fonction passe par une fonction proxy « mcount » qui va être utilisée pour enregistrer quelle fonction est appelée et quand. Comme nous sommes en espace noyau, et afin de ne pas ajouter de la lenteur, il nous faut optimiser cette fonction mcount. Pour cela, nous devons pouvoir activer à la demande les fonctions que nous souhaitons tracer.

Pour se faire, Ftrace repose sur l’optimisation suivante : pour chaque entrée et retour de fonction, une instruction assembleur de type « 5-byte NOP » va être ajoutée. Pour rappel, les instructions assembleur NOP correspondent à « ne rien faire ». Le coût de cet ajout est donc assimilable à un coût proche de zéro d’un point de vue performance (que cela soit en temps d’exécution CPU ou sur les lignes de cache). Lorsqu’on souhaite activer la trace d’une fonction, cet instruction « NOP » va alors être remplacée par une instruction « JUMP » qui va alors exécuter la fonction proxy « mcount ». Même quand une trace est activée, il faut optimiser son temps d’exécution (c’est-à-dire le temps nécessaire à enregistrer la trace) afin que celle-ci n’ait que très peu d’impact sur la dynamique temporelle du système que nous souhaitons mesurer.

Ceci est un principe fondamental qui nous vient de la physique : observer une grandeur physique introduit toujours un biais. Mesurer une grandeur revient toujours à mettre en place un outil qui va de lui-même perturber la grandeur physique que nous cherchons à mesurer. L’outil de mesure ne peut donc être considéré comme valable uniquement si la perturbation qu’il introduit est d’un ordre de grandeur négligeable par rapport à la mesure elle-même.

Dans notre cas, nous cherchons à mesurer (tracer) le comportement temporel de notre système. Pour y arriver de la manière là moins intrusive possible, une stratégie en deux temps est utilisée :

  •  Tout d’abord la sauvegarde : les traces sont sauvegardées en mémoire noyau, dans un « per-cpu lockless ring-buffer », afin de minimiser le temps nécessaire à la sauvegarde des traces;
  • Puis la lecture et l’export : les ring-buffers sont lus et exportés par l’espace utilisateur pour une analyse post-mortem. Nota : ici le temps nécessaire à l’export (affichage ou export réseau) n’est pas impactant puisqu’il est fait à posteriori de la session d’enregistrement.

D’un point de vue utilisateur, Ftrace s’utilise à travers l’outil Linux du même nom.

Nota : Ftrace est à la fois une API (décrite ici) mais aussi un outil qui utilise cette API et d’autres technologies présentes ci-dessous.

Pour plus d’information sur Ftrace :

https://sourceware.org/binutils/docs/gprof/Implementation.html

https://www.kernel.org/doc/Documentation/trace/ftrace.txt

https://www.kernel.org/doc/html/latest/trace/ring-buffer-design.html

https://lwn.net/Kernel/Index/#Development_tools-Kernel_tracing

2.2 Tracepoints

Les Tracepoints (point de trace) sont des traces statiques existantes dans le code du noyau linux qui peuvent être activées à la demande. On peut assimiler ça à des « printk » activables à la demande.

Il y a aujourd’hui plus de 1300 Tracepoints différents permettant de tracer l’ensemble du fonctionnement du noyau linux : de la gestion mémoire, en passant par le « scheduler », les couches réseaux ou celles du système de fichiers.

Le coût d’exécution d’un Tracepoint (activé ou non) doit là aussi être le plus faible possible afin de ne pas alourdir inutilement le système. Le même mécanisme que pour Ftrace est mis en œuvre, à savoir une simple instruction « 5-byte NOP » qui va être remplacée par un JUMP si on l’active. La trace est sauvegardée dans le même « per cpu lockless ring-buffer » du noyau et est ensuite consommée de manière post-mortem par l’espace utilisateur.

D’un point de vue utilisateur, les Tracepoints sont accessibles à travers de nombreux outils : Ftrace/Perf (l’outil), Systemtap, BPF, Lttng…

Par rapport à Ftrace qui nous permettait d’afficher le nom des fonctions appelées, ici on peut avoir des informations supplémentaires prévues par la trace tels que le nom et valeur de variables etc...  

Pour plus d’information sur Tracepoints :

https://www.kernel.org/doc/html/latest/trace/tracepoints.html

2.3 Kernel probes (aka Kprobes)

Kprobes est un mécanisme du noyau linux qui permet de rajouter dynamiquement une fonction d’instrumentation à n’importe quel endroit dans le code du noyau linux. Lorsqu’une Kprobe est enregistrée, elle va faire une copie de l’instruction où elle est insérée et va y mettre à la place un break point (exemple : instruction assembleur int3 sur x86).

Lorsqu’un CPU arrive sur ce break point, ceci va générer une exception CPU (CPU trap), l’« handler d’exception » va alors appeler la fonction Kprobe associée qui va s’exécuter, puis va exécuter l’instruction d’origine qu’elle a copiée.

Avec ce mécanisme il est possible de venir instrumenter n’importe quelle partie du noyau Linux et ceci dynamiquement, sans avoir besoin de recompiler et recharger le noyau. Par rapport au Tracepoints qui sont des points de trace statiques, les Kprobes reposent sur l’utilisation de break point/CPU Trap. Leurs impacts sur la performance d’un système sont donc beaucoup plus importants.

Note : Sous certaines conditions et sur certaines architectures, le break point peut être remplacé par un JUMP (kernel configuration: CONFIG_OPTPROBES).

D’un point de vue utilisateur, les Kprobes sont activables à travers de nombreux outils : Ftrace/Perf, Systemtap, BPF, Lttng...

Pour plus d’informations sur les Kprobes voir :

https://lwn.net/Kernel/Index/#KProbes

https://www.kernel.org/doc/html/latest/trace/kprobes.html

 

3. Les traces côté espace utilisateur

Côté espace utilisateur, il y a aussi plusieurs mécanismes pour ajouter des traces dans une application :

  • Uprobes (user probes);
  • USDT (User land Statically Defined Tracing);
  • lttng-ust (Linux tracing toolkit next generation User-Tracepoint).

3.1 Uprobes (user probes)

Uprobes est l’équivalent Kprobes mais cette fois-ci prévu pour l’espace utilisateur. Le principe de fonctionnement est identique. L’instruction où la sonde est insérée est remplacée par un break point qui va donc générer une exception CPU (trap). Côté espace noyau, cette exception va être prise en compte, la fonction de trace va être chargée, les logs seront sauvegardés dans le ring-buffer du noyau (comme pour les Tracepoints) avant que le programme reprenne son exécution normalement en espace utilisateur.

Uprobe est un mécanisme puissant pour instrumenter n’importe quelle partie d’un code applicatif, mais attention, son coût est important (utilisation d’un break point et donc d’une exception CPU avec change de contexte). Suivant la mesure à effectuer, ceci peut être rédhibitoire en termes d’impacts sur les performances. (Il est souvent difficile de savoir où et quelles traces on souhaite ajouter, nous ne connaissons ou ne maîtrisons pas toujours l’ensemble des codes sources qui tournent en espace utilisateur sur nos systèmes.)

D’un point de vue utilisateur, les Uprobes sont activables à travers de nombreux outils : Ftrace/Perf, Systemtap, BPF, Lttng...

Pour plus d’informations :

https://www.kernel.org/doc/html/latest/trace/uprobetracer.html

3.2 USDT (User land Statically Defined Tracing)

USDT est un service historiquement issu de Solaris et de son outil de tracing Dtrace. Le portage sur linux a été fait par Systemtap qui fournit une header « sys/sdt.h » pour permettre de rajouter des USDT à une application C/C++. (Nota: Pour les langages interprétés, les traces peuvent être ajoutées via l’intermédiaire de la libraire libstapsdt).

Lors de la définition d’un USDT, le code assembleur résultant est l’ajout :

  • de l’instruction NOP à l’endroit de la trace;
  • d’une note «.note.stapsdt » (dans le binaire au format ELF) contenant la fonction de la trace en question avec l’information où se trouve localisée l’instruction NOP dans le code.

Pour rappel, les notes d’un binaire au format ELF ne sont pas chargées par défaut lors d’un chargement en mémoire d’un binaire ELF. Nous n'avons donc ici aucune augmentation de la consommation mémoire lorsque la trace n’est pas activée.

Lorsque la trace est activée, grâce aux informations contenues dans la note « .note.stapsdt », l’instruction NOP est remplacée par un break point qui, une fois atteint, déclenche la fonction de trace (ceci réutilise l’api Uprobes présenté plus haut). Par rapport au mécanisme « Uprobe » seul, les USDT simplifient leurs utilisations. Plutôt que de devoir savoir où et quoi instrumenter via Uprobes, les USDT sont des points de traces prévus et définis par les développeurs activables à la demande.

D’un point de vue utilisateur, les USDT sont activables à travers de nombreux outils : Ftrace/Perf, Systemtap, BPF, Lttng...

Pour plus d’informations :

https://sourceware.org/systemtap/wiki/AddingUserSpaceProbingToApps

3.3 lttng-ust (Linux tracing toolkit next generation User-Tracepoint)

Les traces Lttng sont basées sur les principes des Tracepoints linux mais sont cette fois-ci implémentées entièrement en espace utilisateur. Les traces sont sauvegardées dans un « per-cpu lockless ring buffer » instancié complètement en espace utilisateur (créé via la librairie « liburc aka Userspace ReadCopyUpdate »). Ce ring buffer est dans une mémoire partagée (shm) entre l’application à tracer et le logiciel de trace (lttng).

Cette solution est beaucoup plus performante que l’approche USTD/Uprobes car ici il n’y a aucun « context switch » nécessaire à l’utilisation d’une trace. Lorsqu’une trace est désactivée, la pénalité consiste en un simple test de branchement (if statement unlikely optimized). Le coût est donc légèrement plus élevé qu’un NOP.

La seule contrainte d’utilisation est qu’il faut « linker » son application avec la librairie « lttng-ust (licence LGPLv2.1) », soit statiquement, soit dynamique. Pour tracer en espace utilisateur, lttng-ust doit être la solution à privilégier par défaut car plus efficace et moins impactante. Si la contrainte de lier son application à la librairie « lttng_ust » n’est pas acceptable pour votre projet, alors la solution USDT peut être un bon compromis.)

Pour plus d’informations:

https://lttng.org/man/3/lttng-ust/v2.7/

 

4. En conclusion

Nous venons de voir les différents mécanismes disponibles sous linux pour ajouter de l’observabilité à nos systèmes. Ci-dessous un résumé des différents choix :

articles linux ausy
articles linux ausy

Certaines de ces technologies sont disponibles depuis plus de 10 ans déjà et trop peu de projets pensent à les mettre en place par manque de connaissance. Chez Ausy, nous sommes convaincus de l’avantage et de l’utilité de ces solutions et nous communiquons dessus dès que nous en avons l’occasion.

Alors la prochaine fois que vous serez confrontés à cette problématique d’observabilité de vos logiciels et du besoin d’y rajouter plus d’informations via des traces, au lieu de réinventer la roue au-dessus de l’api de syslog pour en faire un système de trace (ce qu’il n’est pas !), pensez à cet article et à lttng-ust. Et n’hésitez pas à contacter Ausy qui, à travers son “Center Of Expertise Embedded” et sa connaissance de l’état de l’art, saura vous accompagner et vous apporter la meilleure solution.

Remerciement spécial au blog de Brendan Gregg (Senior Performance Engineer chez Netflix : https://www.brendangregg.com/blog/index.html) qui est une référence en la matière sur ce sujet-là.

au sujet de l'auteur.
tangi colin Practice Expert Evangelist Embedded & Secure System
tangi colin Practice Expert Evangelist Embedded & Secure System

tangi colin

practice expert evangelist embedded & secure system

Ce passionné d'open-source, de linux et de technique, du Cloud à l'embarqué, aime pratiquer une veille technique permanente pour maîtriser l'état de l'art des solutions actuelles et anticiper les tendances de demain. Avec 9 années d'expériences dans le développement linux embarqué, il a acquis de solides compétences aussi bien bas niveau (driver, board support packages) qu’en architecture système (On-The-Air update, conteneur micro-service) en passant par la cybersécurité (secureboot, dm-verity, trustzone ...). Il a travaillé dans de nombreux secteurs industriels (automobile, énergie, ferroviaire, médical) de la start-up au grand groupe, de l'audit et conseil à la réalisation et la gestion de projet notamment chez Ausy qu'il a rejoint en 2018.