Una modesta introducción al procesamiento analítico de datos en tiempo real

Introducción al procesamiento analítico en tiempo real

Fundamentos arquitectónicos para construir sistemas distribuidos confiables.

Las redes de datos de transmisión distribuida son ilimitadas y crecen a velocidades increíbles. Imagen creada por el autor a través de MidJourney

Fundamentos del procesamiento de transmisión

Los fundamentos son la base inquebrantable e irrompible sobre la cual se colocan las estructuras. Cuando se trata de construir una arquitectura de datos exitosa, los datos son el núcleo central y el componente principal de esa base.

Dado el modo común en que los datos fluyen ahora hacia nuestras plataformas de datos a través de plataformas de procesamiento de transmisión como Apache Kafka y Apache Pulsar, es fundamental asegurarnos (como ingenieros de software) de proporcionar capacidades higiénicas y barreras sin fricción para reducir el espacio de problemas relacionados con la calidad de los datos “después” de que los datos hayan ingresado en estas redes de datos de flujo rápido. Esto significa que el establecimiento de contratos a nivel de API en torno al esquema de nuestros datos (tipos y estructura), la disponibilidad a nivel de campo (nullable, etc.) y la validez del tipo de campo (rangos esperados, etc.) se convierten en los cimientos críticos de nuestra base de datos, especialmente dada la naturaleza descentralizada y distribuida de los sistemas de datos modernos de hoy en día.

Sin embargo, para llegar al punto en el que podamos comenzar a establecer una “fe ciega” o redes de datos de alta confianza, primero debemos establecer patrones de diseño inteligentes a nivel de sistema.

Construyendo sistemas de datos de transmisión confiables

Como ingenieros de software y datos, construir sistemas de datos confiables es literalmente nuestro trabajo, y esto significa que el tiempo de inactividad de los datos debe medirse como cualquier otro componente del negocio. Probablemente hayas oído hablar de los términos SLAs, SLOs y SLIs en algún momento u otro. En pocas palabras, estos acrónimos están asociados a los “contratos”, “promesas” y “medidas reales” en las que calificamos nuestros sistemas de extremo a extremo. Como responsables del servicio, seremos responsables de nuestros éxitos y fracasos, pero un poco de esfuerzo inicial va a largo plazo, y los metadatos capturados para garantizar que las cosas funcionen sin problemas desde una perspectiva operativa también pueden proporcionar información valiosa sobre la calidad y confianza de nuestros datos en movimiento, y reducir el nivel de esfuerzo para resolver problemas de datos en reposo.

Adoptar la mentalidad de los propietarios

Por ejemplo, los Acuerdos de Nivel de Servicio (SLAs) entre tu equipo u organización y tus clientes (tanto internos como externos) se utilizan para crear un contrato vinculante con respecto al servicio que estás proporcionando. Para los equipos de datos, esto significa identificar y capturar métricas (KPMs – métricas clave de rendimiento) basadas en tus Objetivos de Nivel de Servicio (SLOs). Los SLOs son las promesas que pretendes cumplir en función de tus SLAs, esto puede ser cualquier cosa, desde una promesa de tiempo de actividad del servicio casi perfecto (99.999%) (API o JDBC), hasta algo tan simple como una promesa de retención de datos de 90 días para un conjunto de datos específico. Por último, tus Indicadores de Nivel de Servicio (SLIs) son la prueba de que estás operando de acuerdo con los contratos de nivel de servicio y suelen presentarse en forma de análisis operativos (paneles de control) o informes.

Saber hacia dónde queremos ir puede ayudar a establecer el plan para llegar allí. Este viaje comienza en el punto de inserción (o ingestión), y con los datos. Específicamente, con la estructura formal y la identidad de cada punto de datos. Teniendo en cuenta la observación de que “cada vez más datos se están incorporando a la plataforma de datos a través de plataformas de procesamiento de transmisión como Apache Kafka”, ayuda tener garantías de tiempo de compilación, compatibilidad hacia atrás y serialización binaria rápida de los datos que se emiten en estos flujos de datos. La responsabilidad de los datos puede ser un desafío en sí mismo. Veamos por qué.

Gestionar la responsabilidad de los datos de transmisión

Los sistemas de transmisión funcionan las 24 horas del día, los 7 días de la semana y los 365 días del año. Esto puede complicarse si no se aplica el esfuerzo inicial adecuado al problema, y uno de los problemas que tiende a surgir de vez en cuando es el de los datos corruptos, también conocido como problemas de datos en tránsito.

Lidiar con problemas de datos en tránsito

Existen dos formas comunes de reducir problemas de datos en tiempo real. Primero, puedes introducir guardianes en el borde de tu red de datos que negocien y validen los datos utilizando interfaces de programación de aplicaciones (API) tradicionales, o como segunda opción, puedes crear y compilar bibliotecas auxiliares o Kits de Desarrollo de Software (SDK) para hacer cumplir los protocolos de datos y permitir escritores distribuidos (productores de datos) en tu infraestructura de datos en streaming, incluso puedes utilizar ambas estrategias al mismo tiempo.

