Limites du processeur et limitation agressive dans Kubernetes

Remarque perev. : Ce récit édifiant d'Omio, l'agrégateur de voyages européen, emmène les lecteurs de la théorie de base aux subtilités pratiques captivantes de la configuration de Kubernetes. La connaissance de ces cas aide non seulement à élargir ses horizons, mais aussi à éviter des problèmes non triviaux.



Avez-vous déjà rencontré le fait que l'application "bloquée" en place, a cessé de répondre aux demandes de vérification de l'état et que vous ne compreniez pas la raison de ce comportement? Une explication possible est la limite de quota pour les ressources CPU. Il sera discuté dans cet article.

TL; DR:
Nous vous recommandons fortement de désactiver les limites du processeur dans Kubernetes (ou de désactiver les quotas CFS dans Kubelet) si vous utilisez une version du noyau Linux avec une erreur de quota CFS. Au cœur, ilUn bogue grave et bien connu qui entraîne une limitation excessive et des retards
.

Chez Omio, toute l'infrastructure est gérée par Kubernetes . Toutes nos charges avec état et sans état fonctionnent exclusivement sur Kubernetes (nous utilisons le moteur Google Kubernetes). Au cours des six derniers mois, nous avons commencé à observer des ralentissements aléatoires. Les applications gèlent ou cessent de répondre aux vérifications de l'état, perdent leur connexion au réseau, etc. Un tel comportement nous a longtemps perplexes et, finalement, nous avons décidé de nous attaquer de près au problème.

Résumé de l'article:

  • Quelques mots sur les conteneurs et Kubernetes;
  • Comment les demandes et les limites du processeur sont mises en Ĺ“uvre
  • Fonctionnement de la limite CPU dans les environnements multicĹ“urs;
  • Comment suivre la limitation du processeur;
  • RĂ©soudre le problème et les nuances.

Quelques mots sur les conteneurs et Kubernetes


Kubernetes, en fait, est la norme moderne dans le monde des infrastructures. Sa tâche principale est l'orchestration des conteneurs.

Conteneurs


Auparavant, nous devions créer des artefacts tels que des fichiers JAR / WAR Java, des œufs Python ou des exécutables pour un lancement ultérieur sur des serveurs. Cependant, pour les faire fonctionner, ils devaient faire un travail supplémentaire: installer le runtime (Java / Python), placer les fichiers nécessaires aux bons endroits, assurer la compatibilité avec une version spécifique du système d'exploitation, etc. En d'autres termes, vous deviez porter une attention particulière à la gestion de la configuration (ce qui provoquait souvent des conflits entre les développeurs et les administrateurs système).

Les conteneurs ont tout changé.Maintenant, l'image du conteneur agit comme un artefact. Il peut être représenté comme une sorte de fichier exécutable étendu contenant non seulement un programme, mais aussi un runtime à part entière (Java / Python / ...), ainsi que les fichiers / packages nécessaires préinstallés et prêts à fonctionner. Les conteneurs peuvent être déployés et exécutés sur différents serveurs sans étapes supplémentaires.

De plus, les conteneurs fonctionnent dans leur propre environnement sandbox. Ils ont leur propre adaptateur réseau virtuel, leur propre système de fichiers avec un accès limité, leur propre hiérarchie de processus, leurs limitations sur le CPU et la mémoire, etc. Tout cela est réalisé grâce à un sous-système spécial du noyau Linux - les espaces de noms (espace de noms).

Kubernetes


Comme indiqué précédemment, Kubernetes est un orchestre de conteneurs. Cela fonctionne comme suit: vous lui fournissez un pool de machines, puis vous dites: "Hé Kubernetes, lancez dix instances de mon conteneur avec 2 processeurs et 3 Go de mémoire chacun, et gardez-les opérationnels!" Kubernetes s'occupe du reste. Il trouvera des capacités libres, lancera des conteneurs et les redémarrera si nécessaire, déploiera une mise à jour lors du changement de version, etc. En fait, Kubernetes vous permet d'abstraire du composant matériel et rend toute la variété des systèmes adaptée au déploiement et au fonctionnement des applications.


Kubernetes du point de vue d'un simple profane

Quelles sont la demande et la limite dans Kubernetes


D'accord, nous avons trouvé les conteneurs et Kubernetes. Nous savons également que plusieurs conteneurs peuvent se trouver sur la même machine.

Vous pouvez faire une analogie avec un appartement communal. Une chambre spacieuse est prise (voitures / nœuds) et louée à plusieurs locataires (conteneurs). Kubernetes agit comme un agent immobilier. La question se pose, comment éviter que les locataires n'entrent en conflit les uns avec les autres? Et si l'un d'entre eux, par exemple, décide d'occuper la salle de bain pendant une demi-journée?

