Cuando tenemos un servidor bajo nuestro control, es muy importante saber cómo vamos de recursos. Si hemos contratado un VPS, por ejemplo, debemos saber cuándo es hora de ampliarlo, o de contratar algo más pequeño porque vamos sobrados de recursos… o incluso poder comprobar que una parada del servicio se debe a un pico de uso del sistema. Esto también nos puede ayudar para comprobar cuándo nuestro server se ha parado debido a un kernel panic o algo parecido.
La monitorización vamos a basarla en el comando dstat, con este programa obtendremos dato puntuales del sistema, en otras palabras, una muestra de cómo vamos de recursos, aunque dado lo rápido que genera muestras por defecto, vamos a generarlas cada 30 segundos:
$ dstat 30
Esto resultarían unas 2880 muestras / día, no está mal. Aunque vamos a obtener algo más de información, en concreto:
- -t: Información de tiempo, para saber el momento exacto de captura de la muestra
- -c: Información de CPU
- -m: Información de memoria
- -n: Información de red
- -p: Información de procesos
- -s: Información de swap
Si ejecutamos durante un rato:
$ dstat -tcmnps 30 > stats
Podremos ver un fichero como este:
—-system—- —-total-cpu-usage—- ——memory-usage—– -net/total- —procs— —-swap—
time |usr sys idl wai hiq siq| used buff cach free| recv send|run blk new| used free
10-02 13:26:22| 24 4 70 1 0 0|3304M 32.0M 4136M 293M| 0 0 | 0 0 6.1| 39M 2825M
10-02 13:26:52| 21 2 77 0 0 0|3315M 33.1M 4131M 286M|1453B 596B|0.0 0 3.9| 39M 2825M
10-02 13:27:22| 18 2 80 0 0 0|3316M 34.8M 4132M 283M|4922B 1675B|0.0 0 3.9| 39M 2825M
10-02 13:27:52| 20 2 78 0 0 0|3318M 35.9M 4132M 279M|1435B 688B|0.0 0 3.8| 39M 2825M
Eso sí, un fichero como este no nos dice nada, cuando tenga un montón de líneas, por lo que vamos a generar una gráfica con gnuplot para poder visualizar esa información gráficamente. Hay que tener en cuenta que podemos generar muchas gráficas con esta información. Por lo tanto, quitamos las dos primeras líneas del fichero y empezamos a generar gráficas. Vamos a empezar por una gráfica de CPU:
cpu.gp
1 2 3 4 5 6 7 8 9 10 11 12 | #!/usr/bin/gnuplot set terminal png size 1024,768 set output "cpu.png" set title "CPU usage" set xlabel "time" set ylabel "percent" set xdata time set timefmt "%d-%m %H:%M:%S" set format x "%H:%M" plot "stats" using 1:3 title "system" with lines, \ "stats" using 1:2 title "user" with lines, \ "stats" using 1:4 title "idle" with lines |
Si queremos rellenar las demás gráficas tenemos que tener en cuenta que dstat por defecto, pone unidades a los datos y gnuplot no se lleva bien con el cambio de unidades (es más, las ignora), lo que quiere decir que 1234bytes será más que 200k y eso nos resultará en gráficas incorrectas. Para ello, tendremos que ejecutar dstat de la siguiente forma:
$ dstat -tcmnps 30 –output dstat.csv
Así generamos un archivo CSV sin las unidades, es más, los tamaños vendrán en bytes, que ya se encargue gnuplot de hacer las conversiones tranquilamente…
Para generar la gráfica de memoria hacemos lo siguiente :
mem.gp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #!/usr/bin/gnuplot set terminal png size 1024,768 set output "mem.png" set title "Memory usage" set xlabel "time" set ylabel "size (MB)" set xdata time set timefmt "%d-%m %H:%M:%S" set format x "%H:%M" set datafile separator ',' plot "dstat.csv" using 1:($8/1048576) title "used" with lines, \ "" using 1:($9/1048576) title "buffers" with lines, \ "" using 1:($10/1048576) title "cache" with lines, \ "" using 1:($11/1048576) title "free" with lines, \ "" using 1:($17/1048576) title "swap used" with lines, \ "" using 1:($18/1048576) title "swap free" with lines |
En este caso estamos dividiendo todos los valores por 1048576 (1024*1024=1Mb) para que no se pinten bytes sino megabytes. También, al insertar datos de csv, tenemos que establecer el datafile separator.
Ahora generaremos el gráfico de red:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #!/usr/bin/gnuplot set terminal png set output "network.png" set title "network" set xlabel "time" set ylabel "size(k)" set xdata time set timefmt "%d-%m %H:%M:%S" set format x "%H:%M" set autoscale y set datafile separator ',' plot "dstat.csv" using 1:($12/1024) title "send" with lines, \ "" using 1:($13/1024) title "recv" with lines |
Aunque finalmente, nuestro objetivo será monitorizar estos datos periódicamente y crear gráficas en el tiempo (tampoco será tiempo real, porque si no, nos podemos fundir los recursos de nuestro servidor, por tanto, cada 2 minutos regeneraremos las gráficas.
Podemos hacerlo con un cron-job, aunque no lo haremos, por no implicar a cron y para no desaprovechar recursos, además, nos surge otro problema. El archivo de informe de dstat crecerá y crecerá sin parar llenando el disco duro de nuestro servidor, por lo que no es muy buena idea dejarlo así. Por tanto vamos a utilizar una pipe para escribir el archivo CSV, y desde nuestro script iremos metiendo los contenidos de la pipe en el archivo. Además, cada dos minutos, cuando genere las gráficas preguntará si es buen momento para rotar los CSV. El objetivo, es rotar los archivos cada 4h. Por lo tanto, vamos a crear un shell script para gestionar las gráficas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | #!/bin/bash _SAMPLE_TIME=10 # Tiempo entre muestras de tiempo _GRAPHS_TIME=20 # Tiempo entre generación de gráficas _ROTATION_TIME=600 #14400 # Tiempo entre rotación de archivos _PIPE_FILENAME=/tmp/statpipe # Nuestra pipe _LOGFILE_BASE=systemlog- _LOGFILE_PATH=./logs _LOGFILE_TIMEFORMAT="%s" _LOGFILE_EXTENSION="log" _GRAPH_PATH=./graphs _GRAPH_BASE=graph- function do_exit() { echo "Cerrando"; kill -9 $DSTAT_PID # Parametro 1 indica forzar la salida if [ "$1" -eq "1" 2> /dev/null ] then kill -9 $$ fi exit } function start_new_file() { CURRENT_FILE=$_LOGFILE_PATH/$_LOGFILE_BASE`date "+$_LOGFILE_TIMEFORMAT"`.$_LOGFILE_EXTENSION LAST_ROTATION=`date +%s` } function read_pipe() { while : do read line # Si read falla, salimos if [ "$?" -ne 0 ] then exit fi disc=${line:0:2} # Dos primeros caracteres if ! [ "$disc" -eq "$disc" 2> /dev/null ] then echo "" > /dev/null; else echo $line >> $CURRENT_FILE control_logs fi done } function graph (){ arguments=($@); filetype="png" size="1024,768" filename=$_GRAPH_PATH/$_GRAPH_BASE$1-`date +%s`.png title=$2 xlabel="Time"; ylabel=$3 gnuplot << EOF set terminal $filetype size $size set output "$filename" set title "$title" set xlabel "$xlabel" set xdata time set ylabel "$ylabel" set timefmt "%d-%m %H:%M:%S" set format x "%H:%M" set datafile separator ',' plot ${arguments[@]:4}; EOF LAST_GRAPH=`date +%s` } function control_logs() { now=`date +%s` if [ "$LAST_ROTATION" -lt $(($now-$_ROTATION_TIME)) ] then start_new_file fi if [ "$LAST_GRAPH" -lt $(($now-$_GRAPHS_TIME)) ] then _lastfiles=`ls -t $_LOGFILE_PATH | head -n2 | tac` lastfiles="" for i in $_lastfiles do lastfiles=$lastfiles" $_LOGFILE_PATH/"$i done graph cpu- "CPU Usage" "Percent" ""< cat $lastfiles" using 1:3 title "System" with lines,"" using 1:2 title "User" with lines,"" using 1:4 title "Idle" with lines" graph mem- "Memory Usage" "Mb" ""< cat $lastfiles" using 1:(\$8/1048576) title "RAM used" with lines,"" using 1:(\$9/1048576) title "Buffers" with lines,"" using 1:(\$10/1048576) title "Cache" with lines, "" using 1:(\$11/1048576) title "RAM Free" with lines, "" using 1:(\$17/1048576) title "Swap used" with lines, "" using 1:(\$18/1048576) title "Free Swap" with lines" graph net- "Net Usage" "Kb" ""< cat $lastfiles" using 1:(\$12/1024) title "Sent" with lines,"" using 1:(\$13/124) title "Received" with lines" fi } if [ ! -d "$_LOGFILE_PATH" ] then mkdir -p "$_LOGFILE_PATH" fi if [ ! -d "$_GRAPH_PATH" ] then mkdir -p "$_GRAPH_PATH" fi trap do_exit SIGINT SIGTERM rm $_PIPE_FILENAME 2>/dev/null mkfifo $_PIPE_FILENAME start_new_file dstat -tcmnps --output $_PIPE_FILENAME $_SAMPLE_TIME > /dev/null & DSTAT_PID=$! START_TIMESTAMP=`date +%s` LAST_GRAPH=$START_TIMESTAMP # No hemos pintado, pero queremos esperar # antes de pintar read_pipe < /tmp/statpipe |
Lo primero que hacemos nada más ejecutar es crear los directorios ( si no existen ) donde almacenamos los logs y las gráficas, tras eso definimos señales ( con trap ), por si matan el proceso, o pulsamos Control+C para salir, no devuelva un error de pipe dstat; y tras eso creamos la pipe, fijáos que ahora dstat enviará su salida a la pipe en formato CSV.
Una vez inicializado todo, llamamos a read_pipe, pasándole la información contenida en ésta. read_pipe() se encargará de leer línea por línea, ignorar las que no comiencen por números (las de la cabecera), escribirlas en el fichero de log actual y llamar a control_logs()
control_logs() se encarga de cambiar el fichero actual para escribir en otro log y de generar las gráficas con los dos últimos archivos (esto nos servirá para que, gráficas recién generadas tengan algo más de información).
Aún faltan cosas interesantes por hacer:
- A la hora de generar gráficas, estaría bien generar una gráfica de las últimas 4h y una gráfica de las últimas 24h
- Deberíamos tener un acceso directo para las últimas gráficas generadas
- Deberíamos eliminar los logs con más de X días. Tal vez un mes.
- Deberíamos comprimir los logs con más de Y días (donde Y es menor que X). Tal vez 3 días
- Deberíamos borrar imágenes antiguas cada Z días
Y por fin el script terminado, haciendo uso de find para borrar logs e imágenes antiguas, así como para buscar qué archivos se deben comprimir en uno sólo. El acceso directo a las gráficas se ha hecho con un enlace.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | #!/bin/bash _SAMPLE_TIME=60 # Tiempo entre muestras de tiempo _GRAPHS_TIME=300 # Tiempo entre generación de gráficas _ROTATION_TIME=14400 # Tiempo entre rotación de archivos _PIPE_FILENAME=/tmp/statpipe # Nuestra pipe _LOG_ERASE_DAYS=30 _LOG_GZIP_DAYS=3 _GRAPH_ERASE_DAYS=15 # Primer gráfico (4h por defecto) _TIME_GRAPH_A=14400 _SAMPLES_GRAPH_A=$(($_TIME_GRAPH_A / $_SAMPLE_TIME)); _GRAPH_A_TITLE="last4h" # Segundo gráfico (24h por defecto _TIME_GRAPH_B=86400 _SAMPLES_GRAPH_B=$(($_TIME_GRAPH_B / $_SAMPLE_TIME)); _GRAPH_B_TITLE="last24h" _LOGFILE_BASE=systemlog- _LOGFILE_PATH=./logs _LOGFILE_TIMEFORMAT="%s" _LOGFILE_EXTENSION="log" _GRAPH_PATH=./graphs _GRAPH_BASE=graph- function do_exit() { echo "Cerrando"; kill -9 $DSTAT_PID # Parametro 1 indica forzar la salida if [ "$1" -eq "1" 2> /dev/null ] then kill -9 $$ fi exit } function start_new_file() { CURRENT_FILE=$_LOGFILE_PATH/$_LOGFILE_BASE`date "+$_LOGFILE_TIMEFORMAT"`.$_LOGFILE_EXTENSION LAST_ROTATION=`date +%s` } function read_pipe() { while : do read line # Si read falla, salimos if [ "$?" -ne 0 ] then exit fi disc=${line:0:2} # Dos primeros caracteres if ! [ "$disc" -eq "$disc" 2> /dev/null ] then echo "" > /dev/null; else echo $line >> $CURRENT_FILE control_logs fi done } function graph (){ arguments=($@); filetype="png" size="1024,768" filename=$_GRAPH_PATH/$_GRAPH_BASE$1`date +%s`.png title=$2 xlabel="Time"; ylabel=$3 graphtype=$4 gnuplot << EOF set terminal $filetype size $size set output "$filename" set title "$title" set xlabel "$xlabel" set xdata time set ylabel "$ylabel" set timefmt "%d-%m %H:%M:%S" set format x "%H:%M" set datafile separator ',' plot ${arguments[@]:5}; EOF LAST_GRAPH=`date +%s` rm "$_GRAPH_PATH/$_GRAPH_BASE$1$graphtype.png" 2>/dev/null ln -s "$_GRAPH_BASE$1`date +%s`.png" "$_GRAPH_PATH/$_GRAPH_BASE$1$graphtype.png" } function do_gzip() { files=`cat` if [ -n "$files" ] then cat $files | gzip > $_LOGFILE_PATH/$_LOGFILE_BASE`date "+$_LOGFILE_TIMEFORMAT"`.gz rm $files fi } function control_logs() { now=`date +%s` if [ "$LAST_ROTATION" -lt $(($now-$_ROTATION_TIME)) ] then start_new_file find $_GRAPH_PATH/ -ctime +$_GRAPH_ERASE_DAYS -exec rm {} \; find $_LOGFILE_PATH/ -ctime +$_LOG_ERASE_DAYS -exec rm {} \; find $_LOGFILE_PATH/ -name '*.'$_LOGFILE_EXTENSION -ctime +"$_LOG_GZIP_DAYS" -print | do_gzip fi if [ "$LAST_GRAPH" -lt $(($now-$_GRAPHS_TIME)) ] then lastfiles=`ls -tr $_LOGFILE_PATH/*.$_LOGFILE_EXTENSION | head -n2` graph cpu- "CPU Usage" "Percent" $_GRAPH_A_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_A" using 1:3 title "System" with lines,"" using 1:2 title "User" with lines,"" using 1:4 title "Idle" with lines" graph mem- "Memory Usage" "Mb" $_GRAPH_A_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_A" using 1:(\$8/1048576) title "RAM used" with lines,"" using 1:(\$9/1048576) title "Buffers" with lines,"" using 1:(\$10/1048576) title "Cache" with lines, "" using 1:(\$11/1048576) title "RAM Free" with lines, "" using 1:(\$17/1048576) title "Swap used" with lines, "" using 1:(\$18/1048576) title "Free Swap" with lines" graph net- "Net Usage" "Kb" $_GRAPH_A_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_A" using 1:(\$12/1024) title "Sent" with lines,"" using 1:(\$13/124) title "Received" with lines" lastfiles=`ls -tr $_LOGFILE_PATH/*.$_LOGFILE_EXTENSION` graph cpu- "CPU Usage" "Percent" $_GRAPH_B_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_B" using 1:3 title "System" with lines,"" using 1:2 title "User" with lines,"" using 1:4 title "Idle" with lines" graph mem- "Memory Usage" "Mb" $_GRAPH_B_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_B" using 1:(\$8/1048576) title "RAM used" with lines,"" using 1:(\$9/1048576) title "Buffers" with lines,"" using 1:(\$10/1048576) title "Cache" with lines, "" using 1:(\$11/1048576) title "RAM Free" with lines, "" using 1:(\$17/1048576) title "Swap used" with lines, "" using 1:(\$18/1048576) title "Free Swap" with lines" graph net- "Net Usage" "Kb" $_GRAPH_B_TITLE ""< cat $lastfiles | tail -n $_SAMPLES_GRAPH_B" using 1:(\$12/1024) title "Sent" with lines,"" using 1:(\$13/124) title "Received" with lines" fi } if [ ! -d "$_LOGFILE_PATH" ] then mkdir -p "$_LOGFILE_PATH" fi if [ ! -d "$_GRAPH_PATH" ] then mkdir -p "$_GRAPH_PATH" fi trap do_exit SIGINT SIGTERM rm $_PIPE_FILENAME 2>/dev/null mkfifo $_PIPE_FILENAME start_new_file dstat -tcmnps --output $_PIPE_FILENAME $_SAMPLE_TIME > /dev/null & DSTAT_PID=$! START_TIMESTAMP=`date +%s` LAST_GRAPH=$START_TIMESTAMP # No hemos pintado, pero queremos esperar # antes de pintar read_pipe < /tmp/statpipe |
Y al final se generarán gráficos como este:
Pingback: Bitacoras.com /