Guardianes de Datos

El beneficio de agregar API de puerta de enlace en el borde (al frente) de tu red de datos es que puedes hacer cumplir la autenticación (¿puede este sistema acceder a esta API?), la autorización (¿puede este sistema publicar datos en un flujo de datos específico?) y la validación (¿son estos datos aceptables o válidos?) en el punto de producción de datos. El diagrama en la Figura 1–1 a continuación muestra el flujo de la puerta de enlace de datos.

Figura 1-1: Una arquitectura de sistemas distribuidos que muestra capas de autenticación y autorización en una Puerta de Admisión de Datos. Fluyendo de izquierda a derecha, los datos aprobados se publican en Apache Kafka para su procesamiento posterior. Crédito de la imagen: Scott Haines

El servicio de puerta de enlace de datos actúa como el guardián digital (portero) de tu red de datos protegida (interna). Con el papel principal de controlar, limitar e incluso restringir el acceso no autenticado en el borde (ver API/Servicios en la figura 1-1 anterior), autorizando qué servicios ascendentes (o usuarios) tienen permitido publicar datos (comúnmente manejado a través del uso de ACL de servicios) junto con una identidad proporcionada (piensa en la identidad de servicio y el acceso IAM, la identidad web y el acceso JWT, y nuestro viejo amigo OAUTH).

La responsabilidad principal del servicio de puerta de enlace es validar los datos entrantes antes de publicar datos potencialmente corruptos o simplemente malos. Si la puerta de enlace está haciendo su trabajo correctamente, solo los datos “buenos” llegarán y se introducirán en la red de datos, que es el conducto de datos de eventos y operativos para ser digeridos a través del procesamiento en streaming, en otras palabras:

“Esto significa que el sistema ascendente que produce datos puede fallar rápidamente al producir datos. Esto evita que los datos corruptos entren en las canalizaciones de datos en streaming o estacionarias en el borde de la red de datos y es una forma de establecer una conversación con los productores sobre exactamente por qué y cómo las cosas salieron mal de una manera más automática a través de códigos de error y mensajes útiles.”

Utilizando Mensajes de Error para Proporcionar Soluciones de Autoservicio

La diferencia entre una buena y mala experiencia radica en cuánto esfuerzo se requiere para pasar de malo a bueno. Todos probablemente hemos trabajado con, o en, o hemos oído hablar de servicios que simplemente fallan sin ninguna razón aparente (la excepción del puntero nulo arroja un 500 aleatorio).

Para establecer una confianza básica, un poco es suficiente. Por ejemplo, recibir un HTTP 400 desde un punto final de API con el siguiente cuerpo de mensaje (visto a continuación)

{  "error": {    "code": 400,    "message": "Los datos del evento no contienen el userId y la marca de tiempo no es válida (se espera una cadena con formato ISO8601). Por favor, consulta la documentación en http://coffeeco.com/docs/apis/customer/order#required-fields para ajustar la carga útil."    }}

proporciona una razón para el 400 y capacita a los ingenieros que nos envían datos (como propietarios del servicio) para solucionar un problema sin programar una reunión, activar la alarma o contactar a todos en Slack. Cuando puedas, recuerda que todos somos humanos ¡y nos encantan los sistemas de bucle cerrado!

Pros y Contras de la API para Datos

Este enfoque de API tiene sus pros y contras.

Los pros son que la mayoría de los lenguajes de programación funcionan de manera predeterminada con los protocolos de transporte HTTP (o HTTP/2) o con la adición de una pequeña biblioteca, y los datos JSON son casi universales como formato de intercambio de datos en estos días.

Por otro lado (contras), se puede argumentar que para cualquier dominio nuevo de datos, hay otro servicio por escribir y gestionar, y sin alguna forma de automatización de API o adhesión a una especificación abierta como OpenAPI, cada nueva ruta de API (punto final) tarda más tiempo del necesario.

En muchos casos, la falta de actualizaciones oportunas en las APIs de ingestión de datos, problemas de escalabilidad y/o tiempo de inactividad de la API, fallas aleatorias o simplemente falta de comunicación entre las personas, proporcionan la justificación necesaria para evitar el uso de la “estúpida” API e intentar publicar directamente los datos de eventos en Kafka. Aunque las APIs pueden parecer obstáculos, hay un argumento sólido para mantener un guardián común, especialmente después de que los problemas de calidad de datos, como eventos corruptos o eventos mezclados accidentalmente, comienzan a desestabilizar el sueño de la transmisión.

Para cambiar este problema por completo (y eliminarlo casi por completo), una buena documentación, gestión de cambios (CI/CD) y una higiene general en el desarrollo de software, que incluya pruebas unitarias e integradas reales, permiten ciclos rápidos de funciones e iteraciones que no reducen la confianza.

Idealmente, los propios datos (esquema/formato) podrían dictar las reglas de su propio contrato a nivel de datos mediante la validación a nivel de campo (predicados), la generación de mensajes de error útiles y actuando en su propio interés. Hey, con un poco de metadatos a nivel de ruta o datos, y un poco de pensamiento creativo, la API podría generar automáticamente rutas y comportamientos autodefinidos.

Por último, las APIs de puerta de enlace pueden considerarse como causantes de problemas centralizados, ya que cada fallo de un sistema aguas arriba para emitir datos válidos (por ejemplo, bloqueado por el guardián) hace que la información valiosa (datos de eventos, métricas) se pierda. El problema de la culpa también tiende a ir en ambas direcciones, ya que una implementación incorrecta del guardián puede dejar ciego a un sistema aguas arriba que no está configurado para manejar reintentos en caso de tiempo de inactividad de la puerta de enlace (incluso por unos segundos).

Dejando de lado los pros y los contras, el uso de una API de puerta de enlace para detener la propagación de datos corruptos antes de que ingresen a la plataforma de datos significa que cuando ocurre un problema (porque siempre ocurren), el área de superficie del problema se reduce a un servicio específico. Esto sin duda es mejor que depurar una red distribuida de canalizaciones de datos, servicios y los numerosos destinos finales de datos y sistemas aguas arriba para descubrir que alguien en la empresa está publicando directamente datos incorrectos.

Si elimináramos al intermediario (servicio de puerta de enlace), entonces las capacidades para gobernar la transmisión de datos “esperados” recaerían en las “bibliotecas” en forma de SDK especializados.

Kits de Desarrollo de Software (SDKs)

Los SDKs son bibliotecas (o micro-frameworks) que se importan en una base de código para agilizar una acción, actividad u operación compleja. También se conocen con otro nombre, clientes. Tomemos el ejemplo anterior sobre el uso de buenos mensajes de error y códigos de error. Este proceso es necesario para informar a un cliente que su acción anterior fue inválida, sin embargo, puede ser ventajoso agregar barreras de protección adecuadas directamente en un SDK para reducir el área de superficie de cualquier problema potencial. Por ejemplo, supongamos que tenemos una API configurada para rastrear el comportamiento relacionado con el café de los clientes a través del seguimiento de eventos.

Reducir el Error del Usuario con Barreras de Protección en el SDK

Teóricamente, un SDK de cliente puede incluir todas las herramientas necesarias para gestionar las interacciones con el servidor de la API, incluida la autenticación, autorización y, en cuanto a la validación, si el SDK hace bien su trabajo, los problemas de validación desaparecerían. El siguiente fragmento de código muestra un ejemplo de SDK que se podría utilizar para rastrear eventos de clientes de manera confiable.

import com.coffeeco.data.sdks.client._import com.coffeeco.data.sdks.client.protocol._Customer.fromToken(token)  .track(    eventType=Events.Customer.Order,    status=Status.Order.Initalized,    data=Order.toByteArray  )

Con un poco de trabajo adicional (es decir, el SDK de cliente), el problema de la validación de datos o la corrupción de eventos prácticamente desaparece. Los problemas adicionales se pueden gestionar dentro del propio SDK, como por ejemplo, cómo reintentar el envío de una solicitud en caso de que el servidor esté fuera de línea. En lugar de hacer que todas las solicitudes se reintenten de inmediato o en un bucle que sature un equilibrador de carga de puerta de enlace indefinidamente, el SDK puede tomar acciones más inteligentes, como utilizar un retraso exponencial. Consulta “El Problema del Rebaño Rugiente” para obtener más información sobre lo que sale mal cuando las cosas van, bueno, mal.

El Problema del Rebaño Rugiente Supongamos que tenemos un único servidor de API de puerta de enlace. Has creado una API fantástica y muchos equipos de la empresa están enviando datos de eventos a esta API. Todo va bien hasta que un día un nuevo equipo interno comienza a enviar datos inválidos al servidor (y en lugar de respetar tus códigos de estado HTTP, tratan todos los códigos HTTP que no sean 200 como una razón para reintentar. Pero espera, olvidaron agregar cualquier tipo de heurística de reintentos como el retraso exponencial, por lo que todas las solicitudes se reintentan indefinidamente, en una cola de reintentos cada vez mayor). Cabe mencionar que antes de que este nuevo equipo se uniera, nunca hubo motivo para ejecutar más de una instancia del servidor de API, y nunca fue necesario utilizar ningún tipo de limitador de velocidad a nivel de servicio, porque todo funcionaba sin problemas dentro de los SLA acordados.

</

La Ballena No Tan Fallida. Lo que puede suceder cuando solucionas problemas y sales del agua caliente nuevamente. Imagen a través de Midjourney a través del Autor.

Bueno, eso fue antes de hoy. Ahora tu servicio está fuera de línea. Los datos se están respaldando, los servicios de upstream están llenando sus colas y las personas están molestas porque sus servicios ahora comienzan a tener problemas debido a tu único punto de falla… Todos estos problemas se derivan de una forma de escasez de recursos llamada “El Problema del Rebaño Tronador”. Este problema ocurre cuando muchos procesos están esperando un evento, como la disponibilidad de recursos del sistema, o en este ejemplo, el servidor de API vuelve a estar en línea. Ahora hay una lucha mientras todos los procesos compiten por intentar obtener recursos, y en muchos casos, la carga en el proceso único (servidor de API) es suficiente para volver a poner el servicio fuera de línea. Desafortunadamente, esto inicia el ciclo de escasez de recursos nuevamente. Esto, por supuesto, a menos que puedas calmar al rebaño o distribuir la carga en un mayor número de procesos de trabajo, lo que reduce la carga en la red hasta el punto en que los recursos vuelven a tener espacio para respirar. Si bien el ejemplo inicial anterior es más bien un ataque de denegación de servicio distribuido (DDoS) involuntario, este tipo de problemas se pueden solucionar en el cliente (con retroceso exponencial o auto-limitación) y en el borde de la API mediante equilibrio de carga y limitación de velocidad.

En última instancia, sin el conjunto adecuado de ojos y oídos, habilitados por métricas operativas, monitores y alertas a nivel de sistema (SLAs/SLIs/SLOs), los datos pueden desaparecer y esto puede ser un desafío para resolver.

Ya sea que decidas agregar una API de puerta de enlace de datos al borde de tu red de datos, utilizar un SDK personalizado para la consistencia y responsabilidad aguas arriba, o tomar un enfoque alternativo cuando se trata de incorporar datos en tu plataforma de datos, es bueno saber cuáles son tus opciones. Independientemente del camino por el cual los datos se emiten en tus flujos de datos, esta introducción a los datos en tiempo real no estaría completa sin una discusión adecuada sobre formatos de datos, protocolos y el tema de los datos serializables binarios. ¡Quién sabe, tal vez descubramos un enfoque mejor para manejar nuestro problema de responsabilidad de datos!

Seleccionando el Protocolo de Datos Correcto para el Trabajo

Cuando piensas en datos estructurados, lo primero que viene a la mente podría ser datos JSON. Los datos JSON tienen estructura, son un protocolo de datos estándar basado en la web y, si nada más, son muy fáciles de trabajar. Todos estos son beneficios en términos de comenzar rápidamente, pero con el tiempo, y sin las salvaguardias adecuadas, podrías enfrentar problemas al estandarizar JSON para tus sistemas de transmisión en tiempo real.

La Relación de Amor / Odio con JSON

El primer problema es que los datos JSON son mutables. Esto significa que como estructura de datos, es flexible y, por lo tanto, frágil. Los datos deben ser consistentes para ser responsables, y en el caso de transferir datos a través de una red (on-the-wire), el formato serializado (representación binaria) debe ser altamente compacto. Con datos JSON, debes enviar las claves (para todos los campos) de cada objeto representado en la carga útil. Inevitablemente, esto significa que generalmente estarás enviando una gran cantidad de peso adicional por cada registro adicional (después del primero) en una serie de objetos.

Afortunadamente, este no es un problema nuevo, y resulta que hay mejores prácticas para este tipo de cosas, y múltiples enfoques sobre cuál es la mejor estrategia para serializar datos de manera óptima. Esto no quiere decir que JSON no tenga sus méritos. Solo cuando se trata de establecer una base sólida de datos, cuanto más estructura, mejor, y cuanto mayor sea el nivel de compresión, mejor, siempre y cuando no consuma muchos ciclos de CPU.

Datos Estructurados Serializables

Cuando se trata de codificar y transferir eficientemente datos binarios, dos frameworks de serialización siempre surgen: Apache Avro y Google Protocol Buffers (protobuf). Ambas bibliotecas proporcionan técnicas eficientes en términos de uso de CPU para serializar estructuras de datos basadas en filas, y además de ambas tecnologías, también ofrecen sus propios frameworks y capacidades de llamada a procedimientos remotos (RPC). Veamos avro, luego protobuf, y concluiremos analizando las llamadas a procedimientos remotos.

Formato de Mensaje Avro

Con Avro, defines esquemas declarativos para tus datos estructurados utilizando el concepto de registros. Estos registros son simplemente archivos de definiciones de datos formateados en JSON (esquemas) almacenados con el tipo de archivo avsc. El siguiente ejemplo muestra un esquema de Café en el formato de descriptor Avro.

{  "namespace": "com.coffeeco.data",  "type": "record",  "name": "Coffee",  "fields": [    {"name": "id", "type": "string"},    {"name": "name", "type": "string"},    {"name": "boldness", "type": "int", "doc": "de ligero a audaz. 1 a 10"},    {"name": "available", "type": "boolean"} ]}

Trabajar con datos Avro puede tomar dos caminos que divergen en relación a cómo deseas trabajar en tiempo de ejecución. Puedes tomar el enfoque de tiempo de compilación, o descubrir las cosas sobre la marcha en tiempo de ejecución. Esto permite una flexibilidad que puede mejorar una sesión interactiva de descubrimiento de datos. Por ejemplo, Avro fue creado originalmente como un protocolo eficiente de serialización de datos para almacenar grandes colecciones de datos, como archivos particionados, a largo plazo dentro del sistema de archivos Hadoop. Dado que los datos generalmente se leían de una ubicación y se escribían en otra dentro de HDFS, Avro podía almacenar el esquema (utilizado en el momento de escritura) una vez por archivo.

Formato Binario Avro

Cuando escribes una colección de registros Avro en disco, el proceso codifica el esquema de los datos Avro directamente en el archivo mismo (una vez). Existe un proceso similar cuando se trata de la codificación de archivos Parquet, donde el esquema se comprime y se escribe como un pie de página de archivo binario. Vimos este proceso de primera mano al final del capítulo 4, cuando pasamos por el proceso de agregar documentación a nivel de StructField a nuestro StructType. Este esquema se utilizó para codificar nuestro DataFrame y, cuando lo escribimos en disco, preservó nuestra documentación en línea en la siguiente lectura.

Habilitar la Compatibilidad Hacia Atrás y Prevenir la Corrupción de Datos

En el caso de leer múltiples archivos como una sola colección, pueden surgir problemas en caso de cambios de esquema entre registros. Avro codifica registros binarios como matrices de bytes y aplica un esquema a los datos en el momento de la deserialización (conversión de una matriz de bytes a un objeto).

Esto significa que debes tomar precauciones adicionales para preservar la compatibilidad hacia atrás, de lo contrario te encontrarás con problemas de excepciones ArrayIndexOutOfBounds.

Esto puede ocurrir si se cambia el esquema de formas sutiles. Por ejemplo, digamos que necesitas cambiar un valor entero a un valor largo para un campo específico en tu esquema. No lo hagas. Esto romperá la compatibilidad hacia atrás debido al aumento en el tamaño de bytes de un entero a un largo. Esto se debe al uso de la definición de esquema para definir la posición de inicio y finalización en la matriz de bytes para cada campo de un registro. Para mantener la compatibilidad hacia atrás, deberás deprecar el uso del campo entero en adelante (mientras lo preservas en tu definición Avro) y agregar (anexar) un nuevo campo al esquema para usar en adelante.

Mejores Prácticas para Datos Avro en Streaming

Al pasar de archivos Avro estáticos, con sus útiles esquemas incrustados, a un flujo ilimitado de datos binarios bien estructurados, la principal diferencia es que debes llevar tu propio esquema a la fiesta. Esto significa que deberás admitir la compatibilidad hacia atrás (en caso de que necesites retroceder y volver a procesar datos antes y después de un cambio de esquema), así como la compatibilidad hacia adelante, en caso de que ya tengas lectores existentes que consuman de un flujo.

El desafío aquí es admitir ambas formas de compatibilidad dado que Avro no tiene la capacidad de ignorar campos desconocidos, lo cual es un requisito para admitir la compatibilidad hacia adelante. Para abordar estos desafíos con Avro, los desarrolladores de Confluence han publicado su registro de esquemas de código abierto (para usar con Kafka), que permite la versión de esquemas a nivel de tema de Kafka (flujo de datos).

Cuando admites Avro sin un registro de esquemas, deberás asegurarte de haber actualizado todos los lectores activos (aplicaciones Spark u otros) para usar la nueva versión del esquema antes de actualizar la versión de la biblioteca de esquemas en tus escritores. De lo contrario, en el momento en que cambies el interruptor, podrías encontrarte al comienzo de un incidente.

Formato de Mensaje Protobuf

Con Protobuf, defines tus definiciones de datos estructurados utilizando el concepto de mensajes. Los mensajes se escriben en un formato que se asemeja más a la definición de una estructura en C. Estos archivos de mensajes se escriben en archivos con la extensión de nombre de archivo proto. Los Protocol Buffers tienen la ventaja de utilizar importaciones. Esto significa que puedes definir tipos de mensajes comunes y enumeraciones que se pueden utilizar dentro de un proyecto grande, o incluso importarlos en proyectos externos para permitir un amplio reuso a gran escala. Un ejemplo simple de crear el registro de Café (tipo de mensaje) utilizando Protobuf.

syntax = "proto3";
option java_package="com.coffeeco.protocol";
option java_outer_classname="Common";
message Coffee {
  string id       = 1;
  string name     = 2;
  uint32 boldness = 3;
  bool available  = 4;
}

Con protobuf defines tus mensajes una vez, y luego los compila para el lenguaje de programación que elijas. Por ejemplo, podemos generar código para Scala utilizando el archivo coffee.proto usando el compilador independiente del proyecto ScalaPB (creado y mantenido por Nadav Samet), o aprovechar la brillantez de Buf, que creó un conjunto invaluable de herramientas y utilidades en torno a protobuf y grpc.

Generación de código

Compilar protobuf permite una generación de código sencilla. El siguiente ejemplo se toma del directorio /ch-09/data/protobuf. Las instrucciones en el archivo README del capítulo cubren cómo instalar ScalaPB e incluyen los pasos para configurar las variables de entorno correctas para ejecutar el comando.

