Capítulo 1. Streaming 101

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

En la actualidad, el procesamiento de datos en streaming es un tema importante en big data, y por buenas razones; entre ellas, :

  • Las empresas ansían obtener información cada vez más puntual sobre sus datos, y pasar a la transmisión en tiempo real es una buena forma de conseguir una latencia menor

  • Los conjuntos de datos masivos e ilimitados, cada vez más habituales en la empresa moderna, se controlan más fácilmente con un sistema diseñado para esos volúmenes interminables de datos.

  • Procesar los datos a medida que llegan distribuye las cargas de trabajo de forma más uniforme a lo largo del tiempo, lo que permite un consumo de recursos más coherente y predecible.

A pesar de esta oleada de interés por el streaming impulsada por las empresas, los sistemas de streaming han permanecido durante mucho tiempo relativamente inmaduros en comparación con sus hermanos por lotes. Sólo recientemente la marea ha cambiado de forma concluyente en la otra dirección. En mis momentos más chocarreros, espero que esto se deba en parte a la sólida dosis de persuasión que ofrecí originalmente en mis entradas de blog "Streaming 101" y "Streaming 102" (en las que obviamente se basan los primeros capítulos de este libro). Pero en realidad, también hay mucho interés en la industria por ver madurar los sistemas de streaming y mucha gente inteligente y activa que disfruta construyéndolos.

Aunque la batalla por la defensa general del streaming ha sido, en mi opinión, efectivamente ganada, voy a seguir presentando mis argumentos originales de "Streaming 101" más o menos inalterados. Por un lado, siguen siendo muy aplicables hoy en día, aunque gran parte de la industria haya empezado a prestar atención al grito de guerra. Y en segundo lugar, hay mucha gente ahí fuera que todavía no se ha enterado; este libro es un intento ampliado de transmitir estos puntos.

Para empezar, cubro algunos antecedentes importantes que ayudarán a enmarcar el resto de los temas que quiero tratar. Lo hago en tres secciones específicas:

Terminología

Hablar con precisión sobre temas complejos requiere definiciones precisas de los términos. Para algunos términos que tienen interpretaciones sobrecargadas en el uso actual, intentaré precisar lo que quiero decir exactamente cuando los digo.

Capacidades

Comento las deficiencias a menudo percibidas de los sistemas de streaming. También propongo la mentalidad que creo que deben adoptar los creadores de sistemas de procesamiento de datos para satisfacer las necesidades de los consumidores de datos modernos en el futuro.

Dominios temporales

Presento los dos dominios principales del tiempo que son relevantes en el procesamiento de datos, muestro cómo se relacionan y señalo algunas de las dificultades que imponen estos dos dominios.

Terminología: ¿Qué es el streaming?

Antes de seguir adelante, me gustaría aclarar una cosa en : ¿qué es la transmisión en flujo continuo? El término streaming se utiliza hoy en día para referirse a una gran variedad de cosas diferentes (y para simplificar, hasta ahora lo he utilizado de forma un tanto imprecisa), lo que puede dar lugar a malentendidos sobre lo que es realmente el streaming o de lo que son capaces los sistemas de streaming. Por ello, prefiero definir el término con cierta precisión.

El quid del problema es que muchas cosas que deberían describirse por lo que son (procesamiento ilimitado de datos, resultados aproximados, etc.), han pasado a describirse coloquialmente por cómo se han logrado históricamente (es decir, mediante motores de ejecución de streaming). Esta falta de precisión en la terminología enturbia lo que realmente significa el streaming, y en algunos casos carga a los propios sistemas de streaming con la implicación de que sus capacidades se limitan a las características descritas históricamente como "streaming", como los resultados aproximados o especulativos.

Dado que los sistemas de streaming bien diseñados son tan capaces (técnicamente más) de producir resultados correctos, coherentes y repetibles como cualquier motor batch existente, prefiero aislar el término "streaming" a un significado muy específico:

Sistema de streaming

Un tipo de motor de procesamiento de datos diseñado pensando en conjuntos de datos infinitos.1

Si quiero hablar de resultados de baja latencia, aproximados o especulativos, utilizo esas palabras específicas en lugar de llamarlas imprecisamente "streaming".

Los términos precisos también son útiles cuando se habla de los distintos tipos de datos que uno puede encontrar. Desde mi punto de vista, hay dos dimensiones importantes (y ortogonales) que definen la forma de un conjunto de datos determinado: la cardinalidad y la constitución.

La cardinalidad de un conjunto de datos dicta su tamaño, y el aspecto más destacado de la cardinalidad es si un conjunto de datos determinado es finito o infinito. He aquí los dos términos que prefiero utilizar para describir la cardinalidad gruesa de un conjunto de datos:

Datos limitados

Un tipo de conjunto de datos que tiene un tamaño finito.

Datos no limitados

Un tipo de conjunto de datos de tamaño infinito (al menos teóricamente).

La cardinalidad es importante porque la naturaleza ilimitada de los conjuntos de datos infinitos impone cargas adicionales a los marcos de procesamiento de datos que los consumen. Más sobre esto en la siguiente sección.

La constitución de un conjunto de datos, por otra parte, dicta su manifestación física. En consecuencia, la constitución define las formas en que se puede interactuar con los datos en cuestión. No llegaremos a examinar en profundidad las constituciones hasta el Capítulo 6, pero para que te hagas una idea, hay dos constituciones principales de importancia:

Tabla

Una visión holística de un conjunto de datos en un momento determinado. Los sistemas SQL han tratado tradicionalmente en tablas.

Flujo2

Una visión elemento por elemento de la evolución de un conjunto de datos a lo largo del tiempo. El linaje MapReduce de los sistemas de procesamiento de datos han tratado tradicionalmente en secuencias.

En los Capítulos 6, 8 y 9 profundizamos bastante en la relación entre flujos y tablas, y en el Capítulo 8 también aprendemos sobre el concepto unificador subyacente de las relaciones variables en el tiempo que los vincula. Pero hasta entonces, nos ocuparemos principalmente de los flujos porque ésa es la constitución con la que interactúan directamente los desarrolladores de canalizaciones en la mayoría de los sistemas de procesamiento de datos actuales (tanto por lotes como por flujos). También es la constitución que encarna de forma más natural los retos exclusivos del procesamiento de flujos.

