20210120

Kafka , partitions and consumers

I have been interested in kafka lately. I was curious about their statement that, for a given topic, you can have as many consumers as partitions it has, but not more, that will be assigned to get messages.

What would happen to the consumers in the group, if you add a new one, and all partitions are assigned already? Let's give it a go.

The setup

Before you start, you'll need a kafka broker up and running. I have already running in my local one from https://github.com/confluentinc/cp-all-in-one. Just spin it up with docker-compose and you'll get it available in the usual 9092 port.

I am using python as is quick and easy for this test. You can checkout the git repo from https://github.com/davidfrigola/kafka-consumers-test

Once you have all this, just install dependencies (kafka-python) using

pip install -r requirements.txt

And you are ready to go.

The first step is to create the topic. Use

python kafka-setup.py

This will create a poc-topic, with 2 partitions. Now we can start using it.

The producer

Run producer.py to get some simple messages in the topic. It will generate one each second, with a timestamp-ish value in it.

python producer.py
Message sent %s some data in bytes 1611170322.1077127
Message sent %s some data in bytes 1611170323.1272047
Message sent %s some data in bytes 1611170324.1288335
Message sent %s some data in bytes 1611170325.1305933

...

The consumer

Let's start a couple of consumers. Each will be assigned to a partition, as the are for the same group (poc-group)

This will look like:

python consumer.py
poc-topic:0:523: key=None value=b'some data in bytes 1611170357.2019012'
poc-topic:0:524: key=None value=b'some data in bytes 1611170358.2052937'
poc-topic:0:525: key=None value=b'some data in bytes 1611170359.2070754'

....

The test

So now we have a producer, a topic with 2 partitions, and 2 consumers getting the messages from that topic, assigned to the same group.

Let's add a third consumer:

python consumer.py
poc-topic:1:484: key=None value=b'some data in bytes 1611170278.447001'
poc-topic:1:485: key=None value=b'some data in bytes 1611170279.4486403'
poc-topic:1:486: key=None value=b'some data in bytes 1611170285.457752'
poc-topic:1:487: key=None value=b'some data in bytes 1611170286.4592147'
poc-topic:1:488: key=None value=b'some data in bytes 1611170289.4642537


After a couple of seconds .. it starts consuming messages! What is going on? Well , lets check the other 2 consumer's logs ....

One of them will get stalled. Apparently, the latest you have started will stop consuming in favor of the new one.

Conclusion

The number of consumers actively doing job in a kafka consumers group is limited by the number of partitions in the topic, but adding a new consumer is not a not-effect operation : the new consumer will get a partition assigned, and one of the already assigned consumers will loose it, getting stalled.

Bear it in mind!



20120304

Maven : proyecto web modular

En el trabajo siempre tengo un proyecto web entre manos, sea cual sea la funcionalidad o el cliente. Con suerte, ese proyecto se construye con maven, está estructurado y es fácil de mantener.
Una de las cosas que me ocurren es que, a la hora de realizar una nueva funcionalidad, el despliegue sobre el sistema ya en producción debe ser parcial. No se envía el WAR directamente, sino se que envía un "parche" con los ficheros (jsp, class, configuración,...) que se hayan modificado, a descomprimir en el servidor de producción.

Para evitar enviar multitud de ficheros class, lo mejor es que todas las clases del proyecto se empaqueten en un JAR, de forma que, si se modifica cualquier clase, debe generarse de nuevo el JAR y enviarlo como parte del parche a desplegar en producción.

Viendo la generación de un WAR con maven, he encontrado la opción de generar el JAR de las clases de forma separada, usando la configuración del plugin maven-war-plugin:

<plugin>
     <artifactId>maven-war-plugin</artifactId>
     <version>2.2</version>
     <configuration>
         <archiveClasses>true</archiveClasses>
     </configuration>
</plugin>

Al generar el artefacto, se genera el WAR, y en el /WEB-INF/lib de la aplicación, nos encontramos con un JAR con las clases del proyecto. El directorio /WEB-INF/classes estará vacío.
El problema de esto es que algunos ficheros de configuración suelen estar en /WEB-INF/classes, como por ejemplo el fichero log4j.xml. Generando de la forma indicada, este fichero está contenido en el JAR resultante, y ahí tenemos un problema, ya que la configuración de los logs es totalmente dependiente del entorno de despliegue.

Así que la mejor solución, como casi siempre, es la que recomiendan los chicos de maven : http://maven.apache.org/plugins/maven-war-plugin/faq.html#attached

Básicamente nos insta a crear un proyecto que contenga las clases y otro que contenga la parte web de la vista. De esta forma, podemos meter el los resources del proyecto web ficheros como log4j.xml, que terminarán en /WEB-INF/classes, y las clases en el proyecto que se genera como JAR, de forma que terminará estando en /WEB-INF/lib.

Al final, tendremos un proyecto pom padre, al que he llamado "mvnweb", y dos módulos hijos "mvnweb-classes" y "mvnweb-web". Como sus nombres indican, mvnweb-classes contiene las clases del proyecto y mvnweb-web la parte web del mismo. Añadimos una dependencia de mvnweb-classes en mvnweb-web y, al empaquetar el WAR, obtenemos lo deseado, siempre y cuando los ficheros dependientes de la configuración del entorno de despliegue estén en el proyecto mvnweb-web.

Éste es el pom del proyecto padre mvnweb:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>es.org.nms</groupId>
  <artifactId>mvnweb</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>pom</packaging>
  <modules>
      <module>mvnweb-classes</module>
    <module>mvnweb-web</module>
  </modules>
</project>

Éste es el pom de mvnweb-classes

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <artifactId>mvnweb</artifactId>
    <groupId>es.org.nms</groupId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath>..</relativePath>
  </parent>
  <artifactId>mvnweb-classes</artifactId>
</project>

Y finalmente el pom de mvnweb-web, que contiene la dependencia con mvnweb-classes.

<?xml version="1.0"?>
<project
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>mvnweb</artifactId>
        <groupId>es.org.nms</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>es.org.nms</groupId>
    <artifactId>mvnweb-web</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>mvnweb-web Maven Webapp</name>
    <url>http://maven.apache.org</url>
    <dependencies>
        <dependency>
            <groupId>es.org.nms</groupId>
            <artifactId>mvnweb-classes</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>mvnweb-web</finalName>
    </build>
</project>


* Nota : está claro que la mejor opción para esto es tener un profile para generar un WAR para producción, pero la mayoría de las veces, al menos en mi caso, no es posible tener toda la configuración (conexiones a base de datos, usuarios y passwords) debido a razones de seguridad o a no disponer de esos datos.

20120216

JMeter : peticiones HTTP basadas en consultas a Base de Datos

Esta tarde he estado realizando unas pequeñas pruebas de integración, para comprobar el funcionamiento de un pequeño proyecto en el trabajo. Ha sido a última hora, por lo que no he tenido tiempo de investigar a fondo, así que he decidido mirarlo en casa, sin que sirva de precedente, pero sin por ello dejar de aprender ; )

La prueba consiste en acceder al sistema mediante un token. Este token está almacenado en base de datos, y tenemos una consulta para saber qué tokens son válidos. Este token se pasa por la URL de forma que se valida y luego se puede acceder a la aplicación. No se requiere mayor seguridad, es algo simple.

Así que he montado un entorno similar en mi equipo en casa.

Descripción

Vamos a realizar una prueba en la que se haga una petición HTTP con un parámetro obtenido de la base de datos.

Qué necesitamos
  • JMeter. En mi caso he descargado la última versión de aquí
  • Base de datos : en mi caso MySQL
  • Driver de la BD : hay que tener el driver jdbc en el directorio \lib de la instalación de JMeter para que éste sea capaz de conectarse.

Preparativos previos
He accedido a MySQL, he creado un usuario y un esquema para las pruebas.
He insertado varios valores de prueba en una tabla simple.
Tenemos una tabla jmeter_test1 con varios valores:




Proyecto JMeter

Debemos configurar la conexión a la base de datos.
Para ello, incluimos un elemento "Configuración de la conexión JDBC":



Es importante el nombre de la variable, en este caso jmeter_test1_jdbc_connection, ya que se usará más adelante.

Una vez configurado, debemos obtener los datos de la tabla de ejemplo. Para ello, incluimos un elemento "Petición JDBC":



En este elemento indicamos la variable definida en la configuración de la conexión.
La variable resultado jdbc_result contiene los datos que se obtengan en la consulta, que como vemos es muy simple.

Los resultados obtenidos son:
jmeter_value1
value11
value21
value31
value41

Es importante ver que la primera columna de los datos obtenidos es el nombre de la columna.Teniendo esto en cuenta, debemos definir el extractor de los datos mediante expresion regular, para obtener el primer VALOR de la consulta:





Ver tutorial sobre ER
Simulador para probar tus ER

Como vemos, obtendremos el primer elemento del listado.
El valor en "Nombre de Referencia" indica el nombre que le damos a la variable que contiene el valor obtenido.
La expresión (.+) nos obtiene cada una de las lineas del texto resultado obtenido por el elemento de petición JDBC.
Indicamos la plantilla como $1$
La coincidencia es 2, ya que la primera coincidencia es el nombre de la columna (la primera linea obtenida)
El valor por defecto "FAIL" es indicativo para las pruebas y nos indicará que ha fallado la extracción del valor buscado.


Para ver el uso, lo vamos a utilizar para hacer una petición HTTP a localhost.
Incluimos un elemento "Peticion HTTP":



Una vez lanzamos la ejecución de la prueba JMeter, obtendremos los resultados de cada petición.
Para verlos, añadimos un receptor, por ejemplo "Ver Árbol de Resultados". Al ejecutar, tendremos los resultados ahí guardados.



Vemos en la obtención de los datos usando JDBC que se obtiene la lista de valores.



Vemos en la petición HTTP que se ha usado como parámetro el primer valor obtenido. No es importante que haya fallado, sino ver la url con el parámetro q=value11, que es el que hemos obtenido de la consulta a BD.




20120205

El primer día en bici

Hoy ha hecho un día frío en Málaga, uno de esos días que te recuerdan el invierno, que también existe, y del que nos solemos librar.

Pero eso no ha impedido estrenar las bicis hoy, y salir a dar un paseo. He decidido crear la ruta que he hecho, no será una costumbre, pero así dejo constancia del primer paseo por Málaga con nuestras nuevas bicicletas :).


Ver Primer paseo en bici por Málaga en un mapa más grande

20111111

Bonita fecha

Un día curioso, en notación típica de informático (logs, ficheros fáciles de ordenar por fecha...):


20111111_1111

20110925

Matar proceso que usa un puerto

Ahora mismo estoy trabajando en el PFC, y arrancando y parando el Tomcat integrado en Eclipse hasta la saciedad (debería usar JRebel ahora que hay una versión libre?)

Lo que me ha ocurrido es algo que pasa "a veces" sin saber porqué : al arrancar Tomcat, me dice que alguno de los puertos que necesita está ocupado. Me voy a la vista de Debug para ver si hay algo en ejecución, y la vista de Servers para ver si está levantado, y ... nada, no veo nada desde ahí.

En el monitor del sistema si veo varios procesos java, pero claro, cual matar? No quiero que se me cierre el Eclipse o cualquier otra cosa que tenga levantada en ese momento.

Googleando un poco me encuentro esto.

Básicamente, ejecutando el comando:

netstat -anp|grep :

vas a poder ver qué proceso está usando qué puerto, para lo que necesites (en mi caso, matar el proceso que se ha quedado "suelto")

A veces pasan estas cosas y no sabes porqué, pero mejor tener a mano la forma de solucionarlas sin más y poder seguir trabajando.

20110525

Susto con 'mv *' en linux

Hoy he tenido un pequeño susto en mi equipo de casa, al intentar mover unos ficheros
Quería mover todos los logs a un directorio para mantener históricos, logs de mi aplicación, y he ido a teclear:

mv *.log ../_rutahistoricos_

en ese momento se me ha ido la mano y he tecleado:

mv *

y al pulsar enter... han desaparecido todos los .log y todo lo que había en ese directorio!

La mala suerte hace que, mi configuración, haga que deje las trazas en el HOME de mi usuario, con lo que se había perdido TODO mi contenido.

Perdido? No! Aquí hay un alma caritativa que dice básicamente que:

"si ejecutas 'mv *' se moveran todos los ficheros al último elemento, en caso de ser un subdirectorio, en otro caso se produce un error"

Efectivamente, en mi caso hay un directorio "workspace" (eclipse ;) ) donde estaba TODO mi home.
Curiosamente no se habían movido los directorios/ficheros ocultos.

Así que he podido recuperar todo lo que tenía, simplemente ejecutando de nuevo un mv, esta vez, con mucho cuidado!

He comprobado, además, que ejecutando mv * en un directorio cuyo último elemento no es otro directorio, se da el siguiente error:

mv: el destino, «applogxxxxxx.log», no es un directorio