Gestión de logs con Solandra II

vie, 05 ago 2011 by Foron

Seguimos con el segundo post de la serie, en el que pasamos a dar una descripción un poco más técnica de los componentes necesarios para poner en marcha todo lo descrito en el primero. No vamos a entrar en demasiado detalle. En todo caso, una vez conocidas las aplicaciones es más fácil buscar información en la red.

Aunque Solandra puede encargarse de la instalación de Cassandra, aquí vamos a usar los componentes por separado.

Cassandra

Cassandra es un tipo de base de datos creado siguiendo los principios propuestos por Dynamo (Amazon) y por BigTable (Google). El que quiera entrar en detalle tiene bibliografía y mucha documentación disponible.

Explicar el modelo de datos, la replicación o los niveles de consistencia va más allá del objetivo de este post. Lo más interesante en nuestro contexto es dejar claro que Cassandra es una base de datos completamente distribuida y descentralizada, en la que todas las máquinas del cluster cumplen el mismo y único rol, sin distinciones entre "maestros", "esclavos", "catálogos", .... Esto significa que añadir capacidad a un cluster de Cassandra supone básicamente añadir más hierro. Nada más.

La instalación puede complicarse todo lo que queramos, pero lo básico es:

  # Cuidado con la versión
  cd /tmp/ && wget http://apache.rediris.es/cassandra/0.8.2/apache-cassandra-0.8.2-bin.tar.gz
  cd /usr/local/ && tar xvzf /tmp/apache-cassandra-0.8.2-bin.tar.gz

A partir de aquí se crean los directorios de datos y logs (por ejemplo /var/lib/cassandra y /var/log/cassandra), y se adaptan los ficheros de configuración (que están en /usr/local/apache-cassandra-0.8.2/conf). El que más nos interesa ahora es cassandra.yaml. En github hay un ejemplo de configuración de este fichero, aunque sea casi por defecto, de cada uno de los tres nodos que he usado en esta prueba.

Hay un par de opciones de configuración que pueden servir para comprender la estructura del sistema. Un cluster de Cassandra se entiende como un anillo en el que cada uno de los nodos gestiona un volumen determinado de datos (replicación aparte). Básicamente estamos hablando de una serie de claves (hashes) que se distribuyen de una forma más o menos equilibrada entre todas las máquinas. Aunque no sea estrictamente necesario, como para esta prueba he usado un número fijo de nodos (3), he asignado ya desde el comienzo un 33% de datos a cada uno. Por supuesto, en un entorno real en el que se añaden y quitan nodos dinámicamente la gestión es diferente. La configuración en mi laboratorio es la siguiente:

  # Nodo 1
  initial_token: 0
  # Nodo 2
  initial_token: 56713727820156410577229101238628035242
  # Nodo 3
  initial_token: 113427455640312821154458202477256070485

La otra opción que merece la pena comentar es "seed_provider". Cassandra usa un protocolo tipo Gossip para distribuir la información entre los nodos. Esto significa que, cuando se añade un nuevo miembro al cluster, es suficiente con indicarle un servidor activo del mismo. El protocolo se encarga de propagar esta nueva información en todos los nodos. Por lo tanto, la configuración se limita a especificar una (o varias) IPs activas:

  seed_provider:
      - class_name: org.apache.cassandra.locator.SimpleSeedProvider
        parameters:
            - seeds: 192.168.10.145

Hay mucha más opciones de configuración, por supuesto, pero lo dejamos aquí.

En este momento ya se podría ejecutar Cassandra, pero esperaremos a instalar Solandra.

Solandra

Hablemos antes de lo que es Solr, una vez más, muy muy por encima.

Solr es una plataforma de búsqueda construida sobre la librería Lucene. Simplificando mucho, y en el contexto de este post, ofrece un interfaz XML (es el que nos interesa aquí, pero no el único) para añadir documentos, y un API HTTP a través de cual recibir resultados en formato JSON (entre otros).

Todo se verá más claro cuando creemos el schema para la gestión de logs. Para el que quiera profundizar más en Solr, aquí tiene un libro.

Volvamos a Solandra. La instalación es sencilla. Una vez descargado el tar.gz desde githubsolandra, y con ant y los binarios de java en el path, ejecutamos lo siguiente en cada uno de los nodos:

  # El nombre del fichero cambia
  cd /usr/local && tar xvzf /tmp/tjake-Solandra-4f3eda9.tar.gz
  cd tjake-Solandra-4f3eda9/
  ant -Dcassandra=/usr/local/apache-cassandra-0.8.2 cassandra-dist

Si todo va bien, en unos minutos la salida estándar mostrará lo siguiente:

  ....
  cassandra-dist:
       [copy] Copying 36 files to /usr/local/apache-cassandra/lib
       [copy] Copying 8 files to /usr/local/apache-cassandra/conf
       [copy] Copying 1 file to /usr/local/apache-cassandra/bin
       [echo] Libraries successfully copied into cassandra distribution
       [echo] Start the cassandra server with /usr/local/apache-cassandra/bin/solandra command

  BUILD SUCCESSFUL
  Total time: 2 minutes 43 seconds

Durante la instalación deberían haberse copiado las librerías y scripts en el arbol de Cassandra. Incluyendo algunos ficheros de configuración, que en este caso dejamos por defecto, aunque lo normal sería que los adaptásemos.

Sin más, vamos a arrancar Solandra (y con ello Cassandra), en cada nodo. Como no hemos hecho ningún cambio en el logueo, vamos a ver todos los mensajes por la salida estándar:

  cd /usr/local/apache-cassandra-0.8.2/bin/
  ./solandra &

Y con esto ya debería estar todo listo. El siguiente paso es añadir el schema (parecido a como se haría en un Solr estándar), volcar los datos, y preparar el interfaz web. Pero esto es cosa de un tercer post. No pensaba escribirlo, pero bueno, este ya es demasiado largo.

read more

Gestión de logs con Solandra III

lun, 15 ago 2011 by Foron

Pasamos a la tercera parte de la serie, en la que ya vamos a ver un ejemplo concreto de lo que puede ser un sencillo interfaz para la gestión de logs usando Solandra. Como siempre, todo el código y los ficheros de configuración más importantes están en Github.

Recordemos los objetivos que nos hemos marcado:

  • Ser capaces de gestionar un volumen muy importante de logs, con la máxima escalabilidad y disponibilidad.
  • Poder añadir los logs en el sistema de una forma sencilla.
  • Tener un interfaz web desde el que poder visualizar los datos.

Ya hemos hablado sobre la escalabilidad de Cassandra en los posts anteriores, así que no vamos a volver a entrar en este punto. Veamos los otros dos:

Inserción de datos en el sistema

Solandra es básicamente una adaptación de Solr, por lo que en realidad vamos a tratar conceptos propios de esta aplicación en lo que queda de post. Cuando veamos algo específico de Solandra, lo señalaré.

Uno de los elementos más importantes de la configuración del sistema es el schema; que viene a ser el lugar en el que se definen los atributos que forman cada "documento" (correo en este caso) que se quiere indexar. Como veremos más adelante, una vez creada esta estructura de datos, usaremos el comando curl para insertarla en el cluster a través de una url determinada. Además, Solandra permite trabajar con varios schemas diferentes de manera simultanea.

Simplicando un poco, el schema está compuesto por dos grandes bloques: La definición de los tipos de datos y la lista de atributos que forman cada documento.

Solr ofrece muchos tipos de datos ya creados de antemano: numéricos, texto, fechas, .... Por si esto fuera poco, la aplicación permite definir nuevos tipos siempre que se considere necesario. Para la gestión de logs de correo, por ejemplo, nos podría venir bien un tipo específico para las direcciones email:

  ...
  <fieldType name="email" class="solr.TextField" >
     <analyzer>
        <tokenizer class="solr.PatternTokenizerFactory" pattern="@" />
        <filter class="solr.LowerCaseFilterFactory" />
        <filter class="solr.TrimFilterFactory" />
     </analyzer>
  </fieldType>
  ...

Este tipo se basa en el TextField clásico, pero forma los tokens alrededor de la "@". De esta manera, facilitaremos las búsquedas tanto en base a la parte local de las direcciones como al dominio. Además, elimina los espacios y convierte las mayúsculas en minúsculas.

