Introducción a seguridad Zero Trust

mié, 28 mar 2018 by Foron

No es difícil suponer que cuando tienes la escala de Google te encuentras con problemas que el resto no tenemos, o que sí tendremos, pero en unos años. Es algo que ya ha pasado con un buen número de tecnologías, desde MapReduce hasta Kubernetes y que, quién sabe, también pasará con el tema de este post. Esta vez nos centramos en el modelo de seguridad tradicional que conoce todo el mundo. Acceso limitado a la red perimetral a través de firewalls, redes DMZ, VPNs y similares son algo que todos hemos usado.

Una de las críticas, diría que con razón, que se hacen a este modelo es que una vez se accede a un nivel, todas las máquinas de esa capa quedan a la vista. De esta manera, es probable que un administrador de base de datos tenga acceso a servidores de almacenamiento o red, o a todas las máquinas de un mismo dominio de Windows. Algo a tener en cuenta dada la necesidad que parece que tenemos de pulsar sobre mensajes que nos mandan príncipes nigerianos.

Por supuesto, esta no deja de ser una simplificación. En la práctica se pueden securizar más estos niveles, se pueden usar tecnologías tipo IPSEC y similares, o se puede tener un control más específico de quién accede a dónde. Hoy en día, a menudo, aunque se conzca el problema de base, cuando la cantidad de gente que trabaja en estos entornos es limitada y controlada, puede parecer razonable asumir el riesgo en relación al coste de cualquier otra medida.

Claro, cuando la escala es lo suficientemente grande, el control se hace más complicado. Hay muchos servicios, de todo tipo; algunos se gestionan vía SSH, otros vía Web, o con APIs específicos. En cuanto a los administradores, además de los propios responsables de las plataformas, puede haber personal externo, estudiantes en prácticas, personal de atención al cliente, y tantos otros. Para terminar, el acceso también es más diverso. Lo que antes era un PC en una oficina, ahora puede ser un PC, o un portátil en casa, o una tablet desde la Wifi del tren.

Con estos antecedentes, y dados los ataques y problemas propios del entorno, Google comenzó un proyecto interno para cambiar su gestión de la seguridad. Evidentemente, los recursos y el nivel técnico de estas "grandes" les permiten llegar a desarrollos que para el resto son complicados de alcanzar, pero los conceptos más generales siempre pueden adaptarse y, llegado el momento, implementarse en entornos más pequeños.

En este post voy a hacer un resumen básico de lo que se busca con BeyondCorp, que es como ha llamado Google a su proyecto interno, pero también me gustaría ver qué software podríamos usar para hacer una implementación, aunque sea básica, de las ideas principales que se buscan. Si queréis dejar de leer ahora y pasar a documentación más completa, estos enlaces pueden serviros:

  • BeyondCorp: Web con información básica y enlaces a algunos artículos técnicos. Está desarrollada por ScaleFT, una de las empresas que han creado un producto en la línea de los documentos publicados por Google, y que están documentados en la propia web. Son fáciles de leer, os los recomiendo.
  • Google BeyondCorp: Web de referencia en Google Cloud.
  • Zero Trust Networks: Uno de los pocos libros que he visto sobre la materia. Son poco más de 200 páginas, y siempre podréis sacar conclusiones y ver qué os sirve y qué no.

¿Qué son las redes Zero Trust?

Una red Zero Trust es una red en la que no se confía. Por lo tanto, todos los dispositivos que están el ella se gestionan como si estuviesen directamente conectados a Internet (que no deja de ser una red de este tipo), independientemente de si dan servicio externo o interno. En definitiva, se trata de complementar todo aquello para lo que el modelo tradicional es efectivo, añadiendo medidas contra lo que los estándares actuales, la segmentación de red, DMZs y similares son más limitados:

  • Equipos comprometidos por Phising, Keyloggers, ...
  • Shells inversos en servidores con aplicaciones no actualizadas
  • Movimiento lateral entre equipos de un mismo segmento de red
  • Contraseñas vulnerables
  • Acceso a servidores internos desde servidores públicos vulnerables
  • ...

Las redes Zero Trust se fundamentan en lo siguiente:

  • Asumir que el tráfico dentro de un centro de datos o de una red corporativa es seguro es erróneo. La segmentación en zonas y la seguridad perimetral que se ha venido siguiendo estos años no impiden los movimientos laterales dentro de un mismo nivel en el momento en el que un servidor o dispositivo se vea comprometido.
  • Todas las redes deben considerarse hostiles.
  • Las amenazas pueden ser tanto internas como externas.
  • Que un dispositivo pertenezca a un segmento de red no es suficiente para garantizar la confianza.
  • Todos los dispositivos, usuarios y flujo de red deben estar autorizados y autenticados.
  • Las políticas de acceso deben ser dinámicas.

Elementos principales

Independientemente de cómo se implemente, una parte importante del trabajo va a ser la capa de control. Hablamos del entorno que va a identificar a usuarios y dispositivos, y que va a autorizar el acceso a los elementos de la red. Idealmente, se va a formar una entidad con el dispositivo desde el que se accede, el usuario que lo está intentando, y la aplicación. A partir de aquí, el sistema tomará una decisión basada en las variables que se consideren adecuadas, y que pueden ir en la línea de las siguientes:

  • ¿Se está accediendo desde un equipo inventariado?
  • ¿El equipo tiene todas las actualizaciones de seguridad instaladas?
  • ¿Desde dónde se está intentando acceder?
  • ¿A qué hora?
  • ¿Qué usuario está intentando acceder?
  • ¿A qué aplicación o servidor se quiere acceder?
  • ¿Qué método está usando para acceder? ¿Qué algoritmo de seguridad?

En realidad, esta lista se podría completar con todo tipo de información adicional. El objetivo es que este sistema de control use estos datos para tomar una decisión (permitir o no), y que abra el camino para que se pueda establecer la conexión, de forma dinámica, entre ese cliente (y solo ese cliente) y el destino (y solo ese destino).

¿Cómo autorizamos un intento de acceso?

Por lo general, al hablar de autenticación hablamos de algún tipo de cifrado. En definitiva, se trata de intentar garantizar que un dispositivo origen puede acceder a un recurso, y para esto, estándares tan desarrollados como los certificados basados en X.509 son una elección lógica. Es relativamente común que se usen CAs privadas para crear los certificados que se usan en una organización.

Como siempre, alrededor de este concepto se pueden desarrollar todo tipo de sistemas avanzados que controlan la validez de los certificados, el inventario, y todo lo que se os ocurra. Hay gente que crea certificados válidos por poco tiempo, otros usan ocsp, y también los hay que incluyen información que puede ser útil en la toma de decisiones en alguno de los atributos que define el protocolo.

¿Cómo autenticamos un intento de acceso?

Ojo, que autorizar y autenticar no son lo mismo. En su forma más simple, podemos estar hablando de un formulario web que se enganche a una base de datos de usuarios, sea del tipo que sea. Por lo tanto, un usuario se conectará desde un equipo autorizado, con las credenciales que tenga asignadas en ese momento.

Una vez más, a partir de esta idea básica se pueden hacer todo tipo de desarrollos y comprobaciones. Con los datos que tenemos ya es esta fase, podremos mostrar un listado de servidores a los que permitir la conexión, topes de tiempo para acceder, obtener datos de otros sistemas internos, como puede ser de monitorización, y tantos otros.

¿Cómo hacemos efectivo el acceso?

En estos últimos años la automatización ha sido una de las tecnologías que más han avanzado, y diría que es una de las claves que permiten implementar un sistema Zero Trust con facilidad. En su forma más sencilla, se puede usar ansible, chef, puppet, cfengine y similares para activar los permisos necesarios. Sistemas más avanzados permitirían, además, la configuración de accesos a nivel de red, vlanes o enrutamiento, en función de la infraestructura con la que se trabaje.

Implementación de ejemplo

Cuando se lee documentación de este tipo de tecnologías se tiende a concretar poco el software que podría usarse para implementarlas. Hagamos una prueba de concepto, aunque sin escribir código, y siempre limitándonos a lo más básico.

Uno de los ejes fundamentales sobre el que gira la tecnología Zero Trust es el cifrado. Para conseguirlo, nos interesaría tener una buena gestión de certificados. Hacer un inventario no es trivial, y mucho menos si queremos asociar de manera efectiva los portátiles (por ejemplo) en los que se instalan, con los usuarios a los que pertenecen. Si somos creativos, podemos hacer cosas muy sencillas pero que pueden ser interesantes, como por ejemplo definiendo atributos extra (los certificados X.509 permiten añadir cosas como teléfonos, estados, y muchos otros), o usando números de serie para los certificados. Imaginad, por ejemplo, que llevamos el control de un certificado que pertenece a un usuario (en base a los datos del Subject), y que incrementamos el número de serie cada vez que haya una actualización de seguridad. El usuario se instalará el nuevo certificado cuando actualice su equipo, y el sistema solo validará la sesión SSL si es así. Es algo simple, pero puede ser un buen punto de partida.

