Skip to main content
BlogConteneurs (Kubernetes, Docker)Mise à l'échelle proactive pour les clusters Kubernetes

Mise à l'échelle proactive pour les clusters Kubernetes

Mise à l'échelle proactive pour les clusters Kubernetes

Cet article fait partie de notre série sur la mise à l'échelle de Kubernetes. Inscrivez-vous sur pour regarder en direct ou accéder à l'enregistrement, et consultez les autres articles de cette série :

Lorsque votre cluster manque de ressources, le Cluster Autoscaler provisionne un nouveau nœud et l'ajoute au cluster. Si vous êtes déjà un utilisateur de Kubernetes, vous avez peut-être remarqué que la création et l'ajout d'un nœud au cluster prennent plusieurs minutes.

Pendant cette période, votre application peut facilement être submergée par les connexions car elle ne peut plus évoluer.

Capture d'écran montrant la mise à l'échelle prévue sur la base des demandes par seconde (RPS) par rapport au plateau de mise à l'échelle réel qui se produit en s'appuyant uniquement sur le Cluster Autoscaler.
Le provisionnement d'une machine virtuelle peut prendre plusieurs minutes. Pendant ce temps, il se peut que vous ne puissiez pas faire évoluer vos applications.

Comment pouvez-vous remédier à la longue période d'attente ?

Mise à l'échelle proactive, ou : 

  • comprendre le fonctionnement de l'autoscaler de cluster et maximiser son utilité ;
  • utiliser le planificateur Kubernetes pour affecter des pods à un nœud ; et
  • le provisionnement proactif des nœuds de travail pour éviter une mauvaise mise à l'échelle.

Si vous préférez lire le code de ce tutoriel, vous pouvez le trouver sur le GitHub de LearnK8s.

Fonctionnement du Cluster Autoscaler dans Kubernetes

L'Autoscaler de cluster ne tient pas compte de la disponibilité de la mémoire ou du CPU lorsqu'il déclenche l'autoscaling. Au lieu de cela, le Cluster Autoscaler réagit aux événements et vérifie s'il existe des pods non planifiables. Un pod n'est pas planifiable lorsque le planificateur ne trouve pas de nœud pouvant l'accueillir.

Testons cela en créant un cluster.

bash
$ linode-cli lke cluster-create \
 --label learnk8s \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 1 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig

Vous devez prêter attention aux détails suivants :

  • chaque nœud a 4GB de mémoire et 2 vCPU (i.e. `g6-standard-2`) ;
  • il n'y a qu'un seul nœud dans la grappe ; et
  • le cluster autoscaler est configuré pour passer de 1 à 10 nœuds.

Vous pouvez vérifier que l'installation est réussie avec :

bash
$ kubectl get pods -A --kubeconfig=kubeconfig

L'exportation du fichier kubeconfig avec une variable d'environnement est généralement plus pratique.

Vous pouvez le faire avec :

bash
$ export KUBECONFIG=${PWD}/kubeconfig
$ kubectl get pods

Excellent !

Déploiement d'une application
Déployons une application qui nécessite 1 Go de mémoire et 250 m* de CPU.
Note: m = thousandth of a core, so 250m = 25% of the CPU

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 1
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 1G
             cpu: 250m

Vous pouvez soumettre la ressource au cluster avec :

bash
$ kubectl apply -f podinfo.yaml

Dès que vous faites cela, vous pouvez remarquer quelques choses. Tout d'abord, trois pods sont presque immédiatement en cours d'exécution, et un est en attente.

Diagramme montrant trois pods actifs sur un nœud, et un pod en attente en dehors de ce nœud.

Et puis :

  • après quelques minutes, l'autoscaler crée un nœud supplémentaire ; et
  • le quatrième pod est déployé dans le nouveau nœud.
Diagramme montrant trois pods sur un nœud, et le quatrième pod déployé dans un nouveau nœud.
Finalement, le quatrième pod est déployé dans un nouveau nœud.

