Spark.

Hace ya unos meses les platiqué de MapReduce, que es un framework para procesamiento de grandes sets de datos, en ese video también les hablé de las limitantes de este modelo como el trabajar con algoritmos iterativos o el procesar datos en "tiempo-real".

Hoy les voy a hablar de otra forma de trabajar con grandes de cantidades de datos que es un poco distinta, esta vez utilizando Apache Spark. Spark es un framework de computación desarrollado inicialmente en la Universidad de California Berkeley, pero ahora es parte de los proyectos de la Apache Software Foundation.

Spark funciona al rededor de un concepto muy importante es el de los Resilient Distributed Datasets o RDDs, algo que podría ser traducido a datasets distribuidos y recuperables (?). Estos RDDs son colecciones de datos de solo lectura, es decir, una vez creados ya no son modificables, y cada vez que uno es "modificado" en realidad se está creando uno nuevo.

Estos RDDs no son creados de manera inmediata, sino que su creación se retrasa hasta que son materializados: es decir almacenados en memoria o en disco, o en algunos casos para realizar alguna operación con ellos. En un concepto muy muy parecido a la evaluación perezosa.

Su característica más importante es que en lugar de llevar un registro de la información que un RDD contiene, se lleva un registro de las operaciones que lo crearon, de este modo se puede ordenar la creación de un RDD nuevamente en caso de fallos.

A medida que se van creando RDDs, también se va creando un grafo dirigido y aciciclico, o en terminología de Spark: DAG, este grafo es una manera de representar las operaciones sobre los RDDs que les acabo de mencionar. Cuando un RDD es materializado el sistema se encarga de decidir cuál es la manera más óptima de crearlo a partir de las operaciones con las que se especificaron sobre él y sus derivados.

Distribuido

Cabe señalar que al igual que MapReduce, Spark también es ejecutado en un cluster de servidores y que existe toda una pesadilla de orquestación para que este funcione correctamente, para tu buena suerte esta tarea ya está resuelta y tu solamente te puedes enfocar en resolver tu problema (la mayoría de las veces).

Es justamente el DAG el que va a definir cómo es que los datos se van a mover entre nodos, reduciendo en lo mayor posible el movimiento de información entre servidores.

Ejemplo

Otra de sus ventajas es que al contrario de MapReduce con Spark no necesitas diseñar por separado las partes del sistema, puedes tratar to código como si estuvieras desarrollando un sistema monolítico y sin alguna consideración extra, cierto es que para lograr un buen desempeño es necesario que lidies con asuntos de distribución de cargas y repartición de trabajo, pero para empezar a programar no necesitas saber esto, además de que el código es mucho más sencillo de leer y de escribir.

Este es el ejemplo de el conteo de palabras en Spark usando los tres lenguajes que Spark soporta por default:

Scala

val textFile = sc.textFile("hdfs://...")
val counts = textFile.flatMap(line => line.split(" "))
                 .map(word => (word, 1))
                 .reduceByKey(_ + _)
counts.saveAsTextFile("hdfs://...")

Python

text_file = sc.textFile("hdfs://...")
counts = text_file.flatMap(lambda line: line.split(" ")) \
             .map(lambda word: (word, 1)) \
             .reduceByKey(lambda a, b: a + b)
counts.saveAsTextFile("hdfs://...")

Java

JavaRDD<String> textFile = sc.textFile("hdfs://...");
JavaPairRDD<String, Integer> counts = textFile
    .flatMap(s -> Arrays.asList(s.split(" ")).iterator())
    .mapToPair(word -> new Tuple2<>(word, 1))
    .reduceByKey((a, b) -> a + b);
counts.saveAsTextFile("hdfs://...");

Como puedes ver no es necesario dividir tu código en mapper y reducer como tendrías que haberlo hecho con MapReduce

  public static class TokenizerMapper
       extends Mapper<Object, Text, Text, IntWritable>{

    private final static IntWritable one = new IntWritable(1);
    private Text word = new Text();

    public void map(Object key, Text value, Context context
                    ) throws IOException, InterruptedException {
      StringTokenizer itr = new StringTokenizer(value.toString());
      while (itr.hasMoreTokens()) {
        word.set(itr.nextToken());
        context.write(word, one);
      }
    }
  }

  public static class IntSumReducer
       extends Reducer<Text,IntWritable,Text,IntWritable> {
    private IntWritable result = new IntWritable();

    public void reduce(Text key, Iterable<IntWritable> values,
                       Context context
                       ) throws IOException, InterruptedException {
      int sum = 0;
      for (IntWritable val : values) {
        sum += val.get();
      }
      result.set(sum);
      context.write(key, result);
    }
  }

Spark vs. MapReduce

Pero bueno, por todo lo que he dicho pareciera que debemos todos dejar de usar MapReduce (y en la mayoría de los casos esto parece ser un buen consejo) pero en general debes saber que Spark usa muchísima memoria RAM para cumplir su cometido eficientemente mientras que MapReduce utiliza memoria de disco que es aún un poco más barata, además de que si no necesitas velocidad en el procesamiento de la información considerar Spark podría ser una exageración.

key-value pairs

Se podría decir que Spark es compatible con MapReduce gracias a los tipos de dato llave-valor, así como el hecho de que podemos usar los mismos conectores de lectura de archivos. Por lo que algunas de las cosas que se hacen con MapReduce son directamente portables a Spark. Pero en fin, sigamos hablando de los beneficios de Spark...

Extensiones

Spark ofrece algunas extensiones que lo habilitan para trabajos relacionados con SQL y DataFrames, Machine Learning, Streaming de datos y grafos.

Streaming

En el video de MapReduce me dejaron esta pregunta y respondí que Spark era una alternativa, y es que como mencioné esta herramienta permite trabajar con flujos de datos en tiempo real... pero de esto podemos hablar en otro video, a lo mejor y hasta con un screencast.