Un pod tourne sans histoire, puis disparaît avec OOMKilled et le code de sortie 137. Un autre rame sous charge alors que le nœud qui l’héberge est inoccupé à 70 %. Les deux symptômes remontent aux mêmes deux champs que la plupart des manifests renseignent par réflexe et que peu de gens lisent vraiment : requests et limits. Ce ne sont pas deux réglages du même curseur. Ils agissent à des moments différents, sur des sous-systèmes différents, avec des conséquences opposées quand on les franchit.
Requests et limits ne font pas le même travail
Une request est une donnée d’ordonnancement. Le scheduler place les pods en additionnant leurs requests par rapport à la capacité allouable d’un nœud ; une request est une réservation, le plancher que le pod est certain d’obtenir. Elle ne plafonne jamais l’usage.
Une limit est un plafond d’exécution imposé par le kubelet et le noyau via les cgroups. Ce qui arrive quand un conteneur touche ce plafond dépend entièrement de la ressource concernée, car le CPU et la mémoire se comportent à l’inverse l’un de l’autre.
Franchir la limite CPU : le throttling
Le CPU est une ressource compressible. Le noyau peut accorder moins de cycles à un processus sans le tuer.
La request CPU devient un poids cgroup (cpu.shares en cgroup v1, cpu.weight en v2, où un cœur vaut 1024 shares). Ce poids ne compte qu’en cas de contention : il décide comment des processus affamés se partagent un CPU saturé. Tant que le nœud a de la marge, tout le monde s’exécute librement, requests ou pas.
La limit CPU devient du CFS bandwidth control : un quota par période d’ordonnancement (cpu.cfs_quota_us / cpu.cfs_period_us, ou cpu.max en v2), avec une période de 100 ms par défaut. Une limite à 500m accorde 50 ms de CPU par fenêtre de 100 ms. Épuisez ce budget avant la fin de la fenêtre et le conteneur est throttlé, mis en pause jusqu’à la période suivante. Pas de kill, juste de la latence.
Le piège tient à cette application par période. Le throttling se déclenche que le nœud soit chargé ou non, si bien qu’une charge en pics et sensible à la latence peut être fortement throttlée pendant que la machine reste largement inoccupée. Surveillez le rapport entre container_cpu_cfs_throttled_periods_total et container_cpu_cfs_periods_total : une valeur élevée, c’est du throttling CFS, pas une application lente.
Franchir la limite mémoire : l’OOMKill
La mémoire est incompressible. On ne peut pas demander à un processus de détenir moins de RAM comme on lui donne moins de cycles.
La request mémoire alimente l’ordonnancement et le classement d’éviction ; ce n’est pas un plafond. La limit mémoire, elle, est un plafond dur (memory.max / memory.limit_in_bytes). Touchez un octet au-delà et l’OOM killer du cgroup envoie un SIGKILL à un processus du groupe. Kubernetes marque le conteneur OOMKilled, code de sortie 137 (128 + SIGKILL), le redémarre selon le restartPolicy, et tombe en CrashLoopBackOff si cela se répète.
Deux événements mémoire distincts prêtent à confusion. Atteindre la limite de son propre conteneur, c’est un OOMKill de cgroup : niveau noyau, immédiat, cantonné au pod. Le nœud globalement à court de mémoire déclenche plutôt une éviction par le kubelet, qui trie ses victimes selon la classe QoS et la distance de chaque pod au-dessus de sa request.
Les classes QoS se déduisent, elles ne se déclarent pas
Kubernetes attribue une classe à chaque pod à partir de ce que vous avez posé :
- Guaranteed : chaque conteneur fixe
requests == limitspour le CPU et la mémoire. Il reçoit unoom_score_adjfortement négatif (-997) et est évincé en dernier. - Burstable : au moins une request ou une limit est posée, sans atteindre Guaranteed.
- BestEffort : rien du tout. Il obtient un
oom_score_adjde 1000, et c’est le premier évincé comme le premier que le noyau tue sous pression mémoire.
La classe n’est pas cosmétique : sous pression, elle décide qui meurt d’abord, et cet ordre justifie à lui seul de poser ses requests honnêtement.
Le débat des limites CPU
C’est un vrai sujet, toujours vivant. L’argument pour retirer les limites CPU : le throttling CFS coûte de la latence même sur un nœud inoccupé, et une request CPU garantit déjà une part proportionnelle en cas de contention, donc une limite ne fait souvent qu’acheter du throttling inutile. L’argument pour les garder : une planification de capacité prévisible, une protection contre un voisin qui s’emballe, et le fait que les limites CPU sont requises pour la classe Guaranteed et pour le pinning CPU statique.
Le compromis sur lequel atterrissent la plupart des équipes plateforme : poser presque toujours request == limit en mémoire (ressource incompressible, on veut un plafond dur et prévisible), toujours poser une request CPU, et traiter les limites CPU comme optionnelles, appliquées seulement là où une règle précise l’exige.
Une méthode pour régler les défauts
Mesurer, ne pas deviner. Utilisez le Vertical Pod Autoscaler en mode recommandation (updateMode: "Off") ou interrogez directement l’historique Prometheus. Le même réflexe de mesure que pour un SLO s’applique ici : on règle ce qu’on a observé, jamais ce qu’on imagine.
# pic de working set mémoire sur 7 jours, par pod
max_over_time(container_memory_working_set_bytes{container="app"}[7d])
# 95e centile de cœurs CPU consommés sur 7 jours
quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{container="app"}[5m])[7d:5m])
On dérive ensuite les valeurs de ces chiffres :
resources:
requests:
cpu: 250m # usage typique, pilote l'ordonnancement
memory: 512Mi
limits:
memory: 512Mi # égale à la request : plafond dur et prévisible
# pas de limite CPU : peut déborder sur la capacité libre, garde 250m sous contention
Mémoire request == limit, calée sur le pic de working set observé plus 20 à 30 % de marge. Request CPU à l’usage typique (autour du P90), limite CPU laissée de côté sauf règle contraire. On remesure une fois le vrai trafic arrivé : les premiers chiffres sont une hypothèse, pas un verdict.
Pièges classiques
Ne poser qu’une limit fait recopier cette valeur dans la request, ce qui peut sur-réserver le nœud en silence. Reprendre les chiffres d’une autre équipe ignore que les requests sont propres à la charge : trop hautes elles gâchent des nœuds, trop basses elles invitent l’éviction. Dimensionner la mémoire sur le RSS plutôt que sur le working set oublie le page cache que le noyau compte dans votre limite. Accuser l’application d’une latence qui n’est que du throttling CFS fait perdre des journées : vérifiez le rapport de throttling avant de profiler du code.
À retenir
Les requests sont pour le scheduler, les limits pour le noyau. Au-delà de la limite CPU on est throttlé, au-delà de la limite mémoire on est tué. Posez la mémoire request == limit, posez une request CPU, soyez délibéré sur les limites CPU, et tirez chaque chiffre d’un usage mesuré plutôt que d’un template. Un redémarrage dû à un OOMKill et un redémarrage dû à un liveness probe qui échoue sont indiscernables dans kubectl describe, d’où l’intérêt de savoir comment se comportent vraiment les probes avant de déboguer la mauvaise. Bien dimensionner ces ressources sur tout un parc fait une bonne partie du travail qui rend une migration vers Kubernetes durable en production, et c’est le cœur de mon offre migration d’infrastructure legacy vers Kubernetes. Plus de contenu Kubernetes dans la catégorie kubernetes.