Contenido

Midiéndolo todo con StatsD

Tras el artículo Gráficas basadas en tiempo: Graphite era obligatorio escribir uno sobre StatsD.

En este artículo voy a contar qué es StatsD y cómo usarlo para extraer estadísticas de uso de nuestro código. Aunque al principio pueda parecer un poco extraño, os aseguro que es realmente interesante, sobre todo cuando se usa junto con Graphite.

Diferentes tipos de gráficas

¿Qué es StatsD?

Según la propia página de StatsD:

StatsD is a front-end proxy for the Graphite/Carbon metrics server, originally written by Etsy’s Erik Kastner. It is based on ideas from Flickr and this post by Cal Henderson: Counting and Timing. The server was written in Node, though there have been implementations in other languages since then.

Pero debo decir que la definición no me gusta. No, porque Graphite es sólo uno de los posibles backends.

En lugar de preguntarnos "¿qué es StatsD?" deberíamos preguntarnos "¿Para qué sirve StatsD?".

StatsD permite recopilar datos de… cosas, para su posterior uso. Cosas como número de ejecuciones, veces que se pasa por una función, tiempo que tarda en ejecutarse una acción, etc.

Existen distintas implementaciones del servidor StatsD, aunque la original es la escrita en NodeJS, que va a ser la que voy a comentar, ya que es la más completa. Hay clientes para cualquier lenguaje, ya que basta enviar una trama sencilla por UDP. Veremos esto más adelante.

Instalando StatsD

Como ya he dicho, usaremos la implementación de Etsy, que es la más famosa, y que está implementada en NodeJS, por lo que lo primero será instalar NodeJS si no lo tenemos ya:

1
apt-get install nodejs

A continuación, la instalación estándar nos sugiere clonar el repositorio:

1
git clone https://github.com/etsy/statsd.git

Aunque otro métido alternativo es usar npm:

1
npm install statsd

El primer metodo nos dejará el código en el directorio statsd, mientras que el segundo lo hará en node_modules\statsd, pero el resultado será muy similar. Yo he usado npm para este artículo.

Lo siguiente es configurarlo. Para ello basta copiar el archivo node_modules/statsd/exampleConfig.js al directorio actual y modificarlo. Veréis que el 99% del archivo es ayuda sobre el propio archivo, y que la configuración se reduce a algo como:

1
2
3
4
5
6
{
  graphitePort: 2003
, graphiteHost: "graphite.example.com"
, port: 8125
, backends: [ "./backends/graphite" ]
}

Podéis cambiar graphiteHost por localhost, y si seguísteis el artículo Gráficas basadas en tiempo: Graphite ya estaría todo funcionando con Graphite. De todas maneras, para comenzar recomiendo cambiar el backend por console, y así no es necesario tener Graphite instalado y podremos ver todos los datos en crudo. Por ello usaremos el archivo de configuración:

1
2
3
4
{
  port: 8125
, backends: [ "./backends/console" ]
}

Ejecutando StatsD

Con esta configuración ya podemos ejecutarlo:

1
node node_modules/statsd/stats.js config.js

Cada 10 segundos aparecerá un mensaje parecido a éste:

1
2
3
4
5
6
7
{ counters: { 'statsd.bad_lines_seen': 0, 'statsd.packets_received': 0 },
  timers: {},
  gauges: {},
  timer_data: {},
  counter_rates: { 'statsd.bad_lines_seen': 0, 'statsd.packets_received': 0 },
  sets: {},
  pctThreshold: [ 90 ] }

Ahí se indica la información recolectada. Hay algunos puntos curiosos:

  • counters: Los contadores recibidos. Por defecto ya tiene 2, relacionados con el propio StatsD.
  • timers: contadores de tipo tiempo recibidos.
  • gauges: Es un tipo de métrica que mantiene el último valor hasta recibir uno nuevo.
  • timer_data: Información ya tratada de los timers.
  • counter_rates: estadísticas de uso de los contadores.
  • sets: grupos de métricas recibidos.
  • pctThreshold: o “percentile threshold”, que es el percentil calculado para tiempos. Es decir: si en un intervalo se reciben 100 mediciones de tiempo, se cogerá el percentil 90 de éstas, ya que es estadísticamente más representativo.

Se puede configurar el tiempo del intervalo, pero por defecto son 10 segundos, y eso está bien para nuestro ejemplo.

Cómo enviar métricas a StatsD

Bueno, antes deberíamos saber qué tipos de métricas se pueden enviar, aunque ya están mencionadas en el apartado anterior:

  • Contadores, con formato métrica:N|c, siendo N el valor de la métrica.
  • Tiempos, con formato métrica:N|ms, siendo N el tiempo a registrar.
  • Gauges, con formato métrica:N|g, siendo N el valor de la métrica. Son valores que se mantienen hasta que llegue el siguiente.
  • Sets, con formato métrica:N|s, siendo N un número de ocurrencias a registrar.

El formato, además, permite indicar la agrupación añadiendo |@0.1, donde 0.1 indica que el valor sólo se está enviando 1/10 del tiempo. También se permite el envío de varias métricas en un sólo mensaje, separándolas por \n.

Como ya se dijo antes, basta con enviar estos datos en una trama UDP. Podemos usar Bash, por ejemplo:

1
   echo "this.is.a.counter:1|c" | nc -u -w1 127.0.0.1 8125

que provocará un cambio en la salida del servidor, mostrando el mensaje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
   { counters:
      { 'statsd.bad_lines_seen': 0,
        'statsd.packets_received': 1,
        'this.is.a.counter': 1 },
     timers: { glork: [] },
     gauges: { 'statsd.timestamp_lag': 0 },
     timer_data: {},
     counter_rates:
      { 'statsd.bad_lines_seen': 0,
        'statsd.packets_received': 0.1,
        'this.is.a.counter': 0.1 },
     sets: {},
     pctThreshold: [ 90 ] }

