Las arquitecturas orientadas a eventos y aplicaciones de datos en tiempo real son cada vez más demandadas y Apache Kafka o Confluent se han convertido en la pieza central de dichas soluciones.

En el blog ya se ha hablado anteriormente de las funcionalidades de Kafka, así como de alguno de sus componentes principales como Kafka Streams.

En este caso, vamos a hablar en profundidad del Schema Registry de Kafka, en qué se basa su funcionamiento y cómo utilizarlo de una manera adecuada para gobernar eventos. Si quieres entender mejor la evolución de los esquemas, pero de forma práctica interactuando con Kafka, no te pierdas este artículo.

¿Por qué se necesita un Schema Registry?

Si pensamos en plataformas de streaming de datos, la volumetría de información que se maneja y la cantidad de componentes que las conforman, para su gestión y crecimiento se necesitan ciertas garantías. Estas garantías las proporciona el Schema Registry.

La filosofía de Kafka, y parte fundamental del éxito de su funcionamiento, es que toma bytes de información y los publica a los consumidores, no lee esos datos, no los carga en memoria, ni realiza ninguna verificación de los mismos.

Schema Registry y Avro 1

Teniendo en cuenta esto, nos surgen algunas dudas:

  • ¿Qué pasa si el productor de Kafka publica datos incorrectos o mal estructurados en un topic? ¿Y si el formato cambia? En esos casos el consumidor de datos de Kafka se rompe.
  • ¿Y por qué los broker de Kafka no verifican estos mensajes? Porque si lo hacen consumiría CPU para su procesamiento y perderían parte de la potencia que les hace tan eficientes en el procesado de datos en streaming.

Para dar solución a estos problemas se hace uso de esquemas para los mensajes que se envían. Si pensamos en un ecosistema de aplicaciones de tipo petición / respuesta las APIs son un punto clave, y en un mundo asíncrono los esquemas de los mensajes son las APIs entre productores y consumidores.

El Schema Registry es un componente independiente de Kafka que tiene los siguientes objetivos:

  • Distribuir los esquemas al productor y consumidor almacenando una copia.
  • Forzar el cumplimiento del contrato definido en el esquema entre los productores y consumidores del topic de Kafka.
  • Proteger a los consumidores evitando que reciban información errónea o en formato invalido de un topic.
  • Documentar el uso del evento y el significado de cada campo que conforma el topic de Kafka.
  • Gestionar automáticamente los cambios de esquema, es decir, su evolución.

¿Cómo funciona el Schema Registry?

En Kafka el desacoplamiento entre los productores y consumidores implica que no se comuniquen entre sí, sino a través de un topic. El consumidor necesita saber qué tipo de datos envió el productor para deserializarlos y el productor para serializarlos y enviárselos al consumidor como espera. Para esto se utiliza el Schema Registry, cuyo funcionamiento es el siguiente:

El productor, antes de enviar los datos a Kafka se comunica con el Schema Registry y verifica si el esquema está disponible.

  • Si no encuentra el esquema, el productor lo envía al Schema Registry y éste lo almacena.
  • Si lo encuentra, el productor obtiene el esquema y serializa los datos basándose en él para posteriormente enviar a Kafka la información serializada en formato binario junto con el identificador del esquema utilizado.

El consumidor recibe el mensaje de Kafka y se comunica con el Schema Registry utilizando el identificador de esquema recibido y deserializa los datos utilizando dicho esquema. Si hay alguna discrepancia o problema al deserializar el Schema Registry lanzará un error indicando que se ha incumplido el contrato especificado.

Schema Registry y Avro 2

El uso de este componente tiene como principales ventajas:

  • La eficiencia, gracias a la caché.
  • La gestión de los esquemas forma parte del ciclo de vida de las aplicaciones productoras y consumidoras de datos.
  • Ser la única fuente de la verdad para todos los componentes.
  • Incrementar la agilidad y facilitar el cambio, ya que se pueden añadir, borrar o modificar campos con garantías, es decir, hay un gobierno de los cambios gracias al versionado.

¿Qué es AVRO?

Avro es un formato de serialización de datos binarios open source con el que se pueden representar estructuras de datos complejas. Sus principales características son:

  • Dispone de distintos tipos de datos:
    • Primitivos: null, boolean, integer, long, bytes, string, float, double.
    • Complejos: enums, arrays, maps, unions.
    • Lógicos: decimals, date, time-millis, timestamp-millis.
  • Los esquemas están definidos en JSON.
  • Tiene la documentación embebida en el propio esquema.
  • Permite incluir valores por defecto, lo cual es muy útil para evolucionar esquemas.

