La Scalabilité horizontale que permet Kubernetes est l’une de ses fonctionnalités les plus intéressantes mais scaler horizontalement implique plus de pods, ce qui veut dire aussi plus de processus et donc la demande et la consommation de plus de ressources.
Les ressources ne sont pas illimitées (Mémoire, Espace disque..), si le nœud est surchargé, le Kernel n’aurait plus le choix que de tuer certains de ces processus pour libérer de l’espace (OOM Kill).
Kubernetes nous permet d’avoir un niveau de contrôle sur la consommation de ressources, ainsi que de prioriser certains pods par rapport à d’autres afin d’éviter, en cas de surcharge, que le Kernel tue des pods qui peuvent être critiques, ceci se fait en définissant trois niveaux de QoS à l’aide des limits et requests ainsi que des quotas de ressources, alors qu’est ce que c’est :
Avant de parler des classes QoS et comment cela est défini dans Kubernetes, il est important de distinguer deux types de ressources :
Ce sont les ressources qui sont “inépuisables” du moment qu’on a le temps pour attendre qu’ils soient de nouveau disponibles (ex : CPU, Réseau). En cas de surcharge, on peut toujours avoir un accès limité à ces ressources (Throttling), mais avec un risque de latence et de famine.
Ce sont les ressources limitées, si toutes les ressources disponibles sont consommées l’application ne pourra plus en avoir plus (ex : Mémoire, Disque..), En cas de surcharge, certains processus peuvent être tués (kill, OOM), si le système a besoin de libérer des ressources.
Dans Kubernetes une ressource peut être : Demandée, Allouée, ou Consommée. K8S nous permet de spécifier au niveau du container ces exigences à travers les limits, les requests et les quotas.
C’est la quantité de ressource (cpu ou mémoire) demandée par un container. Les Requests sont évalués au moment du scheduling, par conséquent la somme de toutes les requests de mémoire ne peut pas dépasser la mémoire totale allouable (allocatable) du nœud. En ce qui concerne le cpu, la somme des requests ne peut pas dépasser la taille du cpu du nœud.
Le scheduler choisi le nœud le plus adéquat pour le pod en fonction de ses requests. Ainsi la quantité de ressource demandée sera allouée au container/pod, il s’agit donc d’une "soft limit".
C’est le seuil maximal de ressources consommées que peut atteindre un pod. C’est donc une "hard limit" qui influence le cgroup. La valeur de limits est considérée au runtime par le Kubelet. Un container/pod qui dépasse ses limits de mémoire sera automatiquement tué, il peut être redémarré ou pas par la suite selon sa restartPolicy. Si on dépasse les limits de CPU alors le pod va être "throttle".
- Donner une valeur au limit suffit pour qu’elle soit assignée aux requests s’ils ne sont pas définis.
- Si on ne spécifie pas la valeur de limits, alors elle est considérée comme infinie (unbounded).
En fonction des limits et des requests trois niveaux de QoS peuvent être définis au niveau du pod (BestEffort < Burstable < Guaranteed). Si le nœud est surchargé Kubernetes se base sur ces niveaux de QoS pour décider quels sont les pods à sacrifier en premier.
BestEffort requests et limits non définis
Tant qu’il y a de la mémoire dans le nœud ces pods peuvent consommer jusqu'à la mémoire totale allouable.
Ils sont les premiers candidats à la terminaison en cas de surcharge.
containers:
- image: googlecontainer/echoserver
name: besteffort
Burstable requests<limits
Ces pods peuvent consommer plus de ressources disponibles tant que ça ne dépasse pas leurs limits.
Si le système a besoin de plus de ressources, ces pods sont considérés de deuxième priorité , et seront terminés s’il dépassent leurs requests et qu’il n y a pas de pod moins prioritaire (BestEffort) à tuer.
containers:
- image: googlecontainer/echoserver
resources:
requests:
memory: "500Mi"
limits:
memory: "2Gi"
cpu : "100m"
name: burstable
containers:
- image: googlecontainer/echoserver
resources:
requests:
memory: "500Mi"
limits:
memory: "2Gi"
name: burstable_1
- image: googlecontainer/echoserver
resources:
limits:
memory: "2Gi"
cpu : "100m"
name: burstable_2
Guaranteed requests=limits
Ces pods sont considérés de première priorité et ne seront tués que s’ils dépassent leur limits.
containers:
- image: googlecontainer/echoserver
resources:
limits:
memory: "2Gi"
cpu: "100m"
name: guaranteed
Si la somme totale des limits de tous les pods est supérieure à la mémoire allouable du nœud, alors on dit que le nœud est overcommitted. L'overcommitment part de l’hypothèse que tous les pods ne vont pas atteindre leurs limites en même temps.
Cela qui peut malheureusement arriver (par exemple quand les limits de mémoire sont trop optimistes ou que tous les pods consomment beaucoup plus que leurs requests en même temps. Le nœud sera alors surchargé.
En cas de surcharge du nœud Kubernetes peut faire recours à l'éviction qui est un mécanisme de défense ( paramétré au niveau du kubelet ). Il s’agit de "descheduler" certains pods par ordre de priorité selon le niveau de QoS, Il s’agit d’une hard limit au niveau du nœud sur la mémoire restante:
(--eviction-hard="memory.available<200Mi")
Dans cet exemple, en cas de surcharge du nœud si la mémoire totale restante est inférieure à 200Mi, l’éviction sera enclenchée et les pods qui ont les QoS les plus basses seront évincés en premier pour libérer les ressources aux pods de QoS plus élevés.
Comme on vient de le voir, les limits et les requests influencent le cycle de vie d’un pod ainsi que la stabilité du nœud. Quand on administre un cluster Kubernetes, on a envie de pouvoir définir des "policies" sur les limits et requests pour s’assurer d’une exploitation optimale et contrôlée du nœud.
Ceci peut se faire à l’aide de RessourceQuota au niveau namespace ainsi qu'avec les LimitRanges au niveau container/pod.
Des quotas par namespace sur le total de limits et de requests autorisés. L ’api-server renvoie une erreur "403 Forbidden" si le quota est dépassé.
Si un quota est défini sur limits ou requests pour une ressource donnée (CPU ou mémoire) , alors tout pod est obligé de les définir, et donc par exemple on n’aura plus la possibilité de lancer des pod BestEffort.
Des quotas peuvent être aussi définis sur d’autres entités (nombre de pods, services, secrets, persistentvolumeClaims ..)
apiVersion: v1
kind: ResourceQuota
metadata:
name: mem-cpu-quota
namespace: project1
spec:
hard:
requests.cpu: "1"
requests.memory: "3G"
limits.cpu: "2"
limits.memory: "5G"
Les RessourceQuota peuvent être aussi spécifiées par scope de pod, on pourrait vouloir par exemple associer un quota qui ne s’applique que sur les pods NotBestEffort ou Terminating etc..
apiVersion: v1
kind: ResourceQuota
metadata:
name: pods-quota
namespace: project2
spec:
hard:
pods: "3"
scopes:
- NotBestEffort
Agit au niveau container ou pod par namespace. Il s’agit d’une bonne pratique pour s’assurer d’avoir toujours des valeurs par défaut pour les limits et requests quand ils ne sont pas définis. Le LimitRange permet aussi d’imposer une borne inférieure sur les requests/pod (ex: ne pas créer des pods trop petits), et une borne supérieure sur les limits/pod pour limiter l'overcommitment.
À noter que :
- SI default
non défini ET max
défini ALORS default
vaut max
- SI defaultRequest
non défini ET default
défini ALORS defaultRequest
vaut default
.
- SI defaultRequest
non défini ET default
non défini ET max
défini ALORS defaultRequest
et default
valent max
.
- SI defaultRequest
non défini ET default
non défini ET max
non défini ALORS defaultRequest
ET default
valent min
.
apiVersion: v1
kind: LimitRange
metadata:
name: mem-defaults
namespace: project1
spec:
limits:
- default:
memory: "600M"
cpu: "200m"
defaultRequest:
memory: "200M"
cpu: "200m"
type: Container
apiVersion: v1
kind: LimitRange
metadata:
name: mem-min-max
namespace: project2
spec:
limits:
- max:
memory: "2G"
min:
memory: "100M"
type: Container
Il faut toujours définir des limits et des requests pour les pods les plus critiques :
Quand la limite n’est pas définie, Kubernetes considère que toutes les ressources du nœud peuvent être consommées au runtime.
Définir des requests bien évaluées permet au scheduler de prendre une meilleure décision pour placer un pod sur un nœud ou sur un autre.
Gérer la stabilité des déploiements et des nœuds en se basant sur les classes de QoS:
Utiliser les LimitRange, pour être sûr d’avoir toujours des requests et des limits bien définis, on associant des valeurs par défaut et en imposant des bornes.
Protéger le nœud et les processus critiques de Kubernetes, en ajustant le threshold d'éviction.
Imposer la définition de limits (et/ou) requests et séparer les environnements en associant RessourceQuotas par namespace (ex : un quota limité sur le namespace de l’environnement DEV) .
Monitorer pour bien estimer la consommation de ressources et définir les RessourceQuota et les LimitRange en fonction de cela pour une meilleure exploitation du nœud.