lunes, 18 de enero de 2016

Memoria en Linux Parte I "Overcommit"

En este apartado voy a intentar entender y demostrar el funcionamiento del overcommit de memoria en Linux. Como no es novedad la memoria es un recurso finito y muy valioso para un servidor, por este motivo entender cómo funciona su administración en detalle es muy importante.

El overcommit es una propiedad del kernel que definirá si es posible o no (o en que rangos) pedir mas memoria de la que hay disponible. A priori parece un tanto suicida, cierto? Por qué querríamos asignar mas memoria de la disponible? Bueno en realidad se trata de una especie de apuesta que hace el kernel. Como en todas las apuestas uno puede ganar o perder, y el kernel no esta exento de esta regla.

Cuándo gana el kernel? Gana esencialmente en los siguientes escenarios:

-Cuando el proceso que pidió mucha memoria jamás la utiliza.
-Cuando es capaz de proveer la memoria necesaria, de la forma que sea.

El parámetro que define el comportamiento del kernel con respecto al overcommit es /proc/sys/vm/overcommit_memory o vm.overcommit_memory y puede almacenar uno de los siguientes 3 valores:

-0- El kernel admite overcommit y lo controla a partir de información heurística y decide si otorgar la memoria al proceso o no. Esta opción reduce las posibilidades de dejar sin memoria al sistema, pero incrementa el overhead a la hora de asignar memoria. Esta es la configuración por defecto de Ubuntu Server 14.04.
-1- El kernel hace overcommit ilimitado y no realiza ningún control con respecto a la cantidad de memoria que pidan los procesos. Esta opción incrementa bastante la probabilidad de dejar al sistema sin memoria, pero a la vez simplifica la administración de esta y eso podría mejorar la performance de terminadas aplicaciones.
-2- El kernel no hace overcommit y el sistema se compromete a dar un espacio total de memoria de la siguiente manera: la suma de la Swap disponible mas el porcentaje especificado en "/proc/sys/vm/overcommit_ratio" de la memoria física del sistema. Esta tercera opción intenta ofrecer un termino medio, facilitando la decisión a la hora de asignar y definiendo un límite más concreto para evitar dolores de cabeza. Solo se recomienda utilizar esta opción si se cuenta con buena cantidad de memoria swap, mas que memoria RAM al menos.

Como pueden ver las tres aproximaciones tienen ventajas y desventajas, y como tales no existe la fórmula perfecta. La mejor de ellas será la que mejor se ajuste al caso de uso particular.

Ahora veamos qué comportamiento podemos esperar de cada una de estas opciones. El escenario de prueba es el siguiente:

-Máquina virtual:

juan@ubuntu-server:~$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.3 LTS"
juan@ubuntu-server:~$ uname -a
Linux ubuntu-server 3.19.0-43-generic #49~14.04.1-Ubuntu SMP Thu Dec 31 15:44:49 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
juan@ubuntu-server:~$


-Memoria disponible:

juan@ubuntu-server:~$ free -m
             total       used       free     shared    buffers     cached
Mem:           489        102        387          0         10         50
-/+ buffers/cache:         41        447
Swap:               0          0          0
juan@ubuntu-server:~$


NOTA: La memoria swap se encuentra desactivada para simplificar las cosas. Esto no debería afectar en nada a las pruebas.

El siguiente programita en C es utilizado para simular la asignación de memoria.
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>

int main(int argc, char **argv)
{
        unsigned long size;
        int N,S;
        N=atoi(argv[1]);
        S=atoi(argv[2]);
        size=(1024*1024*(unsigned long)N);
        char *a;
        a=malloc(size);
        if(a==NULL && errno == ENOMEM)
        {
                printf("Memoria insuficiente para asignar %ld Bytes\n",size);
                return -1;
        }
        sleep(S);
        printf("%ld Bytes\n",size);
        free(a);
        return 0;
}
El programa recibe dos parámetros, el primero define la cantidad de MB de memoria a reservar y el segundo la cantidad de segundos que el programa quedará en estado Sleep, esto último es solo a fines de simplificar las pruebas. Luego de leídos los parámetros, intenta reservar size bytes de memoria con malloc.

NOTA: a diferencia de calloc, malloc NO inicializa la memoria. Otro punto importante, es dada la estrategia de asignación optimista de memoria, un puntero NO nulo devuelto por malloc NO garantiza que haya memoria suficiente en el sistema para respaldar la memoria asignada.


