Kubernetes · Chapitre 3

Le scheduling dans Kubernetes

Comment Kubernetes décide sur quel nœud s'exécute chaque Pod : labels, taints et tolerations, node affinity, ressources, DaemonSets, Static Pods et schedulers personnalisés.

Quand vous créez un Pod, quelqu'un doit décider sur quel nœud il s'exécute. Ce quelqu'un est le kube-scheduler, que nous avons vu dans le chapitre des fondamentaux. Dans ce chapitre, nous verrons comment il prend cette décision et toutes les façons de l'influencer.

Comment le scheduler décide

Le scheduler surveille les Pods qui n'ont pas encore de nœud attribué (leur champ nodeName est vide). Pour chacun, il suit ces phases :

Les phases du scheduler : file d'attente, filtrage, notation et affectation

  1. File d'attente : les Pods en attente patientent, classés par priorité.
  2. Filtrage : il écarte les nœuds qui ne conviennent pas (pas assez de CPU ou de mémoire, un taint que le Pod ne tolère pas, une affinité non respectée...).
  3. Notation : il classe les nœuds restants, par exemple en préférant celui qui a le plus de ressources libres ou celui qui possède déjà l'image.
  4. Affectation (binding) : il écrit le nœud retenu dans le champ nodeName du Pod. À partir de là, le kubelet de ce nœud le démarre.

Scheduling manuel avec nodeName

Vous pouvez contourner le scheduler en indiquant vous-même le nœud dans le spec du Pod :

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  nodeName: node02
  containers:
    - name: nginx
      image: nginx
  • On ne peut l'indiquer qu'à la création du Pod. Ensuite, nodeName n'est plus modifiable.
  • Pour affecter un Pod déjà créé, on envoie un objet Binding à l'API (c'est ce que fait le scheduler en interne).
  • S'il n'y a pas de scheduler dans le cluster, les Pods sans nodeName restent à l'état Pending.

Labels, sélecteurs et annotations

Ils sont la base de presque tout ce qui suit :

  • Labels : paires clé-valeur pour regrouper des ressources (app: miapp, env: prod).
  • Sélecteurs : le moyen de filtrer par ces labels.
  • Annotations : métadonnées informatives qui ne servent pas à la sélection : version, contact, détails du build, etc.
kubectl get pods --selector app=miapp
kubectl get pods -l env=prod,tier=front-end

Les ReplicaSets, Deployments et Services utilisent des sélecteurs pour trouver leurs Pods. Notez que ce sont les labels du modèle de Pod (spec.template.metadata.labels) qui comptent, et non ceux du metadata du ReplicaSet lui-même.

metadata:
  name: miapp
  labels:
    app: miapp            # labels de l'objet, pour que d'autres le trouvent
  annotations:
    buildversion: "1.34"  # information, non utilisée pour la sélection

Taints et tolerations

Ils permettent à un nœud de refuser des Pods, sauf ceux qui sont préparés pour lui :

  • Un taint (« tache ») se place sur le nœud. Par défaut, aucun Pod ne le tolère.
  • Une toleration se place sur le Pod et lui permet d'être placé sur les nœuds portant ce taint.

Un nœud avec un taint n'accepte que les Pods qui le tolèrent

Important : une toleration n'oblige pas le Pod à aller sur ce nœud. Elle lui en donne seulement la permission. Le Pod peut finir sur n'importe quel autre nœud sans taint. Pour le diriger vers un nœud précis, on utilise l'affinité, que nous verrons juste après.

Étape 1 : poser le taint sur le nœud

kubectl taint nodes node01 app=bleu:NoSchedule

# Le retirer (avec un tiret à la fin)
kubectl taint nodes node01 app=bleu:NoSchedule-
Effet Ce qu'il fait aux Pods qui ne le tolèrent pas
NoSchedule Ne place pas de nouveaux Pods sur le nœud.
PreferNoSchedule Essaie d'éviter le nœud, sans le garantir.
NoExecute Ne place pas de nouveaux Pods et expulse ceux qui s'y trouvaient.

