Apache Kafka - II - Topics y particiones

Topics

En Kafka, un topic es una colección ordenada de mensajes, con un nombre que lo identifica dentro de un cluster. Los productores envían mensajes a un topic específico, utilizando el nombre del topic para indicarlo. Por su parte, cualquier consumidor de ese mismo cluster puede leer mensajes del topic, indicando también su nombre. Ésta es la visión de un topic como una entidad lógica; la forma en que se almacena y maneja dentro del cluster es irrelevante para productores y consumidores. A ellos sólo les interesa enviar y recibir datos.

Los topics de Kafka tienen las siguientes propiedades esenciales:

  • Orden: Los topics en Kafka son una secuencia ordenada de mensajes. Dicho orden está dado por el orden en que los mensajes ingresan al topic, o sea el orden en que son recibidos por el cluster.
  • Inmutabilidad: Una vez que un mensaje ha sido guardado en un topic, no puede ser modificado, ni eliminado: los topics son "append-only". Vale decir, sólo se puede agregar nuevos mensajes. Esto se debe al orden temporal. Si un productor envía datos incorrectos en un mensaje, es su responsabilidad informar a los consumidores y enviar los datos correctos en un nuevo mensaje. Por su parte, el consumidor deberá encargarse de detectar esa notificación y ajustar su información adecuadamente. Por supuesto, los topics de Kafka no crecen infinitamente. Cuando un mensaje ha estado en el topic más tiempo del definido por el tiempo de expiración del topic, será automáticamente borrado por Kafka. Esto en el caso de una política basada en tiempo; es posible definir una política basada en tamaño de almacenamiento. En dicho caso, cada vez que el tamaño del topic exceda un valor pre configurado, los mensajes más antiguos serán borrados hasta volver a estar por debajo del límite. Este enfoque arquitectural basado en mensajes inmutables ordenados por tiempo se conoce como event sourcing. Por otro lado, trazando una analogía con las bases de datos relacionales, Kafka puede ser definido como una implementación del patrón arquitectural publisher-subscriber basada en un log distribuido de transacciones.

Entonces, ¿cómo hace Kafka para manejar una cantidad teóricamente ilimitada de consumidores heterogéneos, cada uno con su tasa de consumo, bugs y fallas? La solución que propone Kafka es un topic offset: cada consumidor, periódicamente, guarda una referencia al último mensaje que leyó de un topic, como si fuera un señalador de libro. Este offset es persistido en el cluster, no en el consumidor. De esta manera, cada consumidor establece y maneja su propio offset, y así cada uno puede leer el topic a su propio ritmo, sin interferir con los demás consumidores o productores. A esto se hace alusión cuando se dice que en Kafka, productores y consumidores están levemente acoplados: demoras o fallas en un consumidor o productor particular no paralizarán a los demás. Cuando un consumidor se conecta a un topic por primera vez, o desea empezar desde el principio, informa de esa situación al momento de conectarse al cluster. En ese caso, su offset será modificado para apuntar al primer mensaje del topic.

Partitions

Un topic de Kafka consiste de uno o más archivos de log llamados particiones (partitions). La cantidad de particiones que componen un topic es configurable, y dependerá de cada caso de uso. Cada partición es almacenada, en su totalidad, en uno o más brokers. Si está en más de uno, estará replicada: cada broker tendrá una copia completa de la partición. Por lo tanto, si se configura un topic para usar sólo una partición, dado que tiene que caber completa dentro de un nodo broker, la aplicación está limitada por los recursos de los brokers. Concretamente, su espacio de almacenamiento. Evidentemente, para escalar, es necesario utilizar múltiples particiones por topic. Conclusión: en Kafka, la escalabilidad de un topic está directamente asociada a la cantidad de particiones y los nodos broker que las albergan.

El próximo interrogante es: cuando hay más de una partición, ¿cómo se reparten los mensajes entre ellas? En primer lugar, cada partición tendrá sus propios mensajes, en su propio orden. Los productores definirán un esquema de particionado, que es básicamente un algoritmo para decidir a cuál partición va cada mensaje. Esto puede ser tan sencillo como round-robin, donde el productor envía alternadamente un mensaje a cada partición. Además de otros esquemas predefinidos, es posible utilizar un algoritmo personalizado. En todo caso, la meta es siempre mantener a las particiones balanceadas, vale decir, todas de un tamaño similar. Más detalles al respecto en la parte III: productores.

¿Y cómo operan los consumidores cuando hay múltiples particiones? Gracias a Zookeeper, el consumidor puede saber qué brokers tienen qué particiones del topic que le interesa. En base a eso, puede leer mensajes del broker que quiera. Dado que cada partición tiene su propio orden, si es necesario establecer un orden global al topic, será responsabilidad del consumidor. Más detalles al respecto en la parte IV: consumidores.

Por ejemplo, si el topic a tiene 3 particiones: topic a-0, topic a-1, y topic a-2, y están asignadas a los brokers 0, 1 y 2:

Ejemplo de topic con múltiples particiones

Cuando un topic es creado, uno de los parámetros es la cantidad de particiones que tendrá. Zookeeper observará los brokers disponibles, y decidirá cuál alojará cada partición. Una vez hecha esa asignación, cada broker creará un nuevo archivo de log donde guardará los mensajes de su partición. En el ejemplo, el broker 0 tiene la partición 2, y el broker 2 la partición 0. Este mapeo será transmitido por Zookeeper a todo el cluster, de modo que todo broker sabrá qué broker tiene cada partición. Esto permitirá a cualquier broker redireccionar un productor que desee escribir a una partición al broker que físicamente la tiene. Por otro lado, los brokers reportan su estado periódicamente a Zookeeper, lo que mantiene el consenso.

El consumidor le preguntará a Zookeeper qué brokers tienen cada partición, y obtendrá metadatos adicionales que afectarán su modalidad de consumo. Más detalles sobre esto en el post sobre consumidores. Una vez que el consumidor sepa donde están las particiones del topic, comenzará a leer mensajes, utilizando un offset distinto para cada partición.

Compromisos de particionado

  1. A mayor cantidad de particiones, crece el costo administrativo de Zookeeper, ya que la tabla de mapeo crece. Esto puede ser mitigado agregando nodos al ensemble de Zookeeper.
  2. El ordenamiento de los mensajes puede tornarse complejo. Dentro de una partición, los mensajes mantienen su orden, pero ello no está garantizado entre distintas particiones de un mismo topic. Si la aplicación requiere un orden de a nivel del topic, tener una única partición puede ser una solución. La alternativa es manejar el orden en el consumidor, que es más complejo de implementar pero permite escalar.
  3. A mayor cantidad de particiones, mayor tiempo toma la selección de un nuevo broker controlador cuando el actual falla. Una forma de mitigar esto es tener redundancia a nivel del cluster. Esto aumenta los costos rápidamente, por lo que tiene que valer la pena.

Ir a parte III: productores.

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x