Requests et limits
Dans les articles précédents, nous avons vu comment il était possible de communiquer au scheduler de k8s des instructions sur où placer ou pas les pods sur les noeuds grâce aux taints et tolerations, aux nodeSelectors, et aux affinity et anti-affinity.
Le rôle du scheduler est de trouver le ou les meilleurs nœuds du cluster pour placer les pods qu’on voudrait déployer. En plus des instructions qu’on lui donne, le scheduler se base sur les ressources disponibles sur les nœuds du cluster. Nous allons parler principalement de CPU et RAM (mémoire) disponible sur les nœuds.
Chaque nœud qui appartient à notre cluster met à disposition ses ressources en CPU et en RAM pour permettre le lancement des pods. Par exemple, pour un noeud de 2CPU et 8GB de RAM, une partie des ressources sera réservée pour le fonctionnement de l’OS et des services Kubernetes (e.g., Kubelet), et le reste sera allouable aux pods.
Un pod contient un ou plusieurs containers, et l’allocation des ressources dans k8s se fait container par container. Il y a deux niveaux d’instructions à passer au scheduler pour chaque container, les requests, et les limits.
Exemple Prenons l’exemple du pod à déployer suivant:
podquota.yaml:
apiVersion: v1
kind: Pod
metadata:
labels:
run: podquota
name: podquota
spec:
containers:
- image: busybox:latest
name: podquota
args:
- sleep
- "3600"
resources:
requests:
memory: "1G"
cpu: "500m"
limits:
memory: "2G"
cpu: "1"
kubectl apply -f podquota.yaml
kubectl describe pod podquota
...
Limits:
cpu: 1
memory: 2G
Requests:
cpu: 500m
memory: 1G
...
Requests
Les requests sont la réservation des ressources en CPU et RAM, on bloque ces ressources sur le nœud sélectionné. Si par exemple on a des nœuds avec 2 CPU et 8GB RAM allouables, alors quand on met des requests de 500mCPU et 1G de RAM, il ne reste plus que 1,5CPU et 7GB de RAM sur le nœud.
1mCPU correspond à 1 millième de CPU. C’est une unité qui correspond à la fraction de temps que peut consommer un container sur un CPU. 500mCPU veut dire qu’il a droit de tourner au moins la moitié du temps sur un CPU. Donc sur une fenêtre de 10min, le container peut tourner au moins 5min sur 1 CPU.
Pour la mémoire, il y a deux types d’unités, les G,M,K et les Gi,Mi,Ki, les premières étant des puissances de 10 tandis que les secondes dans des puissances de 2. Donc 1G veut dire qu’on veut allouer 1x103M Bytes de mémoire, alors que 1Gi veut dire qu’on veut allouer 1x210Mi Bytes de mémoire: 1G Bytes = 1x103M Bytes = 1x106K Bytes = 1x109 Bytes = 1000000000 Bytes 1Gi Bytes = 1x210Mi Bytes = 1x220Ki Bytes = 1x230 Bytes = 1073741824 Bytes
Pour notre pod, le scheduler va essayer de trouver un nœud du cluster où il y a au moins 500mCPU et 1G de mémoire. S’il n’en trouve pas, il ne pourra pas scheduler le pod et lancera une erreur.
Limits
En ce qui concerne les limits, ce sont les valeurs que le container ne doit pas dépasser. Notre pod a une limit de CPU de 1. C’est-à-dire qu’il a droit au moins à un demi CPU (500mCPU), mais si le CPU n’est pas occupé par un autre process, notre container peut occuper le CPU entièrement. En revanche, il ne dépassera pas l’utilisation d’1 CPU, le système va l’en empêcher. C’est-à-dire qu’il ne pourra pas utiliser 2 CPUs, même si le deuxième est libre. D’ailleurs, c’est une des raisons pour lesquelles il est d’usage de ne pas mettre de limit sur le CPU pour la plupart des applications (il y a des raisons d’en mettre pour certaines). Ne pas en mettre permet au container d’occuper les CPUs disponibles tant qu’ils sont vides. S’ils commencent à être occupés, il consommera uniquement ses 500mCPU qui lui sont réservés.
Pour la mémoire, ici nous avons mis 2G en limit. Cela veut dire que le container occupera 1G de mémoire de base avec ses requests, mais pourra occuper jusqu’à 2G s’il y a de la place en mémoire. En revanche, s’il dépasse la limit à cause d’une mémoire mal gérée par l’application, il risque de se faire OOMKilled, c’est-à-dire que le pod sera détruit et reschedulé. En plus, s’il occupe plus d’1G de mémoire, et qu’il faut lui enlever des pages mémoire pour les allouer à un autre process, cela peut être un peu brutal pour l’application de perdre des éléments qu’elle utilisait en mémoire. Certaines applications ne fonctionnent pas très bien dans ce mode-là. La plupart du temps, par prudence, le mieux est de ne pas mettre de surprovisionnement en mémoire et de se contenter de requests=limits en ce qui concerne l’allocation des ressources en mémoire.
QoS class
La combinaison de requests et limits des containers d’un pod détermine dans quelle classe de qualité de service (QoS).
S’il n’a ni request ni limit, un pod sera dans la classe Best Effort. Il va tourner sur n’importe quel nœud quelques soient les ressources disponibles, mais ce sera le premier à se faire limiter ou détruire s’il y a conflit sur les ressources.
S’il a des requests mais pas de limit, il sera dans la classe Burstable, qui a plus de chance de survivre que la classe Best Effort en cas de conflit sur les ressources.
Si tous les containers du pod ont des requests=limits, le pod sera dans la classe Guaranteed, et c’est le dernier à se faire limiter ou détruire par rapport aux deux autres classes.
Conclusion
Les requests et les limits sont des éléments de la configuration qui ont énormément d’impact sur l’utilisation et le dimensionnement d’un cluster. Si on met beaucoup de requests pour les pods, on risque d’occuper beaucoup de place qui ne sera peut-être pas utilisée par l’application, et on aura besoin de plus de nœuds dans notre cluster. Mais si on met trop peu de request et qu’il y a des pics de trafic, on risque de créer des conflits de ressources entre les pods sur les nœuds, et nos applications risquent d’en souffrir.
Il n’y a pas vraiment de standard car chaque application a des comportements différents et des besoins différents. Le mieux est de bien connaître son application, de faire des tests de performance, de monitorer le fonctionnement de l’application, et d’adapter au fur et à mesure. On ne met pas les requests et les limits une fois pour toute au début de notre déploiement, ce sont des éléments de la configuration qui peuvent évoluer dans le temps.