Sobre las limitaciones muy exageradas del streaming

En este sentido, vamos a hablar un poco de lo que pueden y no pueden hacer los sistemas de streaming, haciendo hincapié en lo de "pueden". Una de las cosas más importantes que quiero transmitir en este capítulo es lo capaz que puede ser un sistema de streaming bien diseñado. Los sistemas de flujo han estado históricamente relegados a un nicho de mercado en el que proporcionaban resultados de baja latencia, imprecisos o especulativos, a menudo junto con un sistema por lotes más capaz para proporcionar finalmente resultados correctos; en otras palabras, la Arquitectura Lambda.

Para los que aún no estéis familiarizados con la Arquitectura Lambda, la idea básica es que ejecutes un sistema de flujo junto con un sistema por lotes, ambos realizando esencialmente el mismo cálculo. El sistema de streaming te proporciona resultados imprecisos y de baja latencia (ya sea por el uso de un algoritmo de aproximación o porque el propio sistema de streaming no proporciona corrección), y algún tiempo después aparece un sistema por lotes que te proporciona un resultado correcto. Propuesto originalmente por Nathan Marz de Twitter (creador de Storm), acabó teniendo bastante éxito porque era, de hecho, una idea fantástica para la época; los motores de flujo eran un poco decepcionantes en el departamento de corrección, y los motores por lotes eran tan intrínsecamente difíciles de manejar como cabría esperar, así que Lambda te dio una forma de tener tu proverbial pastel y comértelo también. Por desgracia, mantener un sistema Lambda es un engorro: tienes que construir, aprovisionar y mantener dos versiones independientes de tu canalización y, además, fusionar de algún modo los resultados de las dos canalizaciones al final.

Como alguien que pasó años trabajando en un motor de streaming fuertemente consistente, también encontré todo el principio de la Arquitectura Lambda un poco desagradable. Como era de esperar, fui un gran fan del post de Jay Kreps "Cuestionando la Arquitectura Lambda" cuando se publicó. Fue una de las primeras declaraciones muy visibles contra la necesidad de la ejecución en modo dual. Encantador. Kreps abordó la cuestión de la repetibilidad en el contexto del uso de un sistema reproducible como Kafka como interconexión de flujo, y llegó a proponer la Arquitectura Kappa, que básicamente significa ejecutar una única canalización utilizando un sistema bien diseñado que esté construido adecuadamente para el trabajo en cuestión. No estoy convencido de que esa noción requiera su propio nombre con letra griega, pero apoyo plenamente la idea en principio.

Sinceramente, yo iría un paso más allá. Yo diría que los sistemas de streaming bien diseñados proporcionan en realidad un superconjunto estricto de la funcionalidad por lotes. Salvo quizás un delta de eficiencia, no deberían ser necesarios los sistemas por lotes tal y como existen hoy en día. Y felicito a la gente de Apache Flink por tomarse a pecho esta idea y construir un sistema que es todo flujo, todo el tiempo, incluso en modo "por lotes"; me encanta.

El corolario de todo esto es que la amplia maduración de los sistemas de streaming, combinada con marcos robustos para el procesamiento de datos sin límites, permitirá con el tiempo relegar la Arquitectura Lambda a la antigüedad de la historia de los grandes datos, donde pertenece. Creo que ha llegado el momento de hacer esto realidad. Porque para hacerlo -es decir, para vencer a batch en su propio juego- en realidad sólo necesitas dos cosas:

Corrección

Esto te proporciona paridad con el lote. En el fondo, la corrección se reduce a un almacenamiento consistente. Los sistemas de flujo necesitan un método para comprobar el estado persistente a lo largo del tiempo (algo de lo que Kreps ha hablado en su post "Por qué el estado local es una primitiva fundamental en el procesamiento de flujos" ), y debe estar lo suficientemente bien diseñado como para seguir siendo coherente a la luz de los fallos de la máquina. Cuando Spark Streaming apareció por primera vez en la escena pública de big data hace unos años, era un faro de consistencia en un mundo de streaming por lo demás oscuro. Afortunadamente, las cosas han mejorado sustancialmente desde entonces, pero llama la atención cuántos sistemas de streaming siguen intentando arreglárselas sin una consistencia sólida.

Para reiterar, porque este punto es importante: se requiere una fuerte coherencia para el procesamiento exactamente una vez,3 que es necesario para la corrección, que es un requisito para cualquier sistema que vaya a tener la oportunidad de alcanzar o superar las capacidades de los sistemas por lotes. A menos que realmente no te importen tus resultados, te imploro que evites cualquier sistema de flujo que no proporcione un estado fuertemente consistente. Los sistemas por lotes no requieren que verifiques de antemano si son capaces de producir respuestas correctas; no pierdas el tiempo con sistemas de streaming que no puedan cumplir ese mismo requisito.

Si tienes curiosidad por saber más sobre lo que se necesita para conseguir una consistencia sólida en un sistema de streaming, te recomiendo que consultes los documentos sobre MillWheel, Spark Streaming y Flink snapshotting. Los tres dedican a una cantidad significativa de tiempo a hablar de la consistencia. Reuven se sumergirá en las garantías de consistencia en el Capítulo 5, y si aún te quedas con ganas de más, hay una gran cantidad de información de calidad sobre este tema en la bibliografía y en otros lugares.

Herramientas para razonar sobre el tiempo

Esto te lleva más allá de los lotes. Unas buenas herramientas para razonar sobre el tiempo son esenciales para tratar con datos ilimitados y desordenados, con una asimetría evento-tiempo variable. Un número cada vez mayor de conjuntos de datos modernos presentan estas características, y los sistemas por lotes existentes (así como muchos sistemas de flujo) carecen de las herramientas necesarias para hacer frente a las dificultades que imponen (aunque esto está cambiando rápidamente, incluso mientras escribo esto). Dedicaremos la mayor parte de este libro a explicar y centrarnos en diversas facetas de este punto.

