La performance es un área difícil de comprender, pero fundamental a la hora de administrar sistemas.
Durante este post vamos a intentar comprender la salida de vmstat(8), un comando disponible en la mayoría de los sistemas Unix, en nuestro caso vamos a enfocarnos en Linux, y analizaremos algunas situaciones las cuales pueden llevar a una sobrecarga de performance.
Existen numerosas herramientas para analizar esto, pero muchas de ellas dan su punto de partida en vmstat(8), para luego enfocar en herramientas más específicas como iostat(8), mpstat(1), o sar(1). Para un mayor entendimiento de performance y las herramientas utilizadas les recomiendo el blog de Brendan Gregg, o también leer el libro "Solaris™ Performance and Tools: DTrace and MDB Techniques for Solaris 10 and OpenSolaris" por Richard McDougall, Jim Mauro y Brendan Gregg
Según su propia manpage vmstat(8): reports information about processes, memory, paging, block IO, traps, disks and cpu activity.
Nosotros enfocaremos este post en entender la información proporcionada por los procesos, la memoría, el I/O y la utilización de la CPU. Pero antes de entrar de lleno en esto, hay algunas cosas que tenemos que comprender primero.
Los sistemas operativos modernos, trabajan en distintos modos a fin de garantizar una mayor seguridad y proteger las aplicaciones unas de otras. Actualmente en PC x86/x86_64 podemos definir algunos modos de operación como es el modo real (utilizado durante el booteo), modo protegido (proteger la memoría virtual), long mode (modo de operación 64 bits), modo virtual 8086 (retrocompatibilidad binaria con antiguos procesadores). El modo protegido, planea una interfaz de llamadas al sistema (syscall) para poder acceder al hardware, evitando de esta manera que un usuario tenga acceso directo al I/O y sea el kernel, el responsable de realizar estas actividades, es por ello que fueron diseñados básicamente un sistema de privilegios basados en rings, el ring0 también conocido como kernel mode, y el ring 3, denominado userspace.
Kernel mode: En este modo es el kernel, y sus drivers (módulos) quienes pueden ejecutarse, se reserva un espacio de memoria virtual donde solo puede estar mapeado el kernel, sus extensiones y como dije anteriormente sus drivers.
Userspace: Solo se ejecutan las aplicaciones de usuario, para acceder al kernel mode se necesitan efectuar llamadas al sistema, reserva un rango de direcciones de memoria donde las aplicaciones y librerías (o bibliotecas, para los puristas) del userspace (denominadas userland) pueden ejecutarse.
Veamos un ejemplo sencillo en ANSI C y luego en ensamblador para INTEL x86 sobre como el sistema operativo hace uso de esta interfaz para poder realizar un sencillo exit.
Las llamadas al sistema en Linux, se realizan de una manera bastante sencilla, cada una de ellas es mapeada a un código hexadecimal, por ejemplo la syscall 0x1h (1 en decimal), representa exit, la syscall 0x4h (4 en decimal), representa a write y 0x37h (55 en decimal) es kill.
Este código hexadecimal es posicionado en un registro de propósito general en la CPU, en el caso de un procesador 32 bits este registro es %EAX o %RAX si contamos con 64 bits.
Luego los siguientes argumentos que lleva la syscall son posicionados en otros registros de propósito general como es el caso de %EBX, %ECX y %EDX (en 32 bits). Si miramos dentro de la manpage de _exit(2) podemos ver:
NAME
_exit, _Exit - terminate the calling process
SYNOPSIS top
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
_Exit():
_XOPEN_SOURCE >= 600 || _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
or cc -std=c99
DESCRIPTION
The function _exit() terminates the calling process "immediately". Any open
file descriptors belonging to the process are closed; any children of the
process are inherited by process 1, init, and the process's parent is sent a
SIGCHLD signal.
The value status is returned to the parent process as the process's exit
status, and can be collected using one of the wait(2) family of calls.
The function _Exit() is equivalent to _exit().
CONFORMING TO
SVr4, POSIX.1-2001, 4.3BSD. The function _Exit() was introduced by C99.
NOTES
For a discussion on the effects of an exit, the transmission of exit status,
zombie processes, signals sent, etc., see exit(3).
The function _exit() is like exit(3), but does not call any functions
registered with atexit(3) or on_exit(3). Whether it flushes standard I/O
buffers and removes temporary files created with tmpfile(3) is implementation-
dependent. On the other hand, _exit() does close open file descriptors, and
this may cause an unknown delay, waiting for pending output to finish. If the
delay is undesired, it may be useful to call functions like tcflush(3) before
calling _exit(). Whether any pending I/O is canceled, and which pending I/O
may be canceled upon _exit(), is implementation-dependent.
In glibc up to version 2.3, the _exit() wrapper function invoked the kernel
system call of the same name. Since glibc 2.3, the wrapper function invokes
exit_group(2), in order to terminate all of the threads in a process.
SEE ALSO
execve(2), exit_group(2), fork(2), kill(2), wait(2), wait4(2), waitpid(2),
atexit(3), exit(3), on_exit(3), termios(3)
Esto, en ensamblador INTEL x86 quedaría así:
Si miramos este código con algo de atención podemos notar que lo primero que se efectua es posicionar el valor 0x1h, dentro del registro %EAX, que se está refiriendo a la syscall exit(2), en el segundo paso, posiciona el exit status (0), dentro del registro %EBX, y paso final llama a la interrupción 0x80h (int 0x80). Esta interrupción es muy importante en Linux, y es la que efectúa el switcheo de un código ejecutándose binariamente en el userspace, a pasar a ejecutarse en el kernel mode. Esto, se lo conoce como Context switching.
Por lo cual, podemos concluir que una aplicación de usuario (userland) se ejecuta en el Userspace, y para poder ejecutarse en kernel mode, necesita hacer uso de llamadas al sistema (syscall), que es provista por el kernel, quién el, junto a los drivers será capaz de interactuar directamente con el hardware. Entonces, podemos plantearnos el siguiente interrogante, ¿que pasaría si notamos un consumo elevado de la CPU, y podemos apreciar una enorme cantidad de context switchs (CS)?. Bien, podemos deducir que una aplicación del userland, es quien esta generando un importante número de llamadas al sistema y debemos revisar nuestros procesos que se estén ejecutando a ver quién es el responsable.
Procesos y threads:
Podemos definir a un proceso como una instancia de un programa en ejecución que a reservado memoria para su uso, todo proceso al momento de su creación lleva asignado un número de PID (Process ID), y un número de PPID (Process Parent ID), esto es debido a que en Linux, y muchos otros Unix, los procesos nacen por medio de una familia de syscalls denominada fork (fork, vfork). Estos procesos, crean en la memoria virtual (VM) ciertos segmentos, que luego mediante el sistema de IPC (Inter Process Comunication), se establecen permisos de acceso. Entre estas secciones podemos encontrar la sección .TEXT donde se encuentra mapeado la porción de código en ejecución a nivel binario, .STACK, una estructura bastante dinamica en forma de pila que hace uso del método LIFO (Last In, First Out) sobre los datos que puede almacenar, el .HEAP, .BSS, entre otras. Los procesos, tienen la particularidad de que su segmento reservado de memoria puede ser accedido únicamente por ellos (a menos que el mismo mapee una porción como SHM). Por su parte los threads, los cuales son procesos livianos, comparten entre ellos la misma "imagen" de memoría, de forma que pueden acceder indistintamente a ellos, dando lugar en los peores casos a race conditions y deadlocks. Para evitar esto, existen algoritmos que emplean por ejemplo la exclusión mutua y se hace uso de los semáforos. Veamos un pequeño ejemplo a continuación, en el cual varios trheads intentan acceder a un mismo recurso, en el mismo instante de tiempo:
Estos trheads, de los cuales hablamos, en un sistema monoprocesador (1 CPU), no tienen demasiado sentido, ya que solamente puede ejecutarse uno por vez, y es el scheduler del sistema operativo quien se encarga de asignarles un tiempo de CPU para que los mismos se ejecuten, en caso de que un proceso haga uso de mayor cantidad de tiempo a la que le fue asignada el mismo será penalizado (mediante semáforos), teniendo que esperar N cantidad de tiempo antes de poder volver a ingresar nuevamente a la CPU. Esto se logra mediante la utilización de el algoritmo Round Robin, el cual asigna tiempos iguales de CPU a todos los procesos, pero en caso de que uno se exceda aplica penalizaciones. En caso de equipos con múltiples cores, esta limitación se ve superada, pero siempre que tengamos una sola CPU física, solo un proceso por vez podrá correr. Para que varios procesos puedan correr concurrentemente necesitamos tener un equipo con múltiples unidades de procesamiento. Pero también suele pasar que muchos procesos o threads esten esperando para entrar a la CPU, y a esto lo realizan generando una cola o también denominada run queue, estaremos tienen una cola de ejecución normal, cuando esta no supere en número a la cantidad de procesadores que tenga el equipo instalado, caso contrario, estaremos ante un bottleneck.
Una métrica que suele ser tomada en cuenta por muchos administradores para medir la carga de un equipo, es mediante la carga promedio (Load average), la misma puede ser consultada mediante el comando top o también mediante uptime, y nos informa la carga del equipo ahora, hace cinco minutos y hace quince minutos. Veamos un ejemplo:
02:20:23 up 3:06, 2 users, load average: 3.03, 2.13, 0.19
Para reportar las estadísticas, vmstat hace uso del procfs, especificamente de /proc/stats, /proc/mem/info y /proc/*/stat, que como muchos de ustedes sabrán, cada proceso en ejecución, crea una entrada (mediante un directorio) en el directorio /proc, cuyo nombre es el PID del proceso y dentro del mismo existe un archivo denominado stat.
La invocación de vmstat es sencilla, si lo invocamos sin parámetros, nos imprimirá por la salida estándar, una sola muestra, podemos especificarle al mismo mediante un entero, la cantidad de tiempo que debe pasar entre muestra y muestra, y además la cantidad de muestras que queremos que nos brinde. Es un factor a tener en cuenta el periodo que debe pasar entre cada una de las muestras, ya que puede afectar notablemente a nuestro entendimiento de la salida. Con el argumento -a, nos brindara estadísticas mas avanzadas (imprimiendo la memoria activa e inactiva) y con -t además nos imprimirá un timestamp sobre cuando se produjo dicha muestra. vmstat Además imprime información sobre el estado de la swap y del IO (discos), utilizando como medida bloques de datos, cada bloque actualmente para kernels 2.6 y 3.x tiene un tamaño total de 1024 bytes, antiguamente estos podían ser reportados por vmstat en bloques de 512 bytes, 2048 bytes o 4096 bytes.
Veamos nuestro primer ejemplo:
$ vmstat 1 10
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 0 71096 200036 12252 277544 0 3 64 45 662 1212 11 4 82 3 0
3 0 71096 199788 12252 277928 0 0 0 0 470 811 2 1 98 0 0
0 0 71096 201276 12260 277980 0 0 0 40 686 839 10 1 88 2 0
1 0 71096 201276 12260 277604 0 0 0 0 479 743 2 1 98 0 0
0 0 71096 201084 12260 277604 0 0 0 232 561 811 2 2 96 0 0
1 0 71096 201180 12260 277604 0 0 0 28 466 804 2 1 98 0 0
0 0 71096 201180 12260 277604 0 0 0 0 702 871 11 1 89 0 0
0 0 71096 201644 12260 277604 0 0 0 0 465 756 2 0 98 0 0
0 0 71096 201668 12260 277604 0 0 0 0 577 750 2 1 97 0 0
0 0 71096 201700 12276 277588 0 0 0 36 641 750 1 0 92 7 0
Interruptible sleep: Aquellos procesos que están esperando alguna señal para despertar y volver a ejecutarse (por ejemplo que se les notifique que X tarea fue completada).
Uninterruptible sleep: Aquellos procesos los cuales se encuentran aguardando algún tipo de evento externo para poder continuar, por ejemplo esperando I/O.
Por lo cual, nuestra columna "b" representa a la cantidad de procesos que se encuentran esperando alguna actividad externa para poder continuar como se dijo, por ejemplo I/O. Por lo cual, hacia el final de la salidad de vmstat encontramos la columna "wa" la cual nos dice que es la cantidad de tiempo gastado por la CPU esperando I/O. Generalmente ambas columnas están relacionadas, y tener altos valores aquí, puede ser sinónimo de algún desperfecto en el filesystem (posiblemente optimizable), baja velocidad en el bus de datos utilizado por las controladoras de disco, o algún tipo de desperfecto en el dispositivo de almacenamiento o sus controladoras. Para tener un detalle superior y resolver problemas de performance de I/O puede ser consultado iostat(1).
Veamos un ejemplo de esto:
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 1 71096 83596 18112 356208 0 3 61 44 644 1182 11 4 83 3 0
2 1 71420 72564 9772 379456 0 324 69760 412 2310 3041 4 45 20 31 0
1 1 71420 70584 4292 391272 0 0 84224 0 2589 3295 3 55 21 21 0
2 0 71420 72744 4292 393968 0 0 79104 0 2489 3131 3 51 23 23 0
3 1 71420 71796 4292 397928 0 0 81280 0 2472 3147 3 52 24 21 0
2 1 71420 69932 4292 402356 0 0 83968 0 2634 3308 3 55 21 21 0
1 1 71420 73220 4284 401044 0 0 84864 40 2673 3282 3 56 20 22 0
1 2 71420 67416 4292 408544 0 0 83208 0 2699 3202 3 54 16 26 0
0 2 71420 71728 4292 408500 0 0 83328 0 2602 3230 3 55 11 32 0
2 0 71420 74236 4300 408988 0 0 60456 32 2069 2752 4 39 12 45 0
Otro campo interesante es "cs", que contabiliza la cantidad de context switchs durante la muestra, si vemos un número elevado de context switchs puede ser indicio de que alguna aplicación en el userspace se encuentra ejecutando una gran cantidad de syscalls que pueden ser o no un problema.
El campo "in", nos muestra la cantidad de interrupciones producidas en el tiempo de muestra por el equipo. Hace un tiempo, tuve una experiencia personal con un firewall ejecutando la tecnología de filtrado IP Filter (IPF), el cual estaba produciendo un número enorme de interrupciones (entre 70000 y 100000) en muestras de 1 segundo, en este caso era debido a un daño físico (aunque podía deberse al driver también) en una interfaz de red. A esto se lo conoce como interrupt storm, y como un dato curioso, la primera de ellas se produjo durante el alunisaje del Apollo XI en 1969. Una tormenta de interrpuciones puede consumir el mayor tiempo (o todo el tiempo) en kernel mode ("sy"), y dejar el sistema sin respuesta, en un estado denominado live lock. Existen algunas formas de mitigar las tormentas de interrupciones, por ejemplo en el siguiente planteamiento: Una NIC ethernet, la cual debe efectuar una interrupción por cada paquete que ingresa o sale de la misma, para que esto no genere una interrupt storm, se hace uso de un mecanismo de polling el cual genera una interrupción cuando X cantidad de paquetes deben ser enviados o recibidos. Caso contrario, mientras mayor througput de red exista, mayor será la cantidad de interrupciones generadas, pudiendo producirse una interrput storm.
El campo "st", hace referencia a la cantidad de tiempo de procesamiento en caso de utilizarse virtualización robado al hypervisor por las máquinas virtuales.
Un grupo de campos bastante relacionados son los de la sección "Memory", en los cuales podemos encontrar "swpd" como la cantidad total de swap en el sistema (mas información: swapon -s), "free" la cantidad de memoria libre y que puede ser ser utilizada. "buff", Es la cantidad de memoria utilizada como buffers. "cache", La cantidad de memoria utilizada como cache por los distintos procesos.
Por su parte dentro de el grupo de campos "Swap", encontramos dos columnas, "si" o swap-in y "so" o swap-on. Esto es la cantidad de bloques que son escritos a swap, y son leídos de swap respectivamente. El sistema operativo hace uso de la memoría swap, cuando se queda sin memoria física a la que direccionar las páginas de memoría, bajando las menos utilizadas a dicha memoría de intercambio. La memoría swap, mas los buffers y la memoria física conforman un grupo al que se denomina virtual memory (VM). Por lo cual, notar actividad en estas columnas, puede ser síntoma de que la memoria física instalada en el equipo esta siendo insuficiente.
Veamos el siguiente ejemplo:
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 3 71420 93312 14888 373588 1 2 97 41 623 1140 84 3 10 3 0
8 2 72280 104204 28608 321432 2 860 1864 2704 2689 9901 61 22 1 17 0
7 2 72784 75128 28040 350996 100 2 100 41 623 1143 93 3 0 3 0
11 5 72812 75128 28116 351024 132 28 11392 668 1962 8491 32 16 11 41 0
8 3 72840 70540 28092 355984 1 28 9184 780 1766 11739 27 16 1 47 0
9 4 72972 74964 28068 351312 7 132 15860 644 2138 6639 34 20 5 40 0
8 2 73104 75264 21200 358408 0 132 11208 628 1775 5542 18 16 8 58 0
3 2 73388 72768 13444 368200 345 284 2724 524 2032 25138 29 20 5 46 0
5 4 73848 74540 31100 347204 123 208 8120 208 3566 59935 41 35 0 24 0
4 3 74092 73084 36244 342524 98 244 5436 1392 3434 43193 33 27 0 40 0
7 4 74196 66204 43588 341380 15 104 6084 148 4437 64503 34 37 8 21 0
5 2 75344 70212 45964 335144 2 1148 4432 1368 3115 56056 30 32 7 31 0
En las muestras proporcionadas por el último ejemplo vemos una clara situación de saturación: La columna "r" en este caso es mayor a nuestra cantidad de cores (2, según /proc/cpuinfo), la columna "b", muestra un número constante y bastante alto de procesos con uninterruptible sleep, lo cual se apoya en el tiempo empleado en Waiting for I/O, según la columna "w", además podemos notar que el equipo esta swappeando ("si" y "so"), por lo cual aquí el problema puede deberse a una combinación de factores como baja velocidad de las controladoras, o daño en ellas o en los discos (para ello buscar por errores en los logs del sistema), además de una baja cantidad de memoria, o la existencia de un proceso o varios que se esten cosumiendo todos los recursos. Además podemos ver que el userspace es bastante alto en proporción a nuestro tiempo idle, por lo cual podría ser una aplicación o varias aplicaciones pequeñas del userland quien este produciendo esto. Para determinar esto, podemos ejecutar en una termianl lo siguiente:
# ps -eo user,pid,pcpu --no-headers | awk '{ if ( $3 > 80 ) print $1,$2,$3}'
zimbra 28442 88.5
# ps aux | grep 28442
zimbra 28442 88.5 60.3 1213164 924904 ? Sl Apr11 306:57 /opt/zimbra/java/bin/java -Dfile.encoding=UTF-8 -server -Djava.awt.headless=true -Dsun.net.inetaddr.ttl=60 -XX:+UseConcMarkSweepGC -XX:PermSize=128m -XX:MaxPermSize=128m -XX:SoftRefLRUPolicyMSPerMB=1 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:-OmitStackTraceInFastThrow -Xss256k -Xms256m -Xmx256m -Xmn64m -Djava.io.tmpdir=/opt/zimbra/mailboxd/work -Djava.library.path=/opt/zimbra/lib -Djava.endorsed.dirs=/opt/zimbra/mailboxd/common/endorsed -Dzimbra.config=/opt/zimbra/conf/localconfig.xml -Djetty.home=/opt/zimbra/mailboxd -DSTART=/opt/zimbra/mailboxd/etc/start.config -jar /opt/zimbra/mailboxd/start.jar /opt/zimbra/mailboxd/etc/jetty.properties /opt/zimbra/mailboxd/etc/jetty-setuid.xml /opt/zimbra/mailboxd/etc/jetty.xml
# ps -eo user,pid,pmem --no-headers | awk '{ if ( $3 > 80 ) print $1,$2,$3}'
zimbra 28468 4.2 3.9 21540 840 ? S Apr11 0:05 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28471 1.2 1.1 109432 6196 ? S Apr11 0:00 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28488 6.0 4.7 109432 6196 ? S Apr11 0:04 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28489 1.3 3.2 54028 3476 ? S Apr11 0:05 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28490 1.8 1.6 54028 3476 ? S Apr11 0:00 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28491 6.2 9.1 54028 3476 ? S Apr11 0:01 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28492 0.4 1.0 54028 3476 ? S Apr11 0:02 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28493 0.1 1.0 54028 3476 ? S Apr11 0:02 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28494 0.7 1.0 54028 3476 ? S Apr11 0:02 /opt/zimbra/httpd-2.2.19/bin/httpd
zimbra 28495 0.4 1.0 54028 3476 ? S Apr11 0:02 /opt/zimbra/httpd-2.2.19/bin/httpd
--- OUTPUT OMMITED ---
La diferencia mas sustancial entre fork(2), y vfork(2), es que este último crea un nuevo child process suspendiendo a su padre (bloqueándolo) hasta que el mismo termine o envíe alguna señal que lo haga finalizar (por ejemplo llamando a _exit(2)). vfork(2) además, no copia su tabla de páginas de su padre, como si lo haría un proceso creado con fork(2), pero este si comparte su propia memoria con el padre, incluido el stack. Veamos un ejemplo, en el cual un webserver (Apache), esta produciendo forks continuamente, consumiendo de manera excesiva la CPU del equipo:
# while true; do sleep 1 && vmstat -f ; done
283986702 forks
283986734 forks
283986769 forks
283986812 forks
283986855 forks
283986938 forks
283986963 forks
283987083 forks
283988034 forks
283988918 forks
Toda esta salida que hemos analizado a lo largo del post puede ser graficada con numerosas herramientas tales como gnuplot, en la cual podemos obtener resultados como este:
Debo agregar ... todo desarrollador que se precie de tal debe tenes las mismas capacidades ;)
ResponderEliminarEste comentario ha sido eliminado por el autor.
ResponderEliminar