El siguiente paso es la definición de los atributos que componen cada documento, y que por supuesto van a ser de alguno de los tipos disponibles en el schema.

  <fields>
      <field name="id" type="uuid" indexed="true" stored="true" required="true" />
      <field name="in_from" type="email" indexed="true" stored="true" />
      <field name="in_size" type="tint" indexed="false" stored="true" />
      <field name="in_fentrada" type="tdate" indexed="true" stored="true" />
      <field name="in_to" type="email" indexed="true" stored="true" />
      <field name="in_to_adicional" type="email" indexed="true" stored="true" multiValued="true" />
      <field name="in_fsalida" type="tdate" indexed="true" stored="true" multiValued="true" />
      <field name="in_estado" type="string" indexed="false" stored="true" multiValued="true" />

      <dynamicField name="*" type="ignored" multiValued="true" />
  </fields>

Casi no hace falta explicar nada. En este sencillo ejemplo tenemos un identificador, un origen, un tamaño, una fecha de entrada y una fecha de entrega. Además, con cada mensaje vamos a guardar el estado de entrega para cada destino (pueden haberse realizado varios intentos, por ejemplo a causa del greylisting), y la lista de destinatarios adicionales para los que iba dirigido.

Obviamente, es una estructura limitada. En un entorno real se debería guardar mucha más información (antivirus, antispam, expansión de aliases, ...).

Y con esto ya lo tenemos. Sólo queda volcar el schema en el cluster:

  curl http://ip_cluster:8983/solandra/schema/correo --data-binary @/root/schema_correo.xml -H 'Content-type:text/xml; charset=utf-8'

Como hemos dicho, al igual que un "/schema/correo", se podría definir un "/schema/web", por ejemplo, y usarlos simultáneamente.

El volcado de datos

El volcado de datos se puede hacer de varias formas, pero vamos a limitarnos al uso de ficheros xml. Lo mejor es ver un ejemplo:

 <add allowDups="false">
    <doc>
       <field name="in_from">origenspam@example.com</field>
       <field name="id">b309842a-1cf7-11e0-9759-f896edbeae14</field>
       <field name="in_fentrada">2011-04-30T00:17:24Z</field>
       <field name="in_size">941</field>
       <field name="in_to">destino1@target.example.net</field>
       <field name="in_fsalida">2011-04-30T00:17:25Z</field>
       <field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_DF2BD74CA71/</field>
       <field name="in_to_adicional">destino1@target.example.net</field>
       <field name="in_to_adicional">destino2@target.example.net</field>
       <field name="in_to_adicional">destino3@target.example.net</field>
       <field name="in_to_adicional">destino4@target.example.net</field>
    </doc>
    <doc>
       <field name="in_from">origenspam@example.com</field>
       <field name="id">b30991b7-1cf7-11e0-9759-e819bb7f7b58</field>
       <field name="in_fentrada">2011-04-30T00:17:24Z</field>
       <field name="in_size">941</field>
       <field name="in_to">destino2@target.example.net</field>
       <field name="in_fsalida">2011-04-30T00:17:24Z</field>
       <field name="in_estado">0 - 192.168.10.24_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_A945A21CD6B/</field>
       <field name="in_to_adicional">destino1@target.example.net</field>
       <field name="in_to_adicional">destino2@target.example.net</field>
       <field name="in_to_adicional">destino3@target.example.net</field>
       <field name="in_to_adicional">destino4@target.example.net</field>
    </doc>
    <doc>
       <field name="in_from">cliente1@example.net</field>
       <field name="id">b43de232-1cf7-11e0-9759-d71ac36a357f</field>
       <field name="in_fentrada">2011-04-30T00:04:41Z</field>
       <field name="in_size">6077</field>
       <field name="in_to">greylister@example.com</field>
       <field name="in_fsalida">2011-04-30T00:07:27Z</field>
       <field name="in_estado">0 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:
          _Sender_address_rejected:_Message_delayed_now.Retry_later,_please./Giving_up_on_172.16.0.14./</field>
       <field name="in_fsalida">2011-04-30T00:12:38Z</field>
       <field name="in_estado">1 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_450_4.7.1_<cliente1@example.net>:
          _Sender_address_rejected:_Message_delayed_now._Retry_later,_please./Giving_up_on_172.16.0.14./</field>
       <field name="in_fsalida">2011-04-30T00:31:53Z</field>
       <field name="in_estado">2 - 172.16.0.14_does_not_like_recipient./Remote_host_said:_451_4.3.0_<greylister@example.com>:
          _Temporary_lookup_failure/Giving_up_on_172.16.0.14./</field>
       <field name="in_fsalida">2011-04-30T01:04:43Z</field>
       <field name="in_estado">3 - 172.16.0.14_accepted_message./Remote_host_said:_250_2.0.0_Ok:_queued_as_CB6A3D4A217/</field>
       <field name="in_to_adicional">greylister@example.com</field>
    </doc>
 </add>

El volcado, otra vez, es muy sencillo.

  curl http://ip_cluster:8983/solandra/correo/update -F stream.file=/tmp/volcado.xml

La conversión de logs desde el más que probable modo texto de syslog a xml, y de ahí al cluster de Solandra, queda fuera de esta serie de posts. De hecho, un servidor piensa que esto es lo realmente importante y difícil para llevar este proyecto a la práctica de una manera "seria".

El Interfaz

¿Qué mejor que un interfaz web para mostrar los datos que hemos almacenado en Solandra? ¡Sorpresa! la gente detŕas del proyecto ajax-solr ya ha hecho la mayoría del trabajo, así que sólo nos queda modificar un puñado de ficheros, algo de código JavaScript, y ya lo tendremos: Demo

Un detalle más: No queremos permitir el acceso directo a Solandra desde la web, así que necesitamos un proxy que filtre las consultas y las redirija al puerto de Solandra (tcp/8983 por defecto), y que en mi laboratorio escucha en localhost. En este caso, como casi siempre que quiero programar algún tipo de servicio para Internet sin dedicarle mucho tiempo, he usado node.js. Para este ejemplo, y por jugar un poco con GeoIP, he escrito un sencillo proxy que permite conexiones sólo si tienen como referer forondarena.net y si vienen desde Europa o América del Norte. Como no podría ser de otra forma, el código de este proxy también está disponible en Github.

(Nota: Esta demo es un Solr normal, pero el funcionamiento es idéntico al de Solandra).

Conclusiones

El "mercado" está lleno de soluciones de todo tipo que nos pueden ayudar en la gestión de logs. Hay aplicaciones comerciales, como Splunk, sistemas basados en software libre, como Solr, tecnología que nos puede permitir crecer "ilimitadamente", como Cassandra, Hadoop o Hbase, pero que requieren algo de trabajo; y también tenemos los mágnificos sistemas de bases de datos, como Mysql o Postgresql. ¿Cuál elegir?

En una primera fase, una buena base de datos con un sencillo interfaz web o un Solr estándar pueden servir perfectamente para gestionar todo tipo de logs. De hecho, tanto Mysql como Solr ofrecen alternativas para el particionado que pueden permitir este esquema en la segunda, tercera o cuarta fase.

Un buen consejo que escuche hace tiempo es el de "no arreglar lo que no está roto". Sólo deberíamos plantearnos el uso de tecnología que probablemente no conozcamos tan bien como las anteriores cuando realmente sea necesario. Llegado ese momento, adelante. Como siempre, la comunidad detrás del software libre es activa y está dispuesta a ayudar. Por si esto fuera poco, cada vez son más comunes las empresas que ofrecen servicios alrededor de este tipo de soluciones, y que pueden asesorarnos llegado el caso.

read more

Inspección de tráfico con tcpdump y tcpflow

sáb, 08 nov 2008 by Foron

Antes de seguir con el segundo post sobre ossec, voy a dar un par de pistas sobre cómo ver el tráfico que pasa por una sesión tcp. Esto sí que es todo un mundo, así que me limito, como casi siempre, a dar cuatro detalles para que quien quiera se haga una idea de lo que se puede hacer y siga investigando.

Por supuesto, el que se pueda "espiar" lo que se está trasmitiendo no significa que debamos (ni podamos) hacerlo, al menos si no queremos tener problemas legales.

