Mostrando entradas con la etiqueta memoria. Mostrar todas las entradas
Mostrando entradas con la etiqueta memoria. Mostrar todas las entradas

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

lunes, 8 de junio de 2015

UDP en GNU/Linux Parte II

Esta entrada es la continuación de UDP en GNU/Linux Parte I y la idea es continuar con las pruebas para entender un poco mas cómo funciona el stack UDP en GNU/Linux.

En la publicación anterior quedó pendiente probar packet receive errors  en una condición de buffer insuficiente. Dijimos que esta condición se puede dar en un escenario donde los paquetes arriban al host a una tasa mayor que con la que son retirados del buffer del socket. Para comprobar esto vamos a precisar algunos números y un receptor UDP lento.

Receptor UDP lento

La verdad es que no se me ocurrió ninguna idea maravillosa para hacer un receptor lento de paquetes UDP, así que hurgando un poco en internet armé un pequeño cliente.c (si ¬¬ .c, ¿qué tiene de malo?). El cliente básicamente realiza las siguientes tareas:
  • Abre un socket UDP y escucha en el puerto 5555.
  • Imprime en pantalla el tamaño del buffer del socket.
  • Luego en un loop toma del buffer del socket 1 paquete por segundo e imprime en pantalla un contador de paquetes leidos.
Acá pueden ver una ejecución abortada de ejemplo:
juan@ubuntu:~$ ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840
^C
juan@ubuntu:~$ 


Como ven el tamaño del buffer por defecto es de 163840 bytes (160Kbytes). Este valor no es aleatorio ni nada que se le parezca, el tamaño está definido por la variable de kernel net.core.rmem_default

root@ubuntu:/home/juan# sysctl -a|grep rmem_default
net.core.rmem_default = 163840
root@ubuntu:/home/juan#


por ejemplo, se podría ampliar de la siguiente manera

root@ubuntu:/home/juan# sysctl -w net.core.rmem_default=1638400
net.core.rmem_default = 1638400
root@ubuntu:/home/juan# sysctl -a|grep rmem_default
net.core.rmem_default = 1638400
root@ubuntu:/home/juan# 


y se vería reflejado en la próxima ejecución de nuestro cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 1638400
^C
root@ubuntu:/home/juan#  


Entonces, resumiendo los números:
  • Tamaño del buffer de recepción 163840 bytes.
  • Enviaremos paquetes de 1024 bytes forjados con Hping3. Por lo tanto, 160 paquetes son suficientes para llenar el buffer.
  • Se enviarán 2000 paquetes a una tasa de 10 paquetes por segundo.
  • La aplicación consume 1 paquete cada 1 segundo.
Está claro que si esta situación se prolonga lo suficiente en el tiempo, en algún momento el buffer se llenará y el sistema deberá descartar paquetes. Si bien es una condición un poco exagerada a fines prácticos resulta útil.

Antes de lanzar nuestro cliente lento tomamos los valores de las estadísticas del sistema:

root@ubuntu:/home/juan# netstat -su
Udp:
    20 packets received
    0 packets to unknown port received.
    0 packet receive errors
    22 packets sent
...
root@ubuntu:/home/juan#


lanzamos el cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840


y apuntamos el cañón hping3 al host:

root@moon:/home/juan/pruebas# hping3 192.168.0.10 -2 -c 2000 --fast -E 1024 -d 1024 -p 5555 -V
using wlan0, addr: 192.168.0.2, MTU: 1500
HPING 192.168.0.10 (wlan0 192.168.0.10): udp mode set, 28 headers + 1024 data bytes
[main] memlockall(): Success
Warning: can't disable memory paging!

--- 192.168.0.10 hping statistic ---
2000 packets transmitted, 0 packets received, 100% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
root@moon:/home/juan/pruebas#