Para empezar, obtendremos una comprensión básica del importante concepto de dominios temporales, tras lo cual profundizaremos en lo que quiero decir con datos no limitados y desordenados de diversa inclinación evento-tiempo. A continuación, dedicaremos el resto de este capítulo a examinar los enfoques habituales del procesamiento de datos acotados y no acotados, utilizando tanto sistemas por lotes como de flujo continuo.

Tiempo del suceso frente a tiempo de procesamiento

Hablar de forma convincente sobre el procesamiento de datos sin límites requiere una comprensión clara de los dominios de tiempo implicados. Dentro de cualquier sistema de procesamiento de datos, suele haber dos dominios de tiempo que nos interesan:

Hora del acontecimiento

Es el momento en que ocurrieron realmente los hechos.

Tiempo de procesamiento

Es el momento en que se observan los acontecimientos en el sistema.

No todos los casos de uso se preocupan por los tiempos de los eventos (y si el tuyo no lo hace, ¡hurra! tu vida es más fácil), pero muchos sí. Algunos ejemplos son la caracterización del comportamiento del usuario a lo largo del tiempo, la mayoría de las aplicaciones de facturación y muchos tipos de detección de anomalías, por nombrar algunos.

En un mundo ideal, el tiempo de evento y el tiempo de procesamiento serían siempre iguales, y los eventos se procesarían inmediatamente en cuanto ocurrieran. Sin embargo, la realidad no es tan amable, y la desviación entre el tiempo de evento y el tiempo de procesamiento no sólo no es cero, sino que a menudo es una función muy variable de las características de las fuentes de entrada subyacentes, el motor de ejecución y el hardware. Entre las cosas que pueden afectar al nivel de desviación se incluyen las siguientes:

  • Limitaciones de recursos compartidos, como congestión de red, particiones de red o CPU compartida en un entorno no dedicado

  • Causas del software, como la lógica del sistema distribuido, la contención, etc.

  • Características de los propios datos, como la distribución de claves, la varianza en el rendimiento o la varianza en el desorden (por ejemplo, un avión lleno de personas que sacan sus teléfonos del modo avión después de haberlos utilizado sin conexión durante todo el vuelo).

Como resultado, si trazas el progreso del tiempo de evento y el tiempo de procesamiento en cualquier sistema del mundo real, normalmente acabas con algo que se parece un poco a la línea roja de la Figura 1-1.

Figura 1-1. Mapeado del dominio temporal. El eje x representa la completitud del tiempo de evento en el sistema; es decir, el tiempo X en el tiempo de evento hasta el que se han observado todos los datos con tiempos de evento inferiores a X. El eje y4 representa el progreso del tiempo de procesamiento; es decir, el tiempo de reloj normal observado por el sistema de procesamiento de datos a medida que se ejecuta.

En la Figura 1-1, la línea negra discontinua con pendiente 1 representa el ideal, en el que el tiempo de procesamiento y el tiempo de evento son exactamente iguales; la línea roja representa la realidad. En este ejemplo, el sistema se retrasa un poco al principio del tiempo de procesamiento, se acerca al ideal en el medio y vuelve a retrasarse un poco al final. A primera vista, hay dos tipos de desviación visibles en este diagrama, cada uno en dominios temporales diferentes:

Tiempo de procesamiento

La distancia vertical entre la línea ideal y la roja es el retraso en el dominio del tiempo de procesamiento. Esa distancia te indica cuánto retraso se observa (en tiempo de procesamiento) entre el momento en que se produjeron los acontecimientos de un tiempo determinado y el momento en que se procesaron. Éste es quizás el más natural e intuitivo de los dos desfases.

Hora del acontecimiento

La distancia horizontal entre el ideal y la línea roja es la cantidad de desviación del tiempo de evento en la canalización en ese momento. Te indica lo retrasada que está actualmente la tubería con respecto al ideal (en tiempo de evento).

En realidad, el retardo en el tiempo de procesamiento y la desviación en el tiempo de los eventos en un momento dado son idénticos; son sólo dos formas de ver la misma cosa.5 Lo importante en relación con el desfase/desviación es lo siguiente: Dado que el mapeo global entre el tiempo de evento y el tiempo de procesamiento no es estático (es decir, el desfase/desviación puede variar arbitrariamente con el tiempo), esto significa que no puedes analizar tus datos únicamente en el contexto del momento en que son observados por tu canalización si te importan sus tiempos de evento (es decir, cuándo ocurrieron realmente los eventos). Por desgracia, así es como han funcionado históricamente muchos sistemas diseñados para datos ilimitados. Para hacer frente a la naturaleza infinita de los conjuntos de datos ilimitados, estos sistemas suelen proporcionar algún tipo de ventana para los datos entrantes. Más adelante hablaremos en profundidad de la ventana, pero básicamente significa dividir un conjunto de datos en partes finitas a lo largo de límites temporales. Si te preocupa la corrección y te interesa analizar tus datos en el contexto de sus tiempos de evento, no puedes definir esos límites temporales utilizando el tiempo de procesamiento (es decir, la ventana de tiempo de procesamiento), como hacen muchos sistemas; sin una correlación coherente entre el tiempo de procesamiento y el tiempo de evento, algunos de tus datos de tiempo de evento acabarán en ventanas de tiempo de procesamiento incorrectas (debido al retraso inherente a los sistemas distribuidos, la naturaleza online/offline de muchos tipos de fuentes de entrada, etc.), tirando la corrección por la ventana, por así decirlo. Examinamos este problema con más detalle en varios ejemplos de las secciones siguientes, así como en el resto del libro.

Por desgracia, el panorama tampoco es precisamente halagüeño cuando se establecen ventanas por tiempo de evento. En el contexto de los datos no limitados, el desorden y la asimetría de las variables inducen un problema de integridad para las ventanas por tiempo de evento: a falta de una correspondencia predecible entre el tiempo de procesamiento y el tiempo de evento, ¿cómo puedes determinar cuándo has observado todos los datos para un tiempo de evento X dado? Para muchas fuentes de datos del mundo real, sencillamente no puedes. Pero la inmensa mayoría de los sistemas de procesamiento de datos que se utilizan hoy en día se basan en alguna noción de exhaustividad, lo que los sitúa en una grave desventaja cuando se aplican a conjuntos de datos ilimitados.