Étape 2 : rendre le Pod tolérant

apiVersion: v1
kind: Pod
metadata:
  name: app-bleu
spec:
  containers:
    - name: web
      image: nginx
  tolerations:
    - key: "app"
      operator: "Equal"
      value: "bleu"
      effect: "NoSchedule"

C'est pour cela qu'on ne place pas de Pods applicatifs sur le plan de contrôle : ses nœuds portent le taint node-role.kubernetes.io/control-plane:NoSchedule.

nodeSelector : choisir des nœuds par label

La façon la plus simple de diriger un Pod vers certains nœuds. On étiquette d'abord le nœud, puis on le demande dans le Pod :

kubectl label nodes node01 size=large
spec:
  nodeSelector:
    size: large
  containers:
    - name: procesador
      image: mi-procesador

Sa limite : il n'accepte que l'égalité. Impossible de demander « des nœuds grands ou moyens » ni « n'importe quel nœud qui n'est pas petit ». C'est le rôle de l'affinité.

Node affinity

L'affinité de nœud est la version avancée de nodeSelector : elle accepte des opérateurs (In, NotIn, Exists, DoesNotExist, Gt, Lt) et des règles obligatoires ou préférées.

spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: size
                operator: In
                values: ["large", "medium"]
  containers:
    - name: procesador
      image: mi-procesador

Les noms à rallonge décrivent le comportement à deux moments :

Type Au placement du Pod Si le nœud change ensuite
requiredDuringSchedulingIgnoredDuringExecution Obligatoire : si aucun nœud ne correspond, le Pod reste Pending. Ignoré : le Pod reste où il est.
preferredDuringSchedulingIgnoredDuringExecution Préféré : si aucun nœud ne correspond, il va ailleurs. Ignoré.

Pour « n'importe quel nœud qui n'est pas petit », on utiliserait operator: NotIn avec values: ["small"].

Taints et tolerations ou affinité

Ils résolvent des problèmes différents et s'utilisent généralement ensemble :

  • Les taints et tolerations empêchent les autres Pods d'entrer sur vos nœuds.
  • L'affinité de nœud garantit que vos Pods vont sur vos nœuds.

Pour réserver des nœuds à une application : un taint sur ces nœuds, une toleration sur les Pods de l'application et une affinité pour que ces Pods n'aillent pas sur d'autres nœuds.

Requests et limits de ressources

Chaque conteneur peut déclarer combien de CPU et de mémoire il demande et combien il peut utiliser au maximum :

spec:
  containers:
    - name: web
      image: nginx
      resources:
        requests:
          cpu: "250m"      # 0,25 cœur
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"
  • requests est ce qu'utilise le scheduler : il ne place le Pod que sur un nœud qui a cette capacité libre.
  • limits est le plafond pendant l'exécution, et il se comporte différemment selon la ressource :
Ressource Si le conteneur essaie de dépasser la limite
CPU Il est bridé (throttling) : il ralentit, mais reste actif.
Mémoire Le conteneur est arrêté avec l'erreur OOMKilled (mémoire épuisée).

Par défaut, Kubernetes ne fixe ni requests ni limits. Une pratique courante : toujours définir les requests et, pour le CPU, rester prudent avec les limits, car elles brident même quand le nœud a du CPU libre.

Deux objets de namespace fixent des valeurs par défaut :

  • LimitRange : valeurs par défaut, minimales et maximales par conteneur.
  • ResourceQuota : le total que peut consommer tout le namespace.

Les DaemonSets : un Pod sur chaque nœud

Un DaemonSet garantit une copie d'un Pod sur chaque nœud du cluster. Quand un nœud est ajouté, il y crée le Pod ; quand un nœud est retiré, il le supprime.

On l'utilise pour les agents qui doivent être partout : supervision, collecte de logs ou réseau du cluster (kube-proxy est déployé ainsi).

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: agente-monitoreo
spec:
  selector:
    matchLabels:
      app: agente-monitoreo
  template:
    metadata:
      labels:
        app: agente-monitoreo
    spec:
      containers:
        - name: agente
          image: agente-monitoreo
