Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka

Continuación de la traducción de un pequeño libro:
Descripción de los agentes de mensajes
autor: Jakub Korab, editorial: O'Reilly Media, Inc., fecha de publicación: junio de 2017, ISBN: 9781492049296.

Parte traducida anterior: Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 1 Introducción

CAPÍTULO 3

Kafka

Kafka se desarrolló en LinkedIn para sortear algunas de las limitaciones de los intermediarios de mensajes tradicionales y evitar tener que configurar múltiples intermediarios de mensajes para diferentes interacciones punto a punto, que se describe en este libro en "Ampliar y ampliar" en la página 28 Casos de uso LinkedIn se ha basado en gran medida en la ingesta unidireccional de grandes cantidades de datos, como clics en páginas y registros de acceso, al mismo tiempo que permite que múltiples sistemas utilicen esos datos sin afectar la productividad de los productores u otros consumidores. De hecho, la razón por la que Kafka existe es para obtener el tipo de arquitectura de mensajería que describe Universal Data Pipeline.

Dado este objetivo final, naturalmente surgieron otros requisitos. Kafka debería:

  • ser extremadamente rápido
  • Proporcione más ancho de banda cuando trabaje con mensajes
  • Compatibilidad con modelos Publisher-Subscriber y Point-to-Point
  • No disminuya la velocidad al agregar consumidores. Por ejemplo, el rendimiento tanto de la cola como del tema en ActiveMQ se degrada a medida que crece la cantidad de consumidores en el destino.
  • Ser escalable horizontalmente; si un agente que persiste los mensajes solo puede hacerlo a la velocidad máxima del disco, entonces tiene sentido ir más allá de una sola instancia de agente para aumentar el rendimiento
  • Limite el acceso para almacenar y recuperar mensajes

Para lograr todo esto, Kafka adoptó una arquitectura que redefinió las funciones y responsabilidades de los clientes y los agentes de mensajería. El modelo JMS está muy orientado al intermediario, donde el intermediario es responsable de distribuir los mensajes y los clientes solo tienen que preocuparse por enviar y recibir mensajes. Kafka, por otro lado, está centrado en el cliente, y el cliente asume muchas de las características de un corredor tradicional, como la distribución justa de mensajes relevantes a los consumidores, a cambio de un corredor extremadamente rápido y escalable. Para las personas que han trabajado con sistemas de mensajería tradicionales, trabajar con Kafka requiere un cambio de mentalidad fundamental.
Esta dirección de ingeniería ha llevado a la creación de una infraestructura de mensajería capaz de aumentar el rendimiento en muchos órdenes de magnitud en comparación con un corredor convencional. Como veremos, este enfoque viene con compensaciones, lo que significa que Kafka no es adecuado para ciertos tipos de cargas de trabajo y software instalado.

Modelo de destino unificado

Para cumplir con los requisitos descritos anteriormente, Kafka ha combinado la publicación-suscripción y la mensajería punto a punto en un solo tipo de destino: tema. Esto es confuso para las personas que han trabajado con sistemas de mensajería, donde la palabra "tema" se refiere a un mecanismo de transmisión desde el cual (del tema) la lectura no es duradera. Los temas de Kafka deben considerarse un tipo de destino híbrido, tal como se define en la introducción de este libro.

En el resto de este capítulo, a menos que indiquemos explícitamente lo contrario, el término "tema" se referirá a un tema de Kafka.

Para comprender completamente cómo se comportan los temas y qué garantías brindan, primero debemos observar cómo se implementan en Kafka.
Cada tema en Kafka tiene su propio registro.
Los productores que envían mensajes a Kafka escriben en este registro y los consumidores leen el registro utilizando punteros que avanzan constantemente. Periódicamente, Kafka elimina las partes más antiguas del registro, ya sea que los mensajes de esas partes se hayan leído o no. Una parte central del diseño de Kafka es que al corredor no le importa si los mensajes se leen o no, esa es responsabilidad del cliente.

Los términos "registro" y "puntero" no aparecen en Documentación de Kafka. Estos términos bien conocidos se utilizan aquí para facilitar la comprensión.