Propongo que, en lugar de intentar agrupar datos ilimitados en lotes finitos de información que acaben siendo completos, deberíamos diseñar herramientas que nos permitan vivir en el mundo de incertidumbre que imponen estos complejos conjuntos de datos. Llegarán nuevos datos, los antiguos pueden retractarse o actualizarse, y cualquier sistema que construyamos debe ser capaz de hacer frente a estos hechos por sí mismo, siendo las nociones de exhaustividad una optimización conveniente para casos de uso específicos y apropiados, más que una necesidad semántica en todos ellos.

Antes de entrar en detalles sobre cómo podría ser un enfoque de este tipo, acabemos con otro dato útil: los patrones comunes de procesamiento de datos.

Patrones de procesamiento de datos

Llegados a este punto, tenemos suficientes antecedentes establecidos para que podamos empezar a examinar los tipos básicos de patrones de uso comunes en el procesamiento de datos acotado y no acotado de hoy en día. Examinaremos ambos tipos de procesamiento y, cuando proceda, en el contexto de los dos tipos principales de motores que nos interesan (batch y streaming, donde en este contexto, básicamente estoy agrupando el microbatch con el streaming porque las diferencias entre ambos no son terriblemente importantes a este nivel).

Datos limitados

Procesar datos acotados es conceptualmente bastante sencillo, y probablemente familiar para todos. En la Figura 1-2, empezamos a la izquierda con un conjunto de datos lleno de entropía. Lo ejecutamos a través de algún motor de procesamiento de datos (normalmente por lotes, aunque un motor de flujo bien diseñado funcionaría igual de bien), como MapReduce, y a la derecha acabamos con un nuevo conjunto de datos estructurado con mayor valor inherente.

Figura 1-2. Procesamiento de datos limitado con un motor de procesamiento por lotes clásico. Un conjunto finito de datos no estructurados a la izquierda se ejecuta a través de un motor de procesamiento de datos, dando como resultado los correspondientes datos estructurados a la derecha.

Aunque, por supuesto, existen infinitas variaciones sobre lo que realmente puedes calcular como parte de este esquema, el modelo general es bastante sencillo. Mucho más interesante es la tarea de procesar un conjunto de datos no acotados. Veamos ahora las distintas formas en que se suelen procesar los datos no limitados, empezando por los enfoques utilizados con los motores por lotes tradicionales y terminando con los enfoques que puedes adoptar con un sistema diseñado para datos no limitados, como la mayoría de los motores de flujo o microlotes.

Datos no limitados: Lote

Los motores por lotes, aunque no se diseñaron explícitamente pensando en los datos no limitados, se han utilizado para procesar conjuntos de datos no limitados desde que se concibieron los sistemas por lotes. Como cabría esperar, estos enfoques giran en torno a la fragmentación de los datos no limitados en una colección de conjuntos de datos limitados apropiados para el procesamiento por lotes.

Ventanas fijas

La forma más habitual de procesar un conjunto de datos no acotado mediante ejecuciones repetidas de un motor por lotes es mediante la ventana de los datos de entrada en ventanas de tamaño fijo y, a continuación, procesando cada una de esas ventanas como una fuente de datos separada y acotada (a veces también llamadas ventanas de volteo), como en la Figura 1-3. Especialmente en el caso de fuentes de entrada como los registros, cuyos eventos pueden escribirse en jerarquías de directorios y archivos cuyos nombres codifican la ventana a la que corresponden, este tipo de cosas parecen bastante sencillas a primera vista, porque básicamente has realizado la mezcla basada en el tiempo para obtener los datos en las ventanas evento-tiempo apropiadas con antelación.

En realidad, sin embargo, la mayoría de los sistemas siguen teniendo un problema de integridad con el que lidiar (¿Qué pasa si algunos de tus eventos se retrasan en su camino a los registros debido a una partición de la red? ¿Y si tus eventos se recogen globalmente y deben transferirse a una ubicación común antes de procesarlos? ¿Y si tus eventos proceden de dispositivos móviles?), lo que significa que puede ser necesario algún tipo de mitigación (por ejemplo, retrasar el procesamiento hasta que estés seguro de que se han recogido todos los eventos o reprocesar todo el lote para una ventana determinada siempre que los datos lleguen con retraso).

Figura 1-3. Procesamiento de datos no acotados mediante ventanas fijas ad hoc con un motor por lotes clásico. Un conjunto de datos no delimitado se recoge por adelantado en ventanas finitas de tamaño fijo de datos delimitados, que luego se procesan mediante ejecuciones sucesivas de un motor por lotes clásico.

Sesiones

Este enfoque se rompe aún más cuando intentas utilizar un motor por lotes para procesar datos no limitados en estrategias de ventanas más sofisticadas, como las sesiones. Las sesiones suelen definirse en como periodos de actividad (por ejemplo, para un usuario concreto) terminados por un intervalo de inactividad. Al calcular sesiones utilizando un motor de lotes típico, a menudo acabas con sesiones que se dividen en lotes, como indican las marcas rojas de la Figura 1-4. Podemos reducir el número de divisiones aumentando el tamaño de los lotes, pero a costa de aumentar la latencia. Otra opción es añadir lógica adicional para unir sesiones de ejecuciones anteriores, pero a costa de una mayor complejidad.

Figura 1-4. Procesamiento de datos no limitados en sesiones mediante ventanas fijas ad hoc con un motor por lotes clásico. Un conjunto de datos no delimitado se recoge por adelantado en ventanas finitas de tamaño fijo de datos delimitados, que luego se subdividen en ventanas de sesión dinámicas mediante ejecuciones sucesivas de un motor por lotes clásico.

En cualquier caso, utilizar un motor por lotes clásico para calcular sesiones no es lo ideal. Una forma más agradable sería acumular sesiones de forma continua, lo que veremos más adelante.

Datos sin límites: Streaming

