Aller au contenu
English
Cloud medium

Pools Kapsule et autoscaling : quand la machine ajoute des nœuds, et quand elle s'arrête

Mesuré live le ·fr-par·scw 2.62.0

70 min de lecture

logo Scaleway

L'autoscaler de Kapsule a mis 175 secondes à livrer les nœuds manquants, puis il s'est arrêté net à son maximum en laissant deux pods bloqués pour toujours. Cette leçon crée un pool autoscalé, y soumet une charge calibrée pour ne pas tenir, et mesure chaque étape. Vous lirez la séquence complète des messages du planificateur, qui raconte la montée bien mieux qu'une capture finale, et vous reconnaîtrez le blocage à max-size : il ne produit aucune erreur, aucune alerte, et ne ressemble donc pas à une panne. C'est ce qui le rend dangereux.

  • Créer un pool autoscalé et relire sa configuration réelle à l'API.
  • Mesurer le délai entre un pod Pending et un nœud utilisable.
  • Lire ce que le planificateur dit pendant la montée en charge.
  • Reconnaître le blocage à max-size, qui n'a l'air de rien.
  • Un cluster Kapsule existant : voir créer un cluster Kapsule.
  • La CLI scw 2.62.0 ou plus récente, sur un profil isolé, et kubectl configuré.
  • jq, pour lire les sorties JSON.

Pourquoi séparer les charges en plusieurs pools ?

Section intitulée « Pourquoi séparer les charges en plusieurs pools ? »

Un pool est une flotte homogène, et c'est l'unité de décision de Kapsule. Tout ce qui distingue des machines, le type, la zone, le volume système, l'autoscaling, se règle au niveau du pool et jamais du nœud.

BesoinPool dédié ?Pourquoi
Charge stable et prévisibleun pool fixe, sans autoscalingon paie une capacité connue, sans délai de montée
Pics ponctuels, traitements par lotsun pool autoscaléon ne paie les nœuds que pendant les pics
Charges qui ne doivent pas cohabiterun pool par charge, avec des taintsun taint repousse tout pod qui ne le tolère pas
Machines plus grosses pour certains travauxun pool par typele type de nœud est une propriété du pool

Le schéma à deux pools est celui qui rend service le plus souvent : un socle fixe et petit, qui accueille ce qui doit toujours tourner, et un pool autoscalé pour le reste. Le socle garantit que le cluster ne descend jamais à zéro nœud, ce qui éviterait à vos composants système de se retrouver sans place ; le pool élastique absorbe les pics sans les payer en permanence.

Quatre arguments suffisent, et min-size comme max-size n'ont d'effet que si autoscaling=true. L'aide de la commande le dit explicitement, et c'est une source d'erreur classique : des bornes posées sans activer l'autoscaling sont simplement ignorées.

  1. Retrouver l'identifiant du cluster, puisque toutes les commandes de pool le réclament.

    Fenêtre de terminal
    CLUSTER_ID=$(scw k8s cluster list region=fr-par -o json \
    | jq -r '.[] | select(.name == "demo-kapsule") | .id')
    echo "cluster : ${CLUSTER_ID}"

    La sortie doit afficher un identifiant UUID. Remplacez demo-kapsule par le nom de votre cluster.

  2. Créer le pool, avec ses bornes et l'autoréparation.

    Fenêtre de terminal
    POOL_ID=$(scw k8s pool create \
    cluster-id="${CLUSTER_ID}" \
    region=fr-par \
    name=eleveur \
    node-type=DEV1-M \
    zone=fr-par-1 \
    size=1 \
    autoscaling=true \
    min-size=1 \
    max-size=3 \
    autohealing=true \
    -o json | jq -r '.id')
    echo "pool : ${POOL_ID}"

    La sortie doit afficher un identifiant UUID.

  3. Relire la configuration réelle, plutôt que de supposer qu'elle a été prise en compte.

    Fenêtre de terminal
    scw k8s pool get "${POOL_ID}" region=fr-par -o json \
    | jq -r '"pool \(.name) : autoscaling=\(.autoscaling) min=\(.min_size) max=\(.max_size) autohealing=\(.autohealing)"'

    La sortie doit afficher autoscaling=true min=1 max=3 autohealing=true. Si autoscaling vaut false, vos bornes ne servent à rien, et rien ne vous le signalera.

    C'est le cas de tout pool créé sans le demander. Relevé le 2026-09-12 sur un pool issu de cluster create pools.0.* : autoscaling=false autohealing=false. Les deux automatismes sont désactivés par défaut.

  4. Attendre le premier nœud du pool.

    Fenêtre de terminal
    until [ "$(kubectl get nodes --no-headers 2>/dev/null | awk '$2=="Ready"' | wc -l)" -ge 2 ]; do
    printf '.'; sleep 10
    done
    kubectl get nodes

    La sortie doit afficher au moins deux nœuds Ready. Mesuré le 2026-09-12 : 111 secondes.