Vamos a empezar por lo básico. Necesitamos capturar tráfico para poderlo analizar después con cierta tranquilidad. Para esto usamos el conocido tcpdump, aunque podemos usar otros, como por ejemplo, snort.

  tcpdump -n -i eth1 -s 1515 -U -w /tmp/captura.pcap '(tcp port 20) or (tcp port 21) or (tcp port 25)'
  tcpdump: listening on eth1, link-type EN10MB (Ethernet), capture size 1515 bytes

Para saber lo que hacen los parámetros nada mejor que el manual :-)

Dejamos esta terminal abierta y con el comando en ejecución. Vamos a ir analizando lo que se vuelca en captura.pcap en otra terminal.

Primera prueba.

Desde otra máquina vamos a hacer un telnet al puerto 25 y a mandar un correo.

  telnet 192.168.10.1 25
  Trying 192.168.10.1...
  Connected to 192.168.10.1.
  Escape character is '^]'.
  220 smtp.example.com ESMTP Postfix
  helo prueba.example.com
  250 smtp.example.com
  rset
  250 2.0.0 Ok
  helo prueba.example.com
  250 smtp.example.com
  mail from: prueba@example.com
  250 2.1.0 Ok
  rcpt to: prueba1@example.com
  250 2.1.5 Ok
  data
  354 End data with .
  Subject: Titulo del correo
  Este texto se ve en la captura
  .
  250 2.0.0 Ok: queued as 1F7426C420
  quit
  221 2.0.0 Bye
  Connection closed by foreign host.

Nuestro volcado tiene datos.... vamos a ver que tiene usando tcpflow.

  # tcpflow -r captura.pcap -c port 25
  192.168.010.001.00025-192.168.010.002.41403: 220 smtp.example.com ESMTP Postfix
  192.168.010.002.41403-192.168.010.001.00025: helo prueba.example.com
  192.168.010.001.00025-192.168.010.002.41403: 250 smtp.example.com
  192.168.010.002.41403-192.168.010.001.00025: rset
  192.168.010.001.00025-192.168.010.002.41403: 250 2.0.0 Ok
  192.168.010.002.41403-192.168.010.001.00025: helo prueba.example.com
  192.168.010.001.00025-192.168.010.002.41403: 250 smtp.example.com
  192.168.010.002.41403-192.168.010.001.00025: mail from: prueba@example.com
  192.168.010.001.00025-192.168.010.002.41403: 250 2.1.0 Ok
  192.168.010.002.41403-192.168.010.001.00025: rcpt to: prueba1@example.com
  192.168.010.001.00025-192.168.010.002.41403: 250 2.1.5 Ok
  192.168.010.002.41403-192.168.010.001.00025: data
  192.168.010.001.00025-192.168.010.002.41403: 354 End data with .
  192.168.010.002.41403-192.168.010.001.00025: Subject: Titulo del correo
  192.168.010.002.41403-192.168.010.001.00025: Este texto se ve en la captura
  192.168.010.002.41403-192.168.010.001.00025: .
  192.168.010.001.00025-192.168.010.002.41403: 250 2.0.0 Ok: queued as 1F7426C420
  192.168.010.002.41403-192.168.010.001.00025: quit
  192.168.010.001.00025-192.168.010.002.41403: 221 2.0.0 Bye

Sorpresa, tcpflow ha generado, en esta caso por salida estándar (-c), todo lo que ha sido capturado en el puerto 25.

Segunda prueba.

Bien, vamos con algo un poco diferente, pero igual de fácil (recordad que esto no son más que ideas)

Ahora vamos a suponer que he programado un rootkit que se llama exploit, y que lo voy a subir por ftp a la máquina que estamos monitorizando.

  ftp 192.168.10.1
  Connected to 192.168.10.1.
  220 Este es un servidor privado. Por favor cierre la sesion inmediatamente.
  Name (192.168.10.1:prueba):
  331 Please specify the password.
  Password:
  230 Login successful.
  Remote system type is UNIX.
  Using binary mode to transfer files.
  ftp> put exploit
  local: exploit remote: exploit
  200 PORT command successful. Consider using PASV.
  150 Ok to send data.
  226 File receive OK.
  101992 bytes sent in 0.00 secs (29433.1 kB/s)
  ftp> quit
  221 Goodbye.

Muy bien, ahora veamos lo que tenemos, empezando por el puerto 21, y después por el 20.

  # tcpflow -r captura.pcap -c port 21
  192.168.010.001.00021-192.168.010.002.50427: 220 Este es un servidor privado. Por favor cierre la sesion inmediatamente.
  192.168.010.002.50427-192.168.010.001.00021: USER prueba
  192.168.010.001.00021-192.168.010.002.50427: 331 Please specify the password.
  192.168.010.002.50427-192.168.010.001.00021: PASS secreto
  192.168.010.001.00021-192.168.010.002.50427: 230 Login successful.
  192.168.010.002.50427-192.168.010.001.00021: SYST
  192.168.010.001.00021-192.168.010.002.50427: 215 UNIX Type: L8
  192.168.010.002.50427-192.168.010.001.00021: TYPE I
  192.168.010.001.00021-192.168.010.002.50427: 200 Switching to Binary mode.
  192.168.010.002.50427-192.168.010.001.00021: PORT 192,168,10,2,205,32
  192.168.010.001.00021-192.168.010.002.50427: 200 PORT command successful. Consider using PASV.
  192.168.010.002.50427-192.168.010.001.00021: STOR exploit
  192.168.010.001.00021-192.168.010.002.50427: 150 Ok to send data.
  192.168.010.001.00021-192.168.010.002.50427: 226 File receive OK.
  192.168.010.002.50427-192.168.010.001.00021: QUIT
  192.168.010.001.00021-192.168.010.002.50427: 221 Goodbye.

Vamos a hacer ahora que tcpflow genere un fichero con el tráfico del puerto 20. Para esto quitamos el parámetro -c.

  # tcpflow -r captura.pcap  port 20
  # file 192.168.010.002.52512-192.168.010.001.00020
  192.168.010.002.52512-192.168.010.001.00020: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.8, stripped

Sorpresa, tenemos un fichero binario ..... con el exploit. Bueno, en realidad en una copia de /bin/ls, pero vale para el ejemplo

  # strings 192.168.010.002.52512-192.168.010.001.00020
  .......
  Usage: %s [OPTION]... [FILE]...
  List information about the FILEs (the current directory by default).
  Sort entries alphabetically if none of -cftuvSUX nor --sort.
  Mandatory arguments to long options are mandatory for short options too.
    -a, --all                  do not ignore entries starting with .
    -A, --almost-all           do not list implied . and ..
        --author               with -l, print the author of each file
    -b, --escape               print octal escapes for nongraphic characters
        --block-size=SIZE      use SIZE-byte blocks
  .......

¿Qué pasa en la realidad, cuando tenemos cientos o miles de sesiones de correo o ftp y queremos ver una concreta? Por un lado debemos guardar el tráfico que queremos investigar, claro, pero luego debemos saber qué sesiones se han establecido, a qué hora, cuánto tráfico ha pasado por ellas, entre que puertos, .... Para esto hay mucho software, pero un buen ejemplo es argus. A partir de aquí, podemos generar expresiones más elaboradas para usar en tcpflow (algo más que "port 20")

read more

Instalaciones automáticas desde usb

dom, 14 jul 2013 by Foron

Vamos a suponer por un momento que estamos en un entorno en el que no podemos tener un servidor PXE en condiciones. Para ponerlo todavía peor, imaginemos que somos de los que no conseguimos encontrar un cd de Knoppix razonablemente reciente cada vez que se nos fastidia un servidor y que, a pesar de las prisas, tenemos que esperar a ver como carga todo un entorno gráfico antes de poder hacer un simple fsck.

Todo esto tendría que ser parte del pasado, al estilo de los videos Beta o los cassettes; pero no, todavía es demasiado común, así que a ver si conseguimos dar algunas ideas útiles y buscamos alternativas que, aunque no sean lo más moderno que existe, nos faciliten un poco el trabajo.

No esperéis nada original en este post. Todo lo que escribo aquí está ya más que documentado, y mucho mejor que en estas cuatro notas. Aún así, a ver si os sirve como punto de partida.

