Limiter la mémoire et le CPU avec systemd
Un processus plafonné peut bloquer votre VPS. Configurez MemoryHigh, MemoryMax, CPUQuota et TasksMax, puis vérifiez le message OOM kill après redémarrage.
Limiter la mémoire et le temps CPU d’un processus avec un drop-in systemd
Vous limitez la mémoire et le temps CPU d’un processus sur un VPS Linux en ajoutant quelques lignes à l’unité qui exécute le processus. MemoryMax= définit la limite stricte de mémoire. CPUQuota= définit la limite de temps processeur. Ces deux limites sont appliquées par cgroup v2 (groupes de contrôle, version 2), la fonctionnalité du noyau que systemd utilise déjà pour comptabiliser chaque service du serveur.
sudo systemctl edit myapp.serviceCette commande ouvre un fichier drop-in contenant des instructions dans les commentaires. Ajoutez ceci au-dessus de ces instructions :
[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMaxsystemctl show doit reprendre vos valeurs dans les unités utilisées par le noyau : MemoryMax=805306368 et CPUQuotaPerSecUSec=800ms. Si la commande affiche MemoryMax=infinity, le drop-in n’a pas été chargé. Vérifiez que le fichier se trouve bien à /etc/systemd/system/myapp.service.d/override.conf et qu’il commence par l’en-tête [Service], car une ligne de configuration sans section au-dessus provoque l’écriture de Assignment outside of section. Ignoring. dans les journaux systemd et le démarrage du service sans aucune limite.
La suite de ce guide explique comment choisir ces valeurs et quels problèmes peuvent encore survenir une fois les limites définies.
Pourquoi un processus incontrôlable fige un VPS sans remplir sa mémoire
Un processus qui atteint une limite stricte de mémoire est arrêté en environ une seconde, puis le service redémarre. C’est le cas favorable. Le cas problématique est celui où rien ne s’arrête : la machine répond au ping, SSH accepte la connexion, mais l’invite du shell n’apparaît jamais. La machine est active et occupée, mais ce travail n’est pas utile.
Voici le mécanisme, car il n’est pas évident. Lorsque la mémoire libre devient insuffisante, le kernel récupère des pages au lieu d’en allouer de nouvelles. Les pages les plus faciles à récupérer sont celles sauvegardées par des fichiers, et le page cache contient le code exécutable de tout ce qui fonctionne. Le kernel évacue donc les pages de code de sshd, et l’instruction suivante exécutée par sshd provoque un page fault qui doit relire ces octets depuis le stockage. Chaque processus finit par attendre le disque au lieu de s’exécuter. Les mêmes pages sont évacuées puis rechargées en boucle. Ce phénomène s’appelle le thrashing.
Deux facteurs aggravent la situation sur un VPS par rapport à un ordinateur portable. Le stockage est souvent connecté au réseau ou partagé, et chaque page fault coûte donc plus de millisecondes qu’avec un périphérique NVMe local. De plus, le kernel ne mesure pas le temps, mais les échecs : tant que la récupération restitue une page, même lentement, le kernel considère qu’il progresse et ne déclenche pas l’out of memory (OOM) killer. Une machine peut rester dans cet état pendant plusieurs minutes avant qu’un processus soit tué.
Vous pouvez observer le phénomène. Le kernel expose les pressure stall information (PSI) sur Linux 4.20 et les versions ultérieures :
cat /proc/pressure/memory
cat /proc/pressure/iosome avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233La ligne full est celle qui compte. full avg10=48.15 signifie qu’au cours des dix dernières secondes, 48 % du temps, toutes les tâches exécutables de la machine étaient bloquées en attente d’une opération liée à la mémoire. Rien ne s’exécutait donc. Sur un serveur sain, full reste proche de zéro. Au-dessus de 10, la lenteur devient perceptible pour un utilisateur. À partir de 40, la machine est dans l’état que l’on décrit comme figé.
C’est également pourquoi une limite seule ne constitue pas une garantie. Une unité soumise à MemoryHigh= est ralentie au lieu d’être tuée. Elle reste donc active et lente, et rien ne la redémarre, car du point de vue de systemd elle n’a jamais échoué. Une unité limitée qui peut encore utiliser le swap génère des lectures et des écritures imputées à cette unité, mais exécutées par un périphérique partagé. Elle peut donc faire augmenter /proc/pressure/io pour tous les autres services de la machine. Les limites déterminent qui subit le coût d’un manque de ressources, mais elles ne peuvent pas créer de capacité.
Vérifier que votre VPS utilise cgroup v2
stat -fc %T /sys/fs/cgroupcgroup2fs correspond à la hiérarchie unifiée, dont tous les paramètres ci-dessous ont besoin. tmpfs signifie que le serveur a démarré avec l’ancienne organisation v1, dans laquelle MemoryHigh= et MemorySwapMax= n’existent pas et où le comportement OOM par unité diffère. Ubuntu 22.04 et les versions ultérieures, ainsi que Debian 11 et les versions ultérieures, utilisent v2 par défaut. Une ancienne image ou un kernel démarré avec systemd.unified_cgroup_hierarchy=0 ne l’utilise pas.
Avec cgroup v2, systemd active par défaut le accounting mémoire pour chaque unité. Les valeurs sont donc déjà disponibles :
systemd-cgtop -mCette commande liste les cgroups triés par utilisation mémoire. C’est le moyen le plus rapide de déterminer « qu’est-ce qui consomme les ressources de ce serveur ? » tant qu’il peut encore répondre. Si le serveur est nouveau, le compte et le firewall présentés dans les dix premières minutes sur un nouveau VPS passent avant cette étape.
MemoryHigh ralentit. MemoryMax tue.
La différence entre ces deux paramètres mémoire détermine la forme de la panne.
MemoryHigh=est une limite souple. Au-delà, le kernel récupère agressivement de la mémoire dans ce cgroup et ralentit volontairement ses allocations. L’utilisation peut dépasser cette valeur, et aucun processus n’est tué.MemoryMax=est une limite stricte. Lorsqu’une allocation ne peut pas être satisfaite dans cette limite, l’OOM killer s’exécute dans ce cgroup et tue l’un des processus de cette unité.
C’est la deuxième partie qui justifie réellement de définir MemoryMax= pour tout ce que vous ne maîtrisez pas complètement. Sans limite, un manque de mémoire affecte tout le serveur, et l’OOM killer global choisit sa victime selon oom_score, ce qui désigne généralement le processus le plus volumineux. Il s’agit le plus souvent de votre base de données, et non du script qui a provoqué la fuite. Avec une limite, le processus tué appartient à l’unité qui a causé le problème.
Définissez les deux paramètres, avec MemoryHigh= environ 20 à 30 pour cent en dessous de MemoryMax=. L’écart constitue une zone d’alerte : une fuite lente dépasse High et se manifeste par un service qui ralentit, tandis qu’un pic soudain dépasse directement Max et provoque l’arrêt du processus.
Les valeurs en pourcentage sont calculées à partir de la mémoire physique installée. Ainsi, MemoryMax=25% sur une offre de 4 GB correspond à 1 GB et reste égal au quart du serveur après l’augmentation de l’offre. MemorySwapMax=0 empêche complètement cette unité d’utiliser le swap, ce qui transforme un ralentissement prolongé en arrêt rapide et évident.
Une limite doit être accompagnée d’une policy de redémarrage. Sinon, l’arrêt du processus laisse simplement le service arrêté.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit* doit être placé dans [Unit] et Restart= dans [Service]. Si vous placez l’un de ces paramètres dans la mauvaise section, systemd l’ignore. Cinq redémarrages en cinq minutes indiquent une fuite plutôt qu’un incident ponctuel. Après cela, systemd abandonne et laisse l’unité en échec. C’est l’état que vous voulez retrouver plus tard, plutôt qu’une boucle de crash qui masque le problème.
Limiter le CPU avec CPUQuota ou le partager avec CPUWeight
CPUQuota= utilise un pourcentage du temps disponible sur un CPU. CPUQuota=50% correspond à la moitié d’un cœur. CPUQuota=200% équivaut à deux cœurs, que l’unité peut répartir sur autant de threads qu’elle le souhaite. Avec une offre de 2 vCPU, CPUQuota=200% correspond à la totalité de la machine.
CPUWeight= constitue le meilleur choix par défaut pour la plupart des services. Il définit une part relative comprise entre 1 et 10000, tandis que la valeur par défaut du kernel est 100. Cette pondération n’intervient que lorsqu’il y a concurrence : une tâche de sauvegarde à CPUWeight=20 cède du temps CPU à un serveur web configuré à 100 lorsque la machine est sollicitée, tout en pouvant utiliser la totalité de la machine lorsqu’elle est inactive. Un quota strict gaspille cette capacité disponible.
Soyez précis sur ce qu’une limite CPU permet d’obtenir. Un processus limité par le CPU bloque rarement Linux, car le scheduler continue d’attribuer du temps CPU aux autres processus. C’est la mémoire qui peut mettre une machine hors service. Utilisez CPUQuota= lorsque vous voulez un plafond prévisible, par exemple pour une compilation ou un agent qui s’exécuterait sinon à pleine capacité pendant une heure. Le dimensionnement de ce type de charge est une question distincte, traitée dans la quantité de RAM et de CPU nécessaire à un VPS pour un agent de codage.
Si le CPU paraît occupé alors qu’aucun de vos processus ne fait grand-chose, la cause peut se trouver de l’autre côté de l’hyperviseur. Il s’agit du temps de vol CPU causé par un voisin bruyant, et aucun quota que vous définissez ne changera cela.
TasksMax arrête une boucle de fork
TasksMax= correspond au nombre de processus et de threads qu’une unité peut contenir. Les threads sont comptés. Un service Java ou Go nécessite donc une marge plus importante que ne le laisse penser la liste des processus. C’est la protection la moins coûteuse contre un script qui exécute un fork en boucle, car l’appel fork échoue dans l’unité au lieu d’épuiser les identifiants de processus du serveur.
TasksMax=128Lorsqu’une unité atteint la limite, le kernel journalise une ligne qui indique le cgroup :
cgroup: fork rejected by pids controller in /system.slice/myapp.serviceLe programme lui-même signale généralement fork: retry: Resource temporarily unavailable. Vérifiez la valeur appliquée par défaut par le manager avec systemctl show -p DefaultTasksMax.
Limiter une tâche ponctuelle avec systemd-run
Vous n’avez pas besoin d’un unit file pour utiliser cette fonctionnalité. systemd-run crée une unité transitoire autour d’une seule commande.
sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh--scope exécute la commande dans votre terminal après avoir affiché Running scope as unit: run-r7c1a....scope. La sortie reste affichée à l’écran et les limites disparaissent lorsque la commande se termine. Toutes les propriétés de systemd.resource-control fonctionnent après -p.
Pour une tâche longue, supprimez --scope et attribuez-lui un nom. Elle s’exécute alors en arrière-plan comme un service transitoire et écrit ses journaux dans le journal systemd :
sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -fLes mêmes options fonctionnent avec --user lorsque vous n’êtes pas root. Toutefois, votre user manager ne dispose que des controllers qui lui ont été délégués, et une propriété peut donc y être refusée. Exécutez-la avec sudo dans ce cas. Lorsqu’une tâche nécessite une configuration permanente, transférez les paramètres tels quels dans une véritable unité : voir exécuter un script comme service et timer systemd.
La question du swap, avec une réponse honnête
Le swap modifie la forme de la panne au lieu de l’empêcher.
Sans swap, une fuite atteint la limite et un processus meurt en quelques secondes. La panne est brutale, brève et facile à comprendre ensuite dans le journal. Avec du swap, le kernel écrit sur le disque les pages anonymes froides et gagne du temps. Si le processus allait se stabiliser, le swap vous sauve. S’il continue à consommer de la mémoire, le swap transforme une panne de cinq secondes en un blocage de vingt minutes. Ce blocage est pire, car un processus arrêté vous laisse quand même un shell fonctionnel, contrairement à une machine en thrashing.
swapon --show
free -hSur un petit VPS, un compromis viable consiste à conserver un fichier de swap modeste pour les pages allouées une seule fois et jamais réutilisées, puis à définir MemorySwapMax=0 sur les unités que vous acceptez de perdre. Les services importants conservent leur swap. Les services imprévisibles atteignent rapidement la limite et redémarrent.
Réduire vm.swappiness est un levier limité, et il est utile de comprendre pourquoi. Ce réglage modifie seulement l’équilibre entre l’éviction du page cache et le swap des pages anonymes. Dans les deux cas, une lecture sur disque sera nécessaire plus tard. Il détermine quelles pages entrent en thrashing, mais pas si la machine entre en thrashing.
Un daemon OOM précoce tue les processus avant le blocage
Le noyau attend que la récupération de mémoire échoue complètement. Sur un petit VPS, cette attente correspond exactement à la fenêtre pendant laquelle vous perdez la machine. Deux daemons en espace utilisateur réduisent cette fenêtre en surveillant eux-mêmes la mémoire et en tuant les processus plus tôt.
earlyoom surveille la mémoire disponible et le swap libre. Il tue le processus ayant le score le plus élevé lorsque l’une de ces valeurs passe sous un seuil.
sudo apt install earlyoom
systemctl status earlyoomLe paquet Debian et Ubuntu démarre le service lors de l’installation. Ses options se trouvent dans /etc/default/earlyoom :
EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"-m PERCENT définit le minimum de mémoire disponible et -s PERCENT le minimum de swap libre, avec une valeur par défaut de 10 pour cent dans les deux cas. Le deuxième nombre de chaque paire correspond au seuil de SIGKILL : earlyoom envoie SIGTERM lorsque la valeur passe sous le premier seuil, puis SIGKILL sous le deuxième. Par défaut, ce dernier correspond à la moitié du premier. Appliquez une modification avec sudo systemctl restart earlyoom, puis consultez journalctl -u earlyoom pour voir quel processus a été tué et quelle quantité de mémoire il occupait.
systemd-oomd est l’autre option. Sa page de manuel le décrit comme « un service système qui utilise cgroups-v2 et les informations de pression (PSI) pour surveiller le système et prendre des mesures correctives avant qu’un OOM ne survienne dans l’espace du noyau ». Il agit sur des cgroups entiers plutôt que sur des processus individuels. Il tue donc une unité, et non un processus enfant isolé. Les unités activent cette fonction avec ManagedOOMMemoryPressure=kill ou ManagedOOMSwap=kill, et les seuils se trouvent dans /etc/systemd/oomd.conf.
systemctl status systemd-oomd
oomctloomctl affiche ce qu’il surveille actuellement. Sur une image serveur, il ne surveille souvent rien, car le paramètre doit être activé explicitement pour chaque unité. Choisissez un seul daemon et arrêtez-vous là. Exécuter les deux daemons signifie qu’ils tentent tous les deux de choisir une victime. Il devient alors plus difficile de déterminer la raison d’un kill.
Quelle unité était responsable ?
Commencez par le kernel, car il enregistre chaque kill qu’il effectue.
journalctl -k --grep "Killed process" --since "2 hours ago"Un kill du global OOM killer ressemble à ceci :
Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0anon-rss correspond à la mémoire que le processus occupait en RAM au moment de sa terminaison, environ 1.8 GB ici. Interprétez le nom entre crochets avec prudence. Il s’agit de la victime choisie par le kernel. Or le kernel choisit le plus gros processus, qui n’est pas toujours celui à l’origine du manque de mémoire.
Un kill provoqué par une limite de cgroup possède un préfixe différent. Le rapport affiché au-dessus indique le cgroup qui a atteint sa propre limite :
Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0Ce préfixe fournit l’essentiel du diagnostic. Memory cgroup out of memory signifie qu’une unité a atteint la MemoryMax= que vous lui avez attribuée, tandis que le reste du système fonctionnait normalement. Un simple Out of memory signifie que la machine entière n’avait plus de mémoire disponible. Vos limites étaient donc absentes ou trop élevées pour leur capacité cumulée.
Demandez ensuite à systemd ce qu’il a observé :
systemctl status myapp.service
journalctl -u myapp.service -n 50myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.systemctl status indique la même chose sur une seule ligne, sous la forme Active: failed (Result: oom-kill).
Les compteurs du cgroup constituent la troisième source d’information. Ce sont les seuls à enregistrer le throttling, qui ne produit jamais de ligne dans les journaux :
cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peaklow 0
high 4213
max 118
oom 12
oom_kill 12high compte le nombre de fois où l’unité a dépassé MemoryHigh= et a été soumise au throttling. max compte le nombre de fois où elle a atteint la limite stricte, et oom_kill compte les processus effectivement tués. Une valeur élevée de high avec oom_kill 0 correspond au cas silencieux décrit précédemment : le service fonctionne, fortement ralenti, sans avoir signalé de défaillance. memory.peak (Linux 5.19 et versions ultérieures) contient l’utilisation maximale atteinte par le cgroup. C’est cette valeur qui sert à dimensionner MemoryMax=. Les deux fichiers sont réinitialisés au redémarrage de l’unité, car systemd recrée alors le cgroup.
Un prérequis sous-jacent est indispensable. Si /var/log/journal n’existe pas, le journal reste en RAM et chaque ligne disparaît après le redémarrage nécessaire pour récupérer la machine.
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-bootsLe fait que journalctl --list-boots affiche plus que le boot courant signifie que l’historique est désormais conservé. journalctl -k -b -1 peut donc afficher les messages du kernel issus du boot qui a échoué.
Point de départ pour un petit VPS
Sur une offre de 2 GB, laissez 300 à 400 MB au kernel et au page cache. Ne faites pas en sorte que la somme des limites atteigne la totalité des 2 GB, car chaque unité peut atteindre son pic en même temps. Accordez la plus grande part au service prioritaire, puis limitez les services plus spéculatifs qui l’entourent.
[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5sConserver un moyen d’accès justifie un réglage supplémentaire. OOMScoreAdjust=-500 dans un drop-in pour ssh.service réduit fortement le risque que l’OOM killer global choisisse votre démon SSH comme victime. Cela peut faire la différence entre réparer le serveur et le redémarrer depuis le control panel. Ce réglage ne modifie que le choix de la victime par le kernel. Il ne réduit pas la durée du blocage.
Les conteneurs s’exécutent dans leurs propres cgroups, créés par le runtime de conteneurs et non par vos fichiers d’unité. Une limite sur docker.service ne devient donc pas une limite appliquée à un conteneur. Les équivalents par conteneur de MemoryMax= et CPUQuota= sont présentés dans définir des limites de mémoire et de CPU dans Docker Compose.
FAQ
Pourquoi mon VPS s’est-il figé au lieu de tuer le processus qui s’emballait ?
Parce que le noyau évalue la progression selon que la récupération libère des pages, et non selon le temps nécessaire. Lorsque la mémoire manque, il évacue le cache de pages, y compris les pages exécutables des programmes en cours, puis les relit à l’instruction suivante. Tout attend le stockage et aucune allocation n’a techniquement échoué. Le OOM killer n’est donc jamais appelé. Consultez /proc/pressure/memory pendant l’incident : une valeur full avg10 supérieure à 40 signifie que presque aucune tâche n’a pu s’exécuter au cours des dix dernières secondes. Un daemon userspace tel que earlyoom tue les processus avant que la machine n’atteigne cet état.
Quelle est la différence entre MemoryHigh et MemoryMax ?
MemoryHigh= est une limite souple qui ralentit le service. Le noyau récupère agressivement la mémoire de l’unité et ralentit ses allocations, mais l’utilisation peut dépasser cette valeur et aucun processus n’est tué. MemoryMax= est une limite stricte : une allocation qui ne peut pas être satisfaite dans cette limite déclenche le OOM killer à l’intérieur du cgroup de cette unité. Le processus à l’origine du problème est donc tué, plutôt que le plus gros processus de la machine. Définissez MemoryHigh= à une valeur inférieure à MemoryMax= et utilisez l’écart entre les deux comme zone d’avertissement.
Comment déterminer quel service le OOM killer a touché ?
Exécutez journalctl -k --grep "Killed process" --since "2 hours ago". Une ligne commençant par Memory cgroup out of memory signifie qu’une unité a atteint son propre MemoryMax=, tandis qu’un simple Out of memory signifie que toute la machine a manqué de mémoire. Exécutez ensuite journalctl -u <unit> -n 50 et recherchez Failed with result 'oom-kill'. Si /var/log/journal n’existe pas sur votre serveur, le journal était conservé en RAM et les informations ont été perdues au redémarrage. Créez donc ce répertoire avant le prochain incident.
Dois-je ajouter du swap à un petit VPS ?
Un petit fichier swap peut aider pour les pages froides qui sont allouées une fois puis ne sont plus utilisées. Il n’aide pas un processus qui s’emballe : il retarde son arrêt et transforme une courte interruption en un blocage prolongé auquel vous ne pouvez pas vous connecter pour remédier au problème. Gardez une quantité de swap modeste et définissez MemorySwapMax=0 sur les unités que vous acceptez de perdre. Elles atteindront ainsi leur limite et redémarreront rapidement, tandis que les services importants conserveront leur swap.
Puis-je limiter une commande sans écrire de fichier d’unité ?
Oui. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh exécute la commande dans votre terminal, à l’intérieur d’un scope transitoire soumis à ces limites. Les limites disparaissent lorsque la commande se termine. Toutes les propriétés de systemd.resource-control sont disponibles après -p. MemorySwapMax=, TasksMax= et CPUWeight= fonctionnent donc également. Supprimez --scope et ajoutez --unit=name pour exécuter la tâche en arrière-plan et envoyer sa sortie dans le journal.