Combien de temps l'autoscaler met-il à fournir un nœud ?

Section intitulée « Combien de temps l'autoscaler met-il à fournir un nœud ? »

175 secondes entre la soumission de la charge et le retour à un cluster capable de l'accueillir, mesuré le 2026-09-12. Provoquez la situation vous-même avec une charge calibrée pour ne pas tenir.

Fenêtre de terminal
kubectl create deployment gourmand --image=nginx:1.29.1@sha256:8adbdcb969e2676478ee2c7ad333956f0c8e0e4c5a7463f4611d7a2e7a7ff5dc --replicas=6
kubectl set resources deployment gourmand --requests=cpu=2

Chaque DEV1-M offre 3 vCPU, dont une partie est déjà consommée par les composants système. Six pods qui réclament 2 vCPU chacun ne peuvent donc pas tenir sur les deux nœuds en place : le planificateur bloque, l'autoscaler réagit.

Observez les deux faces de l'événement en même temps :

Fenêtre de terminal
kubectl get pods -l app=gourmand
kubectl get events --field-selector reason=FailedScheduling \
-o custom-columns=MESSAGE:.message --no-headers | tail -3

La première sortie doit afficher des pods Pending, la seconde le motif exact du refus.

Que dit le planificateur pendant la montée en charge ?

Section intitulée « Que dit le planificateur pendant la montée en charge ? »

Le message change à chaque nœud livré, et cette séquence est plus instructive que l'état final. Capturée telle quelle le 2026-09-12 :

0/2 nodes are available: 2 Insufficient cpu.
0/3 nodes are available: 3 Insufficient cpu.
0/4 nodes are available: 1 node(s) had untolerated taint(s), 3 Insufficient cpu.

Trois choses s'y lisent. D'abord, le dénominateur augmente : 2 nœuds, puis 3, puis 4. L'autoscaler travaille, et le planificateur le raconte sans qu'on ait à consulter le moindre journal Scaleway. Ensuite, Insufficient cpu reste la cause dominante : ce sont bien les requêtes de CPU, et non l'usage réel, qui décident du placement. Enfin, un nœud sur quatre apparaît avec un taint non toléré.

Que se passe-t-il quand l'autoscaler atteint son maximum ?

Section intitulée « Que se passe-t-il quand l'autoscaler atteint son maximum ? »

Rien. Et c'est exactement le problème. Le pool est monté à 3 nœuds, son max-size, puis s'est arrêté. Deux pods sont restés Pending indéfiniment.

Fenêtre de terminal
kubectl get pods -l app=gourmand --no-headers | awk '{print $3}' | sort | uniq -c

La sortie affichait, mesure faite :

1 ContainerCreating
2 Pending
5 Running

Ni le pool ni l'autoscaler ne signalent de panne. Kubernetes, lui, continue d'émettre des événements FailedScheduling sur les pods qui ne trouvent pas de place : l'information existe, elle est simplement au mauvais endroit pour qui surveille l'autoscaler. Le déploiement ne se déclare jamais prêt, kubectl rollout status attend jusqu'à son délai, et le pool affiche size=3 max=3 comme si tout allait bien.

Un tableau de bord qui surveille les pods en échec ne verra rien non plus : un pod Pending n'est pas en échec, il attend.

