virli/tutorial/3/oom.md

167 lines
6.6 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\newpage
Gestion de la mémoire
=====================
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]: 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ù, 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*.
## Trouver un coupable
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.
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` :
<div lang="en-US">
```bash
42sh$ cat /proc/self/oom_score
666
42sh$ cat /proc/1/oom_score
0
```
</div>
Le score est établi entre 0 et 1000.
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.
::::: {.exercice}
#### À vous de jouer {-}
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 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 certains 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 franchit 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.