...

Commutation de contexte de serveur et surcharge CPU : Tout savoir

Context Switching CPU détermine l'efficacité avec laquelle les cœurs du serveur passent d'un thread ou d'un processus à l'autre, tout en réduisant la latence et les coûts. Overhead produire. Je montre concrètement où se situent les coûts, quelles sont les valeurs de mesure qui comptent et comment je peux réduire l'overhead de changement dans les environnements productifs.

Points centraux

  • Coûts directs: sauvegarde/chargement de registres, changement de TLB et de stack
  • Coûts indirects: Mises en cache, migration du noyau, temps de l'ordonnanceur
  • Valeurs seuils: >5.000 switches/core/s comme signal d'alerte
  • Optimisations: affinité CPU, E/S asynchrones, plus de cœurs
  • Suivi: vmstat, sar, perf pour les résultats clairs

Qu'est-ce que la commutation de contexte sur les serveurs ?

Un changement de contexte enregistre l'état actuel d'un thread ou d'un processus et charge le contexte d'exécution suivant afin que plusieurs charges de travail puissent se partager un noyau en multiplexage temporel [7]. Ce mécanisme apporte des avantages, mais génère de la pureté dans le temps de changement. Overhead, car aucun travail d'application n'est en cours [1]. Je considère ici les registres tels que IP, BP, SP et le répertoire de pages (CR3), que le système doit sauvegarder et restaurer en cas de changement [2]. Techniquement, cela semble invisible, mais en pratique, cela détermine fortement le temps de réaction, surtout en cas de nombreuses demandes simultanées. Ceux qui font évoluer les serveurs doivent garder à l'esprit ce taux de changement, sinon le travail de contrôle consomme sensiblement la capacité de l'unité centrale.

L'overhead direct en détail

Les coûts directs sont liés à la sauvegarde et à la restauration du contexte matériel, c'est-à-dire la pile du noyau, les tables de pages et les registres du processeur [2]. Sur x86_64, un changement de thread dans le même processus prend souvent 0,3-1,0 microseconde, un changement de processus avec un autre espace d'adressage plutôt 1-5 microsecondes [1]. Si un thread passe en plus sur un autre noyau, les effets de cache ajoutent 5-15 microsecondes, car le nouveau noyau charge d'abord ses données dans les caches [1]. Ces temps paraissent petits, mais ils s'accumulent très rapidement en cas de milliers de changements par seconde, ce qui entraîne des coûts mesurables. Serveur-perte de temps. J'en tiens compte lors de la planification des budgets de latence et je fixe des limites strictes pour les services qui nécessitent des réponses difficiles.

Overhead indirect et caches

Les coûts indirects dominent souvent, surtout lorsque les charges de travail sont fortement parallèles et migrent [1]. Si un thread se déplace entre les cœurs, il perd ses données chaudes L1/L2, ce qui peut coûter 50-200 nanosecondes par accès [1]. Les flux TLB lors des changements d'espace d'adressage entraînent également des arrêts de pipeline qui font baisser le débit [3]. De plus, le travail de l'ordonnanceur lui-même coûte du temps, ce qui représente plusieurs pour cent de consommation de CPU lorsque la fréquence de commutation est très élevée [1][3]. J'évite cela Thrashing, J'essaie de réduire au minimum les changements de noyau et d'identifier rapidement les goulots d'étranglement.

Reconnaître les valeurs seuils et les lire correctement

J'évalue vmstat et sar et j'examine le taux de commutation par noyau, et pas seulement globalement [2]. Des valeurs autour de 5.000 switches par cœur et par seconde définissent pour moi une zone d'alerte claire dans laquelle je recherche les causes de manière ciblée [2]. Au-delà de 14.000 par CPU et par seconde, je m'attends à des chutes significatives, par exemple pour les serveurs de bases de données ou les serveurs web avec une grande capacité d'exécution secondaire [6]. Sur les machines virtuelles, je compte en outre sur les changements d'hyperviseur, qui peuvent minimiser les métriques du système invité [2]. Une seule valeur n'explique jamais tout, c'est pourquoi je combine Taux, Les données relatives à l'utilisation, à la latence et à la charge de travail forment un ensemble cohérent.

Ordonnanceur, préemption et interruptions

Un ordonnanceur moderne tel que le CFS répartit les noyaux de manière équitable et décide quand il supplante les threads en cours [4]. Une préemption trop agressive augmente le temps de changement, une préemption trop retenue fait perdre du temps de réaction pour les tâches importantes [3]. Je vérifie si la charge des interruptions prend du temps de noyau, car les interruptions très fréquentées entraînent des commutateurs de noyau supplémentaires. Pour une introduction au sujet, je recommande la contribution à Gestion des interruptions, Il explique très clairement les effets sur la latence. Mon objectif reste de créer une Préemption-politique qui protège les chemins difficiles et regroupe les travaux annexes.