Prueba 1: overcommit_memory=0


En esta prueba veremos el comportamiento del kernel cuando overcommit_memory es 0.

root@ubuntu-server:/home/juan/mem_tests# sysctl vm.overcommit_memory
vm.overcommit_memory = 0
root@ubuntu-server:/home/juan/mem_tests#


Según la documentación del kernel, este parámetro habilita el overcommit con un algoritmo heurístico, es decir que a partir de la utilización de memoria y algún que otro dato mas el kernel decide si asignar o no la memoria pedida. Entonces a partir de la siguiente situación

root@ubuntu-server:/home/juan/mem_tests# free -m
             total       used       free     shared    buffers     cached
Mem:           489         79        409          0         17         22
-/+ buffers/cache:         39        449
Swap:               0          0          0
root@ubuntu-server:/home/juan/mem_tests#


409 MBytes libres, intentemos asignar 410MBytes:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 410 30 &
[1] 1151
root@ubuntu-server:/home/juan/mem_tests# free -m
             total       used       free     shared    buffers     cached
Mem:           489         79        409          0         17         22
-/+ buffers/cache:         39        449
Swap:            0          0          0
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1151 424040   624 ./mem-eater 410 30
root@ubuntu-server:/home/juan/mem_tests# 429916160 Bytes

[1]+  Done                    ./mem-eater 410 30
root@ubuntu-server:/home/juan/mem_tests#


Podemos ver con free que la memoria utilizada no cambió en nada. Sin embargo, ps muestra que la asignación se hizo con éxito a partir del valor de la columna VSZ (virtual size). Dado que la memoria nunca se inicializó, nunca se asignó efectivamente a memoria física.

NOTA: VSZ representa la memoria virtual asignada al proceso en unidades de 1 Kbyte. RSZ representa la memoria residente del proceso (NO swap), en unidades de 1 Kbyte, con residente nos referimos a memoria del proceso que se encuentra respaldada en memoria física.

Si consideramos la memoria utilizada para buffers y para cache, podríamos decir que 410 MB se podrían satisfacer fácilmente si fuese necesario. Pero qué sucede si volvemos a lanzarlo pero pidiendo por más, 450MB?

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 450 30 &
[1] 1161
root@ubuntu-server:/home/juan/mem_tests# free -m
             total       used       free     shared    buffers     cached
Mem:           489         79        409          0         17         22
-/+ buffers/cache:         39        449
Swap:            0          0          0
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1161 465000   652 ./mem-eater 450 30
root@ubuntu-server:/home/juan/mem_tests# 471859200 Bytes

[1]+  Done                    ./mem-eater 450 30
root@ubuntu-server:/home/juan/mem_tests#


Ni un problema! Y si voy por 500MB?

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 500 30 &
[1] 1150
root@ubuntu-server:/home/juan/mem_tests# No se pudo allocar 524288000 Bytes

[1]+  Exit 255                ./mem-eater 500 30
root@ubuntu-server:/home/juan/mem_tests#


Vemos que 500MB ya fue demasiado y el kernel no nos asignó la memoria. Claro que 500MB es poco mas del total de memoria física real disponible en el sistema, 489MB así que podríamos decir que es razonable.

Prueba 2: overcommit_memory=1


En esta modalidad el kernel no hace ningún tipo de control a la hora de decidir cuanta memoria puede asignar a un determinado proceso, y por lo tanto se podría decir que es hace un overcommit ilimitado. Veamos un ejemplo:

root@ubuntu-server:/home/juan/mem_tests# sysctl vm.overcommit_memory
vm.overcommit_memory = 1
root@ubuntu-server:/home/juan/mem_tests#


Qué sucede si intentamos asignar 500MBytes de memoria en esta modalidad?

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 500 30 &                   [1] 1175
root@ubuntu-server:/home/juan/mem_tests# free -m
             total       used       free     shared    buffers     cached
Mem:           489         77        411          0         17         21
-/+ buffers/cache:         39        450
Swap:            0          0          0
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1175 516200   792 ./mem-eater 500 30
root@ubuntu-server:/home/juan/mem_tests# 524288000 Bytes

[1]+  Done                    ./mem-eater 500 30
root@ubuntu-server:/home/juan/mem_tests#


Una vez mas desde ps podemos ver que el kernel no nos impidió la asignación. Probemos duplicando la cantidad de memoria:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 1000 30 &
[1] 1181
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1181 1028200  672 ./mem-eater 1000 30
root@ubuntu-server:/home/juan/mem_tests# 1048576000 Bytes