Pourquoi le quatrième pod n'est-il pas déployé dans le premier nœud ? Creusons dans les ressources allouables.

Ressources allouables dans les nœuds de Kubernetes

Les pods déployés dans votre cluster Kubernetes consomment des ressources de mémoire, de CPU et de stockage.

Cependant, sur un même nœud, le système d'exploitation et le kubelet ont besoin de mémoire et de CPU.

Dans un nœud de travail Kubernetes, la mémoire et le processeur sont divisés en deux parties :

  1. Ressources nécessaires à l'exécution du système d'exploitation et des démons du système tels que SSH, systemd, etc.
  2. Ressources nécessaires à l'exécution des agents Kubernetes, telles que le Kubelet, le runtime de conteneurs, le détecteur de problèmes de nœuds, etc.
  3. Ressources disponibles pour les pods.
  4. Ressources réservées pour le seuil d'éviction.
Ressources allouées et réservées dans un nœud Kubernetes, comprenant 1. le seuil d'éviction ; 2. la mémoire et le CPU laissés aux pods ; 3. la mémoire et le CPU réservés au kubelet ; 4. la mémoire et le CPU réservés à l'OS.
Ressources allouées et réservées dans un nœud Kubernetes.

Si votre cluster exécute un DaemonSet tel que kube-proxy, vous devez réduire davantage la mémoire et le CPU disponibles.

Réduisons donc les exigences pour nous assurer que tous les pods peuvent tenir sur un seul nœud :

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
   spec:
     containers:
       - name: podinfo
         image: stefanprodan/podinfo
         ports:
           - containerPort: 9898
         resources:
           requests:
             memory: 0.8G # <- lower memory
             cpu: 200m    # <- lower CPU

Vous pouvez modifier le déploiement avec :

bash
$ kubectl apply -f podinfo.yaml

Choisir la bonne quantité de CPU et de mémoire pour optimiser vos instances peut s'avérer délicat. La calculatrice de l'outil Learnk8s pourrait vous aider à le faire plus rapidement.

Vous avez résolu un problème, mais qu'en est-il du temps nécessaire à la création d'un nouveau nœud ?

Tôt ou tard, vous aurez plus de quatre répliques. Devez-vous vraiment attendre quelques minutes avant que les nouveaux pods soient créés ?

La réponse courte est oui.

Linode doit créer une machine virtuelle à partir de zéro, la provisionner et la connecter au cluster. Ce processus peut facilement prendre plus de deux minutes.

Mais il y a une alternative.

Vous pourriez créer de manière proactive des nœuds déjà provisionnés lorsque vous en avez besoin.

Par exemple, vous pouvez configurer l'autoscaler pour qu'il ait toujours un nœud de rechange. Lorsque les pods sont déployés sur le nœud de réserve, l'autoscaler peut en créer d'autres de manière proactive. Malheureusement, l'autoscaler ne dispose pas de cette fonctionnalité intégrée, mais vous pouvez facilement la recréer.

Vous pouvez créer un pod qui a des demandes égales à la ressource du nœud :

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

Vous pouvez soumettre la ressource au cluster avec :

bash
kubectl apply -f placeholder.yaml

Cette capsule ne fait absolument rien.

Diagramme montrant comment un pod de remplacement est utilisé pour sécuriser toutes les ressources sur le nœud.
Un pod de remplacement est utilisé pour sécuriser toutes les ressources sur le nœud.

Ça permet juste de garder le nœud pleinement occupé.

L'étape suivante consiste à s'assurer que le pod de remplacement est expulsé dès qu'il y a une charge de travail qui nécessite une mise à l'échelle.

Pour cela, vous pouvez utiliser une classe prioritaire.

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning # <--
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.8G

Et le resoumettre au cluster avec :

bash
kubectl apply -f placeholder.yaml

La configuration est maintenant terminée.