A diferencia de la naturaleza ad hoc de la mayoría de los enfoques de procesamiento de datos no limitados basados en lotes, los sistemas de flujo se construyen para datos no limitados. Como hemos dicho antes, para muchas fuentes de entrada distribuidas del mundo real, no sólo te encuentras con datos no limitados, sino también con datos como los siguientes:

  • Altamente desordenados con respecto a los tiempos de los eventos, lo que significa que necesitas algún tipo de barajado basado en el tiempo en tu canalización si quieres analizar los datos en el contexto en el que se produjeron.

  • De desviación variable del tiempo de los acontecimientos, lo que significa que no puedes dar por supuesto que siempre verás la mayoría de los datos de un determinado tiempo de acontecimiento X dentro de un épsilon constante del tiempo Y.

Hay un puñado de enfoques que puedes adoptar cuando tratas con datos que tienen estas características. Generalmente clasifico estos enfoques en cuatro grupos: agnóstico temporal, aproximación, ventana por tiempo de procesamiento y ventana por tiempo de evento.

Dediquemos ahora un poco de tiempo a examinar cada uno de estos enfoques.

Agnóstico en el tiempo

El procesamiento agnóstico del tiempo se utiliza para casos en los que el tiempo es esencialmente irrelevante; es decir, toda la lógica relevante está impulsada por los datos. Como todo en estos casos de uso viene dictado por la llegada de más datos, en realidad no hay nada especial que un motor de streaming tenga que soportar, aparte de la entrega básica de datos. Como resultado, prácticamente todos los sistemas de flujo existentes admiten casos de uso agnósticos en el tiempo desde el primer momento (modulando las variaciones de un sistema a otro en las garantías de coherencia, por supuesto, si te preocupa la corrección). Los sistemas por lotes también son adecuados para el procesamiento independiente del tiempo de fuentes de datos ilimitadas, simplemente dividiendo la fuente ilimitada en una secuencia arbitraria de conjuntos de datos delimitados y procesando esos conjuntos de datos de forma independiente. En esta sección veremos un par de ejemplos concretos, pero dada la sencillez del procesamiento independiente del tiempo (al menos desde una perspectiva temporal), no dedicaremos mucho más tiempo a ello.

Filtrado

Una forma muy básica de procesamiento agnóstico del tiempo es el filtrado, cuyo ejemplo se muestra en la Figura 1-5. Imagina que estás procesando registros de tráfico web y quieres filtrar todo el tráfico que no proceda de un dominio concreto. Mirarías cada registro a medida que llegara, verías si pertenece al dominio de interés y lo descartarías en caso contrario. Como este tipo de cosas sólo dependen de un único elemento en cada momento, el hecho de que la fuente de datos sea ilimitada, desordenada y con una inclinación variable en el tiempo de los eventos es irrelevante.

Figura 1-5. Filtrado de datos no limitados. Una colección de datos (que fluyen de izquierda a derecha) de distintos tipos se filtra en una colección homogénea que contiene un solo tipo.

Juntas interiores

Otro ejemplo agnóstico del tiempo es una unión interna, diagramada en la Figura 1-6. Al unir dos fuentes de datos no limitadas, si sólo te interesan los resultados de una unión cuando llega un elemento de ambas fuentes, no hay ningún elemento temporal en la lógica. Al ver un valor de una fuente, puedes simplemente almacenarlo en búfer en estado persistente; sólo después de que llegue el segundo valor de la otra fuente necesitas emitir el registro unido. (En realidad, probablemente querrías algún tipo de política de recogida de basura para las uniones parciales no emitidas, que probablemente se basaría en el tiempo. Pero para un caso de uso con pocas o ninguna unión no completada, tal cosa podría no ser un problema).

Figura 1-6. Realización de una unión interna en datos no limitados. Las uniones se producen cuando se observan elementos coincidentes de ambas fuentes.

Cambiar la semántica a algún tipo de unión externa introduce el problema de integridad de los datos del que hemos hablado: después de haber visto un lado de la unión, ¿cómo sabes si el otro lado va a llegar o no? A decir verdad, no lo sabes, así que necesitas introducir alguna noción de tiempo de espera, que introduce un elemento de tiempo. Ese elemento de tiempo es esencialmente una forma de ventana, que examinaremos más detenidamente dentro de un momento.

Algoritmos de aproximación

La segunda gran categoría de enfoques de son los algoritmos de aproximación, como Top-N aproximado, k-means en flujo, etc. Toman una fuente ilimitada de datos de entrada y proporcionan unos datos de salida que, si entrecierras los ojos, se parecen más o menos a lo que esperabas obtener, como en la Figura 1-7. La ventaja de los algoritmos de aproximación es que, por su diseño, tienen poca carga y están pensados para datos ilimitados. Las desventajas son que existe un conjunto limitado de ellos, los propios algoritmos suelen ser complicados (lo que dificulta la creación de otros nuevos) y su naturaleza aproximada limita su utilidad.

Figura 1-7. Cálculo de aproximaciones sobre datos no limitados. Los datos se ejecutan a través de un algoritmo complejo, produciendo datos de salida que se parecen más o menos al resultado deseado en el otro lado.

Cabe señalar que estos algoritmos suelen tener algún elemento de tiempo en su diseño (por ejemplo, algún tipo de decaimiento incorporado). Y como procesan los elementos a medida que llegan, ese elemento temporal suele basarse en el tiempo de procesamiento. Esto es especialmente importante para los algoritmos que proporcionan algún tipo de límites de error demostrables en sus aproximaciones. Si esos límites de error se basan en que los datos lleguen en orden, no significan prácticamente nada cuando alimentas al algoritmo con datos desordenados con una desviación variable del tiempo de los eventos. Algo a tener en cuenta.

Los algoritmos de aproximación en sí son un tema fascinante, pero como son esencialmente otro ejemplo de procesamiento agnóstico del tiempo (módulo de las características temporales de los propios algoritmos), son bastante sencillos de utilizar y, por tanto, no merece la pena prestarles más atención, dado nuestro enfoque actual.

Ventana

