tuto3: Rewrite OOM part

This commit is contained in:
nemunaire 2022-10-18 06:15:03 +02:00
parent 412d69a649
commit 6665cfbace
1 changed files with 134 additions and 33 deletions

View File

@ -3,55 +3,87 @@
Gestion de la mémoire
=====================
Linux a une gestion de la mémoire bien particulière[^vm-overcommit] : par
défaut, `malloc(3)` ne retournera jamais `NULL`. En se basant sur l'euristique
qu'un bloc mémoire demandé ne sera pas utilisé directement et que de nombreux
process ne feront pas un usage total des blocks qu'ils ont alloués, le noyau
permet d'allouer plus de mémoire qu'il n'y en a réellement disponible. La
mémoire est donc utilisée de manière plus efficace.
Linux a une gestion de la mémoire bien particulière[^vm-overcommit] : en effet,
par défaut, `malloc(3)` ne retournera jamais `NULL`. En se basant sur
l'euristique qu'un bloc mémoire demandé ne sera pas utilisé directement et que
de nombreux process ne feront pas un usage total des blocs qu'ils ont alloués,
le noyau permet d'allouer plus de mémoire qu'il n'y en a réellement
disponible. La mémoire est ainsi utilisée de manière plus efficace.
[^vm-overcommit]: Dépendant de la valeur de `/proc/sys/vm/overcommit_memory`,
[^vm-overcommit]: Cela dépend de la valeur de `/proc/sys/vm/overcommit_memory`,
généralement 0. Voir
<https://www.kernel.org/doc/html/latest/vm/overcommit-accounting.html>.
Mais évidemment, cela peut donner lieu à des situations où le noyau n'est plus
en mesure de trouver de blocs physiquement disponibles, alors qu'ils avaient
effectivement été alloués au processus. Pour autant, ce n'est pas une raison
pour tuer ce processus, car il est peut-être vital pour le système (peut-être
est-ce `init` qui est en train de gérer le lancement d'un nouveau daemon). On
dit alors que le noyau est *Out-Of-Memory*.
Mais évidemment, cela peut donner lieu à des situations où, au moment où un
processus se met à utiliser un nouveau bloc de mémoire (reçu du noyau d'un
appel précédent à `malloc(3)`) qu'il n'utilisait pas jusque là, le noyau se
trouve dans l'impossibilité d'attribuer un bloc physiquement disponible, car il
n'y en a tout simplement plus (y compris via le swap).
Puisque le noyau ne peut pas honorer sa promesse et qu'il n'a plus la
possibilité de retourner `NULL` au programme qui réclamme sa mémoire (il s'agit
sans doute d'une simple assignation de variable à ce stade), il faut trouver
une solution si l'on veut pouvoir continuer l'exécution du programme.
Le noyau pourrait choisir de suspendre l'exécution du processus tant qu'il n'y
a pas de nouveau bloc mémoire disponible... mais, à moins qu'un processus se
termine ou libère de la mémoire, on risque de se retrouver face à une situation
de faillite où tous les processus seraient suspendus, sans garantie qu'une
solution se produise à un moment donné.
Pour être certain de récupérer de la mémoire, le noyau n'a pas d'autre solution
que de tuer un processus. L'issue la plus simple est de tuer le processus qui
est en train d'accéder à la plage de mémoire que le noyau ne peut pas
honorer. Pour autant, ce n'est pas une raison pour tuer ce processus, car il
est peut-être vital pour le système (peut-être est-ce `init` qui est en train
de gérer le lancement d'un nouveau daemon). On dit alors que le noyau est
*Out-Of-Memory*.
Pour se sortir d'une telle situation, et après avoir tenté de vider les caches,
il lance son arme ultime pour retrouver au plus vite de la mémoire :
l'*Out-Of-Memory killer*.
## OOM killer
## Trouver un coupable
Selon un algorithme dont on raconte qu'il ne serait pas basé entièrement sur
l'aléatoire[^oom-algo], un processus est tiré au sort (plus un processus occupe
de mémoire et plus il a de chance d'être tiré au sort) par l'OOM killer. Le
sort qui lui est réservé est tout simplement une mort brutale, pour permettre
au système de disposer à nouveau de mémoire disponible. Si cela n'est pas
suffisant, un ou plusieurs autres processus peuvent être tués à tour de rôle,
jusqu'à ce que le système retrouve sa sérénité.
Tous les processus, au cours de leur exécution, disposent d'un score permettant
au système de savoir à tout moment quel processus apporterait le plus de gain à
être éliminé, si la mémoire venait à manquer.
[^oom-algo]: <https://linux-mm.org/OOM_Killer>
Lorsqu'une situation d'OOM est déclarée, le noyau tue le processus avec le
score le plus élevé à ce moment là.
Pour connaître le score actuel d'un processus, on affiche le contenu du fichier
`oom_score` dans son dossier de `/proc` :
## Esquiver l'OOM killer ?
<div lang="en-US">
```bash
42sh$ cat /proc/self/oom_score
666
42sh$ cat /proc/1/oom_score
0
```
</div>
Au sein d'un *cgroup* *memory*, les fichiers `memory.oom_control` (v1) ou
`memory.events` (v2) peuvent être utilisés afin de recevoir une notification du
noyau avant que l'OOM-killer ne s'attaque à un processus de ce groupe.
Le score est établi entre 0 et 1000.
Grâce à cette notification, il est possible de figer le processus pour
l'envoyer sur une autre machine. Et ainsi libérer la mémoire avant que l'OOM
killer ne passe.
Le but étant de récupérer le plus vite possible, le plus de mémoire possible,
le score est établi selon la quantité de mémoire que le processus occupe.
Voici un script pour visualiser les programmes ayant les plus gros scores sur
votre système :
<div lang="en-US">
```bash
ps -A | while read pid _ _ comm; do
echo $(cat /proc/$pid/oom_score) $comm;
done | sort -h
```
</div>
A priori, la dernière ligne montre le processus ayant le plus de chance d'être
tué en cas de passage proche de l'OOM-killer.
Jetez un œil à [cet article paru sur LWN](https://lwn.net/Articles/590960/) à
ce sujet :\
<https://lwn.net/Articles/590960/>
::::: {.exercice}
@ -60,6 +92,75 @@ ce sujet :\
Continuons l'exercice précédent où nous avions [fixé les
limites](#Fixer-des-limites) de mémoire que pouvaient réserver les processus de
notre groupe. Que se passe-t-il alors si `memhog` dépasse la quantité de
mémoire autorisée dans le `cgroup` ?
mémoire autorisée au sein du `cgroup` ?
<div lang="en-US">
```bash
42sh$ echo 512M > /sys/fs/cgroup/memory/MYNAME/memory.limit_in_bytes
42sh$ ./monitor MYNAME memhog 500
................ # OK
42sh$ ./monitor MYNAME memhog 700
..............Killed
```
</div>
:::::
Eh oui, l'OOM-killer passe également lorsqu'un `cgroup` atteint la limite de
mémoire qui lui est réservé. Dans ce cas évidemment, les processus pris en
compte sont ceux contenus dans le `cgroup`.
## Esquiver l'OOM killer ?
Le passage de l'OOM killer relevant parfois de la roulette russe, il peut être
intéressant, pour certain processus, de vouloir faire en sorte qu'ils aient
moins de chance d'arriver en tête du classement, même s'ils occupent beaucoup
de mémoire. On pourrait par exemple vouloir que des services importants ou des
programmes contenant notre travail en cours (potentiellement non-enregistré !)
ne soient pas ciblés à moins de ne plus avoir d'autre choix.
Le sujet étant très épineux, et aucune solution ou algorithme n'ayant
réellement démontré sa supériorité pour évincer la tâche idéale, nous avons la
possibilité de modifier le score à la hausse ou à la baisse.
Le fichier `oom_score_adj` peut contenir un valeur entre -1000 et +1000, cette
valeur vient s'ajouter au score du processus à chaque prochain calcul. Une
valeur négative va faire réduire le score et réduire d'autant les chances que
le processus soit ciblé, tandis qu'une valeur positive va augmenter le score et
donc accroître les chances que le processus soit ciblé.
La valeur spéciale de `-1000` fait que le processus ne sera pas considéré comme
une cible potentielle. Au même titre que les *thread*s du noyau ou le processus
`init` du système.
## Être notifié sur l'état de la mémoire
Au sein d'un *cgroup* *memory*, les fichiers `memory.oom_control` (v1) ou
`memory.events` (v2) peuvent être utilisés afin de recevoir une notification du
noyau avant que l'OOM-killer ne s'attaque à un processus de ce groupe.
L'idée est de créer un *eventfd* avec `eventfd(2)`, d'ouvrir le fichier
`memory.oom_control` ou `memory.events` du groupe pour lequel on veut être
notifié, pour obtenir un *file descriptor* ; enfin écrire dans le fichier
`cgroup.event_control` le numéro de l'*eventfd* puis du *file descriptor* sur
`memory.oom_control`, séparé par une espace.
On attend ensuite la notification avec :
<div lang="en-US">
```c
uint64_t ret;
read(<fd of eventfd()>, &ret, sizeof(ret));
```
</div>
D'autres notifications peuvent être mises en place, selon le même principe sur
d'autres fichiers, notamment `memory.usage_in_bytes`, pour être prévenu dès
lors que l'on franchi dans un sens ou dans l'autre un certain palier. Le palier
qui nous intéresse est à indiquer comme troisième argument à
`cgroup.event_control`.
Dans la version 2 des *cgroups*, il est même possible d'utiliser `inotify` pour
surveiller les changements.