Publi

Monitorizar el sistema y obtener gráficas de uso

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:

También podría interesarte...

Only 1 comment left Ir a comentario

  1. Pingback: Bitacoras.com /

Leave a Reply