Tranches de temps, granularité et wakeups

La longueur des tranches de temps et la granularité des réveils déterminent directement la fréquence à laquelle l'ordonnanceur est activé. Des tranches de temps trop petites entraînent des préemptions fréquentes et donc davantage de changements ; des tranches de temps trop grandes augmentent le temps de réponse des chemins interactifs ou sensibles à la latence. Je fais attention à la durée effective min_granularité et wakeup_granularity de l'ordonnanceur, car elles déterminent quand un thread éveillé peut supplanter un thread en cours. Dans les charges de travail comportant de nombreuses tâches de courte durée, je préfère une tolérance de réveil un peu plus grande, afin que les heuristiques ne récompensent pas en permanence les „réveils“ qui ne génèrent finalement que du thrash. Sur les systèmes très sensibles à la latence, le fonctionnement „tickless“ vaut la peine, de sorte que le tick du timer ne déclenche pas inutilement des préemptions. Ce qui reste important : Je mesure chaque modification par rapport aux latences de bout en bout, pas seulement par rapport au taux de commutation pur.

Virtualisation, hyperthreading et effets NUMA

Sous la virtualisation, l'hyperviseur ajoute d'autres couches qui effectuent également des changements de contexte [2]. Cela entraîne un décalage des valeurs de mesure et un taux apparemment modéré dans l'invité peut être plus élevé en réalité sur l'hôte. L'hyperthreading atténue les trous d'attente dans le pipeline, mais n'élimine pas l'overhead de changement ; un mauvais épinglage de thread détériore même la situation du cache [4]. Sur les systèmes NUMA, je fais en outre attention aux accès locaux à la mémoire, car les accès à distance augmentent les latences. Je prévois NUMA-Je suis conscient de l'importance de ces zones et je teste leur comportement dans des conditions réelles de production.

Conteneurs, quotas CPU et impression d'ordonnanceurs

Dans les conteneurs, je définis les partages et les quotas CPU de manière à ce que le régulateur de bande passante CFS n'étrangle pas à la milliseconde. Si un cgroup est régulièrement „déréglé“, cela génère des exécutions courtes, des préemption fréquentes et plus de changements de contexte - avec en même temps un travail net moins bon. Je planifie les CPU par conteneur de manière conservatrice, je préfère mettre plus de Actions comme quotas durs et vérifie si les pics de „burst“ tombent dans la capacité libre de l'hôte. Sur les hôtes avec de nombreux petits conteneurs, je répartis les services sur les nœuds NUMA et je regroupe les charges de travail apparentées en cgroupes afin que l'ordonnanceur ait moins à migrer. Si je constate de fortes différences entre les processus dans pidstat -w et sar, j'augmente de manière ciblée l'affinité par cgroup et j'envisage des noyaux isolés pour les chemins de latence.

Mettre en œuvre directement : Réduire le taux de changement

Je commence par le scaling des ressources : plus de cœurs de CPU et suffisamment de RAM réduisent le taux de changement parce que plus de travail est effectué en parallèle [4]. Ensuite, je mets en place l'affinité CPU pour maintenir les threads sur des noyaux fixes et utiliser la chaleur du cache [4]. Lorsque c'est possible, je mise sur les E/S asynchrones pour que les processus ne se bloquent pas en attendant et ne déclenchent pas de changements inutiles [4]. Pour les chemins de latence, je préfère des threads de niveau utilisateur légers qui changent plus rapidement que les threads du noyau pur [4]. Cette approche pragmatique Ordre apporte des progrès rapidement mesurables dans la pratique.

Utiliser correctement CPU Affinity et NUMA

Avec CPU-Affinity, je lie des services à des noyaux fixes et garde ainsi des ensembles de travail dans le cache, ce qui réduit les migrations inter-cœurs [4]. Sous Linux, j'utilise taskset ou sched_setaffinity et j'intègre les affinités IRQ. Sur les systèmes NUMA, je répartis les services sur les nœuds et je veille à ce que la mémoire soit allouée localement. Pour les détails pratiques, je vous renvoie à mon guide de Affinité CPU dans l'hébergement, qui décrit les étapes de manière compacte. Propreté Épingler me permet souvent d'économiser plusieurs pour cent de CPU et de lisser considérablement les pics de latence [1].

TLB, Huge Pages et séquences KPTI

