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