Esquemas y orden
Dos avisos que no deberían haber llegado
1 de abril, 00:03. A Ana le ingresan la nómina: 1.500 €. Un minuto después se carga el alquiler, 900 €, y al rato una compra de 300 €. A las 00:05 le llega un aviso: «Tu cuenta está en descubierto». Tenía dinero de sobra.
La semana anterior, el equipo de Reportingver en el mapa había hecho lo que proponía el episodio 5 para aguantar más volumen: partir el log de movimientos en tres particiones y poner tres instancias a leer. Funcionó durante días. Hasta que una de las tres se atascó un momento.
Ese mismo mes, el equipo de Cuentasver en el mapa hizo una limpieza: renombró el campo importe de MovimientoRegistrado a cantidad, que les parecía más claro. Sus pruebas pasaron. Nadie se acordó de que Reporting leía importe.
Los dos problemas tienen algo en común: un evento no es un mensaje suelto, sino parte de una secuencia y de un contrato. Este episodio va de cómo no romper ninguno de los dos sin querer.
Tres particiones, dos cuentas, un contrato
Cada vez que pulsas Registrar, Cuentas publica cinco movimientos: los de Ana (A) y los de Luis (L), intercalados. Van a un log con tres particiones, y cada partición la lee una instancia de Reporting, que lleva el saldo de cada cuenta y avisa si baja de cero. A la izquierda, los movimientos se reparten por turnos y cada cual publica el formato que quiere; a la derecha, la cuenta decide la partición y un registro de esquemas revisa los cambios. Ralentiza una instancia y cambia el formato del evento.
Qué ha pasado
Particiones: más velocidad, menos orden
Un log con una sola partición lo lee, como mucho, una instancia de cada consumidor a la vez: es la única forma de que avance en orden. Para leer más rápido, el log se parte en varias particiones, cada una con su propio orden, y cada instancia lee una. Tres particiones, tres instancias en paralelo.
El precio es que el orden solo existe dentro de cada partición. Entre particiones distintas no hay ninguno: cada instancia va a su ritmo. Si los movimientos de Ana se reparten por turnos, la nómina puede acabar en la partición 0 y el alquiler en la 2. Mientras las tres instancias vayan igual de rápido, casi nunca se nota. Cuando la 0 se atasca un segundo, el alquiler llega antes que la nómina, el saldo de Ana pasa por −800 € y Reporting avisa de un descubierto que nunca existió.probar en el laboratorio
No hace falta todo el orden, solo el que importa
¿Importa que el movimiento de Luis se procese antes o después que el de Ana? No. Lo que importa es el orden dentro de cada cuenta. Por eso no hace falta renunciar a las particiones: basta con que todos los movimientos de una misma cuenta vayan a la misma.
Eso es una clave de partición. Cuentas publica cada movimiento con la cuenta como clave, y el broker elige la partición calculando algo como hash(clave) % particiones. Misma cuenta, misma partición, mismo orden. Y cuentas distintas siguen repartidas, así que el paralelismo no se pierde.
La clave tiene sus trampas:
- Claves calientes. Si una cuenta de empresa genera la mitad de los movimientos, su partición carga con la mitad del trabajo, y su instancia no da abasto mientras las otras se aburren.
- Cambiar el número de particiones. Pasar de 3 a 4 cambia el resultado de
% particiones: una cuenta puede empezar a escribir en otra partición mientras la vieja aún tiene movimientos suyos sin leer. - El consumidor también cuenta. Si una instancia lee en orden pero procesa varios mensajes en paralelo, o reintenta uno más tarde, vuelve a desordenarlos.
Hay otra defensa, compatible con la clave: que el evento lleve un número de secuencia por cuenta (el movimiento 41, el 42…). Así el consumidor detecta que le falta el 41 cuando le llega el 42, y puede esperar o descartar lo que llegue viejo. Y la mejor de todas, cuando se puede: que el orden no importe. El saldo final de Ana en la simulación es correcto en los dos paneles, porque sumar importes da igual en qué orden. Lo que dependía del orden era el aviso intermedio.
Un evento es un contrato
El episodio 2 avisaba de que cada campo de un evento es una promesa. Aquí se cobra. Cuando Cuentas renombra importe a cantidad, Reporting sigue buscando importe, no lo encuentra y no puede procesar el movimiento.probar en el laboratorio En la simulación lo aparta a una cola de mensajes muertos para no bloquear su partición (el episodio 5 explica por qué). Los movimientos no se pierden, pero el extracto deja de cuadrar hasta que alguien se da cuenta.
Con eventos, el productor no sabe quién le lee: esa era la gracia. La otra cara es que no puede preguntar a sus consumidores antes de cambiar algo. Hace falta que algo compruebe el cambio por ellos.
Eso es un registro de esquemas. Cada tipo de evento tiene un esquema (qué campos tiene y de qué tipo), guardado en el registro con sus versiones. Antes de publicar con un esquema nuevo, el productor lo registra, y el registro comprueba si es compatible con el anterior según la regla que se haya fijado. Si no lo es, lo rechaza antes de que salga ningún evento.
Añadir un campo, como divisa, sí pasa.probar en el laboratorio Reporting no lo conoce y lo ignora. Esa costumbre de leer solo lo que necesitas e ignorar lo demás se llama tolerant reader, y es lo que hace que añadir campos sea seguro.
Compatible con quién
La compatibilidad tiene dirección, y los nombres confunden:
- Compatibilidad hacia delante (forward): los consumidores con el esquema viejo pueden leer eventos escritos con el nuevo. Es lo que necesitas si el productor cambia primero, como Cuentas en este episodio.
- Compatibilidad hacia atrás (backward): los consumidores con el esquema nuevo pueden leer eventos escritos con el viejo. Es lo que necesitas si los consumidores se actualizan primero, o si releen el log (episodio 5) y se encuentran eventos de hace meses.
- Completa (full): las dos a la vez. Añadir un campo opcional con valor por defecto la cumple; renombrar uno no cumple ninguna.
Renombrar en dos pasos
¿Entonces Cuentas no puede renombrar nunca importe? Puede, pero no de golpe. Se hace en dos pasos, a veces llamados expandir y contraer:
- Expandir. Cuentas publica los dos campos,
importeycantidad, con el mismo valor. Nadie se rompe. - Migrar. Cada consumidor, a su ritmo, pasa a leer
cantidad. - Contraer. Cuando nadie lee
importe, se quita. Saber cuándo pasa eso es lo difícil: exige saber quién consume qué.
Si el cambio es profundo, la alternativa es un evento nuevo (MovimientoRegistrado.v2, o un tema nuevo en el broker) publicado en paralelo al viejo mientras los consumidores migran.
El cambio que ningún registro detecta: cambiar el significado sin cambiar la forma. Si Cuentas pasa a publicar importe en céntimos en vez de en euros, el esquema es idéntico, el registro lo acepta y Reporting multiplica por cien los saldos de todo el banco. Un campo nuevo con otro nombre (importeCentimos) es siempre mejor que reutilizar uno viejo.
Criterio
Para el orden, pregúntate:
-
¿El consumidor puede dar un resultado distinto si procesa los eventos en otro orden?Avisar de descubierto; aplicar «cuenta bloqueada» y luego «cuenta desbloqueada».ordena
- No ordenes: reparte como quieras y gana paralelismo. Sumar importes o contar operaciones da igual en qué orden.
Y si hay que ordenar, ordena solo lo necesario: por la entidad cuyo estado cambia (la cuenta, la transferencia, la tarjeta), usándola como clave de partición. Añade un número de secuencia por entidad si el consumidor necesita detectar huecos.
Para los cambios de esquema:
| Cambio | ¿Rompe a quien ya lee? | Cómo hacerlo |
|---|---|---|
| Añadir un campo opcional | No | Directamente. Los consumidores lo ignoran hasta que lo necesiten. |
| Quitar un campo | Sí, si alguien lo lee | Primero asegúrate de que nadie lo usa. |
| Renombrar un campo | Sí | Expandir y contraer: los dos campos a la vez, migrar, quitar el viejo. |
| Cambiar el tipo (número a texto) | Sí | Campo nuevo con otro nombre, y retirar el viejo como arriba. |
| Cambiar el significado (euros a céntimos) | Sí, y en silencio | Nunca en el mismo campo. Campo nuevo, con un nombre que lo diga. |
En el mundo real
Claves de partición: en Kafka, el productor calcula la partición a partir de la clave del mensaje (con un hash); sin clave, reparte. Amazon Kinesis usa una partition key igual. En colas, la idea equivalente es agrupar: SQS FIFO ordena por MessageGroupId y Azure Service Bus, por sesiones.
Registros de esquemas: Confluent Schema Registry (para Avro, Protobuf y JSON Schema) es el más extendido en el mundo Kafka, con reglas de compatibilidad configurables por tema. AWS Glue Schema Registry y Apicurio Registry hacen lo mismo. El formato también ayuda: en Protobuf, los campos viajan por número y no por nombre, así que renombrar un campo no rompe la lectura (aunque sí el código de quien lo usa); Avro permite declarar alias.
Para documentar qué eventos existen, quién los publica y qué llevan, hay un equivalente de OpenAPI para eventos: AsyncAPI. Es la forma más directa de responder a la pregunta que nadie se hizo en BCPS Bank: ¿quién lee importe?
Final de la serie
Hasta aquí, la serie de eventos. Empezamos preguntando si una llamada o un evento, y hemos acabado con particiones, esquemas y claves de idempotencia. Por el camino, casi todos los problemas han tenido la misma raíz: un servicio que depende de otro más de lo que parecía.
Hay una pregunta que hemos dado por resuelta desde el episodio 0: ¿por qué esos ocho contextos, y no otros? ¿Por qué Transferencias y Cuentas son cosas distintas, y por qué «movimiento» significa algo en Cuentas y nada en Fidelización?
Siguiente · Serie DDD Diseño guiado por el dominio Lenguaje ubicuo, Event Storming, bounded contexts y agregados: de dónde sale el mapa de BCPS Bank, y por qué entre agregados se habla con eventos.