Les changements d'espace d'adressage et les flux TLB sont des moteurs essentiels pour les frais généraux indirects. Là où cela convient, je place des pages plus grandes (Huge Pages) afin de réduire les pressions TLB et de rendre les shootdowns plus rares. Cela est particulièrement efficace pour les bases de données en mémoire et les caches avec de grands tas. Les migrations de sécurité comme KPTI ont historiquement augmenté le taux de coût des transitions utilisateur/noyau ; les CPU modernes avec PCID/ASID atténuent cela, mais une part élevée de syscall reste visible. Mon antidote : regrouper les appels système (batching), moins de petites écritures, moins de changements de contexte entre le userland et le noyau, et des E/S asynchrones aux endroits critiques. Le but n'est pas d'éviter tout flush, mais de presser leur fréquence de manière à ce que les caches puissent fonctionner.

Modèles de threads : Event-driven vs. Thread-per-Request

Le modèle d'architecture influence directement le taux d'alternance, c'est pourquoi je choisis délibérément entre Event-driven et Thread-per-Request. Une boucle d'événements avec E/S asynchrones génère moins de blocages et donc moins d'alternances à charge égale. Le threading classique par requête offre la simplicité, mais produit des changements de contexte en masse avec un parallélisme élevé. Pour les serveurs web et les proxys avec de très nombreuses connexions simultanées, le modèle événementiel est généralement payant. Pour une comparaison plus approfondie, voir Modèles de threading un aperçu ciblé avec des considérations pratiques ; ces Choix décide souvent de la courbe de latence.

Contention de verrouillage et temps hors CPU

En plus des vrais changements de CPU, j'observe Off-CPU-temps de réponse : Attente des verrous, des E/S ou de l'arrivée de l'ordonnanceur. Des pourcentages élevés de hors CPU signifient souvent que les threads sont „parqués“ par la rétention des verrous et que l'ordonnanceur doit constamment démarrer de nouveaux candidats - un générateur de changements inutiles. Je mesure cela avec les événements perf et les points de suivi de l'ordonnanceur (sched_switch) pour voir si les alternances résultent de préemption, de blocage ou de migration. Dans les applications, je réduis la granularité des sections critiques, je remplace les verrous globaux par le sharding et j'utilise des structures sans verrous lorsque c'est judicieux. Ainsi, le flux de réveils diminue et l'ordonnanceur garde les threads productifs plus longtemps sur un noyau.

Playbook de monitoring pour des résultats clairs

Je commence par utiliser vmstat et sar pour voir le taux de commutation et l'utilisation au fil du temps [2]. Ensuite, je vérifie avec perf stat où va le temps CPU et si les mispredictions de branche ou les événements TLB sont élevés [4]. Netdata ou des outils similaires visualisent les valeurs par processus et par cœur, ce qui minimise les points aveugles [4]. Il est important d'effectuer des mesures pendant les véritables horaires de pointe et non pas seulement au ralenti. Ce n'est qu'après ces Profils montrer si l'ordonnanceur change parce que je bloque, migre ou génère trop de threads.

Liste de contrôle pratique : commandes de mesure rapides

  • vmstat 1 : procs r/b, cs/s et tendances de changement de contexte toutes les secondes
  • mpstat -P ALL 1 : Charge de travail et charge d'interruption par cœur
  • pidstat -w 1 : commutateurs volontaires/involontaires par processus
  • perf stat -e context-switches,cpu-migrations,task-clock : rendre visible les inducteurs de coûts durs
  • perf sched timehist : suivre les temps d'attente dans les runqueues et le comportement des wakeups
  • trace-cmd/perf record -e sched:sched_switch : clarifier les origines des changements par trace

Valeurs seuils dans les environnements virtuels

Sur les VM, je lis les taux de commutation avec prudence, car les ordonnanceurs hôtes et le co-ordonnancement introduisent des changements supplémentaires [2]. Je veille à ce que le nombre de vCPU et les cœurs physiques correspondent, afin qu'il n'y ait pas de concurrence pour les timeslices. Le CPU steal time me fournit des indications sur l'ampleur des interruptions de mes vCPU par l'hôte. Si je vois des taux de commutation élevés avec un temps de vol élevé, je donne la priorité à une instance avec plus de cœurs dédiés. Je m'assure ainsi Consistance même si l'hyperviseur sert de nombreux systèmes invités en parallèle.

Tableau des indicateurs et quick-wins