# openssl req -new -key client_new.key -out client_new.csr
-----
Country Name (2 letter code) [AU]:??
State or Province Name (full name) [Some-State]:????
Locality Name (eg, city) []:????
Organization Name (eg, company) [Internet Widgits Pty Ltd]:????
Organizational Unit Name (eg, section) []:????
Common Name (e.g. server FQDN or YOUR name) []:usuario
Email Address []:usuario@example.com
--- No es difícil incluir más atributos aquí

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

#root@ansible:~# openssl x509 -req -days 365 -in client_new.csr -CA ca.cert -CAkey ca.key -set_serial 1 -out client_new.crt

Enseguida veremos que los servidores web o los proxies modernos pueden leer estos valores.

Incluso en los casos en los que la aplicación que usemos no funcione con certificados, los navegadores, en su inmensa mayoría, sí los usan. Gracias a esto podríamos generar las reglas de acceso necesarias para un cliente desde el navegador, y permitir el acceso a esa otra aplicación con la confianza suficiente de que se origina desde un cliente válido. Las implementaciones más avanzadas usan proxies, por ejemplo de SSH, o tienen utilidades que cambian, en el caso de SSH, la configuración de los clientes y del servidor para que usen los nuevos certificados. El termino medio que planteo basa la autenticación y la autorización en un interfaz web muy compatible con los certificados, para que luego el cliente pueda conectarse al servidor de la forma más natural, que en el caso de SSH no dejan de ser las claves públicas y privadas de toda la vida. No es algo perfecto, ni mucho menos, pero es genérico y no obliga a reconfigurar los servidores constantemente.

Pasemos a lo que puede ser el interfaz web con la lógica necesaria para el sistema.

Queremos controlar el estado de los certificados en detalle, y para eso no hay nada como Nginx y su módulo para Lua Este no es un post sobre Nginx, así que solo daré algunas pistas sobre cómo se podría implementar todo esto sobre lo que estamos hablando con Nginx, pero hasta ahí.

Como sabéis los que conocéis Nginx, los certificados de servidor "de siempre" se leen con directivas como estas:

ssl_certificate /ruta/certificado_servicio;
ssl_certificate_key /ruta/clave_servicio;

Y se puede obligar al uso de un certificado cliente, instalado en el navegador, que se haya firmado con una CA

ssl_client_certificate      /ruta/certificado_ca;
ssl_verify_client           on;

Con esto ya sabremos que solo se accederá al sitio desde equipos en los que se haya instalado un certificado que hayamos firmado con nuestra CA. Lo que no hemos hecho es ampliar la lógica y usar el número de serie o el Subject del certificado. No hay problema, Nginx crea variables con todo lo que necesitamos, y que podemos usar en la fase de acceso de los distintos location. Si usamos el módulo Lua, no habrá ningún problema para acceder a los datos que nos manda el navegador, y para aplicar la lógica que queramos con ellos. Podemos leer datos de inventario desde una base de datos, desde redis, en un json desde un servidor independiente, .... Hay librerías para todos los gustos.

location ??? {
  access_by_lua_block {
    -- En la variable $ssl_client_s_dn tenemos un cadena de texto con el email, el CN, OU, O, L, ST, C y otros atributos menos habituales que hayamos configurado
    local dn = ngx.var.ssl_client_s_dn

    -- En la variable $ssl_client_serial tenemos el numero de serie del certificado del cliente.
    local serial = ngx.var.ssl_client_serial

    -- A partir de aqui, accedemos a donde sea que tengamos la gestion del inventario, y permitimos el acceso, o no
    -- ngx.exit(403)
    -- ngx.exit(ngx.OK)
  }
}

La autenticación es básicamente cosa de programación, del estilo de lo que hemos hecho siempre. Una ventana de acceso, login, password con credenciales del usuario, y todo lo demás. Esto se complementaría con el bloque de control de certificado que hemos descrito antes. Además, siempre se pueden incluir ventanas adicionales en las que un usuario autenticado vea los host a los que puede acceder, y quizá puedan definir una franja horaria en la que van a conectarse a ese host o grupo de hosts.

La idea es garantizar que el acceso se ha hecho desde un equipo autorizado, y que se ha hecho por un usuario autenticado. Al final de todo, podremos haber generado una sesión típica con el navegador cliente. Esta sesión puede servir si usamos este servidor como proxy para accesos web. Imaginad, por ejemplo, que queremos que los accesos a wp-login.php de un Wordpress solo puedan realizarse a través de este sistema. Solo necesitamos las reglas básicas para que Nginx haga de proxy a ese destino, controlando la validez de la sesión. La ventaja es que el Wordpress no necesita ningún tipo de modificación, ya que es el proxy de autenticación el encargado de controlar los accesos en este nivel.

Para mantener las cosas simples, imaginad que solo tenemos un número determinado de Wordpress que queremos controlar. De no ser así, esta parte también podría ser dinámica y programada sin mayor dificultad; en Nginx se puede usar Lua para elegir upstream.

