Saltar a contenido

Schema Regitry

Autor Olivera Lucas.

¿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.

byte

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.

  1. Si no encuentra el esquema, el productor lo envía al Schema Registry y éste lo almacena.
  2. 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

El uso de este componente tiene como principales ventajas:

  1. La eficiencia, gracias a la caché.
  2. La gestión de los esquemas forma parte del ciclo de vida de las aplicaciones productoras y consumidoras de datos.
  3. Ser la única fuente de la verdad para todos los componentes.
  4. 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 diferentes lenguajes Java, C#, Python, Go

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.

schema registry avro

¿Qué es IDL?

El IDL (interface definition lenguage) es un lenguaje que permite describir estructuras de datos complejar de una manera más simple y perceptible para el humano en comparación con las sentencias de avro.

Utilizamos IDL para definir las estructuras de nuestros eventos, por ejemplo

@namespace("Wap.Events.Record")
protocol Event{

import idl "EventosDeNegocio.avdl";
import idl "DatosDeReferencia.avdl";

    record SolicitudDeAccionAlmacen {
        Wap.Events.Record.EventoDeNegocio eventoDeNegocio;
        string idTransaccion;
        string contrato;
        string almacen;
        string planta;
        string contratowarehouse;
    }
}

y el avro correspondiente a ese idl es:

{
  "type" : "record",
  "name" : "SolicitudDeAccionAlmacen",
  "namespace" : "Wap.Events.Record",
  "fields" : [ {
    "name" : "eventoDeNegocio",
    "type" : {
      "type" : "record",
      "name" : "EventoDeNegocio",
      "fields" : [ {
        "name" : "timestamp",
        "type" : {
          "type" : "long",
          "logicalType" : "timestamp-millis"
        }
      }, {
        "name" : "remitente",
        "type" : "string"
      }, {
        "name" : "destinatario",
        "type" : [ "null", "string" ],
        "default" : null
      }, {
        "name" : "numeroDeOrden",
        "type" : [ "null", "string" ],
        "default" : null
      }, {
        "name" : "vencimiento",
        "type" : [ "null", {
          "type" : "long",
          "logicalType" : "timestamp-millis"
        } ],
        "default" : null
      } ]
    }
  }, {
    "name" : "idTransaccion",
    "type" : "string"
  }, {
    "name" : "contrato",
    "type" : "string"
  }, {
    "name" : "almacen",
    "type" : "string"
  }, {
    "name" : "planta",
    "type" : "string"
  }, {
    "name" : "contratowarehouse",
    "type" : "string"
  } ]
}

para que un archivo IDL se convierta en una clase útil para un lenguaje de programación sufre varias transformaciones.

  1. A partir del idl se generan los archivos avro
  2. A partir de los archivos avro se generan las clases de código necesarias
    1. Se generan las clases de C#
    2. Se generán las estructuras y funciones de Go

Tip

Para ver como generar eventos ver repositorio de schemas

¿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:

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).

Compatibilidad forward

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 una 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).

Compatibilidad backward

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).

Compatibilidad backward

¿Qué debemos tener en cuenta para crear un evento?

Los eventos que publicamos deben contener toda la información correspondiente a dicho evento, esto es para evitar el acoplamiento con los consumidores ya que si publicamos información incompleta estamos obligando a un consumidor que busque la información restante, esto es aplicable siempre y cuando el evento no supere los 64 KB de tamaño.