J'utilise l'aperçu suivant comme antisèche lorsque je réduis visiblement l'overhead de changement et que je donne la priorité à des étapes concrètes. Elle couvre l'affinité, la mise à l'échelle, l'allègement des threads, l'ordonnancement et les E/S asynchrones, avec des avantages tangibles. Je cible ces points et les mesure avant et après le changement, afin que le succès soit clairement démontré. De petites interventions produisent souvent des effets importants, par exemple si je redistribue seulement des IRQ ou si j'introduis epoll. Ces mesures compactes Actions réduisent les pics de latence et augmentent le débit net de manière mesurable.

Mesure d'optimisation Avantage Exemple
Affinité du CPU Réduit les cache-miss taskset dans Linux
Plus de cœurs Moins de commutateurs Mise à l'échelle sur 16+ cœurs
Fils légers Des changements plus rapides Fils de niveau utilisateur
Ordonnanceur CFS Distribution équitable Norme Linux
E/S asynchrones Évite les switchs d'attente epoll dans Linux

Objectifs de performance et budgets de latence

Je formule des objectifs clairs : Quel pourcentage de CPU le changement peut-il coûter et quelle latence reste-t-il pour l'application ? Dans les configurations bien réglées, je réduis l'overhead de plusieurs pour cent à moins d'un pour cent, selon le profil [1]. Les chemins critiques comme Auth, la mise en cache ou les structures de données en mémoire ont la priorité sur l'affinité et les E/S asynchrones. Je reporte le travail par lots dans les phases calmes afin de réduire les temps de pointe. Un système propre Budget facilite les décisions lorsque les paramètres de l'ordonnanceur doivent être mis en balance [3].

E/S réseau, IRQ et coalescence

Les chemins réseau génèrent souvent des changements sans que l'application s'en rende compte : NAPI, SoftIRQs et ksoftirqd prennent en charge les pics de charge qui occupent davantage l'ordonnanceur. Je contrôle si RSS (plusieurs queues de réception) est actif et je définis les affinités IRQ de manière à ce que les interruptions réseau ciblent les mêmes noyaux que les charges de travail qui traitent les paquets. Les RPS/RFS aident à diriger le chemin des données vers les caches locaux plutôt que de sauter constamment par-dessus le socle. Avec un coalesçage modéré des interruptions, je lisse le flux de réveils sans faire exploser les budgets de latence. L'effet est immédiat : moins de „réveils“ brefs du CPU, des tranches de temps productives plus longues par thread.

Contrôle de la latence de la queue et backpressure

Les taux élevés de changement de contexte sont fortement corrélés à la variance des temps de réponse. C'est pourquoi j'optimise non seulement la médiane, mais aussi les valeurs P95/P99 : des sections critiques plus courtes, des stratégies de backpressure propres (par exemple des files d'attente limitées et des requêtes non critiques éjectables) et le microbatching pour les chemins à forte intensité d'entrées/sorties. Je garde les pools de threads délibérément petits et élastiques, afin qu'ils n„“encombrent„ pas l'ordonnanceur avec des milliers de tâches en attente. En particulier en cas de “Connection Storms" (par ex. vagues de reconnexion), j'effectue des drops sur les bords au lieu de s'effondrer au cœur de l'application - cela réduit les changements, stabilise les files d'attente et protège durablement les budgets de latence.

Éviter les anti-patterns critiques

J'évite un nombre excessif de threads, car cela ne fait que pousser le travail de commutation et n'augmente pas automatiquement le vrai parallélisme. Les boucles busy-wait sans backoff brûlent le CPU tout en obligeant le scheduler à de fréquentes préemptions. Les migrations fréquentes du cœur sans raison indiquent un manque d'affinité ou des IRQ qui tiquent au mauvais endroit. Les E/S bloquantes dans les chemins de requête génèrent des commutations permanentes et font grimper la variance des temps de réponse. De tels Échantillon je les détecte rapidement et les élimine systématiquement avant qu'ils ne touchent la charge utile.

En bref

Le Context Switching CPU fait partie des plus grands facteurs de coûts cachés dans les serveurs fortement sollicités. Je mesure d'abord le taux de commutation par cœur, je classe les latences et le temps de vol et je mets un frein à >5.000 commutations/cœurs/s [2]. Ensuite, je règle l'affinité, les E/S asynchrones et, le cas échéant, plus de cœurs, afin de presser ensemble les effets directs et indirects [4]. J'évalue les paramètres de l'ordonnanceur, la charge d'interruption et la virtualisation dans le contexte, afin qu'aucune couche ne domine l'autre [1][2][3]. Avec cette approche focalisée Procédure je réduis l'overhead à moins d'un pour cent et je maintiens des temps de réponse stables même en cas de charge élevée.

Derniers articles