esta vez hay nuevos parámetros en hping3:
  • --fast: le indica que debe mandar 10 paquetes por segundo.
  • --E 1024: indica que debe meter como payload de los paquetes el contenido del archivo llamado 1024 (un archivo con 1024 bytes en 0).
  • -d 1024: este parámetro es el que realmente indica cuántos bytes del archivo indicado por -E se van a mandar como payload. En esta caso se arman datagramas de 1024 bytes.
  • -c 2000: indica que se enviarán 2000 paquetes.
en definitiva se enviaron 2000 paquetes con 1024 bytes como payload cada uno de ellos (en total se enviaron unos 2048000 bytes, claramente superamos el tamaño del buffer).

Vemos la salida de la aplicación (dado que la función de lectura es bloqueante, la aplicación no termina por sus propios medios después de leer el último paquete, luego de unos segundos sin nuevos paquetes la aborté con Ctrl+C)

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 163840
Paquete numero 0
Paquete numero 1
Paquete numero 2
Paquete numero 3
Paquete numero 4
Paquete numero 5
Paquete numero 6
Paquete numero 7
...

Paquete numero 279
Paquete numero 280
Paquete numero 281
Paquete numero 282

^C
root@ubuntu:/home/juan#


Entonces se enviaron 2000 paquetes pero nuestra aplicación lenta solo pudo leer 283 de ellos!!! A donde fueron los restantes mmmm 1717 paquetes?

root@ubuntu:/home/juan# netstat -su
Udp:
    303 packets received
    0 packets to unknown port received.
    1717 packet receive errors
    22 packets sent
...
root@ubuntu:/home/juan#


Claramente fueron al cielo de los paquetes. Fueron completamente descartados por el kernel debido a que no había espacio donde almacenarlos.

Dado que leemos 1 paquete por segundo y agregamos 10 por segundo, podríamos decir que llegan un neto de 9 paquetes por segundo al buffer. A 9 paquetes por segundo el buffer se llena en unos 17 segundos aproximadamente. Es decir que, si la matemática no me falla, a partir de los 17 segundos estaríamos perdiendo al menos 9 paquetes por segundo.

Hay dos maneras de enfrentar esta situación, dado que UDP no tiene control de flujo e implementarlo en la capa de aplicación sería un delirio, podemos:
  • Incrementar la eficiencia del consumidor, leyendo mas paquetes por segundo.
  • Aumentando el tamaño del buffer de recepción para poder soportar el asedio de paquetes sin perderlos.
Entonces supongamos que mejoramos el código y multiplicamos por 7 la eficiencia de la aplicación y ahora es capaz de leer 7 paquetes por segundo. Matemáticamente estamos en la misma (en el horno), estarían ingresando un neto de 3 paquetes por segundo, ahora el buffer se llenaría en 53 segundos y volvemos a tener pérdida de paquetes. El paso siguiente es aumentar el tamaño del buffer. Sabemos que vamos a recibir una ráfaga de 2000 paquetes de 1024 bytes, osea 2Mbytes es decir que podríamos configurar un buffer de 2MBytes y quedarnos re tranquilos, pero sería algo extremo.

Con el nuevo código llenamos el buffer en 53 segundos, osea que aún nos quedan 147 segundos con pérdida de paquetes. Pero ahora vamos a ampliar el buffer multiplicándolo por 4 (NOTA: vale aclarar que este nuevo cambio se aplicará a todos los nuevos sockets creados).

root@ubuntu:/home/juan# echo "163840*4"|bc
655360
root@ubuntu:/home/juan# sysctl -w net.core.rmem_default=655360
net.core.rmem_default = 655360

root@ubuntu:/home/juan# sysctl -w net.core.rmem_max=655360
net.core.rmem_max = 655360

root@ubuntu:/home/juan# sysctl -a | grep net.core.rmem_default
net.core.rmem_default = 655360
root@ubuntu:/home/juan# 


En teoría, con este nuevo buffer de solo 4 veces mayor tamaño no deberíamos ver pérdida de paquetes.

root@ubuntu:/home/juan# netstat -su 
Udp:
    2072 packets received
    0 packets to unknown port received.
    0 packet receive errors
    21 packets sent