C'est le piège le plus coûteux de ce volet, parce qu'il ne ressemble pas à une panne et qu'il se déclenche au pire moment. Un max-size posé par prudence budgétaire, six mois plus tôt, sur un pool qui n'a jamais eu besoin de le franchir, devient soudain le plafond exact où votre charge s'arrête de croître, un jour de pic. Le service ne tombe pas : il cesse de grandir, ce qui produit des latences et des files d'attente plutôt qu'une alerte franche. Le seul signal fiable est la conjonction de deux faits que rien ne rapproche automatiquement : des pods Pending d'un côté, un pool à size == max_size de l'autre. Surveillez cette conjonction explicitement, car aucune des deux métriques prise isolément ne justifie une alerte.

Fenêtre de terminal
scw k8s pool get "${POOL_ID}" region=fr-par -o json \
| jq -r 'if .size == .max_size then "SATURÉ : \(.size)/\(.max_size) nœuds" else "marge : \(.size)/\(.max_size)" end'

La sortie doit afficher SATURÉ : 3/3 nœuds après cette manipulation.

Les réglages de pool qu'il faut connaître avant d'en avoir besoin

Section intitulée « Les réglages de pool qu'il faut connaître avant d'en avoir besoin »

Trois arguments de scw k8s pool create changent le comportement du cluster, et aucun n'est évident.

ArgumentCe qu'il faitCe qu'il faut savoir
max-termination-grace-perioddélai avant que l'API force le drain et la suppression d'un nœud15 minutes par défaut, 1 heure au maximum, et il écrase votre PodDisruptionBudget et vos terminationGracePeriodSeconds
placement-group-idrépartit les nœuds sur des hyperviseurs distinctslimité à 20 instances par groupe
kubelet-argspasse des arguments au kubelet du poolmarqué expérimental dans l'aide de la commande

Le premier mérite qu'on s'y arrête. Un PodDisruptionBudget protège votre application contre les évictions volontaires, et l'on en déduit souvent qu'il est inviolable. Ce n'est pas le cas ici : passé le délai de grâce du pool, l'API force la suppression du nœud, budget ou pas. Votre protection a donc une date d'expiration, et elle vaut quinze minutes par défaut.

Supprimez la charge, l'autoscaler retire les nœuds devenus inutiles. La descente n'est pas immédiate, l'autoscaler attendant qu'un nœud reste inoccupé un certain temps avant de le rendre.

Fenêtre de terminal
kubectl delete deployment gourmand
scw k8s pool get "${POOL_ID}" region=fr-par -o json | jq -r '"\(.size) nœuds, min \(.min_size)"'

La sortie affichera encore 3 nœuds juste après la suppression : c'est normal. Ne comptez pas sur cette descente pour arrêter la facturation à la fin d'une session de travail. Si vous en avez fini, détruisez plutôt que d'attendre.

Pour forcer un retour immédiat à la taille basse, désactivez temporairement l'autoscaling :

Fenêtre de terminal
scw k8s pool update "${POOL_ID}" region=fr-par autoscaling=false size=1 -o json \
| jq -r '"pool ramené à \(.size) nœud(s)"'

La sortie doit afficher pool ramené à 1 nœud(s).

Le pool se supprime indépendamment du cluster, et c'est utile quand seul le pool est de trop.

Fenêtre de terminal
kubectl delete deployment gourmand --ignore-not-found=true
scw k8s pool delete "${POOL_ID}" region=fr-par -o json | jq -r '.status'
until ! scw k8s pool get "${POOL_ID}" region=fr-par > /dev/null 2>&1; do
printf '.'; sleep 10
done
echo " pool disparu"

La première sortie doit afficher deleting, la dernière pool disparu. Attendez la disparition effective : tant que le pool est listé, ses nœuds sont facturés.

Vérifiez enfin ce qui reste :

Fenêtre de terminal
scw k8s pool list cluster-id="${CLUSTER_ID}" region=fr-par -o json \
| jq -r '.[] | "\(.name) : \(.size) nœud(s)"'

La sortie ne doit plus mentionner que votre pool de base. Pour détruire le cluster entier, reportez-vous à la section de nettoyage de créer un cluster Kapsule.

