Fecha de publicación: 2023-12-01
Apache Kafka es una plataforma de streaming de eventos distribuida. Nació en LinkedIn como solución a los problemas de comunicación entre sus servicios internos, y fue liberada como proyecto de código abierto en 2011. Hoy es una pieza fundamental en arquitecturas de microservicios, procesamiento de datos en tiempo real y sistemas orientados a eventos.
Por que existe Kafka
Imagina un sistema con diez servicios que necesitan comunicarse entre si. En un modelo punto a punto, cada servicio debe conocer la dirección de cada otro servicio al que necesita enviarle datos. Con diez servicios, eso puede significar decenas de conexiones directas. Si agregas un servicio nuevo, debes modificar todos los que le quieren enviar información. Si un servicio cae, los otros pierden mensajes.
Kafka resuelve esto introduciendo un intermediario: en lugar de que el servicio A hable directamente con el servicio B, A publica un mensaje en Kafka y B lo lee cuando puede. A y B no se conocen entre si. A no sabe cuántos consumidores hay, ni si hay alguno. B no sabe quién generó el mensaje. Este desacoplamiento es el núcleo del modelo publicador/suscriptor (pub/sub) que Kafka implementa.
La diferencia clave de Kafka frente a otros sistemas de mensajería tradicionales es que los mensajes son persistentes. No desaparecen cuando se leen. Se almacenan en disco durante un tiempo configurable (días, semanas), lo que permite que múltiples consumidores lean el mismo mensaje de forma independiente, y que sea posible reprocesar mensajes del pasado si algo falla.
Conceptos fundamentales
Para trabajar con Kafka es necesario entender sus bloques de construccion. Son pocos, pero es importante tenerlos claros desde el principio.
Topic: Es el canal lógico de comunicación. Los productores escriben mensajes en un topic y los consumidores los leen de él. Puedes pensar en un topic como un canal de television: muchos pueden transmitir contenido hacia él y muchos pueden sintonizarlo y verlo de forma independiente. Puedes tener tantos topics como necesites, cada uno con su propio propósito:
orders, payments, user-events, etc.Partition: Cada topic se divide en particiones. Cada partición es un log ordenado e inmutable: los mensajes se agregan al final y nunca se modifican. Las particiones son el mecanismo que le da a Kafka su capacidad de escalar horizontalmente: diferentes consumidores pueden leer diferentes particiones en paralelo, lo que permite aumentar el throughput simplemente aumentando el número de particiones y consumidores.
Offset: Es la posición de un mensaje dentro de una partición. El primer mensaje de una partición tiene offset 0, el siguiente 1, y así sucesivamente. Cada consumidor mantiene su propio offset, lo que le permite saber hasta dónde leyó. Esto tiene una implicación importante: si tu consumidor falla y se reinicia, puede retomar exactamente desde donde quedó. Y si necesitas reprocesar mensajes, puedes retroceder el offset a una posición anterior.
Broker: Es un servidor de Kafka. Un cluster típico tiene múltiples brokers para lograr alta disponibilidad: si un broker cae, los otros pueden seguir sirviendo. Las particiones se distribuyen entre los brokers, y cada partición tiene una réplica en uno o más brokers adicionales para garantizar durabilidad.
Producer: Es quien escribe mensajes en un topic. El producer decide a qué topic enviar el mensaje. Opcionalmente, puede especificar una clave (key): si dos mensajes tienen la misma clave, Kafka garantiza que irán a la misma partición, lo que preserva el orden de esos mensajes entre si. Si no se especifica clave, Kafka distribuye los mensajes entre las particiones de forma balanceada.
Consumer y Consumer Group: El consumer lee mensajes de un topic. Los consumers se organizan en consumer groups: cada mensaje de una partición es entregado a exactamente un consumer dentro del grupo. Si tienes tres particiones y tres consumers en un grupo, cada consumer lee una partición. Si agregas un cuarto consumer al mismo grupo, uno quedará ocioso (no puede haber más consumers activos que particiones). Si tienes dos grupos distintos, cada grupo recibe todos los mensajes del topic de forma independiente.
ZooKeeper y KRaft: Históricamente, Kafka dependía de ZooKeeper para coordinar el cluster: elegir líderes de partición, gestionar la membresía de los brokers, etc. Desde la versión 2.8, Kafka introdujo el modo KRaft (Kafka Raft), que elimina la dependencia de ZooKeeper y mueve la coordinacion directamente dentro del cluster de Kafka. KRaft es el modo recomendado en versiones recientes y es el que usaremos en el ejemplo de Docker Compose más adelante.
El flujo de un mensaje
El recorrido de un mensaje en Kafka sigue este camino:
- El Producer crea un mensaje y lo envía a un topic específico, opcionalmente con una clave.
- Kafka asigna el mensaje a una Partición del topic (basándose en la clave o en round-robin).
- El mensaje se persiste en disco en el Broker líder de esa partición y se replica en los brokers seguidores.
- El Consumer Group tiene uno o más consumers leyendo del topic. Cada partición es asignada a un solo consumer del grupo.
- El consumer lee el mensaje, lo procesa, y actualiza su Offset para indicar que ya fue procesado.
Ejemplo practico: productor y consumidor en Kotlin con Spring Boot
Veamos cómo se implementa un producer y un consumer básicos usando Spring Kafka con Kotlin. El producer envía órdenes a un topic llamado
orders, y el consumer las procesa.El producer usa
KafkaTemplate, que es el componente principal de Spring Kafka para enviar mensajes. El método send retorna un CompletableFuture, por lo que usamos whenComplete para manejar el resultado de forma no bloqueante:kotlin
1@Service
2class OrderProducer(
3 private val kafkaTemplate: KafkaTemplate<String, String>
4) {
5 private val logger = LoggerFactory.getLogger(OrderProducer::class.java)
6
7 fun sendOrder(orderId: String, orderData: String) {
8 kafkaTemplate.send("orders", orderId, orderData)
9 .whenComplete { result, ex ->
10 if (ex == null) {
11 logger.info("Orden enviada: $orderId, offset: ${result.recordMetadata.offset()}")
12 } else {
13 logger.error("Error enviando orden: $orderId", ex)
14 }
15 }
16 }
17}El consumer usa la anotacion
@KafkaListener para suscribirse al topic. Spring Kafka gestiona automáticamente el ciclo de vida del consumer, el manejo del offset y la reconexión en caso de fallos. El parámetro ConsumerRecord da acceso a todos los metadatos del mensaje: clave, valor, partición, offset y timestamp:kotlin
1@Service
2class OrderConsumer {
3 private val logger = LoggerFactory.getLogger(OrderConsumer::class.java)
4
5 @KafkaListener(topics = ["orders"], groupId = "order-processor")
6 fun processOrder(record: ConsumerRecord<String, String>) {
7 logger.info(
8 "Orden recibida: key=${record.key()}, value=${record.value()}, " +
9 "partition=${record.partition()}, offset=${record.offset()}"
10 )
11 // Procesar la orden...
12 }
13}La configuracion de Spring Kafka en
application.yml. El campoauto-offset-reset: earliest indica que si el consumer arranca por primera vez (sin offset previo guardado), debe leer desde el principio del topic en lugar del final:yaml
1spring:
2 kafka:
3 bootstrap-servers: localhost:9092
4 producer:
5 key-serializer: org.apache.kafka.common.serialization.StringSerializer
6 value-serializer: org.apache.kafka.common.serialization.StringSerializer
7 consumer:
8 group-id: order-processor
9 auto-offset-reset: earliest
10 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
11 value-deserializer: org.apache.kafka.common.serialization.StringDeserializerDocker Compose para levantar Kafka localmente
La forma más rápida de tener Kafka corriendo en local para desarrollo es con Docker Compose. El siguiente ejemplo usa la imagen de Confluent con el modo KRaft habilitado: no necesitas ZooKeeper, todo corre en un solo contenedor.
yaml
1version: '3.8'
2services:
3 kafka:
4 image: confluentinc/cp-kafka:7.5.0
5 ports:
6 - "9092:9092"
7 environment:
8 KAFKA_NODE_ID: 1
9 KAFKA_PROCESS_ROLES: broker,controller
10 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
11 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
12 KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
13 KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
14 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
15 CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
16 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1Con esto basta para desarrollo. Para levantar el cluster:
bash
1docker-compose up -dPuedes verificar que Kafka esta corriendo y crear un topic de prueba con los siguientes comandos, ejecutados dentro del contenedor:
bash
1# Crear el topic 'orders' con 3 particiones y factor de replicacion 1
2docker exec -it <nombre-contenedor> kafka-topics --create \
3 --topic orders \
4 --partitions 3 \
5 --replication-factor 1 \
6 --bootstrap-server localhost:9092
7
8# Listar todos los topics existentes
9docker exec -it <nombre-contenedor> kafka-topics --list \
10 --bootstrap-server localhost:9092Garantias de entrega
Kafka ofrece tres niveles de garantias de entrega, y elegir el correcto depende de las necesidades de tu sistema:
At-most-once: El mensaje se entrega como máximo una vez. Si algo falla durante el procesamiento, el mensaje se pierde y no se reintenta. Es la opcion más rapida pero la menos confiable. Util cuando perder algún evento ocasionalmente es aceptable (por ejemplo, métricas de telemetría no criticas).
At-least-once: El mensaje se entrega al menos una vez. Si algo falla, Kafka lo reintentará, lo que puede resultar en que el mismo mensaje se procese más de una vez. Es el comportamiento por defecto. Tu consumer debe ser idempotente: procesar el mismo mensaje dos veces no debe producir efectos distintos a procesarlo una sola vez.
Exactly-once: El mensaje se procesa exactamente una vez, sin duplicados y sin perdidas. Kafka lo logra a través de dos mecanismos: productores idempotentes (activados con
enable.idempotence=true) y consumidores transaccionales (que confirman el offset junto con la escritura de resultados en una transaccion atomica). Es la garantia más fuerte pero también la más costosa en terminos de complejidad y latencia.¿Cuándo usar Kafka y cuándo no?
Kafka es una herramienta poderosa, pero no es la solución correcta para todos los problemas.
Casos de uso donde Kafka brilla:
- Comunicacion asincrona entre microservicios: desacopla productores y consumidores, permite que cada servicio escale de forma independiente.
- Event sourcing: Kafka actúa como el log de eventos del sistema. Puedes reconstruir el estado de cualquier entidad reproduciendo sus eventos desde el principio.
- Streaming de datos: procesamiento en tiempo real de flujos de datos con herramientas como Kafka Streams o Apache Flink.
- Logs centralizados: agregacion de logs de múltiples servicios hacia un sistema de almacenamiento o análisis.
- Change Data Capture (CDC): capturar cambios en bases de datos (con conectores como Debezium) y propagarlos a otros sistemas.
Casos donde Kafka no es la eleccion adecuada:
- Comunicacion request/response simple: si necesitas una respuesta inmediata, una llamada HTTP o gRPC directa es mucho más simple.
- Sistemas con muy bajo volumen de mensajes: Kafka tiene un overhead operativo real. Para pocos mensajes, una cola más simple como RabbitMQ o incluso una tabla en base de datos puede ser suficiente.
- Proyectos muy pequeños o equipos sin experiencia en sistemas distribuidos: Kafka añade complejidad operativa. Asegúrate de que el problema que resuelve justifica esa complejidad.
Conclusion
Kafka es una herramienta poderosa pero no trivial. Su modelo pub/sub persistente y distribuido resuelve problemas reales de comunicacion y desacoplamiento en sistemas complejos. Entender sus conceptos fundamentales (topics, particiones, offsets, brokers, producers y consumer groups) es el primer paso para implementarlo correctamente.
El ejemplo de este post es deliberadamente simple, pero cubre los patrones que usarás en la gran mayoría de los casos reales. A partir de aquí, los próximos pasos naturales son explorar la configuracion de réplicas y tolerancia a fallos, el manejo de errores y dead letter queues, la serialización con Avro o Protobuf, y las garantias exactly-once para escenarios donde los duplicados no son aceptables.


