Infrastructure

L’instrumentation de code est une technique puissante pour répondre à toutes sortes de questions de performances. Dès lors qu’on chercher à connaître un paramètre d’un programme qui n’est a priori défini qu’à l’exécution (par exemple la taille typique de messages reçus via le réseau), la manière la plus simple de répondre à la question est d’instrumenter le code concerné pour en extraire cette information, fût-ce par un simple printf() bien placé suivi d’une recompilation.

Mais recompiler du code peut être laborieux, et faire des statistiques sur de l’information issue d’un printf dont le format est défini par l’utilisateur peut l’être tout autant. C’est pourquoi Linux fournit un grand nombre d’outils pour extraire de façon standardisée de l’information d’un code, avec ou sans recompilation, et que le code ait été conçu pour ça à la base ou non.

Types d’instrumentation

Les principales formes d’instrumentation Linux accessible par le biais de perf sont:

  • Les tracepoint, dont nous avons déjà beaucoup parlé. Il s’agit de points du noyau Linux qui ont été manuellement instrumentés par les développeurs de celui-ci, afin de pouvoir suivre quand du code y fait appel ainsi que la valeur de certains paramètres d’exécution (ex: taille d’une écriture de fichier pour le tracepoint syscalls:sys_exit_write).
  • Les kprobes sont un mécanisme permettant d’injecter du code en un point arbitraire du source du noyau Linux, pour détecter quand on passe en ce point du code noyau et éventuellement en extraire des informations (contenu de variables, paramètres et résultats de fonctions…). Cela se fait “à chaud”, il n’est pas nécessaire de recompiler son noyau ou redémarrer.
  • Les uprobes ont une flexibilité analogue aux kprobes, mais permettent d’instrumenter du code extérieur au noyau (exécutables, bibliothèques partagées…). Leur fonctionnement est analogue à celui d’un point d’arrêt dans un débogueur : le code est interrompu et passe la main au noyau, qui récupère l’information voulue avant de lui rendre la main.
  • Les USDT probes, pour User Statically-Defined Tracing, sont un mécanisme basé sur les uprobes qui permet à une application ou une bibliothèque de définir manuellement des points d’instrumentation, à la manière des tracepoints dans le noyau.

Outre le fait que les deux premières ciblent le code noyau et les suivantes ciblent le code applicatif, ces formes d’instrumentation se distinguent par leur performance et leurs garanties de stabilité.

Stabilité

Les tracepoint et USDT probes sont définis par le développeur du code concerné, et sont normalement soumis aux mêmes garanties de stabilité que toute autre API. Par conséquent, on peut s’attendre à ce que l’instrumentation associée reste disponible pendant un certain temps, ce qui permet par exemple de créer des tutoriels ou utilitaires d’analyse de performance basés sur l’information fournie par l’instrumentation.

Les kprobes et uprobes, en revanche, sont définis par la personne qui souhaite instrumenter du code, sans accord préalable avec les développeurs. On n’est pas limité aux interfaces publiques, et on peut instrumenter aussi profond dans l’implémentation qu’on le souhaite. Il n’y a donc aucune garantie de stabilité, et même un petit correctif d’une application peut casser une instrumentation basée sur ces mécanismes. Ils ne sont donc adaptés que pour des analyses ponctuelles.

Performances

L’instrumentation rajoute du code CPU à exécuter à chaque fois que l’exécution passe en un certain point du programme instrumenté, ainsi que du trafic mémoire. Elle a donc un impact sur les performances du code que l’on étudie.

Selon “System Performance: Enterprise and the Cloud” de Brendan Gregg (2e édition, 2020)…

  • Les tracepoints définis par le noyau n’ont pas d’impact significatif sur les performances jusqu’à environ ~100k événements par seconde (ce qui suggère un coût de l’ordre de 100ns).
  • Le coût des kprobes dépend de leur emplacement dans la fonction noyau concernée
    • A l’entrée d’une fonction (ex: suivi des appels, lecture des paramètres), le coût est comparable à celui d’un tracepoint.
    • A la sortie d’une fonction (kretprobe), c’est environ 2x plus cher.
    • Au milieu d’une fonction, ça peut être plus cher encore (ex: boucles).
  • Les uprobes sont plus coûteuses puisqu’elles nécessitent un point d’arrêt, donc une interruption et un basculement vers le code noyau. On parle de ~13x le coût d’un tracepoint pour une uprobe à l’entrée d’une fonction et ~19x en sortie d’une fonction.
  • Les USDT probes sont implémentées via des uprobes et ont donc un coût similaire.

Conclusion

Voici un résumé des informations précédentes.

InstrumentationOù?Quand?Stable?Coût (Gregg2020)
tracepointNoyauCompilationOui~100ns
kprobe (entrée)NoyauExécutionNon~100ns
kretprobeNoyauExécutionNon~200ns
uprobe (entrée)App/LibExécutionNon~1.3µs
uretprobeApp/LibExécutionNon~1.9µs
USDTApp/LibCompilationOui= u(ret)probe

Notons un dernier point concernant les privilèges et la sécurité. En raison de la très grande dangerosité de l’instrumentation de code arbitraire (risque de ralentir fortement le système, de créer des récursions infinies, de dévoiler des informations qui devraient rester secrètes…), la création de kprobes et uprobes est réservée au super-utilisateur. Vous aurez donc besoin d’un peu d’aide de l’administrateur pour certaines commandes de ce TP.

En revanche, l’administrateur peut rendre ces “sondes” accessibles à l’ensemble des utilisateurs d’un groupe par une simple commande chown de ce style :

# Nécessite des privilèges administrateur !
sudo chown -R root:perf /sys/kernel/tracing/events/xyz