Este modelo es completamente diferente de ActiveMQ, donde los mensajes de todas las colas se almacenan en el mismo registro y el intermediario marca los mensajes como eliminados después de haberlos leído.
Ahora profundicemos un poco más y veamos el registro de temas con más detalle.
El registro de Kafka consta de varias particiones (Figura 3-1). Kafka garantiza un orden estricto en cada partición. Esto significa que los mensajes escritos en la partición en un orden determinado se leerán en el mismo orden. Cada partición se implementa como un archivo de registro continuo que contiene subconjunto (subconjunto) de todos los mensajes enviados al tema por sus productores. El tema creado contiene, de forma predeterminada, una partición. La idea de las particiones es la idea central de Kafka para el escalado horizontal.

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-1. Particiones Kafka

Cuando un productor envía un mensaje a un tema de Kafka, decide a qué partición enviar el mensaje. Veremos esto con más detalle más adelante.

Lectura de mensajes

El cliente que quiere leer los mensajes maneja un puntero con nombre llamado grupo de consumidores, que apunta a compensar mensajes en la partición. Un desplazamiento es una posición incremental que comienza en 0 al comienzo de una partición. Este grupo de consumidores, al que se hace referencia en la API a través del group_id definido por el usuario, corresponde a un consumidor o sistema lógico.

La mayoría de los sistemas de mensajería leen datos del destino utilizando múltiples instancias e hilos para procesar mensajes en paralelo. Por lo tanto, normalmente habrá muchas instancias de consumidores que comparten el mismo grupo de consumidores.

El problema de la lectura se puede representar de la siguiente manera:

  • El tema tiene varias particiones
  • Múltiples grupos de consumidores pueden usar un tema al mismo tiempo
  • Un grupo de consumidores puede tener múltiples instancias separadas

Este es un problema no trivial de muchos a muchos. Para comprender cómo maneja Kafka las relaciones entre grupos de consumidores, instancias de consumidores y particiones, veamos una serie de escenarios de lectura cada vez más complejos.

Consumidores y grupos de consumidores

Tomemos como punto de partida un tema con una partición (Figura 3-2).

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-2. El consumidor lee de la partición

Cuando una instancia de consumidor se conecta con su propio group_id a este tema, se le asigna una partición de lectura y un desplazamiento en esa partición. La posición de este desplazamiento se puede configurar en el cliente como un puntero a la posición más reciente (mensaje más nuevo) o posición más antigua (mensaje más antiguo). El consumidor solicita (sondea) los mensajes del tema, lo que hace que se lean secuencialmente desde el registro.
La posición de desplazamiento se vuelve a enviar regularmente a Kafka y se almacena como mensajes en un tema interno. _compensaciones_de_consumidor. Los mensajes leídos aún no se eliminan, a diferencia de un intermediario normal, y el cliente puede rebobinar el desplazamiento para volver a procesar los mensajes ya vistos.

Cuando un segundo consumidor lógico se conecta utilizando un group_id diferente, administra un segundo puntero que es independiente del primero (Figura 3-3). Por lo tanto, un tema de Kafka actúa como una cola donde hay un consumidor y como un tema normal de publicación y suscripción (pub-sub) al que se suscriben múltiples consumidores, con el beneficio adicional de que todos los mensajes se almacenan y se pueden procesar varias veces.

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-3. Dos consumidores en diferentes grupos de consumidores leen de la misma partición

Consumidores en un grupo de consumidores

Cuando una instancia de consumidor lee datos de una partición, tiene control total del puntero y procesa los mensajes como se describe en la sección anterior.
Si varias instancias de consumidores se conectaron con el mismo group_id a un tema con una partición, la última instancia que se conectó tendrá control sobre el puntero y, a partir de ese momento, recibirá todos los mensajes (Figura 3-4).

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-4. Dos consumidores en el mismo grupo de consumidores leen de la misma partición

Este modo de procesamiento, en el que el número de instancias de consumidores supera el número de particiones, puede considerarse como una especie de consumidor exclusivo. Esto puede ser útil si necesita un clúster "activo-pasivo" (o "caliente-tibio") de sus instancias de consumidor, aunque ejecutar varios consumidores en paralelo ("activo-activo" o "caliente-caliente") es mucho más común que consumidores En espera.