[1]+  Done                    ./mem-eater 1000 30
root@ubuntu-server:/home/juan/mem_tests#


está claro que el kernel no piensa intervenir. Ni siquiera si vamos por 4000MB de memoria:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 4000 20 &
[1] 1314
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1314 4100200  628 ./mem-eater 4000 20
root@ubuntu-server:/home/juan/mem_tests#


Esta modalidad es extremadamente laxa y permite asignar cantidades ridículas de memoria, malloc jamás devolverá NULL. Esto podría llevar sencillamente a situaciones donde el sistema quede sin memoria disponible.

Prueba 3: overcommit_memory=2


El último valor posible para overcommit_memory es 2. Esta modalidad de overcommit introduce una segunda variable a la ecuación, la misma se llama overcommit_ratio (también podría utilizarse overcommit_kbytes para un valor fijo en lugar de un porcentaje). Cuando overcommit_memory es 2, overcommit_ratio define el porcentaje de memoria física que se consideraría a la hora del overcommit. El sistema no se comprometerá a asignar mas memoria que la suma de la swap disponible y la memoria física indicada por el porcentaje de overcommit_ratio. Por defecto el overcommit es del 50% de la memoria:

root@ubuntu-server:/home/juan/mem_tests# sysctl vm.overcommit_memory
vm.overcommit_memory = 2
root@ubuntu-server:/home/juan/mem_tests# sysctl vm.overcommit_ratio
vm.overcommit_ratio = 50
root@ubuntu-server:/home/juan/mem_tests#


Podemos ver el commit limit en /proc/meminfo:

root@ubuntu-server:/home/juan/mem_tests# grep CommitLimit /proc/meminfo
CommitLimit:      250480 kB
root@ubuntu-server:/home/juan/mem_tests#


CommitLimit es la mitad de la memoria del sistema:

root@ubuntu-server:/home/juan/mem_tests# cat /proc/meminfo | grep MemTotal
MemTotal:         500964 kB

root@ubuntu-server:/home/juan/mem_tests#


Entonces qué sucede si queremos reservar 200MBytes de memoria?

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 200 20 &
[1] 1598
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd

  PID    VSZ   RSZ CMD
 1598 209000   616 ./mem-eater 200 20
root@ubuntu-server:/home/juan/mem_tests# 209715200 Bytes

[1]+  Done                    ./mem-eater 200 20
root@ubuntu-server:/home/juan/mem_tests#


To bien, pero si intento pasar de los 200MBytes ya no me asigna la memoria:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 230 20 &
[1] 1600
root@ubuntu-server:/home/juan/mem_tests# Memoria insuficiente para asignar 241172480 Bytes

[1]+  Exit 255                ./mem-eater 230 20
root@ubuntu-server:/home/juan/mem_tests#


mmmm, raro, no? Bastante antes de los 250MBytes ya nos rechaza la asignación. Como el límite aplica al sistema en general, probablemente algunos de los servicios corriendo está reservando la memoria que a mi se me está denegando.

Podemos ver la memoria comiteada en /proc/meminfo:

root@ubuntu-server:/home/juan/mem_tests# grep Commit /proc/meminfo
CommitLimit:      250480 kB
Committed_AS:      25516 kB

root@ubuntu-server:/home/juan/mem_tests#


Vemos que de los aproximadamente 250MB unos 25MB ya están prometidos a alguien mas, por lo tanto lo restante debería estar a nuestra disposición:
 
root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 219 20 &
[1] 1614
root@ubuntu-server:/home/juan/mem_tests# grep Commit /proc/meminfo
CommitLimit:      250480 kB
Committed_AS:     249780 kB

root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1614 228456   736 ./mem-eater 219 20
root@ubuntu-server:/home/juan/mem_tests# 229638144 Bytes

[1]+  Done                    ./mem-eater 219 20
root@ubuntu-server:/home/juan/mem_tests#


Si nos pareciese razonable, podríamos elevar el radio de commit:

root@ubuntu-server:/home/juan/mem_tests# sysctl -w vm.overcommit_ratio=75
vm.overcommit_ratio = 75
root@ubuntu-server:/home/juan/mem_tests# sysctl vm.overcommit_ratio
vm.overcommit_ratio = 75

root@ubuntu-server:/home/juan/mem_tests# grep Commit /proc/meminfo
CommitLimit:      375720 kB
Committed_AS:      25300 kB
root@ubuntu-server:/home/juan/mem_tests#