C'est là que la demande et la limite entrent en jeu. La demande de CPU est uniquement à des fins de planification. Cela ressemble à la «liste de souhaits» d'un conteneur, et il est utilisé pour sélectionner le nœud le plus approprié. Dans le même temps, CPU Limit peut être comparé à un bail - dès que nous prenons un nœud pour le conteneur,ne pourra pas dépasser les limites établies. Et ici un problème se pose ...

Comment les demandes et les limites sont implémentées dans Kubernetes


Kubernetes utilise le mécanisme de limitation du noyau (skipping clock) pour implémenter les limites du processeur. Si l'application dépasse la limite, la limitation est activée (c'est-à-dire qu'elle reçoit moins de cycles CPU). Les demandes et les limites de mémoire sont organisées différemment, de sorte qu'elles sont plus faciles à détecter. Pour ce faire, il suffit de vérifier l'état du dernier redémarrage du pod: s'il s'agit de «OOMKilled». Avec la limitation du processeur, tout n'est pas si simple, car K8s ne met à disposition que des métriques à utiliser, et pas pour les groupes de contrôle.

Demande CPU



Comment la demande de CPU est implémentée

Pour plus de simplicité, regardons un processus utilisant un exemple de machine avec un CPU à 4 cœurs.

K8s utilise le mécanisme cgroups pour contrôler l'allocation des ressources (mémoire et processeur). Un modèle hiérarchique lui est disponible: un descendant hérite des limites du groupe parent. Les détails de la distribution sont stockés dans le système de fichiers virtuel ( /sys/fs/cgroup). Dans le cas du processeur, cela /sys/fs/cgroup/cpu,cpuacct/*.

K8s utilise le fichier cpu.sharepour allouer les ressources du processeur. Dans notre cas, le groupe de contrôle racine reçoit 4096 parts de ressources CPU - 100% de la puissance de processeur disponible (1 cœur = 1024; il s'agit d'une valeur fixe). Le groupe racine distribue les ressources proportionnellement en fonction des parts de descendants prescrites danscpu.share, et ceux-ci, à leur tour, font de même avec leurs descendants, etc. Typiquement Kubernetes groupe témoin nœud racine a trois enfants: system.slice, user.sliceet kubepods. Les deux premiers sous-groupes sont utilisés pour répartir les ressources entre les charges système critiques et les programmes utilisateur en dehors des K8. Le dernier - - kubepodsest créé par Kubernetes pour répartir les ressources entre les pods.

Le diagramme ci-dessus montre que les premier et deuxième sous-groupes ont reçu 1024 partages, avec 4096 partages attribués au sous-groupe kuberpod . Comment est-ce possible: après tout, le groupe racine ne dispose que de 4096 actions, et la somme des actions de ses descendants dépasse considérablement ce nombre ( 6144)? Le fait est que la valeur est logique, donc le Planificateur Linux (CFS) l'utilise pour allouer proportionnellement les ressources CPU. Dans notre cas, les deux premiers groupes reçoivent 680 actions réelles (16,6% de 4096) et kubepod reçoit les 2736 actions restantes . En cas d'indisponibilité, les deux premiers groupes n'utiliseront pas les ressources allouées.

Heureusement, le planificateur dispose d'un mécanisme pour éviter la perte de ressources CPU inutilisées. Il transfère les capacités «inactives» au pool global, à partir duquel elles sont réparties en groupes qui nécessitent des capacités de processeur supplémentaires (le transfert a lieu par lots pour éviter les pertes d'arrondi). Une méthode similaire s'applique à tous les descendants de descendants.

Ce mécanisme garantit une répartition équitable de la puissance du processeur et garantit qu'aucun processus ne «vole» les ressources des autres.

Limite CPU


Malgré le fait que les configurations des limites et des demandes dans les K8 se ressemblent, leur mise en œuvre est fondamentalement différente: c'est la partie la plus trompeuse et la moins documentée.

