Technologie
Les équipes d’ingénierie de la performance et du calcul chez Netflix s’attaquent régulièrement aux problèmes de performance dans notre environnement multi-locataire. La première étape consiste à déterminer si le problème provient de l’application ou de l’infrastructure sous-jacente. Un défi fréquent dans ce processus est le problème du « voisin bruyant ». Sur Titus, notre plateforme de calcul multi-locataire, un « voisin bruyant » désigne un conteneur ou un service système qui utilise intensivement les ressources du serveur, entraînant une dégradation des performances des conteneurs voisins. Nous nous concentrons généralement sur l’utilisation du CPU, car c’est la source la plus courante des problèmes de voisin bruyant.
La détection des effets des voisins bruyants est complexe. Les outils d’analyse de performance traditionnels, tels que perf, peuvent introduire une surcharge significative, risquant ainsi d’aggraver la dégradation des performances. De plus, ces outils sont souvent déployés après coup, ce qui est trop tard pour une enquête efficace. Un autre défi réside dans le fait que le débogage des problèmes de voisin bruyant nécessite une expertise technique approfondie et des outils spécialisés. Dans cet article, nous allons expliquer comment nous avons utilisé eBPF pour réaliser une instrumentation continue et à faible surcharge du planificateur Linux, permettant ainsi un suivi efficace des problèmes de voisin bruyant. Vous découvrirez comment l’instrumentation du noyau Linux peut améliorer l’observabilité de votre infrastructure en offrant des informations plus approfondies et un meilleur suivi.
Instrumentation Continue du Planificateur Linux
Pour garantir la fiabilité de nos charges de travail qui dépendent de réponses à faible latence, nous avons instrumenté la latence de la file d’attente d’exécution pour chaque conteneur, mesurant le temps que les processus passent dans la file d’attente de planification avant d’être envoyés au CPU. Une attente prolongée dans cette file d’attente peut être un indicateur de problèmes de performance, surtout lorsque les conteneurs n’utilisent pas leur allocation totale de CPU. L’instrumentation continue est essentielle pour détecter ces problèmes dès leur apparition, et eBPF, avec ses points d’accroche dans le planificateur Linux et une surcharge minimale, nous a permis de surveiller efficacement la latence de la file d’attente d’exécution.
Pour émettre une métrique de latence de la file d’attente d’exécution, nous avons utilisé trois points d’accroche eBPF : sched_wakeup
, sched_wakeup_new
, et sched_switch
.
Les points d’accroche sched_wakeup
et sched_wakeup_new
sont appelés lorsqu’un processus change d’état de ‘dormant’ à ‘exécutable’. Ils nous permettent d’identifier quand un processus est prêt à s’exécuter et attend du temps CPU. Lors de cet événement, nous générons un horodatage et le stockons dans une carte de hachage eBPF en utilisant l’identifiant du processus comme clé.
D’autre part, le point d’accroche sched_switch
est déclenché lorsque le CPU change de processus. Ce point d’accroche fournit des pointeurs vers le processus actuellement actif sur le CPU et celui qui va prendre le relais. Nous utilisons l’identifiant du processus à venir pour récupérer l’horodatage dans la carte eBPF. Cet horodatage représente le moment où le processus est entré dans la file d’attente, que nous avions précédemment stocké. Nous calculons ensuite la latence de la file d’attente d’exécution en soustrayant simplement les horodatages.
Un des avantages d’eBPF est sa capacité à fournir des pointeurs vers les structures de données du noyau représentant les processus ou les threads, également appelés tâches dans la terminologie du noyau. Cette fonctionnalité permet d’accéder à une multitude d’informations stockées sur un processus. Nous avions besoin de l’identifiant de cgroup du processus pour l’associer à un conteneur dans notre cas d’utilisation spécifique. Cependant, les informations de cgroup dans la structure du processus sont protégées par un verrou RCU (Read Copy Update).
Pour accéder en toute sécurité à ces informations protégées par RCU, nous pouvons utiliser des kfuncs dans eBPF. Les kfuncs sont des fonctions du noyau qui peuvent être appelées depuis des programmes eBPF. Il existe des kfuncs disponibles pour verrouiller et déverrouiller les sections critiques de lecture RCU. Ces fonctions garantissent que notre programme eBPF reste sûr et efficace tout en récupérant l’identifiant de cgroup à partir de la structure de tâche.
Une fois les données prêtes, nous devons les emballer et les envoyer à l’espace utilisateur. Pour cela, nous avons choisi de…
Optimisation de la Surveillance des Processus avec eBPF
L’utilisation de l’eBPF (Extended Berkeley Packet Filter) pour la gestion des buffers circulaires s’avère être une solution efficace, performante et conviviale. Ce système est capable de traiter des enregistrements de données de longueur variable tout en permettant la lecture des données sans nécessiter de copies supplémentaires en mémoire ou d’appels système. Cependant, la quantité élevée de points de données entraînait une utilisation excessive du CPU par le programme en espace utilisateur. Pour remédier à cela, nous avons intégré un limiteur de taux dans eBPF afin de prélever les données de manière plus contrôlée.
Structure des Buffers et Événements
Nous avons défini plusieurs structures pour gérer les événements et les timestamps associés aux groupes de contrôle (cgroups). Voici un aperçu de la structure utilisée :
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, RINGBUF_SIZE_BYTES);
} events SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, MAX_TASK_ENTRIES);
__uint(key_size, sizeof(u64));
__uint(value_size, sizeof(u64));
} cgroup_id_to_last_event_ts SEC(".maps");
struct runq_event {
u64 prev_cgroup_id;
u64 cgroup_id;
u64 runq_lat;
u64 ts;
};
Le code ci-dessus illustre comment nous gérons les événements de changement de planification. Nous avons mis en place un mécanisme de limitation de taux par cgroup pour équilibrer l’observabilité et la surcharge de performance.
Traitement des Événements en Utilisant Go
Notre application en espace utilisateur, développée en Go, traite les événements provenant du buffer circulaire pour émettre des métriques vers notre backend de métriques, Atlas. Chaque événement comprend un échantillon de latence de la file d’attente d’exécution associé à un ID de cgroup, que nous relions aux conteneurs en cours d’exécution sur l’hôte. Si aucune association n’est trouvée, nous le classifions comme un service système. Lorsqu’un ID de cgroup est lié à un conteneur, nous émettons une métrique de temps percentile pour ce conteneur. Nous incrémentons également un compteur pour surveiller les préemptions des processus du conteneur.
Importance des Métriques de Latence
Il est crucial de souligner que les métriques de latence de la file d’attente d’exécution et de préemption sont toutes deux nécessaires pour évaluer si un conteneur est affecté par des voisins bruyants. Par exemple, si un conteneur atteint ou dépasse sa limite CPU, le planificateur le ralentira, entraînant une augmentation apparente de la latence. Si nous ne considérions que cette métrique, nous pourrions faussement attribuer la dégradation des performances à des voisins bruyants, alors qu’il s’agit en réalité d’une limitation de quota CPU.
Étude de Cas : Problème de Voisinage Bruyant
Prenons l’exemple d’un serveur exécutant un seul conteneur avec une capacité CPU suffisante. La latence à 99e percentile est de 83,4 microsecondes, ce qui constitue notre référence. Cependant, à 10h35, le lancement d’un second conteneur a provoqué une augmentation significative de 131 millisecondes dans la latence de la file d’attente d’exécution du premier conteneur. Cette augmentation aurait été perceptible si l’application en espace utilisateur avait traité du trafic HTTP.
Les métriques de préemption indiquent que cette augmentation était due à des préemptions accrues par des processus système, illustrant un problème de voisinage bruyant où les services système rivalisent avec les conteneurs pour le temps CPU.
Optimisation du Code eBPF
Nous avons développé un outil de surveillance des processus eBPF open-source, bpftop, pour mesurer la surcharge du code eBPF dans ce chemin critique du noyau. Nos analyses montrent que l’instrumentation ajoute moins de 600 nanosecondes à chaque point d’accroche de planification. Lors de l’analyse des performances d’un service Java exécuté dans un conteneur, l’instrumentation n’a pas introduit de surcharge significative.
Améliorations Identifiées
Au cours de nos recherches sur la mesure des statistiques eBPF dans le noyau, nous avons identifié des opportunités d’amélioration. Nous avons soumis un correctif qui a été intégré dans la version 6.10 du noyau Linux.
À travers des essais et l’utilisation de bpftop, nous avons identifié plusieurs optimisations pour maintenir une faible surcharge :
- L’utilisation de
BPF_MAP_TYPE_HASH
s’est révélée la plus performante pour stocker les timestamps en file d’attente. En revanche,BPF_MAP_TYPE_TASK_STORAGE
a entraîné une diminution de performance presque double. - Les cartes de type
BPF_MAP_TYPE_PERCPU_HASH
ont montré des performances légèrement inférieures à celles deBPF_MAP_TYPE_HASH
, ce qui nécessite une investigation plus approfondie.
40 à 50 nanosecondes plus lents par opération que les cartes de hachage classiques. En raison des préoccupations liées à l’espace causées par le changement de PID, nous les avons initialement utilisées pour les horodatages en file d’attente. nous avons opté pour BPF_MAP_TYPE_HASH avec une taille augmentée afin de réduire ce risque.
L’assistant BPF_CORE_READ ajoute 20 à 30 nanosecondes par appel. Pour les points de trace bruts, en particulier ceux qui sont « activés par BTF » (tp_btf/*), il est plus sûr et plus efficace d’accéder directement aux membres de la structure de tâche. Andrii Nakryiko recommande cette méthode dans son article de blog.
Les événements sched_switch, sched_wakeup et sched_wakeup_new sont tous déclenchés pour les tâches du noyau, identifiables par leur PID de 0. Nous avons jugé inutile de surveiller ces tâches, c’est pourquoi nous avons mis en place plusieurs conditions de sortie anticipée et une logique conditionnelle pour éviter d’exécuter des opérations coûteuses, comme l’accès aux cartes BPF, lors du traitement d’une tâche du noyau. Il est à noter que les tâches du noyau fonctionnent via la file d’attente du planificateur, tout comme tout processus ordinaire.
Synthèse
Nos résultats mettent en lumière l’importance d’une instrumentation continue à faible surcharge du noyau Linux grâce à eBPF. Nous avons intégré ces métriques dans les tableaux de bord des clients, permettant des insights exploitables et orientant les discussions sur la performance en multitenance. Grâce à ces métriques, nous pouvons également affiner nos stratégies d’isolation CPU pour minimiser l’impact des voisins bruyants. De plus, ces données nous ont permis d’approfondir notre compréhension du planificateur Linux.
Ce travail a également enrichi notre connaissance de la technologie eBPF et a souligné l’importance d’outils comme bpftop pour optimiser le code eBPF. Avec l’augmentation de l’adoption d’eBPF, nous prévoyons que davantage d’observabilité d’infrastructure et de logique métier seront transférées vers cette technologie. Un projet prometteur dans ce domaine est sched_ext, qui pourrait révolutionner la manière dont les décisions de planification sont prises et adaptées aux besoins spécifiques des charges de travail.