Una vez ampliado el limite podremos asignar mas memoria:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater 330 20 &
[1] 1645
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1645 342120   620 ./mem-eater 330 20
root@ubuntu-server:/home/juan/mem_tests# grep Commit /proc/meminfo
CommitLimit:      375720 kB
Committed_AS:     363444 kB
root@ubuntu-server:/home/juan/mem_tests# 346030080 Bytes

[1]+  Done                    ./mem-eater 330 20
root@ubuntu-server:/home/juan/mem_tests#

Y qué pasa cuando la memoria asignada no se puede respaldar con memoria física real?


Veamos cuál es el comportamiento del sistema cuando la memoria asignada al proceso no puede ser realmente respaldada por memoria física. Para esto modificamos un poco a mem-eater de la siguiente manera:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>

int main(int argc, char **argv)
{
        unsigned long size,i;
        int N,S;
        N=atoi(argv[1]);
        S=atoi(argv[2]);
        size=(1024*1024*(unsigned long)N);
        char *a;
        a=malloc(size);
        if(a==NULL && errno == ENOMEM)
        {
                printf("Memoria insuficiente para asignar %ld Bytes\n",size);
                return -1;
        }
        for(i=0;i<size;i++)
        {
                *(a+i)=0;
        }
        sleep(S);
        printf("%ld Bytes\n",size);
        free(a);
        return 0;
}
el nuevo código, no solo reserva la memoria, sino que ahora la inicializa escribiendo ceros en ella. Esto va a forzar al kernel a intentar asignar memoria física a la memoria reservada por malloc.

Veamos qué sucede cuando alocamos memoria que podemos respaldar con memoria física (estas pruebas se hacen con overcommit_memory=0):

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater-write 250 30 &
[1] 1832
root@ubuntu-server:/home/juan/mem_tests# ps -C mem-eater-writer -o pid,vsz,rsz,cmd
  PID    VSZ   RSZ CMD
 1832 260200 252880 ./mem-eater-write 250 30
root@ubuntu-server:/home/juan/mem_tests# free -m
             total       used       free     shared    buffers     cached
Mem:           489        330        158          0         18         20
-/+ buffers/cache:        291        197
Swap:            0          0          0
root@ubuntu-server:/home/juan/mem_tests# 262144000 Bytes

[1]+  Done                    ./mem-eater-write 250 30
root@ubuntu-server:/home/juan/mem_tests#


vemos como la memoria residente ahora representa a la memoria asignada e inicializada por mem-eater-writer, esto tambien se puede ver reflejado en free con el incremente de memoria used.

Pero la idea era romper o no?, asi que rompamos! No hay que ir muy lejos para esto, intentando asignar 445MBytes llegamos a lo siguiente:

root@ubuntu-server:/home/juan/mem_tests# ./mem-eater-write 445 30 &
[1] 1235
root@ubuntu-server:/home/juan/mem_tests#
[1]+  Killed                  ./mem-eater-write 445 30
root@ubuntu-server:/home/juan/mem_tests#


jum... el proceso fue asecinado, pero no hay demasiados datos del motivo a simple vista. Sin embargo, si urgamos un poco en los logs encontraremos la respuesta:

root@ubuntu-server:/home/juan/mem_tests# tail -40 /var/log/syslog
Jan 17 21:58:57 ubuntu-server kernel: [79611.296729] Free swap  = 0kB
Jan 17 21:58:57 ubuntu-server kernel: [79611.296729] Total swap = 0kB
Jan 17 21:58:57 ubuntu-server kernel: [79611.296730] 130958 pages RAM
Jan 17 21:58:57 ubuntu-server kernel: [79611.296731] 0 pages HighMem/MovableOnly
Jan 17 21:58:57 ubuntu-server kernel: [79611.296732] 5717 pages reserved
Jan 17 21:58:57 ubuntu-server kernel: [79611.296733] 0 pages cma reserved
Jan 17 21:58:57 ubuntu-server kernel: [79611.296734] 0 pages hwpoisoned
Jan 17 21:58:57 ubuntu-server kernel: [79611.296734] [ pid ]   uid  tgid total_vm      rss nr_ptes swapents oom_score_adj name
Jan 17 21:58:57 ubuntu-server kernel: [79611.296740] [  297]     0   297     4870       45      15        0             0 upstart-udev-br
Jan 17 21:58:57 ubuntu-server kernel: [79611.296743] [  302]     0   302    12883      172      27        0         -1000 systemd-udevd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296779] [  368]   102   368     9808       96      22        0             0 dbus-daemon
Jan 17 21:58:57 ubuntu-server kernel: [79611.296781] [  396]     0   396    10864       88      27        0             0 systemd-logind
Jan 17 21:58:57 ubuntu-server kernel: [79611.296783] [  403]   101   403    63962      149      27        0             0 rsyslogd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296786] [  428]     0   428     3853       74      13        0             0 upstart-file-br
Jan 17 21:58:57 ubuntu-server kernel: [79611.296788] [  547]     0   547     2559      574       9        0             0 dhclient
Jan 17 21:58:57 ubuntu-server kernel: [79611.296790] [  574]     0   574     3816       55      13        0             0 upstart-socket-
Jan 17 21:58:57 ubuntu-server kernel: [79611.296792] [  824]     0   824     3956       42      13        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296795] [  827]     0   827     3956       39      13        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296797] [  832]     0   832     3956       40      13        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296799] [  833]     0   833     3956       39      13        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296801] [  835]     0   835     3956       41      13        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296804] [  861]     0   861    15344      170      35        0         -1000 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296806] [  863]     0   863     5915       62      16        0             0 cron
Jan 17 21:58:57 ubuntu-server kernel: [79611.296808] [  864]     0   864     4786       40      15        0             0 atd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296810] [  882]     0   882     1093       38       8        0             0 acpid
Jan 17 21:58:57 ubuntu-server kernel: [79611.296812] [  982]     0   982     3956       40      12        0             0 getty
Jan 17 21:58:57 ubuntu-server kernel: [79611.296814] [ 1045]     0  1045    30532      313      62        0             0 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296817] [ 1049]  1000  1049    32145      328      61        0             0 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296819] [ 1126]  1000  1126    30532      311      59        0             0 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296821] [ 1127]  1000  1127     5606      446      17        0             0 bash
Jan 17 21:58:57 ubuntu-server kernel: [79611.296823] [ 1152]     0  1152    30494      291      63        0             0 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296825] [ 1201]  1000  1201    30494      287      60        0             0 sshd
Jan 17 21:58:57 ubuntu-server kernel: [79611.296828] [ 1202]  1000  1202     5606      441      17        0             0 bash
Jan 17 21:58:57 ubuntu-server kernel: [79611.296830] [ 1216]     0  1216    20901      198      46        0             0 sudo
Jan 17 21:58:57 ubuntu-server kernel: [79611.296832] [ 1217]     0  1217    20699      157      44        0             0 su
Jan 17 21:58:57 ubuntu-server kernel: [79611.296833] [ 1218]     0  1218     5303      161      14        0             0 bash
Jan 17 21:58:57 ubuntu-server kernel: [79611.296836] [ 1235]     0  1235   114970   112658     228        0             0 mem-eater-write
Jan 17 21:58:57 ubuntu-server kernel: [79611.296837] Out of memory: Kill process 1235 (mem-eater-write) score 874 or sacrifice child
Jan 17 21:58:57 ubuntu-server kernel: [79611.296979] Killed process 1235 (mem-eater-write) total-vm:459880kB, anon-rss:450632kB, file-rss:0kB

root@ubuntu-server:/home/juan/mem_tests#


Long story short, OOM (Out Of Memory) killer mató al proceso porque el sistema se encontró en una situación de poca memoria (memory pressure). En otra oportunidad veremos el OOM y cómo elige qué proceso matar, pero básicamente lo elige a partir una evaluación y puntuación de todos los procesos. Todo esto significa que la sobre venta de memoria que hace el kernel se puede terminar pagando caro.

Entonces, el overcommit nos da la posibilidad de sobre vender el espacio de direcciones de los procesos, y así poder ejecutar mas procesos. Pero como pudimos comprobar, la sobre venta podría decantar en situaciones de poca memoria disponible en el sistema y el OOM killer podría entrar en juego.

Bibliografía


Man malloc - http://man7.org/linux/man-pages/man3/malloc.3.html
Man errno - http://man7.org/linux/man-pages/man3/errno.3.html
Man ps - http://man7.org/linux/man-pages/man1/ps.1.html
Memory accounting - https://www.kernel.org/doc/Documentation/vm/overcommit-accounting
VM - https://www.kernel.org/doc/Documentation/sysctl/vm.txt