Una cola y un flujo de eventos no son lo mismo
Una reparte trabajo y lo olvida. El otro es un registro ordenado que varios consumidores pueden releer. Elegir el equivocado produce una arquitectura que te pelea durante años.
Un equipo necesita desacoplar dos servicios, así que pone una cola entre ellos. Seis meses después otro equipo quiere esos mismos eventos. Añaden una segunda cola y un reparto. Después alguien necesita reprocesar los datos del mes pasado tras un error, y descubre que los mensajes ya no están, porque una cola borra un mensaje cuando se consume.
Eso no es una cola fallando. Eso es un problema con forma de registro resuelto con una cola.
La distinción que lo decide todo
Una cola reparte trabajo. Entra un mensaje, un consumidor lo coge, lo procesa, lo confirma y se borra. Varios consumidores compiten por los mensajes, que es exactamente lo que quieres para repartir trabajo entre workers. Una vez consumido, el mensaje ya no está. El trabajo de la cola es asegurar que cada unidad de trabajo ocurre una vez y absorber ráfagas.
Un flujo es un registro ordenado y retenido. Los eventos se añaden al final y se quedan durante un periodo de retención. Los consumidores llevan su propia posición de forma independiente, así que varios pueden leer los mismos eventos sin estorbarse, uno nuevo puede empezar desde el principio, y uno existente puede rebobinar y reprocesar. El trabajo del flujo es ser el registro de lo que ocurrió.
La prueba que hay que aplicar: ¿le importa este evento a más de una cosa, ahora o plausiblemente después, y querrías alguna vez volver a reproducirlo? Si sí, flujo. Si es una tarea que un worker tiene que hacer una vez, cola.
Equivocarse aquí es caro porque es difícil de revertir. Añadir reproducción a posteriori sobre una arquitectura de colas significa reconstruir el transporte y todo lo que depende de su semántica.
Las opciones gestionadas
Todas las nubes ofrecen las dos cosas, y la correspondencia es directa. Hay un servicio simple de colas, un mecanismo de temas o publicación y suscripción para el reparto, y un producto de flujo o concentrador de eventos para el registro retenido. Kafka gestionado está disponible en todas como servicio, y el proyecto libre sigue siendo el valor por defecto cuando necesitas su ecosistema, sus conectores o portabilidad entre nubes.
La elección entre Kafka gestionado y el producto de flujo nativo de la nube va sobre todo de ecosistema y modelo operativo. Kafka trae un ecosistema grande de conectores y herramientas de procesamiento de flujos, y trae peso operativo incluso gestionado. Los productos nativos se integran de forma más limpia con el resto de la nube, en particular con las funciones, y tienen menos maquinaria alrededor.
Para la mayoría de los equipos la opción nativa es la correcta, y Kafka se gana su sitio cuando necesitas los conectores, cuando eres genuinamente multinube, o cuando un equipo ya lo conoce bien.
Garantías de entrega, y la que no es lo que crees
Como mucho una vez significa que un mensaje puede perderse y nunca duplicarse. Rara vez es lo que quieres.
Al menos una vez es lo que obtienes en la práctica de casi todos los sistemas. Un mensaje se entrega una o más veces, y hay duplicados porque la confirmación puede fallar después de que el procesamiento haya salido bien. Este es el modelo para el que hay que diseñar.
Exactamente una vez lo ofrecen algunos sistemas y es más estrecho de lo que suena. Se aplica en general dentro de la frontera de ese sistema, por ejemplo un ciclo de leer, procesar y escribir enteramente dentro de una plataforma con soporte transaccional. En el momento en que tu consumidor llama a una API externa o escribe en otra base de datos, esa garantía no se extiende al efecto secundario.
Así que la regla práctica: asume al menos una vez y haz idempotentes a los consumidores. Usa una clave de idempotencia derivada del mensaje, registra las claves procesadas, y haz que la escritura sea segura de repetir. Es la mitad aburrida de toda integración y es la mitad que determina si el sistema es fiable, como se explica en reintentos, idempotencia y mensajes fallidos.
El orden te cuesta paralelismo
El orden global en un tema no suele estar disponible a escala, porque significa un solo consumidor.
Lo que obtienes en su lugar es orden dentro de una partición o de una clave de agrupación. Elige la clave de forma que los eventos que tienen que ordenarse entre sí la compartan: todos los eventos de un cliente, de una cuenta, de un documento. Entonces el paralelismo es igual al número de particiones, y los eventos de claves distintas se procesan a la vez.
Dos consecuencias. Una clave caliente, un cliente que genera muchos más eventos que los demás, crea una partición que se retrasa mientras las otras están ociosas. Y el número de particiones es difícil de cambiar después en casi todos los sistemas, porque cambia la correspondencia entre clave y partición, así que elígelo pensando en el crecimiento.
Si te encuentras necesitando orden global estricto, eso suele ser señal de que hay que repensar el diseño y no el transporte.
Reintentos, mensajes fallidos y el mensaje envenenado
Un consumidor que falla tiene que reintentar, y un consumidor que falla siempre tiene que parar.
Usa espera exponencial con variación aleatoria, no un bucle apretado. Una dependencia aguas abajo que falla más reintentos agresivos convierte un problema en una denegación de servicio contra ti mismo, que es una de las formas de pico de coste de encontrar el desbocado antes de que lo haga la factura.
Fija un número máximo de recepciones y encamina los mensajes agotados a una cola de mensajes fallidos. Sin ella, un único mensaje improcesable bloquea su partición o da vueltas para siempre.
Y después vigila esa cola de verdad. Una cola de fallidos sin vigilar es un mecanismo silencioso de pérdida de datos. Alerta cuando su profundidad pase de cero, y construye las herramientas para inspeccionarla y reproducir desde ella antes de necesitarlas a las tres de la madrugada.
Distingue en el consumidor los fallos reintentables de los permanentes. Un tiempo de espera de red merece reintento; una carga útil malformada no, y debería ir directa a la cola de fallidos con su error.
Esquemas, y el contrato que nadie escribió
El evento es una interfaz entre equipos, y una interfaz sin documentar se rompe.
Usa un esquema con definición explícita, idealmente en un registro que valide al publicar. Y después sigue las reglas de compatibilidad: los cambios aditivos con valores por defecto son seguros, quitar o renombrar un campo no lo es, y cambiar un tipo tampoco. Un consumidor tiene que tolerar campos que no reconoce, para que un productor pueda añadir uno sin coordinar una entrega.
Versiona el tipo de evento en su nombre o en un atributo, y cuando necesites un cambio incompatible, publica una versión nueva junto a la antigua hasta que los consumidores hayan migrado. El esquema es la parte de esta arquitectura que sobrevive a todos los servicios que la tocan.
El evento lleva estado, o el evento lleva referencia
Dos formas, y la elección tiene consecuencias.
Notificación con referencia envía un identificador y deja que los consumidores busquen lo que necesiten. Mensajes pequeños, datos siempre actuales, y acopla a los consumidores con la API del productor y crea un pico de carga cuando muchos consumidores consultan a la vez.
El evento lleva el estado incluye los datos relevantes en el mensaje. Los consumidores son independientes y pueden procesar históricos sin llamar a nadie, a cambio de mensajes más grandes y de datos que reflejan el momento en que se publicaron.
Por defecto lleva el estado en los eventos que se consumen cruzando fronteras de equipo, porque la independencia es la razón por la que elegiste eventos. Usa referencias para cargas útiles grandes, poniendo el objeto en almacenamiento y el puntero en el evento, y para todo aquello donde los consumidores no puedan ver un valor rancio.
Lo que se olvida
- Los límites de tamaño de mensaje son menores de lo que crees, y el patrón del resguardo, guardar la carga útil y enviar un puntero, es la respuesta estándar.
- El retraso del consumidor es la métrica que importa. Alerta sobre ella. Un consumidor que está levantado pero se va quedando atrás parece sano y está fallando.
- La retención es un coste y una red de seguridad. Demasiado corta y no puedes reproducir durante un incidente; demasiado larga y pagas por guardar eventos que nadie va a leer.
- El orden y los reintentos interactúan mal. Reintentar un mensaje fallido mientras continúas con el siguiente rompe el orden dentro de la clave, lo que puede importar o no.
- La contrapresión tiene que existir en algún sitio. Un productor al que no se puede frenar llenará una cola más rápido de lo que los consumidores la vacían, y el fallo aparece como memoria o como coste en vez de como error.
Qué hacer esta semana
Coge tu integración asíncrona más importante y responde a dos preguntas: si el consumidor procesara un mensaje dos veces, ¿qué se rompería?, y si necesitaras reprocesar los eventos de ayer, ¿podrías? La primera responde si tienes un problema de idempotencia, la segunda si elegiste el transporte correcto. Merece la pena saber las dos antes del incidente que te las pregunte. Lo trabajamos en la fase de arquitectura de un proyecto cloud.