Las principales ventajas del uso de este formato son:

  • Los datos están completamente tipados.
  • La documentación va embebida en el propio esquema.
  • Los esquemas pueden evolucionar.
  • Soporta compresión y deserialización parcial.
  • Los datos pueden ser leídos por lenguajes mayoritarios como Java o #C.

Las principales desventajas son que los datos no pueden visualizarse sin utilizar herramientas que lean Avro y que ciertos lenguajes no tienen librerías ni herramientas que ayuden a su procesamiento.

Un ejemplo de un esquema en Avro es el siguiente:

{"namespace": "test.avro",
 "type": "record",
 "name": "LogLine",
 "fields": [
   {"name": "ip", "type": "string"},
   {"name": "url",  "type": "string"},
   {"name": "referrer",  "type": "string"},
   {"name": "userAgent",  "type": "string"},
   {"name": "sessionId",  "type": ["null","string"], "default": null}
 ]
}

Volviendo al ecosistema de Kafka y al Schema Registry, Avro se utiliza como uno de los posibles formatos de datos para especificar la estructura de la información que se envía a través de un topic de kafka. Aunque también es posible hacerlo con JSON Schema y Protobuf, en este caso utilizaremos el formato clásico de Avro que nos permite:

  • Definir el esquema de los datos.
  • Generar código desde el esquema.
  • Usar APIs para serializar datos acorde al esquema.
  • Usar APIs para extraer la información y deserializar los datos basándose en el esquema.
Schema Registry y Avro 3

¿Cómo evolucionan los esquemas?

Una parte fundamental de la gestión y el gobierno de eventos es la evolución de los esquemas. Una vez que se establece un esquema inicial, las aplicaciones y los requerimientos de negocio cambian y resulta crítico que los componentes que utilizan el antiguo esquema sigan funcionando. Independientemente de si se utiliza Avro u otro formato es necesario considerar cómo evolucionan dichos esquemas.

El Schema Registry está diseñado para verificar la compatibilidad entre esquemas a través de la versión y del tipo de compatibilidad.

La gestión de las versiones es muy sencilla. El esquema inicial es representado en la versión 1 y las sucesivas evoluciones serán las versiones 2, 3, etc. Para generar una nueva versión el Schema Registry compara si se cumple el tipo de compatibilidad establecida y si se cumple se genera la siguiente versión.

Los tipos de compatibilidades, es decir, los cambios permitidos entre versiones de un mismo esquema son los siguientes:

1 Compatibilidad forward

Es el patrón más común, en el que se quiere actualizar un productor porque el flujo de datos evoluciona y los consumidores no se actualizan.

En este caso el consumidor utiliza el antiguo esquema (v1) para leer los nuevos datos de un productor que los genera con el nuevo esquema (v2).

  • Caso de uso típico: se generan nuevos datos por parte del productor, pero ya se tienen aplicaciones consumidoras de esos datos y no se quieren modificar o se les quiere dar tiempo para que esos consumidores se adapten a la nueva versión.
  • Los cambios permitidos son:
    • Añadir campos, serán ignorados por los antiguos consumidores.
    • Borrar campos opcionales, es decir, que tengan un valor por defecto. Los consumidores que utilizan el esquema antiguo usarán el valor por defecto cuando el nuevo mensaje no incluya ese campo.

Si utilizamos como ejemplo el mismo esquema Avro de un línea de log que hemos visto antes y se configura la compatibilidad forward se observa lo siguiente:

  • Si se añade un productor nuevo (Productor v2) con un nuevo esquema (v2) que incluye un campo adicional (message) y borra un campo opcional (sessionId).
  • El antiguo consumidor (Consumidor v1), es decir, que utiliza el esquema previo (v1), es capaz de procesar mensajes de ambos productores.
  • Los mensajes con esquema antiguo (mensaje v1) se procesarán como hasta ahora y los mensajes con el esquema nuevo (mensaje v2) al deserializarlos el campo adicional (message) se ignora y el campo opcional eliminado (sessionId) se queda con su valor por defecto (null).
Schema Registry y Avro 4

2 Compatibilidad backward

Es el patrón menos habitual, en el que se quiere actualizar el flujo de datos teniendo nuevos consumidores que los procesan pero todavía hay productores que no se van a actualizar.