upstream wordpress {
  ip_hash;
  server ip_wordpress;

Tendríamos una serie de location para gestionar el login, con sus métodos GET y POST, luego tendríamos las ventanas adicionales. Al final tendríamos otro location que haría el proxy_pass. Hay muchas formas de cifrar sesiones y cookies en Nginx. No trato de enseñarlas aquí o de hacer un bloque completo. Es solo una idea básica.

location = /wp-login.php {
  set $dominio 'example.com';

  access_by_lua_block {
    -- Que no se olvide la parte de autorizacion en base al certificado
    --
    -- Lo normal es que usasemos un POST para recibir el host al que redirigir, y que usasemos balancer_by_lua para elegir dinamicamente el upstream
    --
    if ngx.var.request_method == "GET" then
      return
    elseif ngx.var.request_method ~= "POST" then
      ngx.exit(403)
    end
    local ck = require "resty.cookie"
    local cookie, err = ck:new()
    if not cookie then
      ngx.exit(403)
    end

    -- Un usuarios validado con todo OK tendria una cookie AUTHSESSION (por ejemplo) cifrada que usariamos para verificar que el acceso esta permitido
    local authsession = cookie:get("AUTHSESSION")
    if not authsession then
      -- No permitimos si no hay cookie
      ngx.exit(403)
    end
    --
    -- Es un ejemplo basico de uso de cookies, pero hay otras alternativas
    -- Tenemos cookie AUTHSESSION, la descifrariamos y, si fuese ok, seguimos adelante
    -- Tambien hay muchas opciones para ver TTLs y confirmar que no ha expirado
    --
    -- Si llegase el host en un POST, podemos leerlo sin problemas
    ngx.req.read_body()
    local args, err = ngx.req.get_post_args()
    -- En args estan los parametros que han llegado en el POST
    -- Si todas las comprobaciones fuesen bien, hariamos el proxy_pass
    return
  }

  proxy_pass https://wordpress;
  proxy_set_header Host $dominio;
  proxy_set_header X-Real-IP $remote_addr;
}

location / {
  set $dominio 'example.com';

  proxy_pass https://wordpress;
  proxy_set_header Host $dominio;
  proxy_set_header X-Real-IP $remote_addr;
}

Falta mucho código, pero no quiero convertir esto en un post sobre Nginx/Lua. La idea que quiero transmitir es que podemos meter un proxy entre nuestro destino (nos centramos en HTTP en este caso) y el cliente. La única modificación que hay que hacer al Apache o lo que sea que gestione el Wordpress es limitar los accesos a wp-login.php, para que solo se pueda llegar desde el proxy de acceso. De esta manera, quien quiera administrarlo tendrá que pasar por todo el proceso de validación, pero sería transparente para el resto de usuarios.

Para las aplicaciones basadas en HTTP es fácil gestionar un proxy. Para el resto, este tipo de proxies pueden ser más complicados, aunque en este momento se está terminando el soporte Lua para el módulo stream que permitirá hacer proxies muy interesantes.

En cualquier caso, como he comentado al principio, es fácil complementar el control de accesos a, por ejemplo, SSH, usando la misma idea que hemos usado hasta ahora. Un usuario puede usar su navegador desde su portatil, y Nginx puede crear una petición a un servicio externo que sincronice la clave pública y cree una regla iptables que habilite el acceso. Otro proceso podría, vía cron por ejemplo, revocar ese acceso y esa regla llegado el momento.

Si optasemos por ansible sería muy sencillo hacer un pequeño servidor en flask o tornado que usasen el api python del propio ansible para lanzar las tareas que habilitan el acceso. En Nginx tendríamos un nuevo upstream, tipo:

upstream tornado {
  ip_hash;
  server 127.0.0.1:puerto;

y un location parecido al que hemos escrito para wp-login.php haría una llamada a ese tornado que lanzaría las tareas de ansible contra los módulos authorized_key e iptables. Con Nginx/Lua, crear una petición para un upstream es algo trivial. Aquí van unas pistas para los interesados:

location = /acceso_ssh.html {
  set $dominio 'example.com';

  access_by_lua_block {
    -- Que no se olvide la parte de autorizacion en base al certificado
    --
    -- En el POST recibiríamos el servidor destino, la IP origen, y
    -- cualquier otro dato necesario para ansible
    --
    -- Por supuesto, toda la parte de la sesion de navegador sigue
    -- siendo necesaria
    --
    -- Imaginemos que tenemos el host remoto en la variable hostremoto
    -- del formulario que nos llega en el POST
    -- El modulo Lua recibe la IP origen en la variable ngx.var.remote_addr
    -- Podriamos tener en inventario las claves publicas de los usuarios
    --
    ngx.req.read_body()
    if not args then
      ngx.exit(403)
    end
    local args, err = ngx.req.get_post_args()
    local iporigen = ngx.var.remote_addr
    local host_remoto = args["hostremoto"]
    local clave_ssh = args["clave_publica"]

    -- Simulemos que el servidor tornado recibe un json en el cuerpo, por cambiar

    local habilitar_acceso = {}
    habilitar_acceso["ip"] = iporigen
    habilitar_acceso["servidor"] = host_remoto
    habilitar_acceso["clave"] = clave_ssh

    ngx.req.set_body_data(cjson.encode(habilitar_acceso))

    res = ngx.location.capture("/habilitar_ssh", {method = ngx.HTTP_POST})

    -- Si tornado devuelve un codigo HTTP diferente a 200 es un error
    if res.status ~= 200 then
      ngx.exit(403)
    end

    return
  }

  # Si todo ha ido bien el acceso se habra creado (via tornado) y podremos mostrar un html
  # o lo que sea con un mensaje de OK. Podria ser un html estatico o generado
  # via content_by_lua
}

location = /habilitar_ssh {
  # A este location llega el POST con el cuerpo creado a partir
  # de la tabla habilitar_acceso, formateado como json

  internal;
  # Imaginemos que tenemos este endpoint en tornado donde se llama a ansible
  # Tendria que implementar POST, y leer y parsear el json
  # Devolveria un codigo HTTP 200 si todo hubiese ido bien
  proxy_pass http://tornado/habilitar_ssh;
}

Conclusiones

La verdad es que he dudado de si interesaba escribir código en este post. Evidentemente, no estamos hablando de un proyecto de fin de semana, así que, lo que he escrito, en realidad, no sirve para nada. Sin embargo, tampoco siempre se lee documentación con detalles concretos, así que esto podría dar alguna idea a alguien.

En cualquier caso, viniendo en cierta medida de Google (en realidad los conceptos no son nuevos, en absoluto), no sería raro que las palabras "Zero Trust" se vayan escuchando cada vez más, y que se hagan más desarrollos.

En definitiva, aunque la parte del código sea poco útil, espero que las ideas que hay detrás de todo esto hayan quedado claras. Se lo preguntaré a las tres personas que van a leer el post.

read more

Monitorización de aplicaciones con sysdig

mié, 23 nov 2016 by Foron

Historicamente, uno de los argumentos más importantes de la comunidad Solaris en relación a la, no sé si decirlo así, madurez de Linux para entornos profesionales, ha sido la falta de instrumentalización para la monitorización del sistema.

En realidad, no faltaba razón en este argumento, ya que los primeros esfuerzos, por ejemplo de la mano de Systemtap, no conseguían llegar a lo que ofrecía el fantástico Dtrace en este tipo de entornos. A pesar de haber pasado ya varios años, no creo que ninguna de estas aplicaciones haya llegado a un público masivo (en el ámbito empresarial, claro).

Sin embargo, gracias al trabajo de mucha gente, estos últimos meses están siendo espectaculares para dotar a Linux, por fin, de herramientas capaces de igualar a Solaris. De hecho, alguna de las cabezas pensantes detrás de Dtrace, como Brendan Gregg, ahora trabajando en entornos Linux, ha venido a decir que ha sido como si, después de haber estado esperando al autobús, de repente hubiesen llegado dos. Os recomiendo esta lectura, con algo de contexto sobre la evolución de estas nuevas herramientas.

Hoy en día, como decía, tenemos varias alternativas. Personalmente, estoy interesado en dos: a) eBPF, o ya últimamente BPF, a secas, y b) Sysdig.

Con BPF se está dotando al Kernel Linux de toda la instrumentalización necesaria para inspeccionar al detalle el funcionamiento del sistema. A partir de ahí, un toolkit como BCC hace uso de todas estas nuevas estructuras para facilitar la manipulación de estos datos a través de un interfaz Python o Lua. Echad un vistado a la web. Los ejemplos son particularmente interesantes.

El soporte para BPF está integrado en los Kernels estándar recientes. Es algo en plena evolución, así que, si queréis probarlo, os recomiendo que no uséis nada inferior a 4.4, aunque ya haya cosas desde antes. En realidad, la cosa va tan rápido que, ya puestos, lo mejor es que probéis con 4.9 para ver lo último de lo último que se ha incluido.

Parece claro que se tienen muchas espectativas puestas en BPF. Hay varios proyectos importantes que ya se están planteando su uso. SystemTap, por ejemplo, está empezando a implementar un backend bajo BPF. Por otro lado, y bajo el mismo paraguas de BPF, se están desarrollando nuevas tecnologías, como XDP, que prometen una serie de ventajas que ya se están empezando a contemplar en el mundo de las redes software o los contenedores. Si queréis leer algo sobre esto, aquí tenéis un enlace.

La segunda alternativa de la que os hablaba, Sysdig, aunque para el usuario final que solo quiere monitorizar sus sistemas tenga, por así decirlo y cogido con alguna pinza, el mismo objetivo que BPF, lo hace de una manera diferente. Instrumentaliza el Kernel y ofrece un backend de calidad, pero delega gran parte del trabajo a filtros y a pequeños scripts, que llaman chisels (en Lua), que se encargan del trabajo desde el punto de vista del usuario.

En este momento, si dejáis de leer este post e investigáis por vuestra cuenta durante una hora, seguramente lleguéis a la conclusión de que BPF es realmente potente, pero que cuesta empezar (hay mucho hecho ya bajo el paraguas de iovisor, y cada poco sale un nuevo script). Por otro lado, es probable que en esa misma hora lleguéis a entender de qué va Sysdig, y que aunque no sea tan "amplio" como BPF, en realidad es más que suficiente para muchos de los problemas que habitualmente tiene el usuario de a pie. Ojo, que no estoy diciendo que Sysdig sea mejor que BPF, ni remotamente. Sacad vuestras propias conclusiones, pero leed sobre ambos y probadlos antes.

Tanto BPF como Sysdig dan para muchos posts. Os recomiendo leer el blog de Brendan Gregg, la documentación y los ejemplos de github de iovisor, y el blog y la web de Sysdig para ir calentando.

Mi idea original para el artículo era usar Sysdig en algún script real, pero eso lo haría mucho más denso, y he preferido limitarlo a algunas notas de lo que se puede hacer, aunque lo disfrazaré de ejemplo real.

Imaginad que tenéis un script que procesa un fichero de logs. En función de la expresión regular va a una u otra rama de código y, al final, inserta los resultados en mysql. En un entorno real, seguramente paralelizaríamos el script para aprovechar todos los cores, quizá usando zeromq para el paso de mensajes, y quizá usando el patrón que algunos llaman pipeline. Los entornos paralelos suelen complicar la monitorización.

Vamos a suponer la locura (irónico) de que no tenemos tiempo para la optimización o el análisis de ningún script, mucho menos si es cosa de horas, y todavía menos si el mencionado script no funciona rematadamente mal.

Y aquí es cuando alguien se levanta y dice: "Tanto rollo para algo que se puede solucionar con las trazas de toda la vida, (inicio = time.time(); fin = time.time(); dif = fin - inicio) ". No seré yo el que diga que este tipo de trazas no funcionen, aunque estaremos de acuerdo en que son "limitadas". Sirven para decir que algo va lento, pero no el motivo; aparte del tiempo que lleva procesar, digamos, 80 millones de lineas de log multiplicadas por tantos "ifs" como tenga el código, que además se ejecutan en procesos independientes. Es viable, por supuesto, pero mejorable.