Vous devrez peut-être attendre un peu que l'autoscaler crée le nœud, mais à ce stade, vous devriez avoir deux nœuds :

  1. Un nœud avec quatre pods.
  2. Un autre avec un pod de remplacement.

Que se passe-t-il lorsque vous faites passer le déploiement à 5 répliques ? Devrez-vous attendre que l'autoscaler crée un nouveau nœud ?

Testons avec :

bash
kubectl scale deployment/podinfo --replicas=5

Vous devriez observer :

  1. La cinquième nacelle est créée immédiatement et se trouve en état de fonctionnement en moins de 10 secondes.
  2. Le pod de remplacement a été expulsé pour faire de la place pour le pod.
Diagramme montrant comment le pod de remplacement est expulsé pour faire de la place aux pods réguliers.
Le pod de remplacement est expulsé pour faire de la place aux pods réguliers.

Et puis :

  1. Le cluster autoscaler a remarqué le pod en attente et a provisionné un nouveau nœud.
  2. Le pod placeholder est déployé dans le nœud nouvellement créé.
Diagramme montrant comment le pod en attente déclenche l'autoscaler du cluster qui crée un nouveau nœud.
Le pod en attente déclenche l'autoscaler du cluster qui crée un nouveau nœud.

Pourquoi créer proactivement un seul nœud alors que vous pourriez en avoir plusieurs ?

Vous pouvez faire évoluer le pod placeholder vers plusieurs répliques. Chaque réplique préprovisionnera un nœud Kubernetes prêt à accepter des charges de travail standard. Cependant, ces nœuds sont toujours comptabilisés dans votre facture de cloud computing, mais restent inactifs et ne font rien. Vous devez donc être prudent et ne pas en créer un trop grand nombre.

Combinaison de l'Autoscaler de cluster avec l'Autoscaler de pods horizontaux

Pour comprendre l'implication de cette technique, combinons le cluster autoscaler avec le Horizontal Pod Autoscaler (HPA). Le HPA est conçu pour augmenter le nombre de répliques dans vos déploiements.

Lorsque votre application reçoit davantage de trafic, vous pouvez demander à l'autoscaler d'ajuster le nombre de répliques pour traiter davantage de demandes.

Lorsque les pods épuisent toutes les ressources disponibles, l'autoscaler du cluster déclenche la création d'un nouveau nœud afin que le HPA puisse continuer à créer davantage de répliques.

Testons cela en créant un nouveau cluster :

bash
$ linode-cli lke cluster-create \
 --label learnk8s-hpa \
 --region eu-west \
 --k8s_version 1.23 \
 --node_pools.count 1 \
 --node_pools.type g6-standard-2 \
 --node_pools.autoscaler.enabled enabled \
 --node_pools.autoscaler.max 10 \
 --node_pools.autoscaler.min 3 \
 
$ linode-cli lke kubeconfig-view "insert cluster id here" --text | tail +2 | base64 -d > kubeconfig-hpa

Vous pouvez vérifier que l'installation est réussie avec :

bash
$ kubectl get pods -A --kubeconfig=kubeconfig-hpa

L'exportation du fichier kubeconfig avec une variable d'environnement est plus pratique.

Vous pouvez le faire avec :

bash
$ export KUBECONFIG=${PWD}/kubeconfig-hpa
$ kubectl get pods

Excellent !

Utilisons Helm pour installer Prometheus et extraire les métriques des déploiements.
Vous pouvez trouver les instructions d'installation de Helm sur leur site officiel.

bash
$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm install prometheus prometheus-community/prometheus

Kubernetes offre au HPA un contrôleur permettant d'augmenter et de diminuer les répliques de manière dynamique.

Malheureusement, l'APH présente quelques inconvénients :

  1. Il ne fonctionne pas tout de suite. Vous devez installer un serveur de métriques pour agréger et exposer les métriques.
  2. Vous ne pouvez pas utiliser les requêtes PromQL dès le départ.