$SCALAPBC/bin/scalapbc -v3.11.1 \  --scala_out=/Users/`whoami`/Desktop/coffee_protos \  --proto_path=$SPARK_MDE_HOME/ch-09/data/protobuf/ \  coffee.proto

Este proceso ahorra tiempo a largo plazo al liberarte de tener que escribir código adicional para serializar y deserializar tus objetos de datos (a través de límites de lenguaje o dentro de diferentes bases de código).

Formato binario de Protobuf

El formato serializado (formato binario) se codifica utilizando el concepto de separadores de nivel de campo binario. Estos separadores se utilizan como marcadores que identifican los tipos de datos encapsulados dentro de un mensaje protobuf serializado. En el ejemplo, coffee.proto, probablemente notaste que había un marcador indexado junto a cada tipo de campo (string id = 1;), esto se utiliza para ayudar con la codificación/descodificación de mensajes en/desde el cable. Esto significa que hay un poco de sobrecarga adicional en comparación con el binario avro, pero si lees la especificación de codificación, verás que otras eficiencias compensan con creces cualquier byte adicional (como el empaquetado de bits, el manejo eficiente de tipos de datos numéricos y la codificación especial de los primeros 15 índices para cada mensaje). En cuanto al uso de protobuf como tu protocolo binario de elección para la transmisión de datos, los beneficios superan ampliamente las desventajas en el gran esquema de las cosas. Una de las formas en que lo compensa con creces es con el soporte tanto para la compatibilidad hacia atrás como para la compatibilidad hacia adelante.

Habilitar la compatibilidad hacia atrás y prevenir la corrupción de datos

Existen reglas similares a tener en cuenta cuando se trata de modificar los esquemas protobuf, como discutimos con avro. Como regla general, puedes cambiar el nombre de un campo, pero nunca cambias el tipo o la posición (índice) a menos que quieras romper la compatibilidad hacia atrás. Estas reglas pueden pasarse por alto cuando se trata de admitir cualquier tipo de datos a largo plazo y pueden ser especialmente difíciles a medida que los equipos se vuelven más competentes en el uso de protobuf. Existe esta necesidad de reorganizar y optimizar, lo cual puede volver y perjudicarte si no tienes cuidado. (Ver el consejo a continuación llamado Mantener la calidad de los datos a lo largo del tiempo para obtener más contexto).

Mejores prácticas para la transmisión de datos Protobuf

Dado que protobuf admite tanto la compatibilidad hacia atrás como la compatibilidad hacia adelante, esto significa que puedes implementar nuevos escritores sin tener que preocuparte por actualizar primero tus lectores, y lo mismo es cierto para tus lectores, puedes actualizarlos con versiones más nuevas de tus definiciones protobuf sin preocuparte por una implementación compleja de todos tus escritores. Protobuf admite la compatibilidad hacia adelante utilizando el concepto de campos desconocidos. Este es un concepto adicional que no existe dentro de la especificación avro, y se utiliza para rastrear los índices y los bytes asociados que no se pudieron analizar debido a la divergencia entre la versión local del protobuf y la versión que se está leyendo actualmente. Lo bueno aquí es que también puedes optar en cualquier momento por cambios más nuevos en las definiciones protobuf.

Por ejemplo, supongamos que tienes dos aplicaciones de transmisión (a) y (b). La aplicación (a) está procesando datos de transmisión desde un tema Kafka ascendente (x), mejorando cada registro con información adicional y luego escribiéndolo en un nuevo tema Kafka (y). Ahora, la aplicación (b) lee desde (y) y hace su trabajo. Digamos que hay una versión más nueva de la definición protobuf, y la aplicación (a) aún no se ha actualizado a la versión más nueva, mientras que el tema Kafka ascendente (x) y la aplicación (b) ya están actualizados y esperan usar algunos nuevos campos disponibles desde la actualización. Lo sorprendente es que todavía es posible pasar los campos desconocidos a través de la aplicación (a) y hasta la aplicación (b) sin siquiera saber que existen.

Consulte “Consejos para mantener una buena calidad de datos a lo largo del tiempo” para obtener más detalles.

Consejo: Mantener la calidad de los datos a lo largo del tiempo

Cuando trabaje con avro o protobuf, debe tratar los esquemas de la misma manera que trataría el código que desea implementar en producción. Esto significa crear un proyecto que se pueda guardar en el repositorio de GitHub de su empresa (o cualquier otro sistema de control de versiones que esté utilizando), y también significa que debe escribir pruebas unitarias para sus esquemas. Esto no solo proporciona ejemplos prácticos de cómo utilizar cada tipo de mensaje, sino que la razón más importante para probar los formatos de datos es asegurarse de que los cambios en el esquema no rompan la compatibilidad hacia atrás. Además, al realizar pruebas unitarias de los esquemas, deberá compilar primero los archivos (.avsc o .proto) y utilizar la generación de código de la biblioteca respectiva. Esto facilita la creación de código de biblioteca que se puede lanzar y también se puede utilizar el versionado de lanzamientos (versión 1.0.0) para catalogar cada cambio en los esquemas.

