Apache Kafka es una plataforma de transmisión de eventos de alto rendimiento y altamente escalable. Para desbloquear todo el potencial de Kafka, necesita considerar cuidadosamente el diseño de su aplicación. Es muy fácil escribir aplicaciones Kafka que funcionan mal o que acaban topándose con un muro de escalabilidad. Desde 2015, IBM ha proporcionado el servicio IBM Event Streams, que es un servicio Apache Kafka totalmente gestionado que se ejecuta en IBM Cloud. Desde entonces, el servicio ha ayudado a muchos clientes, así como a equipos de IBM, a resolver problemas de escalabilidad y rendimiento con las aplicaciones Kafka que han escrito.
En este artículo se describen algunos de los problemas más comunes de Apache Kafka y se ofrecen algunas recomendaciones para evitar problemas de escalabilidad en sus aplicaciones.
1. Minimizar las esperas para los viajes de ida y vuelta en la red
Ciertas operaciones de Kafka funcionan porque el cliente envía datos al intermediario y espera una respuesta. Un viaje completo de ida y vuelta puede tardar diez milisegundos, lo que parece rápido, pero lo limita a un máximo de 100 operaciones por segundo. Por esta razón, se recomienda intentar evitar este tipo de operaciones siempre que sea posible. Afortunadamente, los clientes de Kafka le ofrecen formas de evitar las esperas en estos tiempos de ida y vuelta. Solo necesita asegurarse de que los está aprovechando.
Consejos para maximizar el rendimiento:
- No revise cada mensaje enviado si ha funcionado. La API de Kafka permite disociar el envío de un mensaje de la verificación de si el intermediario lo recibió correctamente. Esperar la confirmación de que se ha recibido un mensaje puede introducir latencia de ida y vuelta de la red en su aplicación, por lo que conviene minimizarla en la medida de lo posible. Esto podría significar enviar tantos mensajes como sea posible, antes de verificar que se hayan recibido todos. O podría significar delegar la comprobación del envío correcto de mensajes a otro subproceso de ejecución dentro de la aplicación, de modo que pueda ejecutarse en paralelo mientras se envían más mensajes.
- No siga el procesamiento de cada mensaje con una confirmación de compensación. La asignación de compensaciones (sincrónicamente) se implementa como una red de ida y vuelta con el servidor. Realice compensaciones con menos frecuencia o utilice la función de compensación asíncrona para evitar pagar el precio de este viaje de ida y vuelta por cada mensaje que procese. Tenga en cuenta que realizar compensaciones con menos frecuencia puede significar que sea necesario volver a procesar más datos si su aplicación falla.
Si ha leído lo anterior y ha pensado: "Vaya, ¿eso no hará que mi solicitud sea más compleja?», la respuesta es sí, probablemente sí. Hay un equilibrio entre el rendimiento y la complejidad de las aplicaciones. Lo que hace que el tiempo de ida y vuelta de la red sea un escollo especialmente insidioso es que, una vez alcanzas este límite, puede requerir cambios extensos en la aplicación para lograr mejoras en el rendimiento.
2. No permitir que el aumento de los tiempos de procesamiento se confunda con fallos de los consumidores
Una característica útil de Kafka es que monitoriza la “actividad” de las aplicaciones y desconecta las que puedan haber fallado. Esto funciona haciendo que el intermediario rastree cuándo cada cliente consumidor realizó la última llamada "sondeo" (la terminología de Kafka para solicitar más mensajes). Si un cliente no hace encuestas con suficiente frecuencia, el intermediario al que está conectado concluye que debe haber fallado y lo desconecta. Esto está diseñado para permitir a los clientes que no están experimentando problemas intervenir y retomar el trabajo del cliente fallido.
Desafortunadamente, con este esquema, el intermediario de Kafka no puede distinguir entre un cliente que tarda mucho tiempo en procesar los mensajes que recibió y un cliente que realmente ha fallado. Consideremos una aplicación consumidora que realiza un bucle: 1) Llama a poll y obtiene un lote de mensajes; o 2) procesa cada mensaje del lote, tardando un segundo en procesar cada mensaje.
Si este consumidor recibe lotes de diez mensajes, el sondeo tardará aproximadamente 10 segundos entre llamadas. Por defecto, Kafka deja pasar hasta 300 segundos (cinco minutos) entre las encuestas antes de desconectar al cliente, por lo que todo funcionaría bien en este caso. Pero, ¿qué sucede en un día realmente ajetreado cuando comienza a acumularse una acumulación de mensajes sobre el tema del que consume la aplicación? En lugar de recibir solo diez mensajes de respuesta de cada llamada de encuesta, su aplicación recibe 500 mensajes (por defecto este es el número máximo de registros que se pueden devolver mediante una llamada a encuesta). Eso resultaría en suficiente tiempo de procesamiento para que Kafka decida que la instancia de aplicación ha fallado y la desconecte. Esto es una mala noticia.
Le encantará saber que puede empeorar. Es posible que se produzca una especie de bucle de feedback. A medida que Kafka comienza a desconectar a los clientes porque no llaman a la encuesta con la frecuencia suficiente, hay menos instancias de la aplicación para procesar mensajes. La probabilidad de que haya una gran acumulación de mensajes sobre el tema aumenta, lo que aumenta la probabilidad de que más clientes reciban grandes lotes de mensajes y tarden demasiado en procesarlos. Con el tiempo, todas las instancias de la aplicación consumidora entran en un ciclo de reinicio y no se realiza ningún trabajo útil.
¿Qué medidas puede tomar para evitar que esto le ocurra?
- El tiempo máximo entre llamadas de sondeo se puede configurar utilizando la configuración del consumidor de Kafka "max.poll.interval.ms" . El número máximo de mensajes que puede devolver una sola encuesta también se puede configurar mediante la configuración "max.poll.records" . Como regla general, intente reducir el "max.poll.records" en las preferencias para aumentar "max.poll.interval.ms" porque establecer un intervalo máximo de encuesta grande hará que Kafka tarde más en identificar a los consumidores que realmente han fracasado.
- Los consumidores de Kafka también pueden recibir instrucciones para pausar y reanudar el flujo de mensajes. La pausa del consumo evita que el método de sondeo devuelva ningún mensaje, pero aún restablece el temporizador utilizado para determinar si el cliente ha fallado. Pausar y reanudar es una táctica útil si ambos: a) esperan que los mensajes individuales tarden mucho tiempo en procesarse; y b) quieren que Kafka sea capaz de detectar un fallo del cliente a mitad del procesamiento de un mensaje individual.
- No pase por alto la utilidad de las métricas de cliente de Kafka. El tema de las métricas podría ocupar todo un artículo por sí solo, pero en este contexto el consumidor expone métricas tanto para el tiempo medio como para el tiempo máximo entre encuestas. La monitorización de estas métricas puede ayudar a identificar situaciones en las que un sistema descendente es la razón por la que cada mensaje recibido de Kafka tarda más de lo esperado en procesarse.
Volveremos al tema de los fallos de los consumidores más adelante en este artículo, cuando veamos cómo pueden desencadenar el reequilibrio del grupo de consumidores y el efecto disruptivo que esto puede tener.
3. Minimizar el coste de los consumidores inactivos
Bajo el capó, el protocolo utilizado por el consumidor de Kafka para recibir mensajes funciona enviando una solicitud de “búsqueda” a un agente de Kafka. Como parte de esta solicitud, el cliente indica qué debe hacer el intermediario si no hay ningún mensaje que devolver, incluido cuánto tiempo debe esperar el intermediario antes de enviar una respuesta vacía. De forma predeterminada, los consumidores de Kafka indican a los intermediarios que esperen hasta 500 milisegundos (controlados por la configuración del consumidor "fetch.max.wait.ms") para que al menos un byte de datos del mensaje esté disponible (controlado con la configuración “fetch.min.bytes”) .
Esperar 500 milisegundos no parece descabellado, pero si su aplicación tiene consumidores que están mayormente inactivos y se escala a, digamos, 5000 instancias, eso supone potencialmente 2500 solicitudes por segundo que no hacen absolutamente nada. Cada una de estas solicitudes requiere tiempo de CPU en el intermediario para procesarse y, en el extremo, puede afectar el rendimiento y la estabilidad de los clientes de Kafka que desean realizar un trabajo útil.
Normalmente, el enfoque de Kafka para escalar consiste en añadir más intermediarios y, a continuación, reequilibrar uniformemente las particiones de temas entre todos los intermediarios, tanto los antiguos como los nuevos. Desafortunadamente, este enfoque podría no ser de ayuda si sus clientes bombardean Kafka con solicitudes de recuperación innecesarias. Cada cliente enviará solicitudes de recuperación a todos los intermedarios que lideran una partición de tema de la que el cliente está consumiendo mensajes. Así que es posible que, incluso después de escalar el clúster Kafka y redistribuir particiones, la mayoría de tus clientes estén enviando peticiones de obtención a la mayoría de los brokers.
Entonces, ¿qué puede hacer?
- Cambiar la configuración del consumidor de Kafka puede ayudar a reducir este efecto. Si desea recibir mensajes tan pronto como lleguen, el "fetch.min.bytes" debe permanecer en su valor predeterminado de 1; sin embargo, el "fetch.max.wait.ms" se puede aumentar a un valor mayor y, al hacerlo, se reducirá el número de solicitudes realizadas por consumidores inactivos.
- En un ámbito más amplio, ¿su aplicación necesita tener potencialmente miles de instancias, cada una de las cuales consume con muy poca frecuencia de Kafka? Puede haber muy buenas razones para que lo haga, pero quizás haya formas de diseñarlo para hacer un uso más eficiente de Kafka. Abordaremos algunas de estas consideraciones en la siguiente sección.
4. Elegir el número adecuado de temas y particiones
Si llega a Kafka desde un entorno con otros sistemas de publicación–suscripción (por ejemplo, Message Queuing Telemetry Transport, o MQTT para abreviar), entonces podría esperar que los temas de Kafka sean muy ligeros, casi efímeros. No lo son. Kafka se siente mucho más cómodo con un número de temas medido en miles. También se espera que los temas de Kafka tengan una vida relativamente larga. Prácticas como crear un tema para recibir un único mensaje de respuesta y luego eliminar el tema son poco comunes en Kafka y no aprovechan los puntos fuertes de Kafka.
En su lugar, planifique temas de larga duración. Quizás compartan la vida útil de una aplicación o una actividad. También intente limitar el número de temas a cientos o quizás miles. Esto podría requerir adoptar una perspectiva diferente sobre qué mensajes se intercalan sobre un tema en particular.
Una pregunta relacionada que surge a menudo es: "¿Cuántas particiones debe tener mi tema?" Tradicionalmente, el consejo es sobreestimar, porque agregar particiones después de que se haya creado un tema no cambia la partición de los datos existentes sobre el tema (y, por lo tanto, puede afectar a los consumidores que confían en la partición para ofrecer mensajes ordenados dentro de una partición). Este es un buen consejo; sin embargo, nos gustaría sugerir algunas consideraciones adicionales:
- Para los temas que pueden esperar un rendimiento medido en MB/segundo, o donde el rendimiento podría crecer a medida que escala su aplicación, recomendamos encarecidamente tener más de una partición, de modo que la carga pueda distribuirse entre varios intermediarios. El servicio Event Streams siempre ejecuta Kafka con un múltiplo de tres intermediarios. En el momento de escribir esto, cuenta con un máximo de hasta nueve intermediarios, aunque quizá esto aumente en el futuro. Si elige un múltiplo de tres para el número de particiones en su tema, entonces se puede equilibrar de forma equitativa entre todos los intermediarios.
- El número de particiones en un tema es el límite de cuántos consumidores de Kafka pueden compartir de manera útil mensajes de consumo del tema con grupos de consumidores de Kafka (más sobre esto más adelante). Si añade más consumidores a un grupo de consumidores de los que hay particiones en el tema, algunos consumidores se quedarán inactivos sin consumir datos de mensajes.
- No hay nada intrínsecamente malo en tener temas de una sola partición, siempre y cuando esté absolutamente seguro de que nunca recibirán un tráfico de mensajes significativo, o no confiará en ordenar dentro de un tema y estará encantado de añadir más particiones más adelante.
5. El reequilibrio del grupo de consumidores puede ser sorprendentemente disruptivo
La mayoría de las aplicaciones Kafka que consumen mensajes aprovechan las capacidades del grupo de consumidores de Kafka para coordinar qué clientes consumen desde qué particiones de temas. Si su recuerdo de los grupos de consumidores es un poco confuso, aquí tiene un breve repaso de los puntos clave:
- Los grupos de consumidores coordinan un grupo de clientes de Kafka de modo que solo un cliente recibe mensajes de una partición de tema determinada en un momento dado. Esto es útil si necesita compartir los mensajes sobre un tema entre varias instancias de una aplicación.
- Cuando un cliente de Kafka se une a un grupo de consumidores o abandona un grupo de consumidores al que se había unido previamente, el grupo de consumidores se reequilibra. Por lo general, los clientes se unen a un grupo de consumidores cuando se inicia la aplicación de la que forman parte y se van porque la aplicación se cierra, se reinicia o se bloquea.
- Cuando un grupo se reequilibra, las particiones temáticas se redistribuyen entre los miembros del grupo. Por ejemplo, si un cliente se une a un grupo, es posible que a algunos de los clientes que ya están en el grupo se les quiten particiones temáticas (o "revocadas" en la terminología de Kafka) para dárselas al cliente que se acaba de unir. Lo contrario también es cierto: cuando un cliente abandona un grupo, las particiones de temas asignadas a él se redistribuyen entre los miembros restantes.
A medida que Kafka ha ido madurando, se han ideado (y siguen ideándose) algoritmos de reequilibrio cada vez más sofisticados. En las primeras versiones de Kafka, cuando un grupo de consumidores se reequilibraba, todos los clientes del grupo tenían que dejar de consumir, las particiones temáticas se redistribuían entre los nuevos miembros del grupo y todos los clientes volvían a consumir. Este enfoque tiene dos inconvenientes (no te preocupes, se han mejorado desde entonces):
- Todos los clientes del grupo dejan de consumir mensajes mientras se produce el reequilibrio. Esto tiene repercusiones evidentes en el rendimiento.
- Los clientes Kafka suelen intentar mantener un búfer de mensajes que aún no se han entregado a la aplicación y obtener más mensajes del intermediario antes de que se agote el búfer. El objetivo es evitar que se detenga la entrega de mensajes a la aplicación mientras se obtienen más mensajes del intermediario de Kafka (sí, tal y como se ha mencionado anteriormente en este artículo, el cliente de Kafka también intenta evitar la espera en los viajes de ida y vuelta de la red). Por desgracia, cuando un reequilibrio provoca la revocación de las particiones de un cliente, hay que descartar todos los datos almacenados en búfer de la partición. Del mismo modo, cuando el reequilibrio provoca que se asigne una nueva partición a un cliente, este comenzará a almacenar datos en el búfer a partir de la última posición confirmada para la partición, lo que podría provocar un pico en el rendimiento de la red desde el intermediario hasta el cliente. Esto se debe a que el cliente al que se le ha asignado la partición recientemente ha vuelto a leer los datos del mensaje que anteriormente había almacenado en búfer el cliente desde el que se revocó la partición.
Los algoritmos de reequilibrio más recientes han realizado mejoras significativas, para utilizar la terminología de Kafka, añadiendo "adherencia" y "cooperación":
- Los algoritmos "adhesivos" intentan garantizar que, tras un reequilibrio, el mayor número posible de miembros del grupo mantengan las mismas particiones que tenían antes del reequilibrio. Esto minimiza la cantidad de datos de mensajes almacenados en búfer que se descartan o se vuelven a leer de Kafka cuando se produce el reequilibrio.
- Los algoritmos "cooperativos" permiten a los clientes seguir consumiendo mensajes mientras se produce un reequilibrio. Cuando un cliente tiene asignada una partición antes de un reequilibrio y conserva la partición después de que se haya producido el reequilibrio, puede seguir consumiendo de las particiones no interrumpidas por el reequilibrio. Esto es sinérgico con la "adherencia", que actúa para mantener las particiones asignadas al mismo cliente.
A pesar de estas mejoras en los algoritmos de reequilibrio más recientes, si sus aplicaciones están sujetas con frecuencia a reequilibrios de grupos de consumidores, seguirá viendo un impacto en el rendimiento general de la mensajería y desperdiciando ancho de banda de red a medida que los clientes descartan y recuperan datos de mensajes almacenados en búfer. Estas son algunas sugerencias sobre lo que puede hacer:
- Asegurarse de que puede detectar cuándo se está produciendo el reequilibrio. A escala, recopilar y visualizar métricas es su mejor opción. Esta es una situación en la que una variedad de fuentes de métricas ayuda a construir la imagen completa. El intermediario de Kafka tiene métricas tanto para la cantidad de bytes de datos enviados a los clientes como para el número de grupos de consumidores que reequilibran. Si está recopilando métricas de su aplicación, o de su tiempo de ejecución, que muestran cuándo se reinician, correlacionar esto con las métricas del intermediario puede confirmar aún más que el reequilibrio es un problema para usted.
- Evitar reinicios innecesarios de aplicaciones cuando, por ejemplo, una aplicación se bloquea. Si está experimentando problemas de estabilidad con su aplicación, esto puede provocar un reequilibrio mucho más frecuente de lo previsto. Buscar en los registros de la aplicación mensajes de error comunes emitidos por un fallo de la aplicación, por ejemplo las trazas de pila, puede ayudar a identificar con qué frecuencia ocurren los problemas y proporcionar información útil para depurar el problema subyacente.
- ¿Está utilizando el mejor algoritmo de reequilibrio para su aplicación? En el momento de redactar este documento, el estándar de referencia es el "CooperativeStickyAssignor"; sin embargo, el valor predeterminado (a partir de Kafka 3.0) es utilizar el "RangeAssignor" (y el algoritmo de asignación anterior) en lugar del asignador cooperativo persistente. La documentación de Kafka describe los pasos de migración necesarios para que sus clientes recojan el asignador fijo cooperativo. También vale la pena señalar que, si bien el cedente adhesivo cooperativo es una buena opción en todos los sentidos, hay otros cedentes que se adaptan a casos de uso específicos.
- ¿Son fijos los miembros de un grupo de consumidores? Por ejemplo, quizás siempre ejecute cuatro instancias distintas y de alta disponibilidad de una aplicación. Quizás pueda beneficiarse de la característica de membresía estática de grupos de Kafka. Al asignar identificadores únicos a cada instancia de su aplicación, la pertenencia a grupos estáticos le permite eludir por completo el reequilibrio.
- Confirmar el cambio actual cuando se elimine una partición de la instancia de tu aplicación. El cliente consumidor de Kafka proporciona un detector de eventos de reequilibrio. Si una instancia de su aplicación está a punto de perder una partición, el listener le ofrece la oportunidad de confirmar una compensación para la partición que está a punto de desaparecer. La ventaja de realizar una compensación en el momento en que se revoca la partición es que garantiza que cualquier miembro del grupo al que se le asigne la partición continúe desde ese punto, en lugar de tener que volver a procesar algunos de los mensajes de la partición.