Heureusement, vous pouvez utiliser KEDA, qui étend le contrôleur HPA avec quelques fonctionnalités supplémentaires (y compris la lecture des métriques à partir de Prometheus).

KEDA est un autoscaler composé de trois éléments :

  • Un détartreur
  • Un adaptateur de métrique
  • Un contrôleur
Diagramme montrant l'architecture de la KEDA
Architecture KEDA.

Vous pouvez installer KEDA avec Helm :

bash
$ helm repo add kedacore https://kedacore.github.io/charts
$ helm install keda kedacore/keda

Maintenant que Prometheus et KEDA sont installés, créons un déploiement.

Pour cette expérience, vous utiliserez une application conçue pour gérer un nombre fixe de demandes par seconde. 

Chaque pod peut traiter au maximum dix demandes par seconde. Si le pod reçoit la 11e demande, il la laissera en attente et la traitera plus tard.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: podinfo
spec:
 replicas: 4
 selector:
   matchLabels:
     app: podinfo
 template:
   metadata:
     labels:
       app: podinfo
     annotations:
       prometheus.io/scrape: "true"
   spec:
     containers:
       - name: podinfo
         image: learnk8s/rate-limiter:1.0.0
         imagePullPolicy: Always
         args: ["/app/index.js", "10"]
         ports:
           - containerPort: 8080
         resources:
           requests:
             memory: 0.9G
---
apiVersion: v1
kind: Service
metadata:
 name: podinfo
spec:
 ports:
   - port: 80
     targetPort: 8080
 selector:
   app: podinfo

Vous pouvez soumettre la ressource au cluster avec :

bash
$ kubectl apply -f rate-limiter.yaml

Pour générer du trafic, vous utiliserez Locust.

La définition YAML suivante crée un cluster de test de charge distribué :

yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: locust-script
data:
 locustfile.py: |-
   from locust import HttpUser, task, between
 
   class QuickstartUser(HttpUser):
       @task
       def hello_world(self):
           self.client.get("/", headers={"Host": "example.com"})
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-primary
 template:
   metadata:
     labels:
       app: locust-primary
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--master"]
         ports:
           - containerPort: 5557
             name: comm
           - containerPort: 5558
             name: comm-plus-1
           - containerPort: 8089
             name: web-ui
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script
---
apiVersion: v1
kind: Service
metadata:
 name: locust
spec:
 ports:
   - port: 5557
     name: communication
   - port: 5558
     name: communication-plus-1
   - port: 80
     targetPort: 8089
     name: web-ui
 selector:
   app: locust-primary
 type: LoadBalancer
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
 name: locust
spec:
 selector:
   matchLabels:
     app: locust-worker
 template:
   metadata:
     labels:
       app: locust-worker
   spec:
     containers:
       - name: locust
         image: locustio/locust
         args: ["--worker", "--master-host=locust"]
         volumeMounts:
           - mountPath: /home/locust
             name: locust-script
     volumes:
       - name: locust-script
         configMap:
           name: locust-script

Vous pouvez le soumettre au cluster avec :

bash
$ kubectl locust.yaml

Locust lit ce qui suit locustfile.pyqui est stocké dans une ConfigMap :

py
from locust import HttpUser, task, between
 
class QuickstartUser(HttpUser):
 
   @task
   def hello_world(self):
       self.client.get("/")

Le fichier ne fait rien de spécial, si ce n'est d'effectuer une requête vers une URL. Pour vous connecter au tableau de bord de Locust, vous avez besoin de l'adresse IP de son équilibreur de charge.

Vous pouvez le récupérer avec la commande suivante :

bash
$ kubectl get service locust -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

Ouvrez votre navigateur et entrez cette adresse IP.

Excellent !

Il manque une pièce : l'Autoscaler horizontal du pod.
L'autoscaler KEDA enveloppe l'Autoscaler horizontal avec un objet spécifique appelé ScaledObject.

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: podinfo
spec:
scaleTargetRef:
  kind: Deployment
  name: podinfo