PlafondValeurSource
Nœuds par cluster, control plane mutualisé150refus de l'API, 2026-09-12
Instances par groupe de placement20aide de scw k8s pool create
Délai de grâce avant drain forcé15 min par défaut, 1 h au maximumaide de scw k8s pool create
vCPU utiles d'un DEV1-M3, moins les composants systèmescw instance server-type list, 2026-09-12
Prix d'un nœud DEV1-M0,020196 €/hscw instance server-type list, 2026-09-12

Les trois premiers symptômes portent le message exact renvoyé par kubectl.

SymptômeCauseSolution
0/N nodes are available: N Insufficient cpu et rien ne bougele pool est à max-size, l'autoscaler ne peut plus rienrelever max-size, ou réduire les requests des pods
N node(s) had untolerated taint(s) sur un nœud récentle nœud est en cours d'initialisationattendre ; le taint disparaît de lui-même
Pods Pending alors que les nœuds semblent peu chargésle placement se décide sur les requests, pas sur l'usage réelajuster resources.requests au besoin réel
Les bornes min-size et max-size sont ignoréesautoscaling est resté à falserelire la configuration avec scw k8s pool get

Ces quatre erreurs supposent que l'élasticité est automatique et illimitée. Elle est bornée, et vous avez posé la borne.

AntipatternConséquenceDiscipline
Poser max-size « au cas où » et l'oublierla charge s'arrête de croître un jour de pic, sans alertealerter sur size == max_size, pas seulement sur les pods en échec
Compter sur la descente automatique pour arrêter les fraisles nœuds restent plusieurs minutes après la fin de la chargedétruire en fin de session, ne pas attendre
Croire un PodDisruptionBudget inviolablel'API force le drain après le délai de grâce du poolconnaître max-termination-grace-period, 15 minutes par défaut
Dimensionner les requests au pifomètredes pods Pending sur un cluster qui paraît videmesurer l'usage réel, puis ajuster les requests

Que se passe-t-il quand la demande dépasse max-size ? Le cluster ne tombe pas, il cesse de croître, ce qui produit des latences plutôt qu'une alerte.

Discipline : surveiller la conjonction « pods Pending » et « pool saturé », et non chacun des deux signaux isolément, car aucun ne justifie une alerte à lui seul.

Vos mesures de délai décrivent-elles votre plateforme ou le moment où vous les avez prises ? Deux sessions du même jour ont donné un control plane prêt en 106 puis 81 secondes, et un premier nœud en 109 puis 152 secondes.

Discipline : annoncer des ordres de grandeur, refaire la mesure avant d'en tirer un seuil d'alerte, et dater toute valeur publiée.

Qui paie les nœuds ajoutés pendant un pic ? Vous, à la seconde près, au prix des Instances. Un pool autoscalé bien borné est moins cher qu'une flotte fixe dimensionnée pour le pic.

Discipline : garder un socle fixe minimal, confier l'élasticité à un pool séparé, et ne jamais laisser un max-size généreux sans alerte budgétaire en face.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Aucun automatisme n'est actif par défaut : un pool créé par cluster create sort en autoscaling=false autohealing=false, et min-size comme max-size n'ont d'effet qu'une fois l'autoscaling activé.
  • L'autoscaler a mis 175 secondes à livrer les nœuds manquants, mesuré le 2026-09-12.
  • Le message du planificateur raconte la montée : le dénominateur de 0/N nodes are available augmente à chaque nœud livré.
  • Un nœud fraîchement créé porte un taint transitoire qui disparaît seul.
  • À max-size, ni le pool ni l'autoscaler ne signalent de panne : seuls des événements FailedScheduling subsistent, sur des pods Pending qui ne sont pas comptés comme en échec.
  • Le seul signal fiable est la conjonction de pods Pending et d'un pool à size == max_size.
  • max-termination-grace-period écrase votre PodDisruptionBudget après 15 minutes par défaut.
  • Un groupe de placement est limité à 20 instances.
  • Le placement se décide sur les requests, jamais sur l'usage réel.
  • La descente du pool n'est pas immédiate : détruisez en fin de session plutôt que d'attendre.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn