Cuando cargamos un conjunto de datos (por ejemplo proveniente de una fuente externa como los archivos de un directorio de un HDFS, o de una estructura de datos que haya sido previamente generada en nuestro programa) para ser procesado en Spark, la estructura que usaremos internamente para volcar dichos datos es un RDD. Al volcarlo a un RDD, en función de cómo tengamos configurado nuestro entorno de trabajo en Spark, la lista de entradas que componen nuestro dataset será dividida en un número concreto de particiones (se "paralelizará"), cada una de ellas almacenada en uno de los nodos de nuestro clúster para procesamiento de Big Data. Esto explica el apelativo de "distributed" de los RDD.
A partir de aquí, entrará en juego una de las características básicas de estos RDD, y es que son inmutables. Una vez que hemos creado un RDD, éste no cambiará, sino que cada transformación (map, filter, flatMap, etc.) que apliquemos sobre él generará un nuevo RDD. Esto por ejemplo facilita mucho las tareas de procesamiento concurrente, ya que si varios procesos están trabajando sobre un mismo RDD el saber que no va a cambiar simplifica la gestión de dicha concurrencia. Pero, por otro lado, uno podría preguntarse: ¿significa esto que con cada transformación estamos creando nuevas copias de los datos (potencialmente de gran volumen)? ¿Esto no redunda en una gran ineficiencia y acabaría por desbordar la capacidad de almacenamiento?
En realidad no, ya que cada definición de un nuevo RDD no está generando realmente copias de los datos. Cuando vamos aplicando sucesivas transformaciones de los datos, lo que estamos componiendo es una "receta" (una función determinista compuesta por una secuencia de pasos de transformación, que en Spark recibe el nombre de "lineage" o "linaje") que, a partir del RDD inicial, nos permite obtener la versión transformada que buscamos para los datos. Aquí es donde entra en juego la diferenciación que hace Spark de sus operaciones en dos tipos: transformaciones y acciones. Spark utiliza un mecanismo de "evaluación perezosa" de sus transformaciones, de manera que no se materializan (no generan nuevos datos transformados) hasta que se ejecuta una acción (reduce, take, collect, takeOrdered, etc.). Es entonces cuando internamente se evalúa la secuencia de transformaciones que ha sido definida antes, se combina para optimizarla, y se ejecuta para generar una nueva versión de los datos. Es decir, los sucesivos RDD que hemos ido creando con cada transformación no son más que estados intermedios de los datos que no llegan a materializarse a no ser que una acción lo demande, por lo que realmente no estamos creando copias y más copias de los datos a cada paso.
Esta idea del lineage como la "receta" determinista (aplicada sobre los mismos datos de partida, siempre obtendrá el mismo resultado) que va guardando la secuencia de transformaciones para aplicarla sobre los datos cuando sea necesario tiene otro beneficio adicional, y es que puede recrearse en todo momento y, al ser una función determinista, siempre obtendríamos la misma versión transformada de los datos. Esta idea es la que sustenta la tolerancia a fallos en Spark ya que, si en un momento dado uno de los nodos del clúster fallase, no tendríamos más que "tirar de linaje" para recalcular todos los pasos de transformación desde los datos originales y regeneraríamos esa parte de los datos que habíamos perdido. Esta capacidad de regeneración y recuperación del estado de los datos es lo que explica la resiliencia ("resilient") de los RDD. Además, Spark nos permite también hacer persistente (en disco o en memoria principal) un RDD concreto para no tener que recomputarlo en el futuro, dándonos más flexibilidad a la hora de diseñar nuestro tratamiento de los datos en función de las necesidades de procesamiento y consulta que tengamos en nuestro caso y de aquellos datos transformados que vayamos a utilizar más frecuentemente.
A partir de aquí, entrará en juego una de las características básicas de estos RDD, y es que son inmutables. Una vez que hemos creado un RDD, éste no cambiará, sino que cada transformación (map, filter, flatMap, etc.) que apliquemos sobre él generará un nuevo RDD. Esto por ejemplo facilita mucho las tareas de procesamiento concurrente, ya que si varios procesos están trabajando sobre un mismo RDD el saber que no va a cambiar simplifica la gestión de dicha concurrencia. Pero, por otro lado, uno podría preguntarse: ¿significa esto que con cada transformación estamos creando nuevas copias de los datos (potencialmente de gran volumen)? ¿Esto no redunda en una gran ineficiencia y acabaría por desbordar la capacidad de almacenamiento?
En realidad no, ya que cada definición de un nuevo RDD no está generando realmente copias de los datos. Cuando vamos aplicando sucesivas transformaciones de los datos, lo que estamos componiendo es una "receta" (una función determinista compuesta por una secuencia de pasos de transformación, que en Spark recibe el nombre de "lineage" o "linaje") que, a partir del RDD inicial, nos permite obtener la versión transformada que buscamos para los datos. Aquí es donde entra en juego la diferenciación que hace Spark de sus operaciones en dos tipos: transformaciones y acciones. Spark utiliza un mecanismo de "evaluación perezosa" de sus transformaciones, de manera que no se materializan (no generan nuevos datos transformados) hasta que se ejecuta una acción (reduce, take, collect, takeOrdered, etc.). Es entonces cuando internamente se evalúa la secuencia de transformaciones que ha sido definida antes, se combina para optimizarla, y se ejecuta para generar una nueva versión de los datos. Es decir, los sucesivos RDD que hemos ido creando con cada transformación no son más que estados intermedios de los datos que no llegan a materializarse a no ser que una acción lo demande, por lo que realmente no estamos creando copias y más copias de los datos a cada paso.
Esta idea del lineage como la "receta" determinista (aplicada sobre los mismos datos de partida, siempre obtendrá el mismo resultado) que va guardando la secuencia de transformaciones para aplicarla sobre los datos cuando sea necesario tiene otro beneficio adicional, y es que puede recrearse en todo momento y, al ser una función determinista, siempre obtendríamos la misma versión transformada de los datos. Esta idea es la que sustenta la tolerancia a fallos en Spark ya que, si en un momento dado uno de los nodos del clúster fallase, no tendríamos más que "tirar de linaje" para recalcular todos los pasos de transformación desde los datos originales y regeneraríamos esa parte de los datos que habíamos perdido. Esta capacidad de regeneración y recuperación del estado de los datos es lo que explica la resiliencia ("resilient") de los RDD. Además, Spark nos permite también hacer persistente (en disco o en memoria principal) un RDD concreto para no tener que recomputarlo en el futuro, dándonos más flexibilidad a la hora de diseñar nuestro tratamiento de los datos en función de las necesidades de procesamiento y consulta que tengamos en nuestro caso y de aquellos datos transformados que vayamos a utilizar más frecuentemente.
Estan increibles tus blogs.
ResponderEliminar¡Muchas gracias por tu comentario, Eric!
EliminarGracias por el post, muy bien explicado.
ResponderEliminar¡Gracias a ti, Fernando! Me alegro de que sea de utilidad.
Eliminar¡¡Excelentes tus publicaciones!! Me han servido muchísimo. Gracias por hacerlas tan sencillas de entender.
ResponderEliminar¡Gracias a ti por tu comentario!
EliminarMuchas gracias Mikel me aclaró todas las ideas vagas que tenía sobre el tema. Excelente!!!
ResponderEliminar¡Gracias, Geovanny!
Eliminar