minReplicaCount: 1
maxReplicaCount: 30
cooldownPeriod: 30
pollingInterval: 1
triggers:
- type: prometheus
  metadata:
    serverAddress: http://prometheus-server
    metricName: connections_active_keda
    query: |
      sum(increase(http_requests_total{app="podinfo"}[60s]))
    threshold: "480" # 8rps * 60s

KEDA établit un pont entre les métriques collectées par Prometheus et les transmet à Kubernetes.

Enfin, il crée un pod autoscaler horizontal (HPA) avec ces paramètres.

Vous pouvez inspecter manuellement le HPA avec :

bash
$ kubectl get hpa
$ kubectl describe hpa keda-hpa-podinfo

Vous pouvez soumettre l'objet avec :

bash
$ kubectl apply -f scaled-object.yaml

Il est temps de tester si la mise à l'échelle fonctionne.

Dans le tableau de bord de Locust, lancez une expérience avec les paramètres suivants :

  • Nombre d'utilisateurs : 300
  • Taux de reproduction : 0.4
  • Hôte : http://podinfo
Gif de l'enregistrement d'écran qui démontre la mise à l'échelle avec des pods en attente en utilisant autoscaler.
Combinaison du cluster et du pod autoscaler horizontal.

Le nombre de répliques augmente !

Excellent ! Mais avez-vous remarqué ?

Lorsque le déploiement passe à 8 pods, il faut attendre quelques minutes avant que d'autres pods soient créés sur le nouveau nœud.

Pendant cette période, les demandes par seconde stagnent car les huit répliques actuelles ne peuvent traiter que dix demandes chacune.

Réduisons l'échelle et répétons l'expérience :

bash
kubectl scale deployment/podinfo --replicas=4 # or wait for the autoscaler to remove pods

Cette fois, nous allons surprovisionner le nœud avec le pod de remplacement :

yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
 name: overprovisioning
value: -1
globalDefault: false
description: "Priority class used by overprovisioning."
---
apiVersion: apps/v1
kind: Deployment
metadata:
 name: overprovisioning
spec:
 replicas: 1
 selector:
   matchLabels:
     run: overprovisioning
 template:
   metadata:
     labels:
       run: overprovisioning
   spec:
     priorityClassName: overprovisioning
     containers:
       - name: pause
         image: k8s.gcr.io/pause
         resources:
           requests:
             cpu: 900m
             memory: 3.9G

Vous pouvez le soumettre au cluster avec :

bash
kubectl apply -f placeholder.yaml

Ouvrez le tableau de bord de Locust et répétez l'expérience avec les paramètres suivants :

  • Nombre d'utilisateurs : 300
  • Taux de reproduction : 0.4
  • Hôte : http://podinfo
Gif
Combinaison du cluster et du pod autoscaler horizontal avec overprovisioning.

Cette fois, de nouveaux nœuds sont créés en arrière-plan et les demandes par seconde augmentent sans stagner. Excellent travail !

Récapitulons ce que vous avez appris dans ce post :

  • le cluster autoscaler ne suit pas la consommation de CPU ou de mémoire. Il surveille plutôt les pods en attente ;
  • vous pouvez créer un pod qui utilise la totalité de la mémoire et du CPU disponibles pour provisionner un nœud Kubernetes de manière proactive ;
  • Les nœuds Kubernetes disposent de ressources réservées pour les kubelets, le système d'exploitation et le seuil d'éviction ; et
  • vous pouvez combiner Prometheus avec KEDA pour faire évoluer votre pod avec une requête PromQL.

Vous souhaitez suivre notre série de webinaires sur la mise à l'échelle de Kubernetes ? Inscrivez-vous pour commencer et découvrez comment utiliser KEDA pour mettre à l'échelle les clusters Kubernetes.


Commentaires

Laissez un commentaire

Votre adresse électronique ne sera pas publiée. Les champs obligatoires sont marqués d'un *.