tuto3: Rewrite OOM part
This commit is contained in:
parent
412d69a649
commit
6665cfbace
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user