Afortunadamete, ya habéis dedicado una hora a mirar tanto Sysdig como BPF/BCC y, claro, habréis llegado a la conclusión de que cualquiera de las dos os pueden servir. Veamos qué podemos hacer con Sysdig.

Repasemos: Una vez instalado Sysdig (os lo dejo a vosotros), se carga un módulo de Kernel que, simplificando un poco, va a ir recogiendo, eficientemente, los datos que se vayan generando (llamadas al sistema, IO, ...), de tal manera que luego se puedan aplicar filtros y chisels que nos den la información que necesitemos.

Además, tenemos suerte, porque una de las últimas funcionalidades que se han añadido a Sysdig consiste en algo tan sencillo como marcar el inicio y el fin de un bloque de código, y usar después estas marcas para el análisis. Imaginad que tenéis la capacidad de saber fácilmente la distribución del tiempo que necesita un "if" que incluye algunas operaciones sobre una base de datos; y que además podéis saber sin nada de esfuerzo el contenido íntegro del "select" que se mandó a la base de datos, o si falló la conexión o la resolución del nombre de la máquina bbdd para ese pequeño porcentaje de bloques lentos.

Y todavía es mejor, porque para hacer esto de lo que os estoy hablando solo hay que escribir una cadena concreta en /dev/null. Es previsible que esto será muy fácil, sea cual sea el lenguaje que estéis usando. Mirad estos ejemplos, sacados directamente de la web de sysdig.

echo ">:p:mysql::" > /dev/null
... código a analizar ...
echo "<:p:mysql::" > /dev/null

Y así de fácil. Con > y < definimos el comienzo y el final del bloque, con :p: pedimos que se genere un identificador automáticamente a partir del pid del proceso (hay más opciones), y usamos mysql como tag para identificar el span (que es como se llama todo esto, tracer/span).

Pero podemos ir un poco más lejos, y usar cadenas como las siguientes:

echo ">:p:mysql.select::" > /dev/null
echo ">:p:mysql.update::" > /dev/null
echo ">:p:mysql.select:query=from tabla1:" > /dev/null
echo ">:p:mysql.update:query=tabla1:" > /dev/null

Como veis, podemos anidar tags, o incluso añadir argumentos que den más pistas sobre lo que hace cada bloque. Esto es muy útil para saber la iteración exacta dentro de un for en la que ha ocurrido un problema concreto, o el tipo de select que ha generado cierto error, por decir un par de casos.

Volviendo a lo nuestro, recordad, queremos tener controlado un script python que escribe en una base de datos, que usa zeromq, y que se basa en expresiones regulares para hacer una cosa u otra. Sin pensar mucho, tendríamos una estructura de código parecida a esta:

fsysdig = open("/dev/null/", "w")

# Abrimos fichero log, creamos procesos, ...

for linea in fsysdig:
  fsysdig.write(">:p:zmq::\n")
  fsysdig.flush()
  # Preparamos los datos, y mandamos al socket push de ZMQ
  # Este socket se bloquea al llegar al tope de memoria configurado
  # ...
  socketzmq.send_multipart(["LOG",linea])
  # ...
  # Mas actividad para este span
  fsysdig.write("<:p:zmq::\n")
  fsysdig.flush()

En otros procesos tendremos la parte de la gestión de las expresiones regulares

fsysdig = open("/dev/null/", "w")

#
# Leeriamos del socket push via un socket pull de ZMQ
# ...
#

while seguirprocesando:
  # Hacemos match de una expresion regular
  # Si se cumple, hacemos ciertas operaciones
  # ...
  if matchregexp1:
    fsysdig.write(">:p:regexp:re=regexp1:\n")
    fsysdig.flush()
    # Trabajo con la regexp1
    # ...
    # Enviamos el dato al proceso mysql via ZMQ
    # ...
    fsysdig.write("<:p:regexp::\n")
    fsysdig.flush()

  if matchregexp2:
    fsysdig.write(">:p:regexp:re=regexp2:\n")
    fsysdig.flush()
    # Trabajo con la regexp2
    # ...
    # Y enviamos el dato al proceso mysql via ZMQ
    # ...
    fsysdig.write("<:p:regexp::\n")
    fsysdig.flush()
  #
  # Hariamos algo parecido con el resto ...
  #

Ya os hacéis una idea. Como veis, estamos añadiendo argumentos al tag regexp para identificar los bloques.

Por último, otro proceso haría el trabajo contra mysql.

fsysdig = open("/dev/null/", "w")

while seguirprocesando:
  #
  # Leeriamos del socket push via ZMQ, agrupariamos, operaciones, ...
  #
    try:
      fsysdig.write(">:p:mysql:st=update:\n")
      fsysdig.flush()
      # preparariamos la operacion mysql y el resto del trabajo para este span...
      cur.execute('''insert into ...''')
      # ...
    except (MySQLdb.MySQLError, TypeError) as e:
      print "Mysql: ERROR: Al ejecutar comando mysql " + str(e)
      sys.stdout.flush()
    finally:
      # ...
      fsysdig.write("<:p:mysql::\n")
      fsysdig.flush()

Como veis, también hemos creados argumentos para identificar bloques.

Este es el tipo de código que vamos a ejecutar. Antes de eso, vamos a poner en marcha la captura de sysdig (se puede lanzar en cualquier momento).

sysdig -C 500 -s 512 -w volcado_span.scap

Básicamente estamos creando ficheros independientes de 500MB, y estamos capturando 512 bytes de bufer de IO.

Una vez tengamos Sysdig lanzado dejamos el script funcionando un rato. Tendremos varios ficheros volcado_span.scap[0-9]+. Empecemos el análisis!

Una de las utilidades principales de Sysdig es un interfaz ncurses que permite ejecutar chisels fácilmente. Se llama csysdig. En nuestro caso, vamos a leer uno de los volcados para simplificar el proceso, pero tened en cuenta que todo esto se puede hacer sobre una captura en tiempo real, sin el parámetro -r

csysdig -r volcado_span.scap8

En este listado, F2 -> Traces Summary, y nos dará el resumen de spans que se han generado.

Sumario spans

Hagamos algo más interesante. Uno de los chisels más visuales que ha escrito la gente de Sysdig se llama "spectrogram", y se utiliza para ver la distribución de las latencias de ciertos eventos. Csysdig integra una versión que muestra la distribución para los spans, como una unidad. Os dejo que la veáis vosotros (hay videos en los tutoriales de la web de Sysdig). Aquí vamos a ser un poco más brutos y vamos a mostrarla para todos los eventos que se generan dentro de los bloques con el tag "regexp":

sysdig -r volcado_span.scap8 -c spectrogram 'evtin.span.p.tag[0]=regexp'

De donde sacaríamos lo siguiente:

Spectrogram regexp

Aunque lo que os muestro no es demasiado práctico (seguramente sea más interesante empezar por spans en bloque), imaginad que tenéis accesos a red o llamadas más costosas, y que tenéis mucho rojo hacia la derecha (muchos eventos y muy lentos). Esos serían, quizá, los eventos más interesantes a analizar:

sysdig -r volcado_span.scap8 -c spectrogram 'evtin.span.p.tag[0]=regexp and evt.latency > 100000'

Como veis, estamos aplicando filtros para limitar lo que vemos:

Spectrogram regexp lentas

Este tipo de imágenes me parecen interesantes para ver si hay algún tipo de anomalía o algo que no nos parezca razonable; aunque en un caso real antes o después dejaríamos de usar los chisel visuales y pasaríamos a ver los eventos concretos que están dando guerra. De hecho, la vista spectrogram de csysdig permite elegir con el ratón partes de la imágen para pasar a modo texto.

Y esto es, para mí, de lo mejor de Sysdig: Podemos limitarnos a grandes sumarios como los que hemos visto hasta ahora, o a ver lo que está ocurriendo a nivel de llamada a sistema. Además, aunque en este artículo nos hemos centrado en un análisis "a posteriori", en la práctica podemos lanzar csysdig en tiempo real y tener integradas en un mismo interfaz todas las funcionalidades, mejoradas, que dan comandos como htop o netstat, por citar un par.

Depende del caso de uso de cada uno. Yo, por ejemplo, en el día a día uso Sysdig sobre todo para ver el tráfico entre sockets. Por ejemplo, imaginad que tenéis algún tipo de middleware que hace llamadas HTTP a un servicio externo en función de las peticiones HTTP que recibe. Suponed que no logueáis esas peticiones, y que a veces fallan. En estos casos Sysdig es realmente útil.