Los dos enfoques restantes para el tratamiento de datos no limitados son variaciones de las ventanas. Antes de entrar en las diferencias entre ellas, debo aclarar qué quiero decir exactamente con ventana, ya que sólo la hemos tratado brevemente en la sección anterior. La ventana es simplemente la noción de tomar una fuente de datos (ya sea ilimitada o acotada) y dividirla a lo largo de los límites temporales en trozos finitos para su procesamiento. La Figura 1-8 muestra tres modelos diferentes de ventanas.

Figura 1-8. Estrategias de ventanas. Cada ejemplo se muestra para tres claves diferentes, destacando la diferencia entre ventanas alineadas (que se aplican a todos los datos) y ventanas no alineadas (que se aplican a un subconjunto de los datos).

Veamos más detenidamente cada estrategia:

Ventanas fijas (también conocidas como ventanas giratorias)

Ya hemos hablado antes de las ventanas fijas. Las ventanas fijas dividen el tiempo en segmentos con una longitud temporal de tamaño fijo. Normalmente (como se muestra en la Figura 1-9), los segmentos de las ventanas fijas se aplican uniformemente a todo el conjunto de datos, lo que constituye un ejemplo de ventanas alineadas. En algunos casos, es deseable desfasar las ventanas para diferentes subconjuntos de los datos (por ejemplo, por clave) para repartir la carga de finalización de la ventana más uniformemente a lo largo del tiempo, lo que en cambio es un ejemplo de ventanas no alineadas porque varían a lo largo de los datos.6

Ventanas correderas (también conocidas como ventanas saltarinas)

Generalización de las ventanas fijas, las ventanas deslizantes se definen por una longitud y un periodo fijos. Si el periodo es menor que la longitud, las ventanas se solapan. Si el periodo es igual a la longitud, tienes ventanas fijas. Y si el periodo es mayor que la longitud, tienes una extraña especie de ventana de muestreo que sólo observa subconjuntos de los datos a lo largo del tiempo. Al igual que las ventanas fijas, las ventanas deslizantes suelen estar alineadas, aunque pueden no estarlo para optimizar el rendimiento en determinados casos de uso. Observa que las ventanas deslizantes de la Figura 1-8 están dibujadas tal cual para dar una sensación de movimiento deslizante; en realidad, las cinco ventanas se aplicarían a todo el conjunto de datos.

Sesiones

Como ejemplo de ventanas dinámicas, las sesiones se componen de secuencias de eventos terminadas por un intervalo de inactividad superior a cierto tiempo de espera. Las sesiones se suelen utilizar para analizar el comportamiento de los usuarios a lo largo del tiempo, agrupando una serie de eventos relacionados temporalmente (por ejemplo, una secuencia de vídeos vistos de una sentada). Las sesiones son interesantes porque sus duraciones no pueden definirse a priori; dependen de los datos reales de que se trate. También son el ejemplo canónico de ventanas no alineadas, porque las sesiones prácticamente nunca son idénticas en diferentes subconjuntos de datos (por ejemplo, diferentes usuarios).

Los dos dominios de tiempo que hemos discutido antes (tiempo de procesamiento y tiempo de evento) son esencialmente los dos que nos interesan.7 La ventana tiene sentido en ambos dominios, así que veamos cada uno en detalle y veamos en qué se diferencian. Dado que la ventana de tiempo de procesamiento ha sido históricamente más común, empezaremos por ahí.

Ventana por tiempo de procesamiento

Al establecer ventanas por tiempo de procesamiento, el sistema básicamente almacena los datos entrantes en ventanas hasta que haya transcurrido cierto tiempo de procesamiento. Por ejemplo, en el caso de las ventanas fijas de cinco minutos, el sistema almacenaría los datos durante cinco minutos de tiempo de procesamiento, tras los cuales trataría todos los datos que hubiera observado en esos cinco minutos como una ventana y los enviaría a continuación para su procesamiento.

Figura 1-9. Ventana en ventanas fijas por tiempo de procesamiento. Los datos se reúnen en ventanas según el orden en que llegan a la canalización.

La ventana de tiempo de procesamiento tiene algunas propiedades interesantes:

  • Es muy sencillo. La implementación es extremadamente sencilla porque nunca te preocupas de barajar los datos en el tiempo. Te limitas a almacenar los datos en el búfer a medida que llegan y a enviarlos a la salida cuando se cierra la ventana.

  • Juzgar si una ventana está completa es sencillo. Como el sistema sabe perfectamente si se han visto todas las entradas de una ventana, puede tomar decisiones perfectas sobre si una ventana determinada está completa. Esto significa que no hay necesidad de tratar los datos "tardíos" de ninguna manera cuando se establecen ventanas por tiempo de procesamiento.

  • Si quieres inferir información sobre la fuente a medida que se observa, la ventana de tiempo de procesamiento es exactamente lo que quieres. Muchos escenarios de monitoreo entran en esta categoría. Imagina que rastreas el número de peticiones por segundo enviadas a un servicio web de escala global. Calcular una tasa de estas peticiones con el fin de detectar interrupciones es un uso perfecto de la ventana de tiempo de procesamiento.

Dejando a un lado los buenos argumentos, hay un gran inconveniente en la ventana de tiempo de procesamiento: si los datos en cuestión tienen asociados tiempos de eventos, esos datos deben llegar en orden de tiempo de evento para que las ventanas de tiempo de procesamiento reflejen la realidad de cuándo ocurrieron realmente esos eventos. Desgraciadamente, los datos ordenados según la hora del suceso son poco comunes en muchas fuentes de entrada distribuidas del mundo real.

Como ejemplo sencillo, imagina cualquier aplicación móvil que recopile estadísticas de uso para su posterior procesamiento. En los casos en los que un determinado dispositivo móvil se desconecta durante cualquier periodo de tiempo (breve pérdida de conectividad, modo avión mientras vuela por el país, etc.), los datos registrados durante ese periodo no se cargarán hasta que el dispositivo vuelva a conectarse. Esto significa que los datos pueden llegar con una desviación temporal de minutos, horas, días, semanas o más. Es esencialmente imposible extraer ningún tipo de conclusión útil de un conjunto de datos de este tipo cuando se agrupan según el tiempo de procesamiento.