root@ubuntu:/home/juan#


Ejecución cliente

root@ubuntu:/home/juan# ./cliente
--Socket creado!
-Configuracion del socket tamanio del buffer de recepcion: 655360
Paquete numero 7
Paquete numero 14
Paquete numero 21
Paquete numero 28
Paquete numero 35
Paquete numero 42
Paquete numero 49
Paquete numero 56

...
Paquete numero 1960
Paquete numero 1967
Paquete numero 1974
Paquete numero 1981
Paquete numero 1988
Paquete numero 1995
2000 paquetes leidos!!!

root@ubuntu:/home/juan#

Y corroboramos con las estadísticas:

root@ubuntu:/home/juan# netstat -su
Udp:
    4072 packets received
    0 packets to unknown port received.
    0 packet receive errors
    21 packets sent

root@ubuntu:/home/juan#

El aumento del tamaño del buffer y la mejora en la aplicación permitieron que esta vez no haya pérdida de paquetes.

miércoles, 31 de agosto de 2011

Usando tmpfs

Bien, entrada corta si las hay!!! Esto va a ser una PEQUEÑA demostración del uso de tmpfs. Qué es tmpfs??? Wiki knows (http://es.wikipedia.org/wiki/Tmpfs#Linux). Es una suerte de sistema de archivos en memoria volátil (RAM y swap si es necesario, ojo!!!).

Ventajas:
-Las operaciones de lectura y escritura son MUCHO mas rápidas que en otros medios de IO como los discos rígidos (IDEAL para montar directorios de CACHEs).
-Es sencillo de utilizar.
 Desventajas:
-Es volátil!!! no se mantiene luego de un reinicio del equipo por ejemplo.
-Su espacio está limitado por la cantidad de memoria RAM disponible y la swap del sistema.

Demostración simple de uso y rendimiento en escritura:

Creamos un directorio llamado prueba_tmpfs

root@moon:~# mkdir prueba_tmpfs

Antes de montar veamos cómo se el consumo de memoria con free -m.

root@moon:~# free -m
                     total       used       free     shared    buffers     cached
Mem:          1975       1167        807          0         25        538
-/+ buffers/cache:        603       1371
Swap:         5789          0       5789

Montamos un sistema de archivos tipo tmpfs (-t tmpfs) de 1GigaByte en el directorio que acabamos de crear.

root@moon:~# mount -t tmpfs -o size=1G tmpfs prueba_tmpfs/
Vemos los puntos de montaje existentes y nos encontramos con nuestro tmpfs recien montado.

root@moon:~# mount 
...
 tmpfs on /root/prueba_tmpfs type tmpfs (rw,size=1G)


Pegamos una mirada con free para ver qué pasó y vemos que si bien el directorio se montó, no se ocupó memoria del sistema aún.

root@moon:~# free -m
                     total       used       free     shared    buffers     cached
Mem:          1975       1167        807          0         25        538
-/+ buffers/cache:        603       1371
Swap:         5789          0       5789

Veamos qué beneficios nos trae tmpfs, escribamos algo!!! Para probar utilizamos un simple dd.

root@moon:~# dd if=/dev/zero of=prueba_tmpfs/prueba bs=1M count=768
768+0 registros de entrada
768+0 registros de salida
805306368 bytes (805 MB) copiados, 1,58104 s, 509 MB/s

Nos encontramos con una velocidad de escritura de 509 Mbytes por segundo!!! (509MBytes*8 = 4072Mbits por seg aprox 4Gbits por segundo!!! lo cual es consistente con el ancho de banda teórico de las memorias DDR2 de 667 Mhz que tiene mi laptop :D). Una velocidad MAS que interesante.

Podemos ver un cambio significativo en free -m. Pasamos de 807 a 37 Mbytes libres, es decir consumimos 807-37=770 (consistente con los 768 que escribimos con el dd).

root@moon:~# free -m
                     total       used       free     shared    buffers     cached
Mem:          1975       1937         37          0         25       1306
-/+ buffers/cache:        605       1369
Swap:         5789          0       5789

mmm ahora le toca al sata de la laptop hacer su mejor esfuerzo xD (competencia un tanto injusta jajaja). Escribimos de la misma manera que antes, pero a un archivo fuera del directorio donde montamos el tmpfs, en este caso el archivo se llama simplemente prueba.

root@moon:~# dd if=/dev/zero of=prueba bs=1M count=768
768+0 registros de entrada
768+0 registros de salida
805306368 bytes (805 MB) copiados, 12,3028 s, 65,5 MB/s

La velocidad fue de 65,5 Mbytes por segundo (65,5Mbytes * 8 = 524 Mbits por segundo, bastante por debajo del máximo teórico de los SATA II, por debajo inclusive del SATA I). Con esto queda claro que la diferencia es IMPORTANTE, la escritura sobre tmpfs fue casi 8 veces mas rápida (509/65,5=7,7).

Demostración simple de uso y rendimiento en lectura:

Todo muy lindo en la escritura, pero en la lectura qué será? :O.

Primero leemos del tmpfs y vemos una velocidad bastante mayor a la que obtuvimos en escritura. Pasando los 7Gbits por segundo!!!, esto seguramente se debe a algún manejo extraño de caches por parte del kernel, porque esta velocidad excede el límite teórico de las memorias de la laptop.

root@moon:~# dd if=prueba_tmpfs/prueba of=/dev/null bs=1M
768+0 registros de entrada
768+0 registros de salida
805306368 bytes (805 MB) copiados, 0,873211 s, 922 MB/s

Con respecto a la lectura en disco... Obtuve una velocidad muy similar a la de escritura, lo cual NO es muy común en este tipo de dispositivos.

root@moon:~# dd if=prueba of=/dev/null bs=1M
768+0 registros de entrada
768+0 registros de salida
805306368 bytes (805 MB) copiados, 13,5713 s, 59,3 MB/s

Por lo general los discos rígidos leen a mayor velocidad de la que escriben, por lo tanto podríamos suponer que la velocidad de escritura obtenida anteriormente es producto de algún manejo eficiente de cache o una suerte de inteligencia en la escritura de datos repetidos (solamente ceros, obtenidos de /dev/zero).

Qué pasa si...?:

Intentamos escribir mas bytes de los que tiene asignado nuestro directorio en tmpfs:

root@moon:~# dd if=/dev/zero of=prueba_tmpfs/prueba bs=1M count=1024
dd: escribiendo «prueba_tmpfs/prueba»: No hay espacio libre en el dispositivo
1022+0 registros de entrada
1021+0 registros de salida
1071640576 bytes (1,1 GB) copiados, 2,47776 s, 433 MB/s
root@moon:~#

Como era de esperarse no pudimos escribir mas de lo que definimos como tamaño del tmpfs :D.

Creamos un tmpfs mayor que nuestra RAM (mi laptop tiene 2Gbytes de RAM):

Es absolutamente posible hacerlo, PERO...

root@moon:~# mount -t tmpfs -o size=3G tmpfs prueba_tmpfs
root@moon:~# mount | grep prueba_tmpfs
tmpfs on /root/prueba_tmpfs type tmpfs (rw,size=3G)
a medida que comencemos a ocupar el espacio del tmpfs, nos comeremos (si, casi literalmente) la memoria ram del equipo, este comenzará a intercambiar páginas con poco uso a la memoria de intercambio (swap) y eventualmente el sistema podría quedar inutilizable. Moraleja? no es recomendable hacerlo (cosas que pasan xD).


Conclusión:

Definitivamente es una opción muy interesante principalmente para utilizar como directorio para caches.

En mi caso en particular utilicé tmpfs para el directorio donde munin crea/actualiza los archivos rrd, en un servidor que monitorea mas de 100 servidores, y el cambio fue notable. Antes de esto los gráficos se cortaban porque el proceso de graficar no terminaba antes de que llegaran las nuevas mediciones.