El chisel echo_fds, por ejemplo, es muy interesante porque muestra todo el IO de los eventos que cumplan el filtro que apliquemos. Además, lo colorea en función de si es de entrada o de salida. Por supuesto, se puede usar para HTTP, como he comentado, pero también con cualquier otro proceso que genere IO, como por ejemplo, mysqld:

sysdig  -r volcado_span.scap11 -c echo_fds 'proc.name=mysqld'

Nos hemos olvidado de los spans, como veis, y vamos directamente a los procesos mysqld. Sin acotar más, tendremos un churro similar al siguiente

.....
------ Read 4B from   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
....
------ Read 87B from   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
.insert into tabla(timestamp, dato1, dato2) values (1477905741, 'ejemplo1', 'ejemplo2')
------ Write 11B to   127.0.0.1:52398->127.0.0.1:mysql (mysqld)
......

Como veis, estamos logueando todo el tráfico SQL, como el insert que he dejado aquí. Creedme, este tipo de chisels, con filtros como evt.buffer contains son muy útiles para ver tráfico HTTP, cabeceras, respuestas o códigos de error, particularmente en entornos con muchos microservicios y similares.

En fin, no sé si os habéis hecho una idea de lo que se puede hacer con Sysdig. En realidad, no os he dicho nada del otro mundo; teneis mucha mas informacion en la web y el blog de Sysdig, por citar dos fuentes. En cualquier caso, la unica forma de coger soltura con esto es con el uso, asi que lo mejor que podéis hacer es probarlo.

En comparación a lo que hemos hecho, y aprovechando que estamos hablando de mysql, en este enlace tenéis el ejemplo de cómo se mostrarian las consultas lentas con BPF/BCC. Si seguís el texto del enlace, veréis que podéis usar lo que ya esta hecho (usando el script mysqld_query.py de los ejemplos de BCC, o que también podéis pedir pizza y café y llegar a muy bajo nivel gracias al uso que puede hacer BPF/BCC de la instrumentalización que ofrece mysql, antes principalmente para Dtrace, y ahora también para Linux. En todo caso, mejor si leéis el post (y el resto de la web) de Brendan Gregg para ir sacando más conclusiones.

read more

Monitorización del kernel con SystemTap I

sáb, 28 feb 2009 by Foron

Empiezo otra serie de dos posts. En este caso sobre algo que tenía "pendiente" desde hace ya tiempo. De hecho, normalmente escribiría mis propios ejemplos para publicarlos aquí, pero para ir más rápido me voy a limitar a referenciar los scripts que usaré para mostrar las funcionalidades de ..... SystemTap. SystemTap es una herramienta que sirve para monitorizar en tiempo real lo que está pasando con un kernel linux. Quitando el detalle de que el kernel tiene que tener ciertas opciones compiladas (luego las comento), SystemTap tiene la gran ventaja de no requerir ningún tipo de reinicio para empezar a trabajar.

¿Qué podemos monitorizar con SystemTap?

Pues ..... prácticamente todo lo que queramos. La segunda parte de este post tratará algunos ejemplos, pero por dar alguna pista, con este software podemos desde vigilar los procesos que más I/O están generando a programarnos un "nettop" que diga los procesos que más están usando la red.

¿Cómo funciona SystemTap?

Con SystemTap se le dice al kernel que ejecute una rutina cuando ocurre un evento, que puede basarse, por citar dos ejemplos, en el tiempo (cada n segundos) o en una llamada del sistema (al ejecutarse un vfs_read). Para esto se usa un lenguaje de programación propio con el que se definen los "probe points" y las diferentes funciones. A pesar de ofrecer muchas posibilidades, el propio lenguaje tiene un control especial sobre los bucles infinitos, el acceso a memoria o la recursividad, por poner tres ejemplos. ¿Por qué tanto control? Pues porque a partir de este código se genera un módulo de kernel que se carga en el sistema. Claro, no hace falta decir las consecuencias de un bucle "mal hecho" a tan bajo nivel.

¿Qué necesita el kernel de linux para poder usar SystemTap?

Para empezar, cómo no, necesitamos un núcleo capaz de cargar módulos. Después, deberemos activar el soporte para los distintos tipos de debug que tiene el kernel, como el del sistema de ficheros, el del propio kernel o los kprobes. Traducido a formato .config, necesitamos las opciones CONFIG_DEBUG_INFO, CONFIG_KPROBES, CONFIG_RELAY, CONFIG_DEBUG_FS, CONFIG_MODULES y CONFIG_MODULES_UNLOAD. Algunas distribuciones incluyen kernels específicos con estas opciones ya activadas.

¿Por qué SystemTap y no strace o gdb?

Bueno, esta seguramente sea una buena pregunta, al menos hasta ver los ejemplos de la segunda parte de este post; pero resumiendo, con SystemTap podemos:

  • Ver de forma integrada y unificada lo que pasa en el kernel y en las aplicaciones que ejecuta.
  • Probar aplicaciones multihilo.
  • Monitorizar aplicaciones multiproceso, como las cliente-servidor, en las que ambos componentes son procesos independientes.
  • Monitorizar en tiempo real y a prácticamente la velocidad de ejecución original.
  • Escribir nuestros propios monitores que den detalles que aplicaciones "generalistas" no son capaces de dar.

¿Cuáles son los aspectos negativos del invento?

Evidentemente, no todos son ventajas con SystemTap. Podríamos hablar de los inconvenientes técnicos, porque las opciones de debug del kernel ralentizan un poco la velocidad del sistema, pero yo creo que los mayores problemas vienen por el aprendizaje necesario para usar el software. Para empezar, hay que tener un cierto conocimiento del kernel de linux; después, hay que saber las posibilidades del lenguaje de programación, y por último hay que ser capaz de interpretar los resultados. Todo esto desmoralizará a más de uno, seguro, que preferirá seguir con top, htop, vmstat y demás familia antes de meterse en este embolado. Afortunadamente, ya hay multitud de scripts disponibles en Internet para todo tipo de situaciones.

Suelo terminar los posts con una referencia bibliográfica. En este caso no creo que haya ningún libro sobre SystemTap, pero sí que hay un redpaper de IBM (que debería pasar a ser un redBOOK pronto :-) ): SystemTap: Instrumenting the Linux Kernel for Analyzing Performance and Functional Problems

read more

Monitorización del kernel con SystemTap II

sáb, 11 abr 2009 by Foron

A veces (seguro que pocas), nos encontramos ante una máquina que por algún motivo ha dejado de trabajar como se esperaba. Digamos que el rendimiento o el IO del equipo baja, sin motivo visible. Digamos que no hay logs que indiquen problemas, ni errores del sistema. Nada. Este es el momento para el kung fu, el voodoo o todo tipo de técnicas adivinatorias que tanto usamos los informáticos. Muchas veces es suficiente, pero no siempre, y no hay nada peor que no ser capaz de dar una explicación a un problema.

Systemtap

Supongamos que tenemos la arquitectura de la imagen, con un par de balanceadores de carga, unas cuantas máquinas iguales que pueden ser servidores web, ftp, pop, smtp o cualquier otra cosa, y un par de servidores de almacenamiento nfs. Teniendo en cuenta que los balanceadores de carga suelen dar la posibilidad de asignar diferentes pesos a cada máquina balanceada, ¿Por qué no usar uno de estos servidores para la monitorización del sistema? Es perfectamente posible enviar un poco menos de tráfico a la máquina destinada a SystemTap, de tal manera que no afecte al rendimiento global, para que podamos darnos un mecanismo para tener nuestra infraestructura controlada.

En este post sólo voy a referenciar algunos scripts que están disponibles en la web y en el redpaper. Además, el propio software viene con muchos ejemplos. En CentOS se puede instalar el rpm "systemtap-testsuite" para probar varios scripts.

Vamos a empezar con un par de ejemplos para usar más en un entorno académico que en la práctica.

Digamos que queremos aprender "lo que pasa" cuando hacemos un "ls" en un directorio de una partición ext3. Con este sencillo script:

  #! /usr/bin/env stap

  probe module("ext3").function("*").call
  {
     printf("%s -> %s\n", thread_indent(1), probefunc())
  }
  probe module("ext3").function("*").return
  {
     printf("%s <- %s\n", thread_indent(-1), probefunc())
  }