Como otro ejemplo, muchas fuentes de entrada distribuidas pueden parecer que proporcionan datos ordenados en el tiempo de eventos (o casi) cuando el sistema general está sano. Desgraciadamente, el hecho de que el sesgo evento-tiempo sea bajo para la fuente de entrada cuando está sana no significa que siempre vaya a ser así. Piensa en un servicio global que procesa datos recogidos en varios continentes. Si los problemas de red a través de una línea transcontinental de ancho de banda limitado (que, por desgracia, son sorprendentemente comunes) disminuyen aún más el ancho de banda y/o aumentan la latencia, de repente una parte de tus datos de entrada podría empezar a llegar con una desviación mucho mayor que antes. Si divides esos datos en ventanas según el tiempo de procesamiento, tus ventanas ya no son representativas de los datos que realmente se produjeron dentro de ellas; en su lugar, representan las ventanas de tiempo a medida que los eventos llegaban a la tubería de procesamiento, que es una mezcla arbitraria de datos antiguos y actuales.

En ambos casos, lo que realmente queremos es ajustar los datos según la hora de los sucesos, de forma que se respete el orden de llegada de los sucesos. Lo que realmente queremos es una ventana de tiempo de evento.

Ventana por tiempo de evento

La ventana evento-tiempo es lo que utilizas cuando necesitas observar una fuente de datos en trozos finitos que reflejen los momentos en que ocurrieron realmente esos eventos. Es el estándar de oro de las ventanas. Antes de 2016, la mayoría de los sistemas de procesamiento de datos en uso carecían de soporte nativo para ello (aunque cualquier sistema con un modelo de consistencia decente, como Hadoop o Spark Streaming 1.x, podría actuar como un sustrato razonable para construir un sistema de ventanas de este tipo). Me complace decir que el mundo actual es muy diferente, con múltiples sistemas, desde Flink a Spark, Storm o Apex, que soportan de forma nativa algún tipo de ventana en tiempo de eventos.

La Figura 1-10 muestra un ejemplo de ventana de una fuente no limitada en ventanas fijas de una hora.

Figura 1-10. Ventana en ventanas fijas según la hora del evento. Los datos se agrupan en ventanas según la hora en que se produjeron. Las flechas negras señalan datos de ejemplo que llegaron en ventanas de tiempo de procesamiento diferentes de las ventanas de tiempo de evento a las que pertenecían.

Las flechas negras de la Figura 1-10 señalan dos datos especialmente interesantes. Cada uno de ellos llegó en ventanas de tiempo de procesamiento que no coincidían con las ventanas de tiempo de evento a las que pertenecía cada dato. Por tanto, si estos datos se hubieran dividido en ventanas de tiempo de procesamiento para un caso de uso que se preocupara por los tiempos de los eventos, los resultados calculados habrían sido incorrectos. Como era de esperar, la corrección del tiempo de evento es una de las ventajas de utilizar ventanas de tiempo de evento.

Otra cosa buena de la ventana evento-tiempo sobre una fuente de datos no limitada es que puedes crear ventanas de tamaño dinámico, como sesiones, sin las divisiones arbitrarias que se observan al generar sesiones sobre ventanas fijas (como vimos anteriormente en el ejemplo de sesiones de "Datos no limitados: Streaming"), como se muestra en la Figura 1-11.

Figura 1-11. Ventana en ventanas de sesión por hora del evento. Los datos se recogen en ventanas de sesión que capturan ráfagas de actividad en función de las horas en que se produjeron los eventos correspondientes. Las flechas negras indican de nuevo el barajado temporal necesario para colocar los datos en sus ubicaciones temporales correctas.

Por supuesto, una semántica potente rara vez sale gratis, y las ventanas en tiempo de eventos no son una excepción. Las ventanas evento-tiempo tienen dos inconvenientes notables debido a que las ventanas a menudo deben vivir más tiempo (en tiempo de procesamiento) que la duración real de la propia ventana:

Amortiguación

Debido a la mayor duración de las ventanas, es necesario almacenar más datos en el búfer. Afortunadamente, el almacenamiento persistente de suele ser el más barato de los tipos de recursos de los que dependen la mayoría de los sistemas de procesamiento de datos (los otros son principalmente la CPU, el ancho de banda de la red y la RAM). Como tal, este problema suele ser mucho menos preocupante de lo que podrías pensar cuando se utiliza cualquier sistema de procesamiento de datos bien diseñado con un estado persistente fuertemente consistente y una capa de almacenamiento en caché en memoria decente. Además, muchas agregaciones útiles no requieren que todo el conjunto de entrada se almacene en la memoria intermedia (por ejemplo, la suma o el promedio), sino que pueden realizarse de forma incremental, con un agregado intermedio mucho más pequeño almacenado en el estado persistente.

Integridad

Dado que a menudo no tenemos una buena forma de saber cuándo hemos visto todos los datos de una ventana determinada, ¿cómo sabemos cuándo los resultados de la ventana están listos para materializarse? En realidad, sencillamente no lo sabemos. Para muchos tipos de entradas, el sistema puede dar una estimación heurística razonablemente precisa de la finalización de la ventana mediante algo parecido a las marcas de agua que se encuentran en MillWheel, Cloud Dataflow y Flink (de las que hablamos más en los Capítulos 3 y 4). Pero para los casos en los que la corrección absoluta es primordial (de nuevo, piensa en la facturación), la única opción real es proporcionar una forma de que el constructor de la canalización exprese cuándo quiere que se materialicen los resultados de las ventanas y cómo deben refinarse esos resultados con el tiempo. Tratar la completitud de las ventanas (o la falta de ella) es un tema fascinante, pero quizá sea mejor explorarlo en el contexto de ejemplos concretos, que veremos a continuación.

Resumen