Queremos conseguir dos cosa:

  • Un método para arrancar rápidamente una distribución live, sencilla, que permita recuperar particiones, transferir ficheros, ..., este tipo de cosas.
  • Un sistema para instalar distribuciones de forma automática, usando ficheros kickstart para RedHat/CentOS/... y preseed para Debian/Ubuntu/..., pero teniendo en cuenta que no podemos usar PXE, ni Cobbler, ni nada similar.

Ya hace mucho tiempo que se pueden arrancar sistemas desde memorias USB, y además GRUB tiene funcionalidades que permiten arrancar desde una ISO. Siendo esto así, ya tenemos todo lo necesario. Si incluimos un servidor web para guardar nuestros ficheros ks y preseed y, si queremos acelerar un poco las instalaciones, los paquetes de las distribuciones CentOS y Debian (las que voy a usar en este post), conseguiremos además que las instalaciones sean automáticas y razonablemente dinámicas.

Pasos previos

Empezamos. Buscad un pendrive que no uséis para nada y podáis formatear. Desde este momento asumo que el dispositivo que estáis usando corresponde a /dev/sde, y que tiene una única partición fat normal y corriente, en /dev/sde1. Si no es así, lo de siempre: "fdisk /dev/sde" + "mkfs.vfat -n USB_INSTALACIONES /dev/sde1":

#fdisk -l /dev/sde
Disposit. Inicio    Comienzo      Fin      Bloques  Id  Sistema
/dev/sde1   *        2048    15654847     7826400    c  W95 FAT32 (LBA)

Una vez más, aseguráos de que podéis y queréis borrar el contenido del pendrive. Por supuesto, vuestro kernel debe tener soporte para este tipo de particiones, y también necesitáis las utilidades para gestionarlas. En Debian están en el paquete "dosfstools". En cualquier caso, lo normal es que ya lo tengáis todo.

Vamos a montar la partición en "mnt" (o donde queráis), con un simple:

mount /dev/sde1 /mnt

Lo siguiente es instalar grub en /dev/sde. Otra vez, cuidado con lo que hacéis, no os confudáis de dispositivo.

grub-install --no-floppy --root-directory=/mnt /dev/sde

Dejamos estos pasos básicos y vamos ya a por la configuración más específica.

Creando el menú

En realidad, no vamos a hacer nada más que configurar GRUB. Podéis ser todo lo creativos que queráis, pero para este ejemplo voy a simplificar todo lo que pueda: Ni colores, ni imágenes de fondo, ni nada de nada.

Para tener un poco de variedad, desde mi USB se va a poder arrancar lo siguiente:

  • Una Debian Wheezy sin preseed, para ir configurando a mano.
  • Un CD con utilidades de repación. El que más os guste. Para este ejemplo: Ultimate BootCD.
  • Una Slax, por si quisiera arrancar un entorno gráfico completo.
  • Una Debian Wheezy con preseed, completamente automática.
  • Una CentOS 6.4 con kickstart, completamente automática.

Prestad atención a los dos últimos elementos de la lista, porque son los que nos van a permitir ir a un servidor "vacío", arrancar desde el USB, y en 5 minutos tener una Debian o una CentOS perfectamente instalados.

Vamos a crear el menú de GRUB. Necesitamos editar el fichero "/mnt/boot/grub/grub.cfg" con lo siguiente:

menuentry "Debian Wheezy x86_64 installer" {
        set gfxpayload=800x600
        set isofile="/boot/iso/wheezy_mini_amd64.iso"
        loopback loop $isofile
        linux (loop)/linux priority=low initrd=/initrd.gz
        initrd (loop)/initrd.gz
}

menuentry "Ultimate BootCD 5.2.5" {
        loopback loop /boot/iso/ubcd525.iso
        linux (loop)/pmagic/bzImage edd=off load_ramdisk=1 prompt_ramdisk=0 rw loglevel=9 max_loop=256 vmalloc=384MiB keymap=es es_ES iso_filename=/boot/iso/ubcd525.iso --
        initrd (loop)/pmagic/initrd.img
}

menuentry "Slax Spanish 7.0.8 x86_64" {
        set isofile="/boot/iso/slax-Spanish-7.0.8-x86_64.iso"
        loopback loop $isofile
        linux (loop)/slax/boot/vmlinuz load_ramdisk=1 prompt_ramdisk=0 rw printk.time=0 slax.flags=toram from=$isofile
        initrd (loop)/slax/boot/initrfs.img
}

menuentry "Debian Wheezy x86_64 preseed" {
        set isofile="/boot/iso/wheezy_mini_amd64.iso"
        loopback loop $isofile
        linux (loop)/linux auto=true preseed/url=http://192.168.10.40/instalaciones/wheezy_preseed_131.cfg debian-installer/country=ES debian-installer/language=es debian-installer/keymap=es debian-installer/locale=es_ES.UTF-8 keyboard-configuration/xkb-keymap=es console-keymaps-at/keymap=es debconf/priority=critical netcfg/disable_dhcp=true netcfg/get_ipaddress=192.168.10.131 netcfg/get_netmask=255.255.255.0 netcfg/get_gateway=192.168.10.1 netcfg/get_nameservers=192.168.10.1 --
        initrd (loop)/initrd.gz
}

menuentry "Centos 6.4 x86_64 kickstart" {
        set isofile="/boot/iso/CentOS-6.4-x86_64-netinstall.iso"
        loopback loop $isofile
        linux (loop)/images/pxeboot/vmlinuz ip=192.168.10.131 noipv6 netmask=255.255.255.0 gateway=192.168.10.1 dns=192.168.10.1 hostname=fn131.forondarena.net ks=http://192.168.10.40/instalaciones/ks_rh6_131.ks lang=es_ES keymap=es
        initrd (loop)/images/pxeboot/initrd.img
}

menuentry "Restart" {
        reboot
}

menuentry "Shut Down" {
        halt
}

Suficiente, no necesitamos nada más. Repasemos un poco:

  • Las entradas del menú se separan en bloques "menuentry", uno para cada instalación diferente, e incluyendo las dos últimas para reiniciar y para apagar el equipo (no son muy útiles pero sirven de ejemplo).
  • Como no me gusta escribir demasiado, he definido la variable, "isofile" con la imagen que va a usar GRUB para arrancar (ahora hablamos sobre esto) en cada bloque.
  • Vamos a usar imágenes ISO "normales", y GRUB va a asumir que son la raíz de la instalación.
  • Como sabéis, cuando queremos arrancar un sistema, es habitual especificar un kernel en una línea que empieza con "linux", las opciones que queremos usar con este núcleo, y un initrd. Aquí estamos haciendo exactamente esto, pero debemos especificar la ruta dentro de la imagen ISO donde encontrar el kernel y el initrd. Lo más fácil es que montéis la imagen y veáis dónde está cada uno.
  • Las dos instalaciones automáticas usan más opciones que el resto. Lo que estamos haciendo es pasar el fichero de configuración (preseed o kickstart) que el sistema leerá vía http, y luego una serie de opciones básicas (idioma, teclado, ...). Ni todas son necesarias, ni son todas las que se pueden poner.
  • Como hemos dicho que no queremos usar PXE, asumo que tampoco queremos usar DHCP, así que las configuraciones de red son estáticas.
  • La IP que asignamos al servidor en esta fase de instalación no tiene que ser necesariamente la misma que instalaremos en el servidor, aunque en este ejemplo asumo que será así.

Fácil, ¿Verdad? El siguiente paso es descargar las imágenes que estamos usando en cada bloque (opción "isofile"). Tened en cuenta que no hay nada raro en estas ISO. Son las imágenes estándar de las distribuciones, aunque las he renombrado para que todo quede más ordenado. Para guardarlas he creado un directorio "/mnt/boot/iso/", y he copiado ahí los siguientes ficheros:

  • Para Debian, con y sin preseed: mini.iso (renombrada a wheezy_mini_amd64.iso).
  • Para CentOS: netinstall.iso.
  • Para Ultimate BootCD: ubcd.
  • Para Slax: slax.

Cuando lo tengáis todo, desmontad /mnt, y ya habremos terminado con el pendrive. Quedan los preseed/kickstart.