En este caso el consumidor utiliza el nuevo esquema (v2) para leer los datos de un productor que los genera con el antiguo esquema (v1).

  • Caso de uso típico: una aplicación que requiera evolucionar los datos pero tenga productores que no se pueden actualizar inmediatamente, por ejemplo sensores IoT que generan datos en un determinado formato o no se tenga acceso a ellos pero se quieren seguir consumiendo los datos que generen.
  • Los cambios permitidos son:
    • Borrar campos, el nuevo consumidor ignora los campos que recibe y no tiene contemplados en el nuevo esquema.
    • Añadir campos opcionales, es decir, que tengan un valor por defecto. Los consumidores recibirán datos con el antiguo esquema, es decir, sin estos campos y utilizarán el valor por defecto para procesarlos.

Si volvemos al ejemplo de nuestro esquema Avro de un línea de log y se configura la compatibilidad backward se observa lo siguiente:

  • Si se añade un nuevo consumidor (consumidor v2), con un nuevo esquema (v2) que incluye un campo adicional (message) con un valor por defecto y borra un campo (userAgent).
  • El nuevo consumidor utiliza el nuevo esquema (v2) para procesar mensajes de ambos productores.
  • Los mensajes con el nuevo esquema (mensaje v2) se procesan sin problema y los mensajes con el anterior esquema (mensaje v1) al procesarlos el campo eliminado se ignora (userAgent) y el campo añadido se deja el valor por defecto (null).
Schema Registry y Avro 5

3 Compatibilidad completa o full

Es el patrón ideal y cumple las compatibilidades forward y backward. En el que se quiere actualizar el flujo de datos y se pueden actualizar tanto productores como consumidores y consumir o producir información en ambas versiones.

  • Los cambios permitidos son:
    • Borrar campos que tengan un valor por defecto. Los consumidores que utilizan el esquema antiguo usarán el valor por defecto cuando el nuevo mensaje no incluya ese campo.
    • Añadir campos opcionales, es decir, que tengan un valor por defecto. Los consumidores recibirán datos con el antiguo esquema, es decir, sin estos campos y utilizarán el valor por defecto.

Si volvemos al ejemplo de nuestro esquema Avro y se configura la compatibilidad full y se cambia el esquema inicial (v1) añadiendo un nuevo campo con valor por defecto (message) y se elimina un campo opcional (sessionId) en el esquema evolucionado (v2) se tiene lo siguiente:

  • El antiguo consumidor (consumidor v1) procesa los mensajes de ambos productores:
    • El mensaje con antiguo esquema (mensaje v1) se procesa sin problema al ser la misma versión de esquema.
    • El mensaje con nuevo esquema (mensaje v2) se procesa ignorando el campo adicional (message) y dejando el valor por defecto del campo opcional eliminado (sessionId = null)
  • El nuevo consumidor (consumidor v2) procesa los mensajes de ambos productores:
    • El mensaje con nuevo esquema (mensaje v2) se procesa sin problema al ser la misma versión de esquema.
    • El mensaje con antiguo esquema (mensaje v1) el campo eliminado (sessionId) lo ignora y el campo adicional lo incluye con su valor por defecto (message = null).
Schema Registry y Avro 6

4 Breaking o incompatible

Es el patrón a evitar, pero que a veces es inevitable. Esto sucede cuando el flujo de datos nuevo tiene un esquema que rompe la versión anterior.

  • Caso de uso típico: quieren mantener la completa compatibilidad evolucionando el flujo de información pero tienes tanto consumidores como productores que no se van a actualizar de forma inmediata.
  • Algunos ejemplos de cambios no compatibles son:
    • Cambiar el tipo de un campo, por ejemplo de String a Integer.
    • Renombrar un campo obligatorio, es decir, con valor por defecto.
    • Añadir o eliminar valores de un enumerado.

Conclusiones

  • El uso de esquemas es imprescindible para la viabilidad y la gestión de ecosistemas asíncronos a lo largo del tiempo.
  • Los esquemas se pueden usar de forma fácil y rápida en Kafka con Avro a través del Schema Registry.
  • Utilizar el Schema Registry proporciona un desacople total entre consumidores y productores, un gobierno de los cambios en el flujo de información que se maneja y, además, es una garantía para que no se puedan inyectar datos incorrectos.
  • La evolución de los esquemas es un punto crítico y resulta fundamental tener claro los cambios y las implicaciones que tienen para no romper componentes y garantizar su correcto funcionamiento.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.