K8s utilise le mécanisme de quota CFS pour appliquer les limites. Leurs paramètres sont spécifiés dans les fichiers cfs_period_uset cfs_quota_usdans le répertoire cgroup (le fichier s'y trouve également cpu.share).

En revanche cpu.share, le quota est basé sur une période de temps et non sur la puissance de processeur disponible. cfs_period_usdéfinit la durée de la période (ère) - elle est toujours de 100 000 μs (100 ms). K8s a la possibilité de modifier cette valeur, mais elle n'est actuellement disponible que dans la version alpha. Le planificateur utilise l'ère pour redémarrer les quotas utilisés. Deuxième dossiercfs_quota_us, définit le temps disponible (quota) à chaque époque. Veuillez noter qu'il est également indiqué en microsecondes. Le quota peut dépasser la durée de l'ère; en d'autres termes, elle peut être supérieure à 100 ms.

Examinons deux scénarios sur des machines à 16 cœurs (le type d'ordinateurs le plus courant que nous avons dans Omio):


Scénario 1: 2 threads et une limite de 200 ms. Sans étranglement


Scénario 2: 10 flux et une limite de 200 ms. La limitation commence après 20 ms, l'accès aux ressources du processeur reprend après 80 ms supplémentaires.

Supposons que vous définissez la limite du processeur sur 2 cœurs; Kubernetes traduira cette valeur à 200 ms. Cela signifie que le conteneur peut utiliser un temps processeur maximum de 200 ms sans limitation.

Et ici, le plaisir commence. Comme mentionné ci-dessus, le quota disponible est de 200 ms. Si vous avez dix threads exécutés en parallèle sur une machine à 12 cœurs (voir l'illustration du scénario 2), alors que tous les autres pods sont inactifs, le quota sera épuisé en seulement 20 ms (puisque 10 * 20 ms = 200 ms), et tous les threads de ce pod est accélérateur pour les 80 ms suivantes. Le bogue du planificateur déjà mentionné aggrave la situation , en raison de laquelle une limitation excessive se produit et le conteneur ne peut même pas déterminer le quota existant.

Comment Ă©valuer la limitation dans les pods?


Allez simplement au pod et courez cat /sys/fs/cgroup/cpu/cpu.stat.

  • nr_periods - le nombre total de pĂ©riodes de l'ordonnanceur;
  • nr_throttled- le nombre de pĂ©riodes Ă©tranglĂ©es dans la composition nr_periods;
  • throttled_time - temps d'Ă©tranglement cumulĂ© en nanosecondes.



Qu'est-ce qui se passe réellement?


En conséquence, nous obtenons un étranglement élevé dans toutes les applications. Parfois, il est une fois et demie plus fort que le calculé!

Cela conduit à diverses erreurs - échecs de vérification de l'état de préparation, blocages de conteneurs, coupures de connexion réseau, délais d'attente dans les appels de service. En fin de compte, cela se traduit par une latence accrue et des erreurs accrues.

Décision et conséquences


Ici, tout est simple. Nous avons abandonné les limites du processeur et commencé à mettre à jour le noyau du système d'exploitation dans les clusters vers la dernière version dans laquelle le bogue a été corrigé. Le nombre d'erreurs (HTTP 5xx) dans nos services a immédiatement chuté de manière significative:

Erreurs HTTP 5xx



Erreurs HTTP 5xx d'un service critique

Temps de réponse P95



DĂ©lai de demande de service critique, 95e centile

Les coûts d'exploitation



Nombre d'heures passées

Quel est le piège?


Comme indiqué au début de l'article:

Vous pouvez faire une analogie avec un appartement communal ... Kubernetes agit comme un agent immobilier. Mais comment éviter que les locataires n'entrent en conflit? Et si l'un d'entre eux, par exemple, décide d'occuper la salle de bain pendant une demi-journée?

C’est le hic. Un conteneur négligent peut absorber toutes les ressources processeur disponibles sur la machine. Si vous avez une pile d'applications intelligente (par exemple, JVM, Go, Node VM sont correctement configurés), ce n'est pas un problème: vous pouvez travailler dans de telles conditions pendant longtemps. Mais si les applications sont mal optimisées ou pas du tout optimisées ( FROM java:latest), la situation peut devenir incontrôlable. Chez Omio, nous avons automatisé les Dockerfiles de base avec des paramètres par défaut adéquats pour la pile des langues principales, donc il n'y avait pas un tel problème.

Nous vous recommandons de surveiller les métriques USE (utilisation, saturation et erreurs), les retards API et les taux d'erreur. Assurez-vous que les résultats sont comme prévu.

Références


Telle est notre histoire. Les documents suivants ont grandement aidé à comprendre ce qui se passe:


Rapport d'erreurs Kubernetes:


Avez-vous rencontré des problèmes similaires dans votre pratique ou avez-vous l'expérience de la limitation dans des environnements de production conteneurisés? Partagez votre histoire dans les commentaires!

PS du traducteur


Lisez aussi dans notre blog:


All Articles