Ficheros kickstart y preseed

Si os fijáis en el menú de GRUB, para Debian estamos usando una referencia al fichero wheezy_preseed_131.cfg. Este fichero no es más que un preseed normal y corriente que, obviamente, está preparado para la instalación que queremos hacer. Los ficheros preseed consisten en escribir todas las respuestas a todas las opciones de menú que pueden aparecer en el instalador. Esto hace que el sistema sea muy flexible, pero también muy denso. Si queréis ver un listado con todas las opciones disponibles, id a una máquina Debian y ejecutad lo siguiente:

debconf-get-selections --installer >> wheezy_preseed_131.cfg
debconf-get-selections >> wheezy_preseed_131.cfg

Ahí tenéis, todo un "wheezy_preseed_131.cfg"; una locura. Afortunadamente, no siempre hacen falta todas las opciones. De hecho, yo en mis instalaciones para KVM uso esta versión, mucho más reducida. Claro, esto implica que si os sale un diálogo durante la instalación para el que no hemos previsto una respuesta, quizá porque vuestro hardware pida "algo" extra, la instalación automática se va a parar. En este caso tendréis que buscar la opción que os falta y añadirla.

Os recomiendo que abráis el fichero y que le déis una vuelta. Tened en cuenta que está pensado para instalaciones sobre KVM y los drivers virtio, así que el dispositivo de disco que se usa es /dev/vda. Además, uso sólo un interfaz de red, eth0. En cuanto al particionado, uso una partición para boot, primaria y de 50MB, otra para swap de unos 512MB, y por último, el resto del disco, en un grupo LVM para la raíz.

Además de esto, suelo usar un mirror local de Debian para agilizar la primera instalación. No es necesario, podéis usar un mirror público y, con ello, simplificar aún más la infraestructura. Bueno, "simplificar" por decir algo, porque no se puede decir que copiar el contenido de los DVD de instalación de Debian en un servidor web sea complicado.

Aunque el sistema sea diferente, en realidad todo esto que he dicho para Debian se aplica igual para kickstart y las instalaciones automatizadas de CentOS. Revisad si queréis este ejemplo, subidlo a un servidor web, y adaptadlo a lo que os haga falta. Tened en cuenta que también suelo usar un mirror local en estos casos (otra vez, se trata sencillamente de descomprimir los DVD).

Pruebas en KVM

Una vez instalado GRUB, con el menú y las ISO copiadas en el pendrive, podemos probar el nuevo sistema en KVM. Es muy sencillo. De hecho, si usáis virt-manager, casi no tendréis que hacer nada. Una vez creada la máquina virtual, id a los detalles y pulsad sobre "agregar nuevo hardware". Después no tenéis más que elegir "usb host device" y, de la lista, la memoria USB. Una vez agregado el dispositivo, en el arranque de la máquina virtual, justo al principio, aparecerá una opción para acceder al menú de arranque pulsando F12. Entre las opciones que os van a aparecer debería estar el pendrive.

Por último, esto es lo que veréis si todo ha ido bien:

Pantallazo GRUB

Y ya está, con esto hemos terminado. Romped vuestros CDs!!

Nota

En el post hablo sobre un servidor web, pero luego no escribo nada más sobre ello. No hay demasiado que decir; en mi caso uso un directorio "instalaciones", y en ese directorio pongo los preseed/ks. Junto a esto, debajo de "instalaciones", creo un directorio "centos/6.4-x86_64" y otro "debian/wheezy" y copio ahí el contenido de los DVD de cada distribución.

read more

Introducción a BPF

lun, 04 sep 2017 by Foron

Aquí va otro post sin grandes cosas que aportar a los ya iniciados, pero que sí puede servir a todos aquellos que no conocen qué es el nuevo BPF; en su inicio eBPF (extended Berkeley Packet Filter), pero que cada vez más va pasando a ser, simplemente, BPF.

La inmensa mayoría de lo que voy a escribir no es contenido original. Consideradlo una introducción en la que os mostraré varios ejemplos de desarrollos ya hechos. Por supuesto, trataré de dar referencias de toda la documentación que voy a usar en este post.

¿Qué es BPF?

Hablemos del antiguo BPF, del original.

Brevemente, porque todas las presentaciones que podáis ver sobre el tema ya hacen una introducción, BPF fue un esfuerzo de allá por el 1992 para evitar la transferencia de paquetes, sobre todo de red, desde el Kernel al entorno de usuario. El objetivo era generar un bytecode seguro que pudiese ejecutarse lo más cerca posible de la red. La mayoría habéis hecho cosas tipo tcpdump -i eth0 "port 80". Internamente, y pecando de básico, ese filtro "port 80" se escribia de tal manera que pudiese engancharse al socket y así procesarse más eficientemente.

En el 2013, con Alexei Starovoitov como cabeza más visible, se dio una vuelta a este concepto y se comenzó el trabajo en el nuevo BPF. Como comentaba, todas las charlas que veáis tienen una introducción, así que mejor un par de enlaces, intro 1 y intro 2, que repetir lo que ya se ha dicho. En todo caso, resumiéndolo todo mucho, y con el riesgo de que simplificar demasiado me haga escribir algo incorrecto, lo que permite BPF es añadir código que se va a ejecutar en el Kernel ante ciertos "eventos". Dicho de otra forma, podemos ejecutar nuestro código cuando el Kernel llame o salga de las funciones tcp_v4_connect o ext4_file_open, por poner dos ejemplos. Pero no solo esto; también se pueden instrumentalizar sockets, librerías o aquellas aplicaciones que lo permitan.

Traducido a algunos ejemplos, con BPF pod(r)emos:

  • Ejecutar código para la gestión de red en las capas más bajas del Kernel. XDP (eXpress Data Path) es el nombre clave que conocer aquí (intro 3 , intro 4 e intro 5). Los usos son variados; Facebook ya están documentando que empiezan a usarlo para el balanceo de carga, otros como Cloudflare en entornos anti DDoS, tc ya tiene un clasificador basado en BPF, o el proyecto Cillium, cada vez más conocido, que está más orientado a contenedores. En realidad, no sería raro pensar que una buena parte del software que usamos en estos entornos (quizá incluyendo Open vSwitch y similares) pasen a usar BPF en mayor o menor medida en un futuro no muy lejano.
  • Pasar del entorno del Kernel al espacio de usuario para instrumentalizar librerías. Uno de los ejemplos ya disponibles del uso de uprobes muestra cómo podríamos capturar el tráfico HTTPS de un servidor web justo antes de ser cifrado. Veremos el script más adelante, pero básicamente lo que se hace es incluir nuestro código en las llamadas a SSL_read y SSL_write de openssl.
  • Usar la funcionalidad que ofrecen muchas de las aplicaciones más importantes que usamos habitualmente, com Mysql, Postgresql o lenguajes de programación como Java, Ruby, Python o Node, para conocer al detalle lo que está ocurriendo cuando se ejecutan. Las USDT (User Statically Defined Tracing) se han venido usando con Dtrace y SystemTap, y ahora también con BPF.

He dejado para el final el caso de uso seguramente más documentado. La monitorización y el análisis de rendimiento tienen ya decenas de scripts listos para su uso. Veamos algunos ejemplos.

Ejemplos

No voy a dedicar ni una línea a la instalación del software. Con la llegada de las últimas versiones estábles de las distribuciones Linux, en muchos casos con versiones de Kernel a partir de 4.9, tenemos una versión que suele recomendarse para tener ya la mayoría de funcionalidades. Esto en lo que al Kernel se refiere. El resto de utilidades alrededor de esta base se instalará en función de la distribución que uséis. Con Ubuntu puede ser tan sencillo como un apt-get install, y con otras podría obligaros a trabajar un poco más. Aquí tenéis algunas instrucciones.

Si alguno habéis seguido este último enlace, habréis visto que hemos pasado del acrónimo BPF a BCC. En el paquete BPF Compiler Collection tenemos todo lo necesario para que escribir código BPF sea más fácil. Para los que no somos capaces de escribir 20 líneas en C sin tener algún segmentation fault, lo mejor que nos ofrece este toolkit son a) aplicaciones ya hechas y b) la posibilidad de integrar el código que se ejecuta en el Kernel en scripts Python o Lua, además del propio C++. Últimamente también se está dando soporte a Go, pero no os puedo dar muchos más detalles porque no lo he probado.