Vamos a hacer un printf cada vez que comience y termine la ejecución de cualquier función del módulo ext3. En el printf vamos a tabular el nombre de la función que se ha ejecutado. El resultado es algo así:

  ...
  2 ls(31789): <- ext3_permission
   0 ls(31789): -> ext3_dirty_inode
   8 ls(31789):  -> ext3_journal_start_sb
  21 ls(31789):  <- ext3_journal_start_sb
  26 ls(31789):  -> ext3_mark_inode_dirty
  31 ls(31789):   -> ext3_reserve_inode_write
  35 ls(31789):    -> ext3_get_inode_loc
  39 ls(31789):     -> __ext3_get_inode_loc
  52 ls(31789):     <- __ext3_get_inode_loc
  56 ls(31789):    <- ext3_get_inode_loc
  61 ls(31789):   <- ext3_reserve_inode_write
  67 ls(31789):   -> ext3_mark_iloc_dirty
  74 ls(31789):   <- ext3_mark_iloc_dirty
  77 ls(31789):  <- ext3_mark_inode_dirty
  83 ls(31789):  -> __ext3_journal_stop
  87 ls(31789):  <- __ext3_journal_stop
  90 ls(31789): <- ext3_dirty_inode
   0 ls(31789): -> ext3_permission
  ...

Como he dicho, no es algo demasiado práctico, pero puede servir a algún profesor para suspender a un buen porcentaje de pobres alumnos :-) Sigamos con algún otro ejemplo. Digamos que queremos programar un "nettop" que nos diga qué conexiones se están abriendo en una máquina en cada momento. Con un script similar al siguiente lo tendremos en unos minutos:

  #! /usr/bin/env stap

  global ifxmit, ifrecv

  probe netdev.transmit
  {
    ifxmit[pid(), dev_name, execname(), uid()] <<< length
  }

  probe netdev.receive
  {
    ifrecv[pid(), dev_name, execname(), uid()] <<< length
  }

  function print_activity()
  {
    printf("%5s %5s %-7s %7s %7s %7s %7s %-15s\n",
           "PID", "UID", "DEV", "XMIT_PK", "RECV_PK",
           "XMIT_KB", "RECV_KB", "COMMAND")

    foreach ([pid, dev, exec, uid] in ifrecv-) {
      n_xmit = @count(ifxmit[pid, dev, exec, uid])
      n_recv = @count(ifrecv[pid, dev, exec, uid])
      printf("%5d %5d %-7s %7d %7d %7d %7d %-15s\n",
             pid, uid, dev, n_xmit, n_recv,
             n_xmit ? @sum(ifxmit[pid, dev, exec, uid])/1024 : 0,
             n_recv ? @sum(ifrecv[pid, dev, exec, uid])/1024 : 0,
             exec)
    }
    print("\n")
    delete ifxmit
    delete ifrecv
  }

  probe timer.ms(5000), end, error
  {
    print_activity()
  }

¿Qué es lo que hemos hecho? Hemos generado tres "probes". Por un lado, cuando se recibe o trasmite a través de la red añadimos a los arrays "ifxmit, ifrecv" información sobre qué proceso y en qué interfaz ha enviado o recibido. Por otro lado, cada 5000 ms o cuando haya un error o termine el script mostramos la información por pantalla. El resultado puede ser algo similar a lo siguiente:

   PID   UID DEV     XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
 31485   500 eth0          1       1       0       0 sshd

 y pasados unos miles de milisegundos ...

   PID   UID DEV     XMIT_PK RECV_PK XMIT_KB RECV_KB COMMAND
 15337    48 eth0          4       5       5       0 httpd
 31485   500 eth0          1       1       0       0 sshd

Como se ve, tengo una sesión ssh abierta permanentemente, y he consultado una página web en el servidor monitorizado. Por supuesto, esto puede no ser muy práctico en servidores muy activos, pero puede dar alguna idea a algún administrador. Igual que hemos hecho un "nettop", también podemos hacer un "disktop" que muestre un resultado como el siguiente:

 Sat Apr 11 17:22:03 2009 , Average:   0Kb/sec, Read:       0Kb, Write:      0Kb
      UID      PID     PPID                       CMD   DEVICE    T        BYTES
       48    15342    15239                     httpd     dm-0    W          210
       48    15343    15239                     httpd     dm-0    W          210
       48    15337    15239                     httpd     dm-0    W          210
       48    15336    15239                     httpd     dm-0    W          210