Este comportamiento de distribución de mensajes descrito anteriormente puede ser sorprendente en comparación con el comportamiento de una cola JMS normal. En este modelo, los mensajes enviados a la cola se distribuirán uniformemente entre los dos consumidores.

La mayoría de las veces, cuando creamos varias instancias de consumidores, lo hacemos para procesar mensajes en paralelo, para aumentar la velocidad de lectura o para aumentar la estabilidad del proceso de lectura. Dado que solo una instancia de consumidor puede leer datos de una partición a la vez, ¿cómo se logra esto en Kafka?

Una forma de hacer esto es usar una sola instancia de consumidor para leer todos los mensajes y pasarlos al grupo de subprocesos. Si bien este enfoque aumenta el rendimiento del procesamiento, aumenta la complejidad de la lógica del consumidor y no hace nada para aumentar la solidez del sistema de lectura. Si una copia del consumidor deja de funcionar debido a un corte de energía o un evento similar, la resta se detiene.

La forma canónica de resolver este problema en Kafka es usar bОmás particiones.

Fraccionamiento

Las particiones son el mecanismo principal para paralelizar la lectura y escalar un tema más allá del ancho de banda de una única instancia de intermediario. Para comprender mejor esto, consideremos una situación en la que hay un tema con dos particiones y un consumidor se suscribe a este tema (Figura 3-5).

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-5. Un consumidor lee desde múltiples particiones

En este escenario, el consumidor tiene control sobre los punteros correspondientes a su group_id en ambas particiones y comienza a leer mensajes de ambas particiones.
Cuando se agrega un consumidor adicional para el mismo group_id a este tema, Kafka reasigna una de las particiones del primer consumidor al segundo. Después de eso, cada instancia del consumidor leerá de una partición del tema (Figura 3-6).

Para asegurarse de que los mensajes se procesen en paralelo en 20 subprocesos, necesita al menos 20 particiones. Si hay menos particiones, se quedará con consumidores que no tienen nada en lo que trabajar, como se describió anteriormente en la discusión de consumidores exclusivos.

Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 3. Kafka
Figura 3-6. Dos consumidores en el mismo grupo de consumidores leen desde diferentes particiones

Este esquema reduce en gran medida la complejidad del agente Kafka en comparación con la distribución de mensajes necesaria para mantener la cola JMS. Aquí no necesita preocuparse por los siguientes puntos:

  • Qué consumidor debe recibir el siguiente mensaje, en función de la asignación por turnos, la capacidad actual de los búferes de captación previa o los mensajes anteriores (como para los grupos de mensajes JMS).
  • Qué mensajes se envían a qué consumidores y si deben volver a enviarse en caso de falla.

Todo lo que tiene que hacer el bróker de Kafka es pasar mensajes secuencialmente al consumidor cuando este último los solicita.

Sin embargo, los requisitos para paralelizar la revisión y el reenvío de mensajes fallidos no desaparecen; la responsabilidad por ellos simplemente pasa del corredor al cliente. Esto significa que deben tenerse en cuenta en su código.

Enviando mensajes

Es responsabilidad del productor de ese mensaje decidir a qué partición enviar un mensaje. Para comprender el mecanismo por el cual se hace esto, primero debemos considerar qué es exactamente lo que estamos enviando.

Mientras que en JMS usamos una estructura de mensaje con metadatos (encabezados y propiedades) y un cuerpo que contiene el payload (carga útil), en Kafka el mensaje es par "clave-valor". La carga útil del mensaje se envía como un valor. La clave, por otro lado, se usa principalmente para particionar y debe contener clave específica de lógica de negociospara poner mensajes relacionados en la misma partición.

En el Capítulo 2, discutimos el escenario de apuestas en línea donde los eventos relacionados deben ser procesados ​​en orden por un solo consumidor:

  1. La cuenta de usuario está configurada.
  2. El dinero se acredita en la cuenta.
  3. Se realiza una apuesta que retira dinero de la cuenta.

Si cada evento es un mensaje publicado en un tema, la clave natural sería el ID de la cuenta.
Cuando se envía un mensaje mediante la API de Kafka Producer, se pasa a una función de partición que, dado el mensaje y el estado actual del clúster de Kafka, devuelve el ID de la partición a la que se debe enviar el mensaje. Esta función se implementa en Java a través de la interfaz Partitioner.