Resumiendo, para los vagos, los que no tienen tiempo o los que no se ven capaces de escribir esa parte en C de la que os he hablado, BCC nos permite usar y aprender de los scripts que va publicando la comunidad en los directorios de herramientas y de ejemplos, y adaptarlos a nuestro entorno que, muchas veces, solo implicará escribir Python.

Para los que os veáis más capaces, un buen primer paso es leer este tutorial y la guía de referencia. Merece la pena tener ambos enlaces siempre a mano.

Imaginad, por ejemplo, que queréis registrar todo lo que se ejecuta en vuestras máquinas. Los ejemplos incluidos en el repositorio de BCC incluyen un script para mostrar lo que se lanza desde bash, y otro para lo que se ejecuta vía exec. Como bashreadline.py es más sencillo de leer, vamos a centrarnos en hacer un history remoto.

El esqueleto básico de todos los scripts es el siguiente:

  1. Import de la clase BPF
  2. Preparar el código C, ya sea vía fichero externo o como una variable de texto
  3. Crear una instancia de BPF haciendo referencia al código C
  4. Definir dónde (kprobe, uprobe, ...) queremos ejecutar las funciones C que hemos escrito
  5. Usar los recursos que ofrece la instancia BPF para trabajar con lo que va devolviendo el Kernel.

En el caso de bashreadline.py, y quitando lo menos relevante en este momento:

bpf_text = """
    #include <uapi/linux/ptrace.h>
    struct str_t {
        u64 pid;
        char str[80];
    };
    BPF_PERF_OUTPUT(events);

    int printret(struct pt_regs *ctx) {
        struct str_t data  = {};
        u32 pid;
        if (!PT_REGS_RC(ctx)) return 0;
        pid = bpf_get_current_pid_tgid();
        data.pid = pid;
        bpf_probe_read(&data.str, sizeof(data.str), (void *)PT_REGS_RC(ctx));
        events.perf_submit(ctx,&data,sizeof(data));
        return 0;
    };
"""

from bcc import BPF
bpf_text = """ ... """

b = BPF(text=bpf_text)
b.attach_uretprobe(name="/bin/bash", sym="readline", fn_name="printret")

def print_event(cpu, data, size):
    event = ct.cast(data, ct.POINTER(Data)).contents
    print("%-9s %-6d %s" % (strftime("%H:%M:%S"), event.pid, event.str.decode()))

b["events"].open_perf_buffer(print_event)
while 1:
    b.kprobe_poll()

El script original no llega a 60 líneas, así que no penséis que me haya comido demasiado. Si lo ejecutáis, veréis todo lo que se está ejecutando desde todos los bash de la máquina:

# ./bashreadline
TIME      PID    COMMAND
05:28:25  21176  ls -l
05:28:28  21176  date
05:28:35  21176  echo hello world
05:28:43  21176  foo this command failed

Nada os impide editar este código, así que ¿Por qué no cambiar ese print de print_event por algo que escriba en un syslog o un graphite remoto? Yo por ejemplo uso mucho ZeroMQ para estas cosas:

import zmq
contexto = zmq.Context()
eventos = contexto.socket(zmq.REQ)
eventos.connect("tcp://172.16.1.2:7658")
...
salida = print("%-9s %-6d %s" % (strftime("%H:%M:%S"), event.pid, event.str.decode()))
eventos.send_string(salida)
eventos.recv()

Vamos, que con 6 líneas (dejando a un lado el servidor escuchando en 172.16.1.2:7658), nos hemos hecho un history remoto.

Independientemente de lo útil que os haya parecido esto, ya veis que es muy fácil adaptar los scripts y hacer cosas realmente interesantes.

Vamos a ver algunos ejemplos de scripts. Todo está en github, así que podéis ir allí directamente.

Pregunta típica: ¿Cuál es el rendimiento de nuestro sistema de ficheros ext4?

El rendimiento de los dispositivos de bloque y de los diferentes sistemas de ficheros es uno de los temas más tratados entre las herramientas que ya tenemos disponibles. Hay scripts para ver la distribución de las latencias en un sistema o para hacer cosas parecidas al comando top (ejemplo top) y responder a la pregunta ¿Quién está accediendo a dispositivo en este momento? También los tenemos para mostrar distribuciones de latencias (ejemplo latencias) en un periodo de tiempo, o para analizar los sistemas de ficheros más habituales. Para ext4, por ejemplo, podemos usar ext4slower (ejemplo ext4slower) para ver qué reads, writes, opens y syncs han ido lentas, o ext4dist (ejemplo ext4dist) para ver un histograma con la distribución de las latencias de estas operaciones. En ext4slower.py, por ejemplo, se instrumentaliza la entrada y la salida de estas funciones (mirad el argumento event=)

...
# initialize BPF
b = BPF(text=bpf_text)

# Common file functions. See earlier comment about generic_file_read_iter().
b.attach_kprobe(event="generic_file_read_iter", fn_name="trace_read_entry")
b.attach_kprobe(event="ext4_file_write_iter", fn_name="trace_write_entry")
b.attach_kprobe(event="ext4_file_open", fn_name="trace_open_entry")
b.attach_kprobe(event="ext4_sync_file", fn_name="trace_fsync_entry")
b.attach_kretprobe(event="generic_file_read_iter", fn_name="trace_read_return")
b.attach_kretprobe(event="ext4_file_write_iter", fn_name="trace_write_return")
b.attach_kretprobe(event="ext4_file_open", fn_name="trace_open_return")
b.attach_kretprobe(event="ext4_sync_file", fn_name="trace_fsync_return")
...

Aunque estos scripts son un poco más difíciles de leer, si habéis echado un ojo al tutorial y a la referencia del principio del post los podréis seguir fácilmente.

El uso de la CPU es otra área muy trabajada. Aunque siempre ha habido herramientas para su análisis (no siempre se ha hecho bien), con BPF se han mejorado mucho. Un buen ejemplo son los flame graphs. Aunque la pregunta principal siempre ha sido ¿Qué está haciendo la CPU ahora?, con los últimos desarrollos que se están haciendo podéis ver tranquilamente tanto el tiempo "On-CPU" como "Off-CPU" de las tareas. Dicho de otra forma, podéis saber cuánto tiempo pasa el comando tar ejecutándose, y cuánto esperando a disco. Como vamos a ver más scripts en este post, en lugar de ver los ejemplos (que los hay, y muchos) para instrumentalizar el scheduler y todo lo relacionado con la ejecución de tareas, os voy a dar el enlace a una fantástica presentación de Brendan Gregg donde podéis ver mucho de lo que se puede hacer hoy en día con los Flame Graphs, ya sea vía BPF o vía perf. Os recomiendo que dediquéis un poco de tiempo a esto, porque es realmente interesante. Intenet está llena de referencias, así que ya tenéis un pasatiempo para un rato.

Nos saltamos la CPU y pasamos, por ejemplo, al análisis del uso de memoria.

Pongámonos en el escenario de un entorno en el que el consumo de memoria de una máquina va subiendo y subiendo. Una opción para tratar de ver si hay algo raro en la asignación de memoria es el script memleak (ejemplo memleak). Otra alternativa, como veremos más adelante, es centrar la investigación más en las propias aplicaciones.

Vamos a ver algo más de código. Digamos que queremos saber si estamos haciendo un uso eficiente de la memoria Cache. Justo para esto tenemos cachetop (ejemplo cachetop). La parte escrita en C es sencilla, una única función:

bpf_text = """
#include <uapi/linux/ptrace.h>
struct key_t {
    u64 ip;
    u32 pid;
    u32 uid;
    char comm[16];
};
BPF_HASH(counts, struct key_t);
int do_count(struct pt_regs *ctx) {
    struct key_t key = {};
    u64 zero = 0 , *val;
    u64 pid = bpf_get_current_pid_tgid();
    u32 uid = bpf_get_current_uid_gid();
    key.ip = PT_REGS_IP(ctx);
    key.pid = pid & 0xFFFFFFFF;
    key.uid = uid & 0xFFFFFFFF;
    bpf_get_current_comm(&(key.comm), 16);
    val = counts.lookup_or_init(&key, &zero);  // update counter
    (*val)++;
    return 0;
}
"""