Lo que hecho es hacer el mismo wget varias veces. Para el que tenga curiosidad, los procesos httpd (que por cierto usa el usuario id=48) ejecutando en el servidor son:

 # pstree -p | grep http
         |-httpd(15239)-+-httpd(15336)
         |              |-httpd(15337)
         |              |-httpd(15338)
         |              |-httpd(15339)
         |              |-httpd(15340)
         |              |-httpd(15341)
         |              |-httpd(15342)
         |              `-httpd(15343)

El script disktop está disponible en "/usr/share/systemtap/testsuite/systemtap.examples/io" del rpm "systemtap-testsuite".

Volvamos al primer ejemplo del post. Teníamos un grupo de servidores que han dejado de funcionar como deben. Digamos que sospechamos de la velocidad con la que nuestros servidores nfs están sirviendo el contenido de los servidores web. Podemos probar aquellos ficheros que necesiten más de 1 segundo para abrirse:

 #!/usr/bin/stap

 global open , names
 probe begin {
         printf("%10s %12s %30s\n","Process" , "Open time(s)" , "File Name")
 }
 probe kernel.function("sys_open").return{
         open[execname(),task_tid(task_current()),$return] = gettimeofday_us()
         names[execname(),task_tid(task_current()),$return] = user_string($filename)
 }
 probe kernel.function("sys_close"){
         open_time_ms = (gettimeofday_us() - open[execname(),task_tid(task_current()), $fd])
         open_time_s = open_time_ms / 1000000
         if ((open_time_s >= 1) && (names[execname(),task_tid(task_current()), $fd] != "")) {
                 printf("%10s %6d.%.5d %30s\n", execname(),
 open_time_s,open_time_ms%1000000,names[execname(),task_tid(task_current()), $fd])
         }
 }

Con este sencillo script estaría hecho:

    Process Open time(s)                      File Name
      httpd      8.471285 /var/www/html/lectura_lenta.html

En este caso al servidor web le ha costado 8.5 segundos servir el fichero lectura_lenta.html. Después será responsabilidad nuestra buscar, en su caso, la solución.

En definitiva, systemtap es una herramienta muy completa, pero que como tal requiere algo de práctica para ser útil. No sé si alguna vez va a tener mucho éxito en entornos de producción, pero no está de más saber que existe.

read more

Monitorización en serio. Práctica

lun, 24 jun 2013 by Foron

Después de haber hablado un poco sobre la teoría de la monitorización tal y como la veo yo, sigo con la parte práctica. Como dice Pieter Hintjens, la teoría está bien, en teoría; pero en la práctica, la práctica es mejor. Vamos a ver si doy algunas pistas de alternativas para llegar más allá de lo que permiten las herramientas de monitorización estándares.

Empecemos suponiendo que uno de nuestros servidores, un martes cualquiera, a las 10:30 a.m., aparece con el load average (uno de esos parámetros tan usados como mal interpretados) tres veces más alto que lo normal en otros martes a la misma hora. ¿Cómo de malo es esto?

  • Peligroso desde el punto de vista de la seguridad, porque puede implicar algún ataque, o que estemos mandando spam, o a saber qué.
  • Peligroso porque podría ser debido a un problema hardware, y con ello ser la antesala de una caida total del servidor.
  • Moderadamente serio en el caso en que simplemente se deba a que la conexión con nuestro servidor NFS se haya "atascado" (no es muy profesional, lo sé) de forma puntual, con lo que podría ser simplemente una pequeña congestión en la red, por decir algo.
  • ¡Estupendo! Si se debe a que la última campaña de publicidad ha tenido éxito y nuestro servidor está a pleno rendimiento. En este caso tendríamos que ver si necesitamos más hierro, pero si el rendimiento es bueno podremos estar satisfechos por estar aprovechando esas CPUs por las que pagamos un buen dinero.

En definitiva, que nos vendría bien una monitorización algo más trabajada y capaz de ir algo más allá de los simples números.

Pongámonos en el escenario de un servidor IMAP que en momentos puntuales rechaza más validaciones de lo normal. Sabemos que los usuarios se quejan porque no pueden autenticarse contra el servidor, pero no sabemos el motivo exacto. Aquí el problema es otro, porque necesitaremos saber cuándo tenemos un ratio acierto/fallo alto, y a partir de ese momento decidir qué acciones tomar, ya sea en la línea de revisar conexiones de red, carga del sistema, descriptores de ficheros, estado del backend de validación .... El gran problema en este tipo de fallos puntuales es que son "difíciles de pillar", al poder pasar en cualquier momento, y sólo durante unos pocos segundos. Además, no siempre se deben a problemas fáciles de monitorizar, como son la carga o el consumo de memoria de los servidores. El que un sistema de monitorización se conecte bien a los equipos A y B no siempre significa que A no pierda tráfico cuando habla con B.

¿Y qué aplicaciones hay que revisen todo esto? Pues no lo sé; pero aunque las hubiera, como este blog no va sobre apt-get install y siguiente-siguiente, vamos a hacer algo moderadamente artesano.

No nos volvamos locos. Por mucho que nos lo curremos, y salvo el improbable caso en el que nos den tiempo suficiente para programarlo, es muy complicado picar un sistema de monitorización, con todo lo que implica, desde cero. Hacer una aplicación capaz de leer datos del sistema, analizarlos y parsearlos, con buen rendimiento y estabilidad, no es fácil. Además, si algo hay en el "mercado", son aplicaciones para monitorización de infraestructuras, que además funcionan muy bien. Dediquemos nuestro tiempo a escribir esa capa extra propia de nuestro entorno a la que no pueden llegar las herramientas generalistas.

Si hay un sistema de monitorización que destaca sobre los demás que conozco, ese es Collectd. ¡Ojo! No digo que Collectd haga mejores gráficos que Graphite, o que sea más configurable que Cacti o Munin. Lo que quiero remarcar es que Collectd es perfecto para esto que queremos hacer. ¿Por qué?

  • Como la mayoría de aplicaciones serias, una vez configurada y puesta en marcha, podemos despreocuparnos de que se caiga o deje de funcionar.
  • Tiene buen rendimiento. Todo el núcleo y los plugins están programados en C, y aunque esto no implique automáticamente que vaya a ir rápido, en mi experiencia funciona muy bien.
  • Es modular. Tiene más de 90 plugins de todo tipo, desde los habituales para revisar la memoria o CPU, hasta otros más especializados, como Nginx o Netapp.
  • Se divide en dos grandes grupos de plugins (hay más), uno para leer datos (de CPU, memoria, ...), y otro para escribirlos (a gráficos rrd, a un Graphite externo, ...).

Pero me he dejado lo mejor para el final: El que sea modular significa que podemos quitar todo lo que no necesitemos, como por ejemplo todos los plugins que pueden afectar al rendimiento del servidor (escribir en ficheros rrd, sin ir más lejos, puede ser delicado), y con ello tener un sistema de monitorización muy poco intrusivo, pero completo. Además, y aquí está lo bueno, podemos escribir nuestros propios plugins, ya sea en perl, python o C. Usaremos esta funcionalidad para la lógica de nuestra aplicación.

El ejemplo

Volvamos al caso del servidor de correo que genera errores de validación en algunos momentos de carga alta. En este contexto, para un diagnóstico correcto, lo normal es pensar que vamos a necesitar, por lo menos, los plugins de lectura relacionados con el uso de CPU, el load-average, el consumo de memoria y el de conexiones TCP para saber la cantidad de sesiones abiertas contra el servidor de validación. Pero, además, tenemos que saber cuándo está fallando el servidor, y esto lo haremos a partir de los logs de la aplicación, y del número de "Login OK" en relación a los "Login Error". Para conseguir esta información de logs usaremos el módulo "tail". El plugin de salida que escribiremos recogerá todos estos datos, los analizará, y generará un informe que nos mandará por correo (o reiniciará el servidor, o arrancará una nueva instancia de KVM, o lo que sea que programemos).

En otros posts he escrito demasiado código, y no tengo claro que esto no sea más una forma de despistar a la gente que algo útil. Lo que sí voy a hacer es escribir una estructura de ejemplo que puede seguirse a la hora de programar plugins de Collectd.

Empecemos con la configuración más básica de Collectd, una vez instalado en el equipo a monitorizar. Tened en cuenta que siempre es interesante usar una versión razonablemente reciente (para escribir plugins en perl se necesita una versión por encima de 4.0, y para python de 4.9, que salió en el 2009).

El fichero de configuración principal de Collectd es collectd.conf, independientemente de que esté en /etc, /etc/collectd, /usr/local/etc, o en cualquier otro sitio. Es fácil de interpretar, así que me voy a limitar a lo fundamental para el post. En un entorno real deberíais leer la documentación.

Interval 10

Con esta opción especificamos cada cuánto vamos a leer datos. Si estamos leyendo el consumo de memoria del servidor, hablamos simplemente de una lectura cada 10 segundos, pero si hablamos del módulo tail, como veremos más adelante, estaremos calculando el número de veces que aparece cierto mensaje en ese intervalo determinado.

Empezamos por los plugins de entrada que no necesitan configuración, y que se instancian simplemente con un "loadplugin":

LoadPlugin cpu
LoadPlugin load
LoadPlugin memory

Otros, como no puede ser de otra forma, necesitan alguna opción:

LoadPlugin tcpconns
<Plugin "tcpconns">
  ListeningPorts false
  RemotePort "3306"
</Plugin>

Tcpconns monitoriza las conexiones TCP del servidor. En este ejemplo necesitamos saber las sesiones abiertas hacia servidores Mysql, ya que es el backend que usamos para la autenticación. En realidad, deberíamos usar el plugin de mysql, que da toda la información que se obtiene a partir de un "show status", pero para este ejemplillo nos vale con esto.

Por último, en cuanto a los plugins de lectura se refiere, necesitamos el plugin "tail", que configuraremos para que siga el log de validaciones de usuarios (maillog), y las cadenas de texto "Login OK" y Login Failed":

LoadPlugin tail
<Plugin "tail">
        <File "/var/log/maillog">
                Instance "Email_auth"
                <Match>
                        Regex "^.*Login[[:blank:]]OK.*$"
                        DSType "CounterInc"
                        Type "counter"
                        Instance "login_ok"
                </Match>
                <Match>
                        Regex "^.*Login[[:blank:]]Failed.*$"
                        DSType "CounterInc"
                        Type "counter"
                        Instance "login_failed"
                </Match>
        </File>
</Plugin>

Podéis complicar la expresión regular todo lo que queráis. Hay algunas opciones de configuración adicionales que no se muestran en este ejemplo, pero que suelen venir bien, como "ExcludeRegex", con la que se pueden quitar ciertas cadenas de la búsqueda; útil en casos como cuando necesitamos eliminar de la búsqueda los "Login OK" de usuarios de prueba que lanzan otros sistemas de monitorización. A los que conozcáis MRTG y familia, además, os sonarán los "DSType" y "Type" de la configuración. Efectivamente, podemos hacer gráficos de todo lo que encontremos usando valores medios, máximos, .... En nuestro caso viene bien un "CounterInc", que no hace más que ir sumando todos los "Login OK|Fail", y que por lo tanto va a servirnos para hacer cálculos sencillos cada 10 segundos, y también en otros periodos más largos.

Y con esto terminamos la parte de lectura de datos. La información obtenida desde estos plugins servirá para detectar anomalías en el servicio y, a partir de ahí, para hacer otra serie de tests más específicos siempre que sea necesario.

En nuestro caso de uso no queremos generar ningún gráfico, así que solo necesitamos que collectd lance una instancia del script que vamos a escribir en lo que a plugins de salida se refiere. Por ejemplo, "/usr/local/bin/monitorcorreo.py" (sí, esta vez en python):

<LoadPlugin python>
        Globals true
</LoadPlugin>
<Plugin python>
        ModulePath "/usr/local/bin"
        LogTraces true
        Import "monitorcorreo"
        <Module monitorcorreo>
                Argumento1 1
                Argumento2 "Podemos pasar argumentos al script"
        </Module>
</Plugin>

Vale, ahora solo queda escribir la lógica de lo que queremos conseguir con la monitorización. Vamos, lo importante. Recordad que tenemos que recoger los datos que nos mandan el resto de plugins, hacer las comprobaciones que tengamos que hacer y, de ser así, tomar una acción. En realidad, el que programemos el script en Python o Perl hace que no tengamos demasiados límites, más allá de los que tenga el usuario con el que ejecutemos Collectd.

El Script

Centrándonos ya en lo que sería "/usr/local/bin/monitorcorreo.py", el script debe registrarse en Collectd llamando al método collectd.register_config(funcionConfig). Por supuesto, antes debéis haber importado el módulo collectd, y deberéis haber escrito una función "funcionConfig", que básicamente debería leer las opciones de configuración que hayamos escrito en collectd.conf y hacer con ellas lo que sea que necesitemos.

El siguiente método a llamar es collectd.register_init(funcionInit). En este caso, Collectd va a llamar a la función "funcionInit" antes de empezar a leer datos. Por lo tanto, suele usarse para inicializar las estructuras, conexiones de red y demás estado del plugin. En mi caso, por ejemplo, uso este método para inicializar una instancia de la clase donde guardo el histórico de los datos que voy leyendo (hay que programarla, claro), y también creo un socket PUB/SUB basado en ZeroMQ con el que publicar toda la información relevante. Puedo usar este socket para mandar información a otros equipos (a mi PC, por ejemplo), o para conectar el proceso sin privilegios que es Collectd con otro menos expuesto que sea capaz de reiniciar servicios o de tomar otras acciones que requieran permisos de root.

Lo siguiente es registrar lo que sirve para indicar que estamos ante un plugin de escritura, con "collectd.register_write(funcionWrite)". Esta es la función que llamará Collectd cada vez que quiera escribir los datos que haya leido. "funcionWrite" es, por lo tanto, donde se ejecuta toda la lógica de nuestro script.

Como he venido diciendo, la clave de lo que haga la función funcionWrite es algo ya demasiado particular como para escribirlo aquí. Las pistas que puedo daros, sin embargo, son las siguientes:

  1. Si escribís algunas clases, con sus estructuras de datos y sus métodos, y las instanciais como "global", tendréis todo el histórico de datos (si lo queréis) durante todo el tiempo que esté collectd funcionando.

  2. Collectd va a ejecutar funcionWrite cada vez que lea desde los plugins de lectura.

  3. Si queréis hacer un ratio entre los "Login OK" y los "Login Failed" de una misma iteración de 10 segundos, con un contador incremental como CounterInc tendréis que restar los valores actuales a los de la iteración anterior, para sacar así los casos en estos 10 segundos. Dicho de otra forma, si ahora mismo hay 20 "Login OK" y 2 "Login Failed", y dentro de 10 segundos hay "27 Login OK" y 2 "Login Failed", en este intervalo de 10 segundos han habido 7 logins correctos y 0 fallidos. Este cálculo lo podéis hacer con comodidad si seguís la recomendación del punto 1. A partir de aquí podéis hacer las sumas, restas, divisiones o lo que sea que os apetezca.

  4. Todas las mediciones que manda Collectd llevan un timestamp. Cuando llegue una medición con una marca de tiempo 10 segundos mayor, será el momento de hacer todos los cálculos que queráis, porque ya tendréis la imagen completa de lo que ha pasado en ese intervalo.

  5. Para cada plugin de entrada, Collectd va a llamar a la función "funcionWrite" tantas veces como datos se generen, pasando como argumento un diccionario. En el caso del plugin de conexiones TCP, por ejemplo, se hace una llamada para cada estado posible (str(argumento)):

    collectd.Values(type='tcp_connections',type_instance='SYN_RECV',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[20.0])
    
    collectd.Values(type='tcp_connections',type_instance='FIN_WAIT1',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[2.0])
    
    collectd.Values(type='tcp_connections',type_instance='FIN_WAIT2',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[4.0])
    
    collectd.Values(type='tcp_connections',type_instance='TIME_WAIT',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[1.0])
    
    collectd.Values(type='tcp_connections',type_instance='CLOSED',plugin='tcpconns',plugin_instance='3306-remote',host='192.168.10.20',time=1372071055.051519,interval=10.0,values=[0.0])
    
    ...
    

    Otro ejemplo, este caso para el consumo de memoria:

    collectd.Values(type='memory',type_instance='used',plugin='memory',host='192.168.10.20',time=1372071405.0433152,interval=10.0,values=[415285248.0])
    
    collectd.Values(type='memory',type_instance='buffered',plugin='memory',host='192.168.10.20',time=1372071405.0441294,interval=10.0,values=[28184576.0])
    
    collectd.Values(type='memory',type_instance='cached',plugin='memory',host='192.168.10.20',time=1372071405.0494869,interval=10.0,values=[163659776.0])
    
    collectd.Values(type='memory',type_instance='free',plugin='memory',host='192.168.10.20',time=1372071405.050016,interval=10.0,values=[2551083008.0])
    

    Por último, esto es lo que manda el plugin tail.

    collectd.Values(type='counter',type_instance='login_ok',plugin='tail',plugin_instance='Email_auth',host='192.168.10.20',time=1372071405.0442178,interval=10.0,values=[27])
    
    collectd.Values(type='counter',type_instance='login_failed',plugin='tail',plugin_instance='Email_auth',host='192.168.10.20',time=1372071405.044635,interval=10.0,values=[2])
    

    Cada plugin genera los datos propios de lo que esté monitorizando, pero la estructura es siempre la misma. Hay que tener un poco de cuidado con los valores que se devuelven en "values", porque no son siempre una medición puntual aislada. Con nuestra configuración para tail sabemos que ese "values" tiene el número de líneas con login ok o failed desde que arrancamos Collectd, pero si lo hubiésemos definido como Gauge (por ejemplo), tendríamos otro valor diferente, y entraríamos en el terreno de los valores medios, máximos y mínimos tan de MRTG.

  6. Si en una iteración se dieran las condiciones de fallo que hubiérais definido, como sería por ejemplo un 0.2% de fallos de Login en relación a los correctos, podéis usar la librería que más os guste de Python para hacer pruebas de todo tipo, desde un traceroute a una conexión a Mysql para lanzar una consulta determinada. En el caso de las validaciones, podríais completar el diagnóstico usando la librería IMAP de Python para capturar el error que devuelve el servidor. En definitiva, no hay límites.

  7. Podéis enviar el informe de diagnósito por correo, o en un fichero de texto, o en un socket ZeroMQ, o de cualquier otra forma que permita Python. Podéis reiniciar aplicaciones, lanzar instancias de KVM, ....

Para no pecar de "abstracto", este es un esqueleto de ejemplo de un monitorcorreo.py cualquiera:

import collectd
'''import time, imaplib, socket, smtplib ...'''

class ClasesDeApoyo(object):
        '''
        Estrucutras de datos para guardar los valores que recibimos desde los plugins.
        Métodos para trabajar con los datos, ya sean actuales, o históricos.
        Métodos para relacionar los datos de distintos plugins.
        Métodos para generar informes, mandar correos, ....
        Métodos para hacer traceroutes, abrir sesiones IMAP, ....
        '''

def funcionConfig(argconfig):
        '''
        En argconfig se encuentran, entre otros, los argumentos que han entrado desde collectd.conf.
        '''
        global instanciasClasesDeApoyo
        '''
        Crear una instancia de las clases de apoyo, aunque se puede dejar para Init.
        Si se van a usar los argumentos de collectd.conf, se pueden leer en un bucle.
        '''

def funcionInit():
        '''
        Esta función se usa para inicializar datos. Puede ser interesante para llamar a métodos que abran conexiones, ficheros, ....
        '''
        global instanciasClasesDeApoyo
        '''
        Inicializar estructuras.
        Si todo ha ido bien, se registra en collectd la función Write.
        '''
        collectd.register_write(funcionWrite)

def funcionWrite(argdatos):
        '''
        Este es el método al que se llama cada vez que se genere un dato.
        Este método se encarga del trabajo real del script.
        '''
        global instanciasClasesDeApoyo
        '''
        Todos los valores vienen con un timestamp. Una idea es ir guardando estos valores en una estructura.
        Idea 1: Cuando el dato que se lea tenga un timestamp 10 segundos mayor que el anterior, es el momento de aplicar los calculos que tengamos que hacer, porque en ese punto ya tendremos la información de todos los plugins.
        Idea 2: Cuando hayáis leido los n datos que sabéis que se escriben en cada iteración, es el momento de aplicar los calculos que tengamos que hacer, porque en ese punto ya tendremos la información de todos los plugins.
        No siempre hacen falta todos los datos que recibimos desde collectd. Lo siguiente es un ejemplo.
        '''
        datos = {}
        datos["host"] = str(argdatos.host)
        datos["plugininstance"] = str(argdatos.plugin_instance)
        datos["typeinstance"] = str(argdatos.type_instance)
        datos["value"] = int(argdatos.values[0])
        datos["time"] = int(argdatos.time)
        datos["localtime"] =  str(time.strftime("%F %T",time.localtime(int(argdatos.time))))

        '''
        if datos["time"] < anteriordatos["time"]:
                instanciasClasesDeApoyo.hacerCalculos(datos)
        else:
                instanciasClasesDeApoyo.guardarDatos(datos)
        '''

collectd.register_config(funcionConfig)
collectd.register_init(funcionInit)
read more