Esta interfaz se ve así:

interface Partitioner {
    int partition(String topic,
        Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
}

La implementación del particionador utiliza el algoritmo hash de uso general predeterminado sobre la clave para determinar la partición o la rotación si no se especifica ninguna clave. Este valor predeterminado funciona bien en la mayoría de los casos. Sin embargo, en el futuro querrás escribir el tuyo propio.

Escribir su propia estrategia de partición

Veamos un ejemplo en el que desea enviar metadatos junto con la carga útil del mensaje. La carga útil en nuestro ejemplo es una instrucción para realizar un depósito en la cuenta del juego. Una instrucción es algo que nos gustaría garantizar que no se modifique en la transmisión y queremos estar seguros de que solo un sistema ascendente confiable puede iniciar esa instrucción. En este caso, los sistemas de envío y recepción acuerdan el uso de una firma para autenticar el mensaje.
En JMS normal, simplemente definimos una propiedad de "firma de mensaje" y la agregamos al mensaje. Sin embargo, Kafka no nos proporciona un mecanismo para pasar metadatos, solo una clave y un valor.

Dado que el valor es una carga útil de transferencia bancaria cuya integridad queremos preservar, no tenemos más remedio que definir la estructura de datos que se usará en la clave. Suponiendo que necesitamos un ID de cuenta para la partición, dado que todos los mensajes relacionados con una cuenta deben procesarse en orden, obtendremos la siguiente estructura JSON:

{
  "signature": "541661622185851c248b41bf0cea7ad0",
  "accountId": "10007865234"
}

Debido a que el valor de la firma variará según la carga útil, la estrategia de hash predeterminada de la interfaz del particionador no agrupará de manera confiable los mensajes relacionados. Por lo tanto, necesitaremos escribir nuestra propia estrategia que analizará esta clave y dividirá el valor de accountId.

Kafka incluye sumas de verificación para detectar la corrupción de los mensajes en la tienda y tiene un conjunto completo de funciones de seguridad. Aun así, a veces aparecen requisitos específicos de la industria, como el anterior.

La estrategia de partición del usuario debe garantizar que todos los mensajes relacionados terminen en la misma partición. Si bien esto parece simple, el requisito puede complicarse por la importancia de ordenar los mensajes relacionados y cuán fijo es el número de particiones en un tema.

La cantidad de particiones en un tema puede cambiar con el tiempo, ya que se pueden agregar si el tráfico supera las expectativas iniciales. Por lo tanto, las claves de mensaje se pueden asociar con la partición a la que se enviaron originalmente, lo que implica que una parte del estado se comparte entre las instancias de productor.

Otro factor a considerar es la distribución uniforme de los mensajes entre las particiones. Por lo general, las claves no se distribuyen uniformemente entre los mensajes y las funciones hash no garantizan una distribución justa de los mensajes para un pequeño conjunto de claves.
Es importante tener en cuenta que, independientemente de cómo elija dividir los mensajes, es posible que deba reutilizar el separador.

Considere el requisito de replicar datos entre clústeres de Kafka en diferentes ubicaciones geográficas. Para este propósito, Kafka viene con una herramienta de línea de comandos llamada MirrorMaker, que se usa para leer mensajes de un clúster y transferirlos a otro.

MirrorMaker debe comprender las claves del tema replicado para mantener el orden relativo entre los mensajes al replicar entre clústeres, ya que la cantidad de particiones para ese tema puede no ser la misma en dos clústeres.

Las estrategias de particionamiento personalizadas son relativamente raras, ya que el hashing predeterminado o el round robin funcionan bien en la mayoría de los escenarios. Sin embargo, si necesita garantías sólidas de pedido o necesita extraer metadatos de las cargas útiles, entonces la partición es algo que debe analizar más de cerca.

Los beneficios de escalabilidad y rendimiento de Kafka provienen de transferir algunas de las responsabilidades del corredor tradicional al cliente. En este caso, se toma la decisión de distribuir mensajes potencialmente relacionados entre varios consumidores que trabajan en paralelo.

Los corredores de JMS también deben lidiar con tales requisitos. Curiosamente, el mecanismo para enviar mensajes relacionados al mismo consumidor, implementado a través de grupos de mensajes JMS (una variación de la estrategia de equilibrio de carga permanente (SLB), también requiere que el remitente marque los mensajes como relacionados. En el caso de JMS, el intermediario es responsable de enviar este grupo de mensajes relacionados a un consumidor entre muchos, y de transferir la propiedad del grupo si el consumidor se cae.

Acuerdos de productores

La partición no es lo único a considerar al enviar mensajes. Echemos un vistazo a los métodos send() de la clase Producer en la API de Java:

Future < RecordMetadata > send(ProducerRecord < K, V > record);
Future < RecordMetadata > send(ProducerRecord < K, V > record, Callback callback);

Cabe señalar de inmediato que ambos métodos devuelven Future, lo que indica que la operación de envío no se realiza de inmediato. El resultado es que se escribe un mensaje (ProducerRecord) en el búfer de envío para cada partición activa y se envía al intermediario como un subproceso en segundo plano en la biblioteca del cliente de Kafka. Si bien esto hace que las cosas sean increíblemente rápidas, significa que una aplicación sin experiencia puede perder mensajes si se detiene su proceso.

Como siempre, existe una manera de hacer que la operación de envío sea más confiable a costa del rendimiento. El tamaño de este búfer se puede establecer en 0, y el subproceso de la aplicación de envío se verá obligado a esperar hasta que se complete la transferencia del mensaje al intermediario, de la siguiente manera:

RecordMetadata metadata = producer.send(record).get();

Más sobre leer mensajes

La lectura de mensajes tiene complejidades adicionales sobre las que es necesario especular. A diferencia de la API de JMS, que puede ejecutar un detector de mensajes en respuesta a un mensaje, el Consumidores Kafka solo encuestas. Echemos un vistazo más de cerca al método. encuesta()utilizado para este propósito:

ConsumerRecords < K, V > poll(long timeout);

El valor de retorno del método es una estructura de contenedor que contiene múltiples objetos. registro de consumo de potencialmente varias particiones. registro de consumo es en sí mismo un objeto contenedor para un par clave-valor con metadatos asociados, como la partición de la que se deriva.

Como se discutió en el Capítulo 2, debemos tener en cuenta lo que sucede con los mensajes después de que se hayan procesado con éxito o sin éxito, por ejemplo, si el cliente no puede procesar el mensaje o si se cancela. En JMS, esto se manejó a través de un modo de reconocimiento. El corredor eliminará el mensaje procesado con éxito o volverá a enviar el mensaje sin procesar o falso (suponiendo que se usaron transacciones).
Kafka funciona de manera muy diferente. Los mensajes no se eliminan en el intermediario después de la revisión, y lo que sucede en caso de falla es responsabilidad del propio código de revisión.

Como hemos dicho, el grupo de consumidores está asociado con el desplazamiento en el registro. La posición de registro asociada con este desplazamiento corresponde al siguiente mensaje que se emitirá en respuesta a encuesta(). El momento en el que aumenta este desplazamiento es decisivo para la lectura.

Volviendo al modelo de lectura discutido anteriormente, el procesamiento de mensajes consta de tres etapas:

  1. Recuperar un mensaje para leer.
  2. Procesar el mensaje.
  3. Confirmar mensaje.

El consumidor de Kafka viene con una opción de configuración habilitar.auto.commit. Esta es una configuración predeterminada de uso frecuente, como es común con las configuraciones que contienen la palabra "auto".

Antes de Kafka 0.10, un cliente que usaba esta opción enviaba el desplazamiento del último mensaje leído en la próxima llamada encuesta() después de procesar. Esto significaba que los mensajes que ya se habían obtenido podían volver a procesarse si el cliente ya los había procesado pero se destruyeron inesperadamente antes de llamar. encuesta(). Debido a que el corredor no mantiene ningún estado sobre cuántas veces se ha leído un mensaje, el próximo consumidor que recupere ese mensaje no sabrá que sucedió nada malo. Este comportamiento era pseudo-transaccional. El desplazamiento solo se comprometía si el mensaje se procesaba con éxito, pero si el cliente abortaba, el intermediario enviaba el mismo mensaje nuevamente a otro cliente. Este comportamiento fue consistente con la garantía de entrega de mensajes "al menos una vez«.

En Kafka 0.10, el código del cliente se ha cambiado para que la biblioteca del cliente active periódicamente la confirmación, tal como se configuró. auto.commit.interval.ms. Este comportamiento se encuentra entre los modos JMS AUTO_ACKNOWLEDGE y DUPS_OK_ACKNOWLEDGE. Cuando se usa la confirmación automática, los mensajes pueden confirmarse independientemente de si se procesaron realmente; esto podría suceder en el caso de un consumidor lento. Si un consumidor anulaba, el siguiente consumidor recuperaría los mensajes, comenzando en la posición comprometida, lo que podría resultar en un mensaje perdido. En este caso, Kafka no perdió los mensajes, el código de lectura simplemente no los procesó.

Este modo tiene la misma promesa que en la versión 0.9: los mensajes se pueden procesar, pero si falla, es posible que la compensación no se confirme, lo que podría duplicar la entrega. Cuantos más mensajes obtenga al ejecutar encuesta(), más este problema.

Como se discutió en “Lectura de mensajes de una cola” en la página 21, no existe tal cosa como una entrega única de un mensaje en un sistema de mensajería cuando se tienen en cuenta los modos de falla.

En Kafka, hay dos formas de comprometer (commit) un desplazamiento (offset): automáticamente y manualmente. En ambos casos, los mensajes se pueden procesar varias veces si el mensaje se procesó pero falló antes de la confirmación. También puede optar por no procesar el mensaje en absoluto si la confirmación ocurrió en segundo plano y su código se completó antes de que pudiera procesarse (quizás en Kafka 0.9 y versiones anteriores).

Puede controlar el proceso de confirmación de compensación manual en la API del consumidor de Kafka configurando el parámetro habilitar.auto.commit a falso y llamando explícitamente a uno de los siguientes métodos:

void commitSync();
void commitAsync();

Si desea procesar el mensaje "al menos una vez", debe confirmar la compensación manualmente con commitSync()ejecutando este comando inmediatamente después de procesar los mensajes.

Estos métodos no permiten que los mensajes sean reconocidos antes de que sean procesados, pero no hacen nada para eliminar posibles retrasos en el procesamiento mientras dan la apariencia de ser transaccionales. No hay transacciones en Kafka. El cliente no tiene la capacidad de hacer lo siguiente:

  • Retrocede automáticamente un mensaje falso. Los propios consumidores deben manejar las excepciones que surjan de las cargas útiles problemáticas y las interrupciones del backend, ya que no pueden confiar en el corredor para volver a entregar los mensajes.
  • Envíe mensajes a múltiples temas en una operación atómica. Como veremos en breve, el control sobre diferentes temas y particiones puede residir en diferentes máquinas en el clúster de Kafka que no coordinan las transacciones cuando se envían. En el momento de escribir este artículo, se ha trabajado para que esto sea posible con el KIP-98.
  • Asocia leer un mensaje de un tema con enviar otro mensaje a otro tema. Nuevamente, la arquitectura de Kafka depende de muchas máquinas independientes que funcionan como un solo bus y no se intenta ocultar esto. Por ejemplo, no hay componentes API que le permitan vincular consumidor и Prouduser en una transacción. En JMS, esto lo proporciona el objeto Sesióna partir de los cuales se crean Productores de mensajes и MensajeConsumidores.

Si no podemos confiar en las transacciones, ¿cómo podemos proporcionar una semántica más cercana a la proporcionada por los sistemas de mensajería tradicionales?

Si existe la posibilidad de que el desplazamiento del consumidor aumente antes de que se procese el mensaje, como durante un bloqueo del consumidor, entonces el consumidor no tiene forma de saber si su grupo de consumidores perdió el mensaje cuando se le asigna una partición. Entonces, una estrategia es rebobinar el desplazamiento a la posición anterior. La API del consumidor de Kafka proporciona los siguientes métodos para esto:

void seek(TopicPartition partition, long offset);
void seekToBeginning(Collection < TopicPartition > partitions);

método buscar() se puede utilizar con el método
compensaciones por tiempos (mapa marcas de tiempo para buscar) para rebobinar a un estado en algún punto específico en el pasado.

Implícitamente, el uso de este enfoque significa que es muy probable que algunos mensajes que se procesaron previamente se lean y se procesen nuevamente. Para evitar esto, podemos usar la lectura idempotente, como se describe en el Capítulo 4, para realizar un seguimiento de los mensajes vistos anteriormente y eliminar los duplicados.

Alternativamente, su código de consumidor puede mantenerse simple, siempre que la pérdida o duplicación de mensajes sea aceptable. Cuando observamos los casos de uso para los que Kafka se usa comúnmente, como el manejo de eventos de registro, métricas, seguimiento de clics, etc., nos damos cuenta de que es poco probable que la pérdida de mensajes individuales tenga un impacto significativo en las aplicaciones circundantes. En tales casos, los valores predeterminados son perfectamente aceptables. Por otro lado, si su aplicación necesita enviar pagos, debe cuidar cuidadosamente cada mensaje individual. Todo se reduce al contexto.

Las observaciones personales muestran que a medida que aumenta la intensidad de los mensajes, disminuye el valor de cada mensaje individual. Los mensajes grandes tienden a ser valiosos cuando se ven en forma agregada.

Alta disponibilidad

El enfoque de Kafka para la alta disponibilidad es muy diferente del enfoque de ActiveMQ. Kafka está diseñado en torno a clústeres de escalamiento horizontal en los que todas las instancias de agentes reciben y distribuyen mensajes al mismo tiempo.

Un clúster de Kafka consta de varias instancias de agentes que se ejecutan en diferentes servidores. Kafka fue diseñado para ejecutarse en hardware independiente ordinario, donde cada nodo tiene su propio almacenamiento dedicado. No se recomienda el uso de almacenamiento conectado a la red (SAN) porque varios nodos de cómputo pueden competir por el tiempo.Ыe intervalos de almacenamiento y crear conflictos.

Kafka es siempre encendido sistema. Muchos grandes usuarios de Kafka nunca cierran sus clústeres y el software siempre se actualiza con un reinicio secuencial. Esto se logra garantizando la compatibilidad con la versión anterior para mensajes e interacciones entre intermediarios.

Agentes conectados a un clúster de servidores guardián del zoológico, que actúa como un registro de datos de configuración y se utiliza para coordinar los roles de cada intermediario. ZooKeeper en sí mismo es un sistema distribuido que brinda alta disponibilidad a través de la replicación de información al establecer quórum.

En el caso base, se crea un tema en un clúster de Kafka con las siguientes propiedades:

  • El número de particiones. Como se discutió anteriormente, el valor exacto utilizado aquí depende del nivel deseado de lectura paralela.
  • El factor de replicación (factor) determina cuántas instancias de intermediario en el clúster deben contener registros para esta partición.

Usando ZooKeepers para la coordinación, Kafka intenta distribuir de manera justa las nuevas particiones entre los intermediarios del clúster. Esto lo hace una única instancia que actúa como controlador.

En tiempo de ejecución para cada partición de tema Controlador asignar roles a un corredor el líder (líder, maestro, presentador) y seguidores (seguidores, esclavos, subordinados). El intermediario, que actúa como líder de esta partición, es responsable de recibir todos los mensajes que le envían los productores y de distribuir los mensajes a los consumidores. Cuando los mensajes se envían a una partición de tema, se replican en todos los nodos intermediarios que actúan como seguidores de esa partición. Cada nodo que contiene registros para una partición se llama una réplica. Un intermediario puede actuar como líder para algunas particiones y como seguidor para otras.

Un seguidor que contiene todos los mensajes del líder se llama réplica sincronizada (una réplica que está en un estado sincronizado, réplica sincronizada). Si un intermediario que actúa como líder para una partición deja de funcionar, cualquier intermediario que esté actualizado o sincronizado para esa partición puede asumir la función de líder. Es un diseño increíblemente sostenible.

Parte de la configuración del productor es el parámetro acusaciones, que determina cuántas réplicas deben acusar recibo (acusar recibo) de un mensaje antes de que el subproceso de la aplicación continúe enviando: 0, 1 o todas. Si se establece en todos, luego, cuando se recibe un mensaje, el líder enviará una confirmación al productor tan pronto como reciba confirmaciones (reconocimientos) del registro de varias señales (incluido él mismo) definidas por la configuración del tema min.insync.réplicas (predeterminado 1). Si el mensaje no se puede replicar con éxito, el productor generará una excepción de aplicación (NoEnoughReplicas o NoEnoughReplicasAfterAppend).

Una configuración típica crea un tema con un factor de replicación de 3 (1 líder, 2 seguidores por partición) y el parámetro min.insync.réplicas se establece en 2. En este caso, el clúster permitirá que uno de los intermediarios que administran la partición del tema se desactive sin afectar las aplicaciones cliente.

Esto nos lleva de vuelta a la ya familiar compensación entre rendimiento y confiabilidad. La replicación ocurre a expensas del tiempo de espera adicional para las confirmaciones (reconocimientos) de los seguidores. Aunque, debido a que se ejecuta en paralelo, la replicación en al menos tres nodos tiene el mismo rendimiento que dos (ignorando el aumento en el uso del ancho de banda de la red).

Al utilizar este esquema de replicación, Kafka evita de manera inteligente la necesidad de escribir físicamente cada mensaje en el disco con la operación sincronizar (). Cada mensaje enviado por el productor se escribirá en el registro de la partición, pero como se explicó en el Capítulo 2, la escritura en un archivo se realiza inicialmente en el búfer del sistema operativo. Si este mensaje se replica en otra instancia de Kafka y está en su memoria, la pérdida del líder no significa que el mensaje en sí se haya perdido; una réplica sincronizada puede asumirlo.
Negativa a realizar la operación. sincronizar () significa que Kafka puede recibir mensajes tan rápido como puede escribirlos en la memoria. Por el contrario, cuanto más tiempo pueda evitar vaciar la memoria en el disco, mejor. Por este motivo, no es raro que a los agentes de Kafka se les asignen 64 GB o más de memoria. Este uso de memoria significa que una sola instancia de Kafka puede ejecutarse fácilmente a velocidades miles de veces más rápidas que un intermediario de mensajes tradicional.

Kafka también se puede configurar para aplicar la operación sincronizar () a los paquetes de mensajes. Dado que todo en Kafka está orientado a paquetes, en realidad funciona bastante bien para muchos casos de uso y es una herramienta útil para los usuarios que requieren garantías muy sólidas. Gran parte del rendimiento puro de Kafka proviene de los mensajes que se envían al intermediario como paquetes y que estos mensajes se leen del intermediario en bloques secuenciales utilizando copia cero operaciones (operaciones durante las cuales no se realiza la tarea de copiar datos de un área de memoria a otra). Este último es una gran ganancia de rendimiento y recursos y solo es posible mediante el uso de una estructura de datos de registro subyacente que define el esquema de partición.

Es posible obtener un rendimiento mucho mejor en un clúster de Kafka que con un solo agente de Kafka, ya que las particiones de temas se pueden escalar horizontalmente en muchas máquinas separadas.

resultados

En este capítulo, analizamos cómo la arquitectura Kafka reinventa la relación entre los clientes y los intermediarios para proporcionar una canalización de mensajería increíblemente sólida, con un rendimiento mucho mayor que el de un intermediario de mensajes convencional. Hemos discutido la funcionalidad que utiliza para lograr esto y mirado brevemente la arquitectura de las aplicaciones que proporcionan esta funcionalidad. En el próximo capítulo, veremos los problemas comunes que las aplicaciones basadas en mensajería deben resolver y analizaremos las estrategias para abordarlos. Terminaremos el capítulo describiendo cómo hablar sobre las tecnologías de mensajería en general para que pueda evaluar su idoneidad para sus casos de uso.

Parte traducida anterior: Comprender los intermediarios de mensajes. Aprendiendo la mecánica de la mensajería con ActiveMQ y Kafka. Capítulo 1

Traducción hecha: tele.gg/middle_java

To be continued ...

Solo los usuarios registrados pueden participar en la encuesta. Registrarsepor favor

¿Se utiliza Kafka en su organización?

  • No

  • Anteriormente utilizado, ahora no

  • Planeamos usar

38 usuarios votaron. 8 usuarios se abstuvieron.

Fuente: habr.com

Añadir un comentario