Un método sencillo para habilitar este proceso es serializar y almacenar una copia binaria de cada mensaje, a través de todos los cambios de esquema, como parte del ciclo de vida del proyecto. He tenido éxito al agregar este paso directamente en las pruebas unitarias mismas, utilizando el conjunto de pruebas para crear, leer y escribir estos registros directamente en el directorio de recursos de prueba del proyecto. De esta manera, cada versión binaria, a través de todos los cambios de esquema, está disponible dentro del propio código base.

Con un poco de esfuerzo adicional al principio, puede ahorrarse mucho dolor en el gran esquema de las cosas, y descansar tranquilo sabiendo que sus datos están seguros (al menos en el lado de la producción y el consumo)

Usando herramientas de Buf y Protobuf en Spark

Desde que escribí este capítulo en 2021, Buf Build (https://buf.build/) se ha convertido en la compañía de todo lo relacionado con protobuf. Sus herramientas son fáciles de usar, gratuitas y de código abierto, y aparecieron en el momento adecuado para impulsar algunas iniciativas en la comunidad de Spark. El proyecto Apache Spark introdujo soporte nativo completo para Protocol Buffers en Spark 3.4 para admitir spark-connect, y está utilizando Buf para compilar servicios GRPC y mensajes. Después de todo, Spark Connect es un conector nativo GRPC para incrustar aplicaciones de Spark fuera de la JVM.

Una aplicación tradicional de Apache Spark debe ejecutarse como una aplicación de controlador en algún lugar, y en el pasado esto significaba usar pyspark o Spark nativo, que en ambos casos todavía se ejecutan en un proceso de JVM.

Estructura de directorios a través de Spark Connect. Muestra las definiciones de protobuf, junto con buf.gen.yaml y buf.work.yaml que ayudan con la generación de código.

Al final del día, Buf Build ofrece tranquilidad en el proceso de compilación. Para generar el código, simplemente se debe ejecutar un comando sencillo: buf generate. Para una comprobación de formato y formato consistente, buf lint && buf format -w. Sin embargo, la joya de la corona es la detección de cambios que rompen la compatibilidad. buf breaking --against .git#branch=origin/main es todo lo que se necesita para asegurarse de que los nuevos cambios en las definiciones de mensajes no afecten negativamente a nada que se esté ejecutando actualmente en producción. *En el futuro, escribiré un artículo sobre cómo utilizar buf para la analítica empresarial, pero por ahora, es hora de concluir este capítulo.

Entonces, ¿en qué estábamos? Ahora sabe que hay beneficios al utilizar avro o protobuf cuando se trata de su estrategia de responsabilidad de datos a largo plazo. Al utilizar estos formatos de datos estructurados, independientes del lenguaje y basados en filas, se reduce el problema de la dependencia del lenguaje a largo plazo, dejando abiertas las puertas a cualquier lenguaje de programación en el futuro. Porque honestamente, puede ser una tarea ingrata brindar soporte a bibliotecas y bases de código heredadas. Además, los formatos serializados ayudan a reducir los costos y la congestión del ancho de banda de la red asociados con el envío y recepción de grandes cantidades de datos. Esto también ayuda a reducir los costos generales de almacenamiento para conservar sus datos a largo plazo.

Por último, veamos cómo estos protocolos de datos estructurados permiten eficiencias adicionales cuando se trata de enviar y recibir datos a través de llamadas a procedimientos remotos.

Llamadas a Procedimientos Remotos

Los frameworks de RPC, en pocas palabras, permiten a las aplicaciones cliente llamar de manera transparente a métodos (procedimientos) remotos (en el lado del servidor) a través de llamadas de función local, pasando mensajes serializados de ida y vuelta. Las implementaciones cliente y servidor utilizan la misma definición de interfaz pública para definir los métodos y servicios RPC funcionales disponibles. El lenguaje de definición de interfaz (IDL) define el protocolo y las definiciones de mensajes y actúa como un contrato entre el cliente y el servidor. Veamos esto en acción observando el popular framework de RPC de código abierto gRPC.

gRPC

Conceptualizado y creado por primera vez en Google, gRPC, que significa “llamada de procedimiento remoto” genérica, es un sólido framework de código abierto utilizado para servicios de alto rendimiento que van desde la coordinación de bases de datos distribuidas, como se ve en CockroachDB, hasta la analítica en tiempo real, como se ve en Microsoft Azure Video Analytics.

Figura 1–2. El RPC (en este ejemplo gRPC) funciona pasando mensajes serializados hacia y desde un cliente y un servidor. El cliente implementa la misma interfaz del lenguaje de definición de interfaz (IDL) y esto actúa como un contrato de API entre el cliente y el servidor. (Crédito de la foto: https://grpc.io/docs/what-is-grpc/introduction/)

El diagrama mostrado en la Figura 9–3 muestra un ejemplo de cómo funciona gRPC. El código del lado del servidor está escrito en C++ para mayor velocidad, mientras que los clientes escritos en ruby y java pueden interoperar con el servicio utilizando mensajes protobuf como medio de comunicación.

Utilizando protocol buffers para definiciones de mensajes, serialización, así como la declaración y definición de servicios, gRPC puede simplificar la forma en que capturas datos y construyes servicios. Por ejemplo, digamos que queremos continuar con el ejercicio de crear una API de seguimiento para pedidos de café de clientes. El contrato de la API podría definirse en un archivo de servicios simple, y a partir de ahí se podría construir la implementación del lado del servidor y cualquier número de implementaciones del lado del cliente utilizando la misma definición de servicio y tipos de mensajes.

Definir un servicio gRPC

Puedes definir una interfaz de servicio, los objetos de solicitud y respuesta, así como los tipos de mensajes que deben pasarse entre el cliente y el servidor de manera tan fácil como 1–2–3.

syntax = "proto3";service CustomerService {    rpc TrackOrder (Order) returns (Response) {}    rpc TrackOrderStatus (OrderStatusTracker) returns (Response) {}}message Order {    uint64 timestamp    = 1;    string orderId      = 2;        string userId       = 3;    Status status       = 4;}enum Status {  unknown_status = 0;  initalized     = 1;  started        = 2;  progress       = 3;  completed      = 4;  failed         = 5;  canceled       = 6;}message OrderStatusTracker {  uint64 timestamp = 1;  Status status    = 2;  string orderId   = 3;}message Response {    uint32 statusCode = 1;    string message    = 2;}

Con la adición de gRPC, puede ser mucho más fácil implementar y mantener tanto el código del lado del servidor como del lado del cliente utilizado en su infraestructura de datos. Dado que protobuf admite compatibilidad hacia atrás y hacia adelante, esto significa que los clientes gRPC más antiguos aún pueden enviar mensajes válidos a los servicios gRPC más nuevos sin encontrar problemas y puntos de dolor comunes (discutidos anteriormente en “Problemas de datos en vuelo”).

gRPC habla HTTP/2

Como beneficio adicional, con respecto a las pilas de servicios modernas, gRPC puede utilizar HTTP/2 para su capa de transporte. Esto también significa que puede aprovechar las mallas de datos modernas (como Envoy) para el soporte de proxy, enrutamiento y autenticación a nivel de servicio, al tiempo que reduce los problemas de congestión de paquetes TCP que se ven con el HTTP estándar sobre TCP.

Mitigar los problemas de datos en vuelo y lograr el éxito en cuanto a la responsabilidad de los datos comienza con los datos y se expande desde ese punto central. Establecer procesos para controlar cómo los datos pueden ingresar a su red de datos debe considerarse un requisito previo antes de sumergirse en el torrente de datos en continuo movimiento.

Resumen

El objetivo de esta publicación es presentar las partes móviles, conceptos e información de fondo necesaria para armarnos antes de saltar a ciegas desde una mentalidad más tradicional (estacionaria) basada en lotes a una que comprenda los riesgos y recompensas de trabajar con datos de transmisión en tiempo real.

Aprovechar los datos en tiempo real puede conducir a ideas rápidas y acciones concretas, y abrir las puertas al aprendizaje automático y la inteligencia artificial de vanguardia.

Sin embargo, la gestión distribuida de datos también puede convertirse en una crisis de datos si no se tienen en cuenta los pasos correctos de antemano. Recuerde que sin una base de datos sólida y sólida, construida sobre datos válidos (confiables), el camino hacia el tiempo real no será un esfuerzo simple, sino que tendrá su parte justa de obstáculos y desvíos en el camino.

Espero que hayas disfrutado de la segunda mitad del Capítulo 9. Para leer la primera parte de esta serie, dirígete a Una introducción suave al procesamiento analítico de transmisión.

Una introducción suave al procesamiento analítico de transmisión

Construyendo un modelo mental para ingenieros y cualquier persona en el medio

towardsdatascience.com

Si quieres profundizar aún más, consulta mi libro o apóyame con un alto cinco.

Ingeniería de datos moderna con Apache Spark: una guía práctica para construir transmisiones críticas para la misión…

Amazon.com: Ingeniería de datos moderna con Apache Spark: una guía práctica para construir transmisiones críticas para la misión…

www.amazon.com

Si tienes acceso a O’Reilly Media, puedes leer el libro completamente gratis (bueno para ti, no tan bueno para mí), pero por favor encuentra el libro gratis en algún lugar si tienes la oportunidad, o consigue un libro electrónico para ahorrar en costos de envío (o necesidad de encontrar un lugar para un libro de más de 600 páginas).

Ingeniería de datos moderna con Apache Spark: una guía práctica para construir transmisiones críticas para la misión…

Aprovecha Apache Spark dentro de un ecosistema moderno de ingeniería de datos. Esta guía práctica te enseñará cómo escribir completamente…

learning.oreilly.com