Como vemos, hay una ocurrencia del contador. A los 10 segundos se volverá a mostrar un 0, ya que en ese intervalo no ha habido ocurrencias.

Recomendaría jugar aquí con los distintos tipos de métricas, varias métricas en un mensaje, cambiar la agrupación, etc. Incluso intentarlo desde el lenguaje que vayáis a utilizar después.

¿Para qué sive StatsD?

Pero nada de todo esto explica para qué podemos utilizar StatsD. Vamos a verlo con ejemplos. Utilizaré Python, junto con el [ejemplo de cliente Python proporcionado por Ets]y

Tenemos una función muy costosa, que permite multiplicar dos matrices y queremos registrar cuánto tarda en ejecutarse. Aquí está nuestra función original:

1
2
3
4
5
6
7
8
9
def multiply_matrix(a, acols, b, bcols):
    c = [0] * (acols * bcols)
    for i in range(acols):
        for j in range(bcols):
            partial = 0
            for k in range(bcols):
                partial += a[pos(i, k, acols)] * b[pos(k, j, bcols)]
            c[pos(i, j, acols)] = partial
    return c

Dado que el cliente de StatsD no va a cambiar con el tiempo, podemos considerar que todos sus métodos son estáticos y usar una única instancia, ya configurada con el host y port deseados, y guardarlo todo en una variable global (cosa que no debería ser lo habitual, pero en este caso estaría justificado). He marcado las líneas nuevas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import time  # +
from python_example import StatsdClient  # +

statsd = StatsdClient(host='localhost', port=8125)  # +

def multiply_matrix(a, acols, b, bcols):
    start = time.time()  # +
    c = [0] * (acols * bcols)
    for i in range(acols):
        for j in range(bcols):
            partial = 0
            for k in range(bcols):
                partial += a[pos(i, k, acols)] * b[pos(k, j, bcols)]
            c[pos(i, j, acols)] = partial
    elapsed = time.time() - start  # +
    statsd.timing('matrix.%s_colsx%s_cols.time' % (acols, bcols), elapsed)  # +
    return c

6 líneas contando include y configuración.

Además, quiero contar cuántas veces se llama a la función por cada tamaño de matrices:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import time
from python_example import StatsdClient

statsd = StatsdClient(host='localhost', port=8125)

def multiply_matrix(a, acols, b, bcols):
    statsd.count('matrix.%s_colsx%s_cols.count' % (acols, bcols), elapsed)  # +
    start = time.time()
    c = [0] * (acols * bcols)
    for i in range(acols):
        for j in range(bcols):
            partial = 0
            for k in range(bcols):
                partial += a[pos(i, k, acols)] * b[pos(k, j, bcols)]
            c[pos(i, j, acols)] = partial
    elapsed = time.time() - start
    statsd.timing('matrix.%s_colsx%s_cols.time' % (acols, bcols), elapsed)
    return c

Una línea más.

Ventajas

Pero… ¿En qué afecta esto al tiempo de ejecución? Bueno, por el principio de Heisenberg, donde el observador siempre altera lo observado, en algo, pero en muy poco, ya que al utilizar el protocolo UDP no hay tiempos de latencia.

UDP tiene un problema: Es posible que la trama se pierda. Así que estas estadísticas no serán muy exactas, pero pueden darnos una idea muy aproximada de la evolución del sistema. De todas maneras, si el servidor StatsD está en local, es raro que se pierda una trama.

Si se conectara contra Graphite directamente por TCP entonces sí que habría retrasos: hay que establecer la conexión, enviar la información, esperar ACK, etc.

Otra ventaja es que no puede fallar. Si nuestro servidor TCP está caído, debemos tratar la excepción o el programa fallará. Si usamos UDP, perderemos la métrica, pero seguiremos dando servicio.

¿Dónde es especialmente útil?

  • Para tiempos de ejecución de queries. En pruebas de estrés podría mostrar la evolución del tiempo de ejecución en función de los datos ya existentes.
  • Contabilizar logins en una web u otras operaciones, así como logins fallidos, permitiendo detectar ataques por fuerza bruta.
  • Medir tiempos de respuesta en una web.

En Tuenti nos hicimos un wrapper sobre mercurial, para detectar la evolución del tiempo necesario para ejecutar cada subcomando, para medir la evolución de los más lentos. Gracias a esto, detectamos un problema grave de rendimiento y pudimos medir su evolución.

Clusters de StatsD

Todo esto tiene un problema. Si tenemos un balanceador de carga con dos servidores, cada uno con su servidor StatsD, y queremos medir los tiempos de request, el último que mande su valor a Graphite es el que quedará. Por eso debemos configurar los servidores StatsD como Proxies, indicando la ubicación del resto de servidores StatsD.

Conclusión

Todo se puede medir, y es importante medirlo todo. Desgraciadamente, eso requiere muchos recursos. Pero StatsD es una herramienta muy interesante para medir allá donde no se puede con otras herramientas.

Finalmente os dejo con un vídeo de cómo hacer todo lo que he contado aquí:

También os proporciono otro vídeo de cómo instalar Graphite y Statsd en 3 minutos, mostrando los datos. Es importante tener en cuenta que Statsd dejará las métricas en stats/gauges/ en lugar de colgar del árbol principal.

Quizá os parezca extraño ver cómo se altera la gráfica en parte del vídeo, pero es algo normal: Al llegar varias métricas durante el mismo cupo de tiempo, el último predomina. Para cambiar eso habría que modificar la configuración de Carbon, como se explicó en el artículo Gráficas basadas en tiempo: Graphite.