Noter. trad.: Cette histoire révélatrice d'Omio, un agrégateur de voyages européen, emmÚne les lecteurs de la théorie de base aux fascinantes subtilités pratiques de la configuration de Kubernetes. La connaissance de tels cas permet non seulement d'élargir vos horizons, mais également d'éviter des problÚmes non triviaux.

Avez-vous déjà vu une application rester bloquée, cesser de répondre aux contrÎles de santé et ne pas comprendre pourquoi ? Une explication possible est liée aux limites de quota de ressources CPU. C'est ce dont nous parlerons dans cet article.
TL; DR:
Nous recommandons fortement de désactiver les limites de CPU 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. Dans le noyau sérieux et un bug qui entraßne une limitation et des retards excessifs.
Sur Omio toute l'infrastructure est gérée par Kubernetes. Toutes nos charges de travail avec et sans état s'exécutent exclusivement sur Kubernetes (nous utilisons Google Kubernetes Engine). Au cours des six derniers mois, nous avons commencé à observer des ralentissements aléatoires. Les applications se bloquent ou cessent de répondre aux contrÎles de santé, perdent la connexion au réseau, etc. Ce comportement nous a longtemps intrigués et nous avons finalement décidé de prendre le problÚme au sérieux.
Résumé de l'article:
- Quelques mots sur les conteneurs et Kubernetes ;
- Comment les demandes et les limites du processeur sont mises en Ćuvre ;
- Comment fonctionne la limite du processeur dans les environnements multicĆurs ;
- Comment suivre la limitation du processeur ;
- Solution du problĂšme et nuances.
Quelques mots sur les conteneurs et Kubernetes
Kubernetes est essentiellement la norme moderne dans le monde des infrastructures. Sa tĂąche principale est l'orchestration des conteneurs.
Containers
Dans le passĂ©, nous devions crĂ©er des artefacts tels que des JAR/WAR Java, des Python Eggs ou des exĂ©cutables Ă exĂ©cuter sur des serveurs. Cependant, pour les faire fonctionner, un travail supplĂ©mentaire a dĂ» ĂȘtre effectuĂ© : installer l'environnement d'exĂ©cution (Java/Python), placer les fichiers nĂ©cessaires aux bons endroits, s'assurer de la compatibilitĂ© avec une version spĂ©cifique du systĂšme d'exploitation, etc. En dâautres termes, il fallait prĂȘter une attention particuliĂšre Ă la gestion de la configuration (qui Ă©tait souvent source de conflits entre dĂ©veloppeurs et administrateurs systĂšme).
Les conteneurs ont tout changĂ©. L'artefact est dĂ©sormais une image conteneur. Il peut ĂȘtre reprĂ©sentĂ© comme une sorte de fichier exĂ©cutable Ă©tendu contenant non seulement le programme, mais Ă©galement un environnement d'exĂ©cution Ă part entiĂšre (Java/Python/...), ainsi que les fichiers/packages nĂ©cessaires, prĂ©installĂ©s et prĂȘts Ă ĂȘtre utilisĂ©s. courir. Les conteneurs peuvent ĂȘtre dĂ©ployĂ©s et exĂ©cutĂ©s sur diffĂ©rents serveurs sans aucune Ă©tape supplĂ©mentaire.
De plus, les conteneurs s'exĂ©cutent dans leur propre environnement isolĂ©. Ils disposent de leur propre carte rĂ©seau virtuelle, de leur propre systĂšme de fichiers Ă accĂšs restreint, de leur propre hiĂ©rarchie de processus, de leurs propres limites de processeur et de mĂ©moire, etc. Tout cela est implĂ©mentĂ© grĂące Ă un sous-systĂšme du noyau dĂ©diĂ©. Linux â espaces de noms.
Kubernetes
Comme indiqué précédemment, Kubernetes est un orchestrateur de conteneurs. Cela fonctionne comme ceci : vous lui donnez un pool de machines, puis dites : « Hé, Kubernetes, lançons dix instances de mon conteneur avec 2 processeurs et 3 Go de mémoire chacune, et faisons-les fonctionner ! Kubernetes s'occupera du reste. Il va trouver de la capacité libre, lancer des conteneurs et les redémarrer si nécessaire, déployer une mise à jour lors d'un changement de version, etc. Essentiellement, Kubernetes vous permet d'abstraire le composant matériel et de créer une grande variété de systÚmes adaptés au déploiement et à l'exécution d'applications.

Kubernetes du point de vue du profane
Que sont les requĂȘtes et les limites dans Kubernetes
D'accord, nous avons couvert les conteneurs et Kubernetes. Nous savons Ă©galement que plusieurs conteneurs peuvent rĂ©sider sur la mĂȘme machine.
Une analogie peut ĂȘtre faite avec un appartement communal. Un local spacieux (machines/unitĂ©s) est pris et louĂ© Ă plusieurs locataires (conteneurs). Kubernetes agit en tant qu'agent immobilier. La question se pose, comment Ă©viter les conflits entre locataires ? Et si lâun dâeux, par exemple, dĂ©cide dâemprunter la salle de bain pour une demi-journĂ©e ?
Câest lĂ quâinterviennent les demandes et les limites. CPU Demander nĂ©cessaire uniquement Ă des fins de planification. C'est quelque chose comme une « liste de souhaits » du conteneur, et elle est utilisĂ©e pour sĂ©lectionner le nĆud le plus appropriĂ©. En mĂȘme temps le CPU Limiter peut ĂȘtre comparĂ© Ă un contrat de location - dĂšs que nous sĂ©lectionnons une unitĂ© pour le conteneur, le ne sera pas capable dĂ©passer les limites Ă©tablies. Et c'est lĂ que le problĂšme se pose...
Comment les requĂȘtes et les limites sont implĂ©mentĂ©es dans Kubernetes
Kubernetes utilise un mécanisme de limitation (saut de cycles d'horloge) intégré au noyau pour implémenter les limites du processeur. Si une 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, elles sont donc plus faciles à détecter. Pour cela, il suffit de vérifier le dernier statut de redémarrage du pod : s'il est « OOMKilled ». La limitation du processeur n'est pas si simple, puisque K8 ne rend les métriques disponibles que par utilisation, et non par groupes de contrÎle.
Demande de processeur

Comment la requĂȘte CPU est implĂ©mentĂ©e
Pour plus de simplicitĂ©, examinons le processus en utilisant comme exemple une machine dotĂ©e d'un processeur Ă 4 cĆurs.
K8s utilise un mécanisme de groupe de contrÎle (cgroups) pour contrÎler l'allocation des ressources (mémoire et processeur). Un modÚle hiérarchique lui est proposé : l'enfant hérite des limites du groupe parent. Les détails de la distribution sont stockés dans un systÚme de fichiers virtuel (/sys/fs/cgroup). Dans le cas d'un processeur, c'est /sys/fs/cgroup/cpu,cpuacct/*.
K8s utilise un fichier cpu.share pour allouer les ressources du processeur. Dans notre cas, le groupe de contrĂŽle racine obtient 4096 100 parts de ressources CPU, soit 1 % de la puissance du processeur disponible (1024 cĆur = XNUMX XNUMX ; il s'agit d'une valeur fixe). Le groupe racine rĂ©partit les ressources proportionnellement en fonction des parts des descendants inscrits dans cpu.share, et eux, Ă leur tour, font de mĂȘme avec leurs descendants, etc. Sur un nĆud Kubernetes typique, le groupe de contrĂŽle racine a trois enfants : system.slice, user.slice Đž 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 - kubepods â créé par Kubernetes pour distribuer les ressources entre les pods.
Le diagramme ci-dessus montre que le premier et le deuxiÚme sous-groupes ont reçu chacun 1024 partages, avec le sous-groupe kuberpod attribué 4096 actions Comment est-ce possible : aprÚs tout, le groupe racine n'a accÚs qu'à 4096 actions, et la somme des actions de ses descendants dépasse largement ce nombre (6144L'important est que la valeur ait une signification logique, donc le planificateur Linux (CFS) l'utilise pour répartir proportionnellement les ressources du processeur. Dans notre cas, les deux premiers groupes reçoivent 680 actions réelles (16,6% de 4096), et kubepod reçoit le reste 2736 actions 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 de gaspiller les ressources CPU inutilisées. Il transfÚre la capacité « inactive » vers un pool global, à partir duquel elle est distribuée aux groupes ayant besoin de puissance de processeur supplémentaire (le transfert s'effectue par lots pour éviter les pertes d'arrondi). Une méthode similaire est appliquée à tous les descendants des 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 du processeur
MalgrĂ© le fait que les configurations de limites et de requĂȘtes dans les K8 se ressemblent, leur mise en Ćuvre est radicalement diffĂ©rente : le plus trompeur et la partie la moins documentĂ©e.
Les K8 s'engagent pour mettre en Ćuvre des limites. Leurs paramĂštres sont spĂ©cifiĂ©s dans des fichiers cfs_period_us Đž cfs_quota_us dans le rĂ©pertoire cgroup (le fichier s'y trouve Ă©galement cpu.share).
Contrairement Ă cpu.share, le quota est basĂ© sur pĂ©riode de temps, et non sur la puissance disponible du processeur. cfs_period_us spĂ©cifie la durĂ©e de la pĂ©riode (Ă©poque) - elle est toujours de 100000 100 ÎŒs (8 ms). Il existe une option pour modifier cette valeur dans KXNUMX, mais elle n'est disponible qu'en alpha pour le moment. Le planificateur utilise l'Ă©poque pour redĂ©marrer les quotas utilisĂ©s. DeuxiĂšme fichier cfs_quota_us, spĂ©cifie le temps disponible (quota) Ă chaque Ă©poque. Notez qu'il est Ă©galement spĂ©cifiĂ© en microsecondes. Le quota peut dĂ©passer la durĂ©e de l'Ă©poque ; autrement dit, elle peut ĂȘtre supĂ©rieure Ă 100 ms.
Examinons deux scĂ©narios sur des machines Ă 16 cĆurs (le type d'ordinateur le plus courant dont nous disposons chez Omio) :

Scénario 1 : 2 threads et une limite de 200 ms. Pas de limitation

Scénario 2 : 10 threads et 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
Disons que vous définissez la limite du processeur sur 2 graines; Kubernetes traduira cette valeur en 200 ms. Cela signifie que le conteneur peut utiliser un maximum de 200 ms de temps CPU sans limitation.
Et c'est lĂ que le plaisir commence. Comme mentionnĂ© ci-dessus, le quota disponible est de 200 ms. Si vous travaillez en parallĂšle dix threads sur une machine Ă 12 cĆurs (voir 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 se bloqueront » (Manette de Gaz) pour les 80 prochaines ms. Le dĂ©jĂ mentionnĂ© , Ă cause de quoi une limitation excessive se produit et le conteneur ne peut mĂȘme pas remplir le quota existant.
Comment évaluer la limitation dans les pods ?
Connectez-vous simplement au pod et exécutez cat /sys/fs/cgroup/cpu/cpu.stat.
-
nr_periodsâ le nombre total de pĂ©riodes de planification ; -
nr_throttledâ nombre de pĂ©riodes limitĂ©es dans la compositionnr_periods; -
throttled_timeâ temps de limitation cumulĂ© en nanosecondes.

Que se passe-t-il réellement ?
En conséquence, nous obtenons une limitation élevée dans toutes les applications. Parfois, il est dans une fois et demie plus fort que prévu !
Cela entraĂźne diverses erreurs : Ă©checs du contrĂŽle de prĂ©paration, gels des conteneurs, ruptures de connexion rĂ©seau, dĂ©lais d'attente lors des appels de service. Cela se traduit finalement par une latence accrue et des taux dâerreur plus Ă©levĂ©s.
Décision et conséquences
Tout est simple ici. Nous avons abandonné les limites du processeur et commencé à mettre à jour le noyau du systÚme d'exploitation en cluster vers la derniÚre version, dans laquelle le bug a été corrigé. Le nombre d'erreurs (HTTP 5xx) dans nos services a immédiatement diminué de maniÚre significative :
Erreurs HTTP 5xx

Erreurs HTTP 5xx pour un service critique
Temps de réponse p95

Latence des demandes de service critiques, 95e percentile
Les coûts d'exploitation

Nombre d'heures d'instance passées
Quel est le piĂšge?
Comme indiqué au début de l'article :
Une analogie peut ĂȘtre faite avec un appartement communal... Kubernetes agit en tant qu'agent immobilier. Mais comment Ă©viter les conflits entre locataires ? Et si lâun dâeux, par exemple, dĂ©cide dâemprunter la salle de bain pour une demi-journĂ©e ?
Voici le piÚge. Un conteneur imprudent peut consommer toutes les ressources CPU disponibles sur une machine. Si vous disposez d'une pile d'applications intelligente (par exemple, JVM, Go, Node VM sont correctement configurés), alors ce n'est pas un problÚme : vous pouvez travailler longtemps dans de telles conditions. Mais si les applications sont mal optimisées ou pas optimisées du tout (FROM java:latest), la situation pourrait devenir incontrÎlable. Chez Omio, nous avons des Dockerfiles de base automatisés avec des paramÚtres par défaut adéquats pour la principale pile de langues, ce problÚme n'existait donc pas.
Nous vous recommandons de surveiller les métriques (utilisation, saturation et erreurs), délais API et taux d'erreur. S'assurer que les résultats répondent aux attentes.
références
C'est notre histoire. Les documents suivants ont grandement aidé à comprendre ce qui se passait :
- ;
- ;
- ;
- ;
- - recherchez « limitation du processeur ».
Rapports de bogues Kubernetes :
- ;
- ;
- .
Avez-vous rencontré des problÚmes similaires dans votre pratique ou avez-vous une expérience liée à la limitation dans des environnements de production conteneurisés ? Partagez votre histoire dans les commentaires !
PS du traducteur
A lire aussi sur notre blog :
- «";
- «";
- «».
Source: habr.com