¡Uf! Eso ha sido un montón de información. Si has llegado hasta aquí, ¡te felicito! Pero no hemos hecho más que empezar. Antes de pasar a examinar en detalle el enfoque del Modelo de Viga, retrocedamos brevemente y recapitulemos lo que hemos aprendido hasta ahora. En este capítulo hemos hecho lo siguiente:

  • Aclaramos la terminología, centrando la definición de "flujo" para referirnos a los sistemas construidos teniendo en cuenta los datos no limitados, al tiempo que utilizamos términos más descriptivos como resultados aproximados/especulativos para conceptos distintos a menudo categorizados bajo el paraguas del "flujo". Además, destacamos dos dimensiones importantes de los conjuntos de datos a gran escala: la cardinalidad (es decir, acotada frente a no acotada) y la constitución (es decir, tabla frente a flujo), la última de las cuales consumirá gran parte de la segunda mitad del libro.

  • Evaluó las capacidades relativas de los sistemas batch y streaming bien diseñados, postulando que el streaming es, de hecho, un superconjunto estricto del batch, y que nociones como la Arquitectura Lambda, que se basan en que el streaming es inferior al batch, están destinadas a la jubilación a medida que maduran los sistemas de streaming.

  • Propuso dos conceptos de alto nivel necesarios para que los sistemas de streaming alcancen y, en última instancia, superen a los batch, que son la corrección y las herramientas para razonar sobre el tiempo, respectivamente.

  • Estableció las importantes diferencias entre el tiempo de acontecimiento y el tiempo de procesamiento, caracterizó las dificultades que esas diferencias imponen al analizar los datos en el contexto del momento en que se produjeron, y propuso un cambio de enfoque que se aleje de las nociones de exhaustividad y se oriente simplemente hacia la adaptación a los cambios de los datos a lo largo del tiempo.

  • Se han estudiado los principales enfoques de procesamiento de datos de uso común en la actualidad para datos limitados y no limitados, tanto mediante motores de lotes como de flujo, clasificando a grandes rasgos los enfoques no limitados en: agnósticos del tiempo, de aproximación, de ventana por tiempo de procesamiento y de ventana por tiempo de evento.

A continuación, nos sumergimos en los detalles del Modelo Beam, echando un vistazo conceptual a cómo hemos dividido la noción de procesamiento de datos en cuatro ejes relacionados: qué, dónde, cuándo y cómo. También echamos un vistazo detallado al procesamiento de un conjunto de datos de ejemplo sencillo y concreto a través de múltiples escenarios, destacando la pluralidad de casos de uso que permite el Modelo Beam, con algunas API concretas para asentarnos en la realidad. Estos ejemplos nos ayudarán a asimilar las nociones de tiempo de evento y tiempo de procesamiento introducidas en este capítulo, al tiempo que exploramos nuevos conceptos, como las marcas de agua.

1 En aras de la exhaustividad, quizá merezca la pena señalar que esta definición incluye tanto el verdadero streaming como las implementaciones de microlotes. Para quienes no estén familiarizados con los sistemas de microlotes, se trata de sistemas de streaming que utilizan ejecuciones repetidas de un motor de procesamiento por lotes para procesar datos ilimitados. Spark Streaming es el ejemplo canónico en la industria.

2 Los lectores que estén familiarizados con mi artículo original "Streaming 101" recordarán que recomendé encarecidamente que se abandonara el término "flujo" para referirse a los conjuntos de datos. Nunca se impuso, lo que en un principio pensé que se debía a lo pegadizo que resultaba y al uso generalizado que se hacía de él. Sin embargo, en retrospectiva, creo que simplemente me equivoqué. En realidad, es muy útil distinguir entre los dos tipos diferentes de constitución de conjuntos de datos: tablas y flujos. De hecho, la mayor parte de la segunda mitad de este libro está dedicada a comprender la relación entre ambos.

3 Si no estás familiarizado con lo que quiero decir cuando hablo de exactamente una vez, me refiero a un tipo específico de garantía de coherencia que proporcionan ciertos marcos de procesamiento de datos. Las garantías de coherencia suelen dividirse en tres clases principales: procesamiento al máximo una vez, procesamiento al mínimo una vez y procesamiento exactamente una vez. Ten en cuenta que los nombres que se utilizan aquí se refieren a la semántica efectiva que se observa en los resultados generados por la canalización, no al número real de veces que una canalización puede procesar (o intentar procesar) un registro determinado. Por este motivo, a veces se utiliza el término "efectivamente una vez" en lugar de "exactamente una vez", ya que es más representativo de la naturaleza subyacente de las cosas. Reuven trata estos conceptos con mucho más detalle en el Capítulo 5.

4 Desde la publicación original de "Streaming 101", numerosas personas me han señalado que habría sido más intuitivo colocar el tiempo de procesamiento en el eje x y el tiempo de evento en el eje y. Estoy de acuerdo en que intercambiar los dos ejes parecería inicialmente más natural, ya que el tiempo de evento parece la variable dependiente de la variable independiente del tiempo de procesamiento. Sin embargo, como ambas variables son monótonas y están íntimamente relacionadas, en realidad son variables interdependientes. Así que creo que, desde una perspectiva técnica, sólo tienes que elegir un eje y ceñirte a él. Las matemáticas son confusas (sobre todo fuera de Norteamérica, donde de repente se vuelven plurales y se te echan encima).

5 Este resultado no debería sorprenderte (pero a mí sí, por eso lo señalo), porque al medir los dos tipos de desviación/retraso estamos creando un triángulo rectángulo con la línea ideal. Las matemáticas molan.

6 Veremos en detalle las ventanas fijas alineadas en el capítulo 2, y las ventanas fijas no alineadas en el capítulo 4.

7 Si husmeas lo suficiente en la literatura académica o en los sistemas de streaming basados en SQL, también te encontrarás con un tercer dominio temporal de ventanas: las ventanas basadas en tuplas (es decir, ventanas cuyo tamaño se cuenta en número de elementos). Sin embargo, la ventana basada en tuplas es esencialmente una forma de ventana de tiempo de procesamiento en la que a los elementos se les asignan marcas de tiempo monotónicamente crecientes a medida que llegan al sistema. Por ello, no trataremos más en detalle la ventana basada en tuplas.

Get Sistemas de streaming now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.