kubectl get daemonsets
kubectl describe daemonset agente-monitoreo

Depuis Kubernetes 1.12, les DaemonSets utilisent le scheduler normal avec des règles d'affinité de nœud pour placer chaque Pod.

Les Static Pods

Le kubelet peut fonctionner sans le plan de contrôle : si vous lui donnez un dossier contenant des fichiers YAML de Pods, il les crée et les maintient en vie tout seul. Ce sont les Static Pods.

  • Le dossier se configure dans le kubelet (staticPodPath dans sa configuration). En général, c'est /etc/kubernetes/manifests.
  • Si vous ajoutez un fichier, le kubelet crée le Pod ; si vous le modifiez, il le recrée ; si vous le supprimez, le Pod disparaît.
  • Si le nœud fait partie d'un cluster, le kubelet publie une copie en lecture seule (mirror Pod) pour qu'il apparaisse dans kubectl get pods, avec le nom du nœud en suffixe.
  • C'est ainsi que kubeadm démarre le plan de contrôle lui-même : kube-apiserver, etcd, kube-scheduler et kube-controller-manager sont des Static Pods sur les nœuds du plan de contrôle.

Pour les voir sur le nœud sans passer par l'API : crictl ps.

Static Pods DaemonSets
Qui les crée Le kubelet, à partir de fichiers locaux Le plan de contrôle, via l'API
Où Uniquement sur le nœud qui a ces fichiers Sur tous les nœuds (ou ceux que vous indiquez)
Usage typique Composants du plan de contrôle Agents de supervision et de logs
Ignorés par le scheduler Oui Non

Plusieurs schedulers et profils

Kubernetes permet des schedulers personnalisés qui cohabitent avec celui par défaut. Chacun a un nom, et chaque Pod choisit par lequel il est placé :

spec:
  schedulerName: mi-scheduler
  containers:
    - name: nginx
      image: nginx

Un scheduler supplémentaire se déploie comme n'importe quel Pod ou Deployment, avec son propre fichier de configuration :

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: mi-scheduler

Pour savoir quel scheduler a placé chaque Pod :

kubectl get events -o wide
kubectl logs mi-scheduler -n kube-system

Plugins et points d'extension

Le scheduler est construit à partir de plugins, chacun rattaché à une phase (un point d'extension) :

Phase Exemples de plugins intégrés
queueSort PrioritySort (trie par priorité)
filter NodeResourcesFit, NodeName, NodeUnschedulable, TaintToleration, NodeAffinity
score NodeResourcesFit, ImageLocality
bind DefaultBinder

Il existe aussi des points intermédiaires (preFilter, postFilter, preScore, reserve, permit, preBind, postBind) où l'on peut ajouter ses propres plugins.

Plutôt que de maintenir plusieurs schedulers séparés, un même kube-scheduler peut avoir plusieurs profils, chacun avec son schedulerName et sa propre combinaison de plugins activés ou désactivés :

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
  - schedulerName: default-scheduler
  - schedulerName: sin-puntuacion
    plugins:
      score:
        disabled:
          - name: "*"

À retenir

  • Le scheduler filtre les nœuds, les note et affecte le Pod en écrivant son nodeName.
  • nodeName permet un placement manuel, uniquement à la création du Pod.
  • Les labels regroupent, les sélecteurs filtrent et les annotations ne font qu'informer.
  • Taints sur les nœuds et tolerations sur les Pods : le nœud refuse qui ne le tolère pas. NoExecute expulse en plus.
  • nodeSelector dirige les Pods par égalité de labels ; l'affinité de nœud ajoute des opérateurs et des règles obligatoires ou préférées.
  • Les requests décident où un Pod a sa place ; dépasser la limit CPU bride, dépasser celle de mémoire provoque OOMKilled.
  • DaemonSet : un Pod par nœud. Static Pod : créé par le kubelet à partir de fichiers locaux.
  • On peut utiliser plusieurs schedulers ou profils, choisis avec schedulerName.