ProHoster > Blog > administration > Kubernetes : pourquoi est-il si important de mettre en place une gestion des ressources système ?
Kubernetes : pourquoi est-il si important de mettre en place une gestion des ressources système ?
En règle générale, il est toujours nécessaire de fournir un pool de ressources dédié à une application pour son fonctionnement correct et stable. Mais que se passe-t-il si plusieurs applications fonctionnent avec la même puissance ? Comment fournir à chacun d’entre eux le minimum de ressources nécessaires ? Comment limiter la consommation des ressources ? Comment répartir correctement la charge entre les nœuds ? Comment garantir le fonctionnement du mécanisme de mise à l’échelle horizontale si la charge de l’application augmente ?
Vous devez commencer par les principaux types de ressources qui existent dans le système - il s'agit bien sûr du temps processeur et de la RAM. Dans les manifestes k8, ces types de ressources sont mesurés dans les unités suivantes :
CPU - en cœurs
RAM - en octets
De plus, pour chaque ressource, il est possible de définir deux types d'exigences : demandes и limites. Requêtes - décrit les exigences minimales en matière de ressources libres d'un nœud pour exécuter un conteneur (et un pod dans son ensemble), tandis que les limites fixent une limite stricte aux ressources disponibles pour le conteneur.
Il est important de comprendre que le manifeste ne doit pas nécessairement définir explicitement les deux types, mais le comportement sera le suivant :
Si seules les limites d'une ressource sont explicitement spécifiées, alors les requêtes pour cette ressource prennent automatiquement une valeur égale aux limites (vous pouvez le vérifier en appelant des entités descriptives). Ceux. en fait, le conteneur sera limité à la même quantité de ressources dont il a besoin pour fonctionner.
Si seules les demandes sont explicitement spécifiées pour une ressource, alors aucune restriction supérieure n'est définie sur cette ressource - c'est-à-dire le conteneur n'est limité que par les ressources du nœud lui-même.
Il est également possible de configurer la gestion des ressources non seulement au niveau d'un conteneur spécifique, mais également au niveau de l'espace de noms en utilisant les entités suivantes :
LimitRange — décrit la politique de restriction au niveau du conteneur/pod en ns et est nécessaire pour décrire les limites par défaut sur le conteneur/pod, ainsi qu'empêcher la création de conteneurs/pods manifestement gros (ou vice versa), limiter leur nombre et déterminer la différence possible dans les valeurs des limites et des demandes
Quotas de ressources — décrit la politique de restriction en général pour tous les conteneurs dans ns et est utilisée, en règle générale, pour délimiter les ressources entre les environnements (utile lorsque les environnements ne sont pas strictement délimités au niveau des nœuds)
Voici des exemples de manifestes qui définissent des limites de ressources :
Ceux. dans ce cas, pour exécuter un conteneur avec nginx, vous aurez besoin d'au moins 1 Go de RAM libre et de 0.2 CPU sur le nœud, alors qu'au maximum le conteneur peut consommer 0.2 CPU et toute la RAM disponible sur le nœud.
Ceux. la somme de tous les conteneurs de requêtes dans le ns par défaut ne peut pas dépasser 300 mo pour le CPU et 1G pour l'OP, et la somme de toutes les limites est de 700 mo pour le CPU et 2G pour l'OP.
Ceux. dans l'espace de noms par défaut pour tous les conteneurs, la demande sera définie sur 100 m pour le CPU et 1G pour l'OP, limite : 1 CPU et 2G. Parallèlement, une limite est également fixée sur les valeurs possibles en requête/limite pour le CPU (50m < x < 2) et la RAM (500M < x < 4G).
Ceux. pour chaque pod dans le ns par défaut, il y aura une limite de 4 vCPU et 1G.
Je voudrais maintenant vous dire quels avantages l'imposition de ces restrictions peut nous apporter.
Mécanisme d'équilibrage de charge entre les nœuds
Comme vous le savez, le composant k8s est responsable de la répartition des pods entre les nœuds, tels que ordonnanceur, qui fonctionne selon un algorithme spécifique. Cet algorithme passe par deux étapes lors de la sélection du nœud optimal à lancer :
filtration
Variant
Ceux. selon la politique décrite, les nœuds sont initialement sélectionnés sur lesquels il est possible de lancer un pod en fonction d'un ensemble prédicats (y compris vérifier si le nœud dispose de suffisamment de ressources pour exécuter le pod - PodFitsResources), puis pour chacun de ces nœuds, selon priorités des points sont attribués (y compris, plus un nœud possède de ressources libres, plus il est attribué de points - LeastResourceAllocation/LeastRequestedPriority/BalancedResourceAllocation) et le pod est lancé sur le nœud avec le plus de points (si plusieurs nœuds satisfont à cette condition à la fois, alors un aléatoire est sélectionné).
Dans le même temps, vous devez comprendre que le planificateur, lorsqu'il évalue les ressources disponibles d'un nœud, est guidé par les données stockées dans etcd - c'est-à-dire pour la quantité de ressource demandée/limitée de chaque pod exécuté sur ce nœud, mais pas pour la consommation réelle de ressources. Ces informations peuvent être obtenues à partir de la sortie de la commande kubectl describe node $NODE, Par exemple:
Ici, nous voyons tous les pods exécutés sur un nœud spécifique, ainsi que les ressources demandées par chaque pod. Et voici à quoi ressemblent les journaux du planificateur lorsque le pod cronjob-cron-events-1573793820-xt6q9 est lancé (ces informations apparaîtront dans le journal du planificateur lorsque vous définirez le 10ème niveau de journalisation dans les arguments de la commande de démarrage -v=10) :
se connecter
I1115 07:57:21.637791 1 scheduling_queue.go:908] About to try and schedule pod nxs-stage/cronjob-cron-events-1573793820-xt6q9
I1115 07:57:21.637804 1 scheduler.go:453] Attempting to schedule pod: nxs-stage/cronjob-cron-events-1573793820-xt6q9
I1115 07:57:21.638285 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s5 is allowed, Node is running only 16 out of 110 Pods.
I1115 07:57:21.638300 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s6 is allowed, Node is running only 20 out of 110 Pods.
I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s3 is allowed, Node is running only 20 out of 110 Pods.
I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s4 is allowed, Node is running only 17 out of 110 Pods.
I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, Node is running only 16 out of 110 Pods.
I1115 07:57:21.638365 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s12 is allowed, Node is running only 9 out of 110 Pods.
I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s11 is allowed, Node is running only 11 out of 110 Pods.
I1115 07:57:21.638385 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s1 is allowed, Node is running only 19 out of 110 Pods.
I1115 07:57:21.638402 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s2 is allowed, Node is running only 21 out of 110 Pods.
I1115 07:57:21.638383 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, Node is running only 16 out of 110 Pods.
I1115 07:57:21.638335 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, Node is running only 18 out of 110 Pods.
I1115 07:57:21.638408 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s13 is allowed, Node is running only 8 out of 110 Pods.
I1115 07:57:21.638478 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, existing pods anti-affinity terms satisfied.
I1115 07:57:21.638505 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, existing pods anti-affinity terms satisfied.
I1115 07:57:21.638577 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, existing pods anti-affinity terms satisfied.
I1115 07:57:21.638583 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s7 is allowed, Node is running only 25 out of 110 Pods.
I1115 07:57:21.638932 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 9
I1115 07:57:21.638946 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 8
I1115 07:57:21.638961 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: BalancedResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 9
I1115 07:57:21.638971 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7
I1115 07:57:21.638975 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: LeastResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 8
I1115 07:57:21.638990 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7
I1115 07:57:21.639022 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: TaintTolerationPriority, Score: (10)
I1115 07:57:21.639030 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: TaintTolerationPriority, Score: (10)
I1115 07:57:21.639034 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: TaintTolerationPriority, Score: (10)
I1115 07:57:21.639041 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: NodeAffinityPriority, Score: (0)
I1115 07:57:21.639053 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: NodeAffinityPriority, Score: (0)
I1115 07:57:21.639059 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: NodeAffinityPriority, Score: (0)
I1115 07:57:21.639061 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: InterPodAffinityPriority, Score: (0)
I1115 07:57:21.639063 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639073 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: InterPodAffinityPriority, Score: (0)
I1115 07:57:21.639077 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639085 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: InterPodAffinityPriority, Score: (0)
I1115 07:57:21.639088 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639103 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639109 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639114 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10)
I1115 07:57:21.639127 1 generic_scheduler.go:781] Host nxs-k8s-s10 => Score 100037
I1115 07:57:21.639150 1 generic_scheduler.go:781] Host nxs-k8s-s8 => Score 100034
I1115 07:57:21.639154 1 generic_scheduler.go:781] Host nxs-k8s-s9 => Score 100037
I1115 07:57:21.639267 1 scheduler_binder.go:269] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10"
I1115 07:57:21.639286 1 scheduler_binder.go:279] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10": all PVCs bound and nothing to do
I1115 07:57:21.639333 1 factory.go:733] Attempting to bind cronjob-cron-events-1573793820-xt6q9 to nxs-k8s-s10
On voit ici qu'au départ le planificateur filtre et génère une liste de 3 nœuds sur lesquels il peut être lancé (nxs-k8s-s8, nxs-k8s-s9, nxs-k8s-s10). Ensuite, il calcule des scores en fonction de plusieurs paramètres (dont BalancedResourceAllocation, LeastResourceAllocation) pour chacun de ces nœuds afin de déterminer le nœud le plus adapté. En fin de compte, le pod est planifié sur le nœud avec le plus grand nombre de points (ici deux nœuds à la fois ont le même nombre de points 100037, donc un nœud aléatoire est sélectionné - nxs-k8s-s10).
conclusion: si un nœud exécute des pods pour lesquels aucune restriction n'est définie, alors pour les k8 (du point de vue de la consommation des ressources), cela équivaudra à comme s'il n'y avait aucun pod de ce type sur ce nœud. Par conséquent, si vous disposez, conditionnellement, d'un pod avec un processus glouton (par exemple, wowza) et qu'aucune restriction n'est définie pour celui-ci, alors une situation peut survenir lorsque ce pod a réellement mangé toutes les ressources du nœud, mais pour k8s, ce nœud est considéré comme déchargé et il recevra le même nombre de points lors du classement (précisément en points évaluant les ressources disponibles) qu'un nœud qui ne dispose pas de pods fonctionnels, ce qui peut finalement conduire à une répartition inégale de la charge entre les nœuds.
L'expulsion de Pod
Comme vous le savez, chaque pod se voit attribuer l'une des 3 classes QoS :
garanti - est attribué lorsque pour chaque conteneur du pod une demande et une limite sont spécifiées pour la mémoire et le processeur, et ces valeurs doivent correspondre
éclatable — au moins un conteneur dans le pod a une requête et une limite, avec request < limit
meilleur effort - lorsqu'aucun conteneur du pod n'est limité en ressources
Parallèlement, lorsqu'un nœud rencontre un manque de ressources (disque, mémoire), kubelet commence à classer et expulser les pods selon un algorithme spécifique qui prend en compte la priorité du pod et sa classe QoS. Par exemple, si nous parlons de RAM, alors en fonction de la classe QoS, les points sont attribués selon le principe suivant :
Ceux. avec la même priorité, le kubelet expulsera d'abord du nœud les pods avec la classe QoS de meilleur effort.
conclusion: si vous souhaitez réduire la probabilité que le pod souhaité soit expulsé du nœud en cas de manque de ressources sur celui-ci, alors en plus de la priorité, vous devez également prendre soin de définir la demande/limite pour celui-ci.
Mécanisme d'autoscaling horizontal des pods d'application (HPA)
Lorsque la tâche consiste à augmenter et diminuer automatiquement le nombre de pods en fonction de l'utilisation des ressources (système - CPU/RAM ou utilisateur - rps), une entité k8s telle que HPA (Autoscaler horizontal de pods). Dont l'algorithme est le suivant :
Les lectures actuelles de la ressource observée sont déterminées (currentMetricValue)
Les valeurs souhaitées pour la ressource sont déterminées (desiredMetricValue), qui pour les ressources système sont définies à l'aide d'une requête
Le nombre actuel de répliques est déterminé (currentReplicas)
La formule suivante calcule le nombre souhaité de répliques (desiredReplicas)
wantedReplicas = [ currentReplicas * ( currentMetricValue / wantedMetricValue )]
Dans ce cas, la mise à l'échelle n'aura pas lieu lorsque le coefficient (currentMetricValue / wantedMetricValue) est proche de 1 (dans ce cas, nous pouvons définir nous-mêmes l'erreur tolérée ; par défaut elle est de 0.1).
Regardons le fonctionnement de hpa en utilisant l'exemple de l'application app-test (décrite comme Déploiement), où il est nécessaire de modifier le nombre de répliques en fonction de la consommation CPU :
Ceux. nous voyons que le pod d'application est initialement lancé dans deux instances, dont chacune contient deux conteneurs nginx et nginx-exporter, pour chacun desquels un spécifié demandes pour le processeur.
Ceux. Nous avons créé un hpa qui surveillera le test de l'application de déploiement et ajustera le nombre de pods avec l'application en fonction de l'indicateur du processeur (nous nous attendons à ce que le pod consomme 30 % du processeur qu'il demande), le nombre de répliques étant en la plage de 2 à 10.
Voyons maintenant le mécanisme de fonctionnement du hpa si l'on applique une charge sur l'un des foyers :
# kubectl top pod
NAME CPU(cores) MEMORY(bytes)
app-test-78559f8f44-pgs58 101m 243Mi
app-test-78559f8f44-cj4jz 4m 240Mi
Au total, nous avons les éléments suivants :
La valeur souhaitée (desiredMetricValue) - selon les paramètres hpa, nous avons 30%
Valeur actuelle (currentMetricValue) - pour le calcul, le contrôleur-gestionnaire calcule la valeur moyenne de la consommation des ressources en %, c'est-à-dire conditionnellement, il effectue ce qui suit :
Reçoit les valeurs absolues des métriques du pod du serveur de métriques, c'est-à-dire 101m et 4m
Obtient la valeur absolue de la consommation de ressources souhaitée (pour cela, les demandes de tous les conteneurs sont résumées) 60m + 30m = 90m
Calcule le pourcentage moyen de consommation CPU par rapport au pod de requête, c'est-à-dire 53m / 90m * 100% = 59%
Nous avons maintenant tout ce qu'il faut pour déterminer s'il faut modifier le nombre de répliques ; pour cela, nous calculons le coefficient :
ratio = 59% / 30% = 1.96
Ceux. le nombre de répliques doit être augmenté d'environ 2 fois et s'élever à [2 * 1.96] = 4.
Conclusion: Comme vous pouvez le constater, pour que ce mécanisme fonctionne, une condition nécessaire est la présence de requêtes pour tous les conteneurs dans le pod observé.
Mécanisme d'autoscaling horizontal des nœuds (Cluster Autoscaler)
Afin de neutraliser l'impact négatif sur le système lors des surtensions de charge, disposer d'un hpa configuré ne suffit pas. Par exemple, selon les paramètres du gestionnaire de contrôleur hpa, il décide que le nombre de répliques doit être augmenté de 2 fois, mais les nœuds ne disposent pas de ressources libres pour exécuter un tel nombre de pods (c'est-à-dire que le nœud ne peut pas fournir le ressources demandées au pod de requêtes) et ces pods passent à l'état En attente.
Dans ce cas, si le fournisseur dispose d'un IaaS/PaaS correspondant (par exemple, GKE/GCE, AKS, EKS, etc.), un outil comme Mise à l'échelle automatique des nœuds. Il vous permet de définir le nombre maximum et minimum de nœuds dans le cluster et d'ajuster automatiquement le nombre actuel de nœuds (en appelant l'API du fournisseur de cloud pour commander/supprimer un nœud) en cas de manque de ressources dans le cluster et les pods. ne peuvent pas être planifiés (sont à l’état En attente).
Conclusion: Pour pouvoir mettre à l'échelle automatiquement les nœuds, il est nécessaire de définir des requêtes dans les conteneurs de pods afin que les k8 puissent évaluer correctement la charge sur les nœuds et signaler par conséquent qu'il n'y a pas de ressources dans le cluster pour lancer le pod suivant.
Conclusion
Il convient de noter que la définition de limites de ressources de conteneur n'est pas une condition nécessaire au bon fonctionnement de l'application, mais il est néanmoins préférable de le faire pour les raisons suivantes :
Pour un fonctionnement plus précis du planificateur en termes d'équilibrage de charge entre les nœuds k8s
Pour réduire la probabilité qu’un événement d’« expulsion de pod » se produise
Pour que la mise à l'échelle automatique horizontale des modules d'application (HPA) fonctionne
Pour la mise à l'échelle automatique horizontale des nœuds (Cluster Autoscaling) pour les fournisseurs de cloud