A la que se hace referencia en la parte en Python:

...
b = BPF(text=bpf_text)
b.attach_kprobe(event="add_to_page_cache_lru", fn_name="do_count")
b.attach_kprobe(event="mark_page_accessed", fn_name="do_count")
b.attach_kprobe(event="account_page_dirtied", fn_name="do_count")
b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
...

Como veis, el mismo esquema que hasta ahora. El resultado nos muestra el porcentaje de acierto en Cache. Ejecutemos un find / mientras tenemos ejecutando el script:

# ./cachetop.py
13:01:01 Buffers MB: 76 / Cached MB: 114 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
...
984 vagrant  find                 9529     2457        4      79.5%      20.5%

Si ejecutamos una segunda vez ese mismo find /, veremos que el uso del Cache es mucho mś eficiente:

# ./cachetop.py
13:01:01 Buffers MB: 76 / Cached MB: 115 / Sort: HITS / Order: ascending
PID      UID      CMD              HITS     MISSES   DIRTIES  READ_HIT%  WRITE_HIT%
...
1071 vagrant  find                12959        0        0     100.0%       0.0%

Con esto terminamos esta parte. Recordad, tenéis muchos más ejemplos en el repositorio de github.

Hasta ahora, con la excepción de bashreadline.py, hemos estado muy centrados en el propio Kernel. En la introducción hemos visto que BPF permite subir un poco y llegar a librerías y aplicaciones.

Cuando subimos al userspace y a las librerías, en este caso concreto, entramos en el terreno de las uprobes. En el repositorio de BCC hay un ejemplo muy gráfico de lo que se puede hacer, por ejemplo, para la instrumentalización de la librería openssl. Al principio del post ya hemos hablado, muy por encima, de sslsniff (ejemplo sslsniff). La estructura es parecida a los kprobes, aunque en este caso instrumentalizamos las llamadas a librería:

...
b = BPF(text=prog)

# It looks like SSL_read's arguments aren't available in a return probe so you
# need to stash the buffer address in a map on the function entry and read it
# on its exit (Mark Drayton)
#
if args.openssl:
    b.attach_uprobe(name="ssl", sym="SSL_write", fn_name="probe_SSL_write",
                    pid=args.pid or -1)
    b.attach_uprobe(name="ssl", sym="SSL_read", fn_name="probe_SSL_read_enter",
                    pid=args.pid or -1)
    b.attach_uretprobe(name="ssl", sym="SSL_read", fn_name="probe_SSL_read_exit", pid=args.pid or -1)
...

probe_SSL_write es la función en C del script para, en este caso, la entrada a SSL_write de libssl.

...
int probe_SSL_write(struct pt_regs *ctx, void *ssl, void *buf, int num) {
    u32 pid = bpf_get_current_pid_tgid();
    FILTER
    struct probe_SSL_data_t __data = {0};
    __data.timestamp_ns = bpf_ktime_get_ns();
    __data.pid = pid;
    __data.len = num;
    bpf_get_current_comm(&__data.comm, sizeof(__data.comm));
    if ( buf != 0) {
            bpf_probe_read(&__data.v0, sizeof(__data.v0), buf);
    }
    perf_SSL_write.perf_submit(ctx, &__data, sizeof(__data));
    return 0;
}
...

Al final, el resultado del script completo es que somos capaces de ver en texto plano lo que entra en libssl desde, por ejemplo, un servidor web.

Para terminar con esta parte vamos a ver un par de ejemplos de USDT (Userland Statically Defined Tracing). Los que habéis usado SystemTap o Dtrace ya sabréis de lo que estamos hablando. Para usar las USDT necesitamos que las aplicaciones tengan soporte para este tipo de prueba. Muchas de las más importantes lo tienen, aunque no siempre están compiladas en las versiones "normales" que se instalan en las distintas distribuciones. Para ver qué tenemos disponible en un ejecutable podemos usar el script tplist. Sacando un extracto de este buen post de Brendan Gregg, tplist muestra lo siguiente:

# tplist -l /usr/local/mysql/bin/mysqld
/usr/local/mysql/bin/mysqld mysql:filesort__start
/usr/local/mysql/bin/mysqld mysql:filesort__done
/usr/local/mysql/bin/mysqld mysql:handler__rdlock__start
/usr/local/mysql/bin/mysqld mysql:handler__rdlock__done
/usr/local/mysql/bin/mysqld mysql:handler__unlock__done
/usr/local/mysql/bin/mysqld mysql:handler__unlock__start
/usr/local/mysql/bin/mysqld mysql:handler__wrlock__start
/usr/local/mysql/bin/mysqld mysql:handler__wrlock__done
/usr/local/mysql/bin/mysqld mysql:insert__row__start
/usr/local/mysql/bin/mysqld mysql:insert__row__done
/usr/local/mysql/bin/mysqld mysql:update__row__start
/usr/local/mysql/bin/mysqld mysql:update__row__done
/usr/local/mysql/bin/mysqld mysql:delete__row__start
/usr/local/mysql/bin/mysqld mysql:delete__row__done
/usr/local/mysql/bin/mysqld mysql:net__write__start
/usr/local/mysql/bin/mysqld mysql:net__write__done
...
/usr/local/mysql/bin/mysqld mysql:command__done
/usr/local/mysql/bin/mysqld mysql:query__start
/usr/local/mysql/bin/mysqld mysql:query__done
/usr/local/mysql/bin/mysqld mysql:update__start
...

Viendo las opciones, podemos usar query_start y query_end para sacar las consultas lentas. El script se llama dbslower (ejemplo dbslower):

...
# Uprobes mode
bpf = BPF(text=program)
bpf.attach_uprobe(name=args.path, sym=mysql_func_name, fn_name="query_start")
bpf.attach_uretprobe(name=args.path, sym=mysql_func_name, fn_name="query_end")
...

Y al final tendremos algo parecido a esto:

# dbslower mysql -m 0
Tracing database queries for pids 25776 slower than 0 ms...
TIME(s)        PID          MS QUERY
6.003720       25776     2.363 /* mysql-connector-java-5.1.40 ( Revision: 402933ef52cad9aa82624e80acbea46e3a701ce6 ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_conn
6.599219       25776     0.068 SET NAMES latin1
6.613944       25776     0.057 SET character_set_results = NULL
6.645228       25776     0.059 SET autocommit=1
6.653798       25776     0.059 SET sql_mode='NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES'
6.682184       25776     2.526 select * from users where id = 0
6.767888       25776     0.288 select id from products where userid = 0
6.790642       25776     2.255 call getproduct(0)
6.809865       25776     0.218 call getproduct(1)
6.846878       25776     0.248 select * from users where id = 1
6.847623       25776     0.166 select id from products where userid = 1
6.867363 25776 0.244 call getproduct(2)

Hay muchas aplicaciones que permiten compilarse con soporte para USDT. En realidad, no solo aplicaciones; Python, PHP, Ruby, Java o Node, por ejemplo, también nos permiten ver dónde se está atascando nuestro código.

Vamos terminando con algo un poco más didáctico que práctico.

Imaginemos que tenemos unos cuantos volúmenes NFS en una máquina y que queremos saber qué está pasando. La primera opción que usaríamos la mayoría: nfsiostat. Es una buena aplicación, basada en deltas, como tantas otras, y que nos permite saber que en x tiempo se han servido z bytes en un determinado punto de montaje. Hasta aquí todo bien.

El problema con este tipo de aplicaciones, entre otros, es que no siempre capturan bien las distribuciones bimodales; aquellas en las que el acceso es correcto la mayoría del tiempo pero, puntualmente, se dan picos de latencia más altos. Estamos hablando de un servidor web que funciona bien "casi siempre". Estas distribuciones bimodales (de acceso a disco, carga de CPU, ...) son las que hacen que la percepción de un servicio sea desastrosa, y no siempre son fáciles de tratar.

Analizar el rendimiento NFS a fondo no es para nada trivial. Incluso centrándonos solo en la parte cliente, tenemos que vigilar la red, RPC, Caches NFS, bloqueos, .... Al final, salvo que tengáis tiempo y conocimientos suficientes para hacer algo completo vía BPF, lo normal, creo, sería empezar monitorizando a nivel VFS. Para esto hay varios scripts ya hechos en el repositorio de BCC. Por ejemplo, fileslower:

root@bpf-bcc:/usr/share/bcc/tools# python fileslower 0
Tracing sync read/writes slower than 0 ms
TIME(s)  COMM           TID    D BYTES   LAT(ms) FILENAME
4.089    bash           869    W 12         0.04 prueba5.txt

Con este tipo de herramientas tampoco tendremos los detalles de los que hemos hablado (red, RPC, ...), pero sí podremos, al menos, detectar anomalías en rutas concretas y distribuciones bimodales. TIP: BPF tiene una estructura para generar histogramas, que son algo más útiles con este tipo de distribución de datos.

Vamos a terminar viendo algunos scripts más y, de paso, damos un repaso a lo que hace el Kernel cuando hacemos algunas operaciones en sistemas de ficheros NFS. No pretendo que os sean útiles, aunque lo son, por ejemplo para generar Flame Graphs.

Vamos a usar funccount un par de veces. ¿Qué se ejecuta en el Kernel que empiece por nfs_ cuando se monta un volumen de este tipo?

# python funccount -i 1  'nfs_*'
FUNC                                    COUNT
nfs_fscache_get_client_cookie               1
nfs_alloc_client                            1
nfs_show_mount_options                      1
nfs_show_path                               1
nfs_create_server                           1
nfs_mount                                   1
nfs_fs_mount                                1
nfs_show_options                            1
nfs_server_insert_lists                     1
nfs_alloc_fhandle                           1
nfs_fscache_init_inode                      1
nfs_path                                    1
nfs_try_mount                               1
nfs_setsecurity                             1
nfs_init_locked                             1
nfs_init_server                             1
nfs_set_super                               1
nfs_init_server_rpcclient                   1
nfs_parse_mount_options                     1
nfs_probe_fsinfo                            1
nfs_fhget                                   1
nfs_get_client                              1
nfs_fs_mount_common                         1
nfs_show_devname                            1
nfs_create_rpc_client                       1
nfs_alloc_inode                             1
nfs_init_timeout_values                     1
nfs_set_sb_security                         1
nfs_start_lockd                             1
nfs_verify_server_address                   1
nfs_alloc_server                            1
nfs_init_server_aclclient                   1
nfs_init_client                             1
nfs_request_mount.constprop.19              1
nfs_fill_super                              1
nfs_get_root                                1
nfs_get_option_ul                           2
nfs_alloc_fattr                             2
nfs_fattr_init                              5

¿Y cuando escribimos algo?

FUNC                                    COUNT
nfs_writeback_result                        1
nfs_put_lock_context                        1
nfs_file_clear_open_context                 1
nfs_lookup                                  1
nfs_file_set_open_context                   1
nfs_close_context                           1
nfs_unlock_and_release_request              1
nfs_instantiate                             1
nfs_start_io_write                          1
nfs_dentry_delete                           1
nfs_writehdr_free                           1
nfs_generic_pgio                            1
nfs_file_write                              1
nfs_alloc_fhandle                           1
nfs_post_op_update_inode_force_wcc_locked        1
nfs_fscache_init_inode                      1
nfs_pageio_add_request                      1
nfs_end_page_writeback                      1
nfs_writepages                              1
nfs_page_group_clear_bits                   1
nfs_initiate_pgio                           1
nfs_write_begin                             1
nfs_pgio_prepare                            1
nfs_flush_incompatible                      1
nfs_pageio_complete_mirror                  1
nfs_sb_active                               1
nfs_writehdr_alloc                          1
nfs_init_locked                             1
nfs_initiate_write                          1
nfs_generic_pg_pgios                        1
nfs_file_release                            1
nfs_refresh_inode                           1
nfs_pageio_init_write                       1
nfs_pgio_data_destroy                       1
nfs_fhget                                   1
nfs_inode_remove_request                    1
nfs_create_request                          1
nfs_free_request                            1
nfs_end_io_write                            1
nfs_open                                    1
nfs_create                                  1
nfs_pageio_init                             1
nfs_pgio_result                             1
nfs_writepages_callback                     1
nfs_writeback_update_inode                  1
nfs_do_writepage                            1
nfs_key_timeout_notify                      1
nfs_alloc_inode                             1
nfs_writeback_done                          1
nfs_write_completion                        1
nfs_pageio_complete                         1
nfs_pgio_release                            1
nfs_lock_and_join_requests                  1
nfs_page_group_destroy                      1
nfs_fscache_open_file                       1
nfs_pageio_doio                             1
nfs_pgio_header_free                        1
nfs_generic_pg_test                         1
nfs_write_end                               1
nfs_post_op_update_inode                    1
nfs_updatepage                              1
nfs_pageio_cond_complete                    1
nfs_get_lock_context                        1
nfs_inode_attach_open_context               1
nfs_file_open                               1
nfs_fattr_set_barrier                       1
nfs_init_cinfo                              1
nfs_pgheader_init                           1
nfs_create_request.part.13                  1
nfs_sb_deactive                             1
nfs_post_op_update_inode_locked             2
nfs_commit_inode                            2
nfs_refresh_inode.part.18                   2
nfs_scan_commit                             2
nfs_commit_end                              2
nfs_setsecurity                             2
nfs_release_request                         2
nfs_page_find_head_request_locked           2
nfs_reqs_to_commit                          2
nfs_unlock_request                          2
nfs_ctx_key_to_expire                       2
nfs_file_fsync                              2
nfs_file_flush                              2
nfs_page_group_sync_on_bit                  3
nfs_revalidate_inode_rcu                    3
nfs_alloc_fattr                             3
nfs_revalidate_inode                        3
nfs_do_access                               3
nfs_update_inode                            4
nfs_permission                              4
nfs_init_cinfo_from_inode                   4
nfs_refresh_inode_locked                    4
nfs_file_has_buffered_writers               4
nfs_page_group_lock                         5
nfs_fattr_init                              5
nfs_page_group_unlock                       5
nfs_pgio_current_mirror                     6
nfs_set_cache_invalid                       8
nfs_attribute_cache_expired                10

También tenemos scripts para ver la pila, por ejemplo cuando se ejecuten nfs_file_write y nfs_write_end:

# python stacksnoop nfs_file_write -p 1080
TIME(s)            FUNCTION
0.556759834        nfs_file_write
    nfs_file_write
    new_sync_write
    vfs_write
    sys_write
    system_call_fast_compare_end


#python stacksnoop nfs_write_end -p 1080
TIME(s)            FUNCTION
1.933541059        nfs_write_end
    nfs_write_end
    generic_perform_write
    nfs_file_write
    new_sync_write
    vfs_write
    sys_write
    system_call_fast_compare_end

No quiero terminar sin que veamos el script trace, una especie de navaja suiza que vale para casi todo, com podéis ver en los ejemplos trace. Sigamos viendo más información sobre las escrituras en NFS. En una terminal voy a ejecutar esto:

while true
do
  echo "hola" >> /mnt/prueba.txt
  sleep 2
done

Y en otra un par de llamadas a trace:

# python trace -p 1647 'r:c:write ((int)retval > 0) "write ok: %d", retval' -T
TIME     PID    TID    COMM         FUNC             -
21:28:15 1647   1647   bash         write            write ok: 5
21:28:17 1647   1647   bash         write            write ok: 5

# python trace -p 1647 'sys_write "written %d file", arg1'
PID    TID    COMM         FUNC             -
1647   1647   bash         sys_write        written 1 file
1647   1647   bash         sys_write        written 1 file

Y ya está. No merece la pena seguir con más ejemplos, porque todo está razonablemente bien documentado, como habéis visto. Me he dejado muchos scripts, de todo tipo, pero seguro que os habéis hecho una idea.

Ojo! Que no haya dicho ni pio sobre XDP no significa que no sea interesante. Las posibilidades y el rendimiento de los desarrollos que está haciendo la gente (firewalls para contenedores, balanceadores de carga, ...) son muy prometedores, y seguro que van a llegar muy lejos. Quién sabe si será el tema del próximo post.

read more