Sunteți pe pagina 1din 74

Kai, una herramienta para el procesado de datos

a gran escala en clusteres Hadoop


Luis Belloch Gomez
Master de Computacion Paralela y Distribuida
Universitat Polit`ecnica de Val`encia
Julio de 2013

Resumen
En el documento actual se presenta Kai, una biblioteca escrita en Scala destinada a simplificar la escritura de aplicaciones de procesado de datos a gran escala
en cl
usteres basados en Hadoop. La biblioteca esta basada en la definicion de
tres operaciones b
asicas, map, filter y fold, sobre las que se implementan otras
operaciones m
as complejas como take, join, group, zip, destinadas a manipular y
transformar grandes conjuntos de datos de forma eficiente. Para ello el trabajo
recoge algunos conceptos cl
asicos de la programacion funcional que permiten
traducir esas operaciones b
asicas en secuencias de trabajos de MapReduce de
forma sencilla, aplicando optimizaciones previamente a la ejecucion del flujo de
trabajo dentro del cluster.

Indice general
1. Introducci
on
1.1. Resumen de la propuesta . . . . .
1.2. Contexto del trabajo . . . . . . . .
1.3. Que es Kai . . . . . . . . . . . . .
1.3.1. Prop
osito y vista general .
1.3.2. Operaciones soportadas . .
1.4. C
omo est
a organizada la memoria

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

3
3
4
5
5
6
8

2. Fundamentos
2.1. Notaci
on y convenciones . . . . . . . . . . . . . . . . . . . . . . .
2.2. Funciones de orden superior . . . . . . . . . . . . . . . . . . . . .
2.3. Primitivas b
asicas: Map, Filter y Fold . . . . . . . . . . . . . . .
2.3.1. Filter: Filtrado de secuencias . . . . . . . . . . . . . . . .
2.3.2. Map: Transformacion de secuencias . . . . . . . . . . . . .
2.3.3. Fold: Plegado de secuencias . . . . . . . . . . . . . . . . .
2.4. Operaciones adicionales . . . . . . . . . . . . . . . . . . . . . . .
2.4.1. Identidad . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.2. Orden y mezcla . . . . . . . . . . . . . . . . . . . . . . . .
2.4.3. Filtros . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.4. Cuantificadores . . . . . . . . . . . . . . . . . . . . . . . .
2.4.5. Particionado . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.6. Agrupaci
on . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.7. Agregaci
on . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.8. Generaci
on . . . . . . . . . . . . . . . . . . . . . . . . . .
2.4.9. Combinaci
on (Experimental) . . . . . . . . . . . . . . . .
2.5. Hadoop y MapReduce . . . . . . . . . . . . . . . . . . . . . . . .
2.5.1. MapReduce . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.2. Modelo te
orico de MapReduce . . . . . . . . . . . . . . .
2.5.3. Correspondencia del modelo con la implementacion interna de Hadoop . . . . . . . . . . . . . . . . . . . . . . . . .

9
9
10
12
12
13
13
14
14
14
15
15
16
16
16
17
17
18
19
20

3. Implementaci
on
3.1. Vista General . . . . . . . . . . . . . . . . .
3.1.1. Hadoop, HDFS y MapReduce . . . .
3.1.2. El papel de Kai en Hadoop . . . . .
3.2. Definici
on de trabajos . . . . . . . . . . . .
3.2.1. Flujos de ejecucion . . . . . . . . . .
3.2.2. Piezas b
asicas: DataFlow y Mutable

23
23
23
25
26
26
28

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

21

3.2.3. Construcci
on del flujo de operaciones . . . . . . . . .
3.2.4. Seguimiento de tipos . . . . . . . . . . . . . . . . . .
3.2.5. Implementaci
on de operaciones en base a primitivas
3.3. Transformaci
on en mappers y reducers . . . . . . . . . . . .
3.3.1. Mappers y Reducers de referencia . . . . . . . . . .
3.3.2. Distribuci
on del programa en el cluster . . . . . . .
3.3.3. Infraestructura para lanzar trabajos . . . . . . . . .
3.4. Planificaci
on y optimizacion . . . . . . . . . . . . . . . . . .
3.4.1. El pipeline de optimizaciones . . . . . . . . . . . . .
3.5. Serializaci
on de Entidades: Apache Avro . . . . . . . . . . .
3.5.1. Utilizaci
on de Avro . . . . . . . . . . . . . . . . . . .
3.5.2. Comparativa contra otros formatos . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

28
29
31
33
33
35
38
40
41
43
43
44

4. Uso de la Biblioteca
4.1. Primeros Pasos . . . . . . . . . . . . . . . . . . . . . .
4.1.1. Definiendo transformaciones de datos . . . . .
4.1.2. Avro, el formato de datos de Kai . . . . . . . .
4.1.3. Compilar y enviar trabajos al cluster . . . . . .
4.1.4. Depurando la salida del planificador . . . . . .
4.2. Extendiendo Kai . . . . . . . . . . . . . . . . . . . . .
4.2.1. Extendiendo el conjunto de operaciones de Kai
4.2.2. Extendiendo la generacion de trabajos . . . . .
4.2.3. Extendiendo el planificador . . . . . . . . . . .
4.3. Algunos ejemplos pr
acticos . . . . . . . . . . . . . . .
4.3.1. Comprobar si existen elementos . . . . . . . . .
4.3.2. Ranking de documentos . . . . . . . . . . . . .
4.3.3. Paginaci
on de resultados . . . . . . . . . . . . .
4.3.4. Obtener contenedores mas pesados por destino
4.3.5. Factora para la aplicacion de reglas de negocio

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

47
47
47
49
49
50
51
51
52
53
54
54
54
55
55
55

5. Trabajos relacionados
5.1. Sawzall . . . . . . . . . . . . . . .
5.1.1. Funcionamiento basico . . .
5.1.2. Rendimiento . . . . . . . .
5.1.3. Resumen de caractersticas
5.2. Pig . . . . . . . . . . . . . . . . . .
5.2.1. Funcionamiento basico . . .
5.2.2. Extensibilidad . . . . . . .
5.2.3. Resumen de caractersticas
5.3. Dryad . . . . . . . . . . . . . . . .
5.3.1. DryadLINQ . . . . . . . . .
5.3.2. SCOPE . . . . . . . . . . .
5.4. Otras libreras similares . . . . . .

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

57
57
58
58
59
59
60
60
61
62
62
63
63

6. Conclusiones

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

65

Captulo 1

Introducci
on
Si quelquun veut un mouton,
cest la preuve quil en existe un.
A. de S. Exupery, Le Petit Prince

Este documento describe el proyecto realizado como trabajo final del Master
de Computaci
on Paralela y Distribuida de la Universidad Politecnica de Valencia.
En el presentamos el dise
no y una implementacion de referencia que hemos
llamado Kai. Kai es una biblioteca escrita en Scala destinada a simplificar la
escritura de aplicaciones de procesado de datos a gran escala, en particular
dentro de la infraestructura de MapReduce que proporciona Hadoop.
Este trabajo de m
aster est
a pensado para establecer las bases sobre las que
desarrollar un trabajo posterior dentro del area de la computacion distribuida, y
en particular dentro de la optimizacion de programas basados en abstracciones
de alto nivel sobre el hardware subyacente.

1.1.

Resumen de la propuesta

Kai permite definir una serie de operaciones de transformacion a ejecutar


sobre conjuntos de datos para posteriormente distribuir dicha ejecucion en un
cluster de m
aquinas con Hadoop.
Dicha definici
on se hace en el contexto de un lenguaje de programacion
funcional fuertemente tipado [34], por lo que se tiene control absoluto sobre los
tipos de datos en uso y se refuerza una definicion formal de operaciones que
permita aplicar optimizaciones mas tarde.
La biblioteca proporciona un conjunto basico de operadores de transformaci
on tales como filter, fold, join, group, take, etc. directamente traducibles a
una serie de trabajos MapReduce que se distribuiran posteriormente para su
ejecuci
on en el cluster.
Si bien el primer objetivo de la biblioteca es simplificar la escritura de trabajos de transformaci
on de datos, se ha puesto especial cuidado en la definicion
estricta de los operadores para que nuestro sistema pueda aplicar optimizaciones
previamente a la ejecuci
on. Adicionalmente, el dise
no de los mismos recoge algunos conceptos fundamentales de la programacion funcional aplicables al control
3

de errores, que permiten especificar a nivel de registro distintas opciones de


recuperaci
on.
La definici
on de las operaciones basicas de Kai y algunas de las optimizaciones planteadas est
an completamente separadas de la infraestructura de componentes de Hadoop, por lo que se abre tambien la puerta a futuros trabajos
donde se traduzcan dichas operaciones en otro tipo de sistemas distribuidos.

1.2.

Contexto del trabajo

En los u
ltimos a
nos y en particular con la aparicion de las grandes empresas
de Internet han surgido toda una serie de sistemas destinados al procesamiento y la transformaci
on de ingentes cantidades de datos. Desde la informacion
generada por redes sociales como Facebook, al creciente intercambio mensajes
de texto en WhatsApp, el uso de sistemas distribuidos se plantea como la u
nica
forma de abastecer la creciente demanda de hacer frente al consumo de grandes
vol
umenes de datos.
Con la aparici
on de estos vol
umenes de informacion muchas empresas que
vieron su nacimiento en Internet liberaron al p
ublico distintos sistemas distribuidos con objeto de simplificar el mantenimiento de los mismos, cediendo parte
de su control a la comunidad de software libre. Sistemas como Cassandra [28],
Voldemort o Redis van poco a poco dando soporte a otras organizaciones de
ambito privado pertenecientes a distintos sectores no relacionados con Internet
o las redes sociales.
Un claro ejemplo de este crecimiento es Hadoop, desarrollado en el a
no 2005
por Doug Cutting y Mike Cafarella mientras trabajaban en Yahoo. El sistema se
desarrollo implementando inicialmente dos artculos publicados por Google describiendo su sistema de procesamiento interno [13] [7], con el objetivo de replicar
gran parte de su funcionalidad. Esto inclua un sistema de archivos distribuido
orientado denominado HDFS y un conjunto de bibliotecas para la programaci
on de aplicaciones dentro de un paradigma de memoria distribuida basado en
la localidad de la informaci
on dentro de clusters heterogeneos, denominado de
forma com
un MapReduce. Entre las caractersticas fundamentales que podran
definir Hadoop, se establece como un sistema que garantiza la consistencia de
los datos y es tolerante a particiones dentro del cluster.
Si bien el
ambito de aplicacion de Hadoop es bastante extenso, existe particularmente un conjunto de casos de uso para los cuales el desarrollo de aplicaciones
MapReduce resulta bastante conveniente:
El objetivo central del sistema es garantizar la disponibilidad de los datos
manteniendo la consistencia de los mismos, por lo que es un candidato
id
oneo para, con muchas limitaciones, suplir sistemas de bases de datos
relacionales.
Los trabajos en MapReduce intentan explotar al maximo la localidad de
la informaci
on dentro del cluster, lo que lo hace idoneo para problemas
con una entrada/salida dominante en los que la potencia de calculo no es
el factor crtico.
Al permitir el uso de sistemas heterogeneos muchas empresas escogieron
Hadoop debido al bajo ROI del hardware necesario, lo que proporciona
4

una puerta de entrada de relativo bajo coste a empresas de peque


no y
mediano tama
no.
Al popularizar Hadoop y el uso de MapReduce comenzaron a aparecer algunas bibliotecas que permitan ejecutar trabajos dentro del cluster utilizando
otros lenguajes y tecnicas distintos a la implementacion tradicional con Java.
Uno de esos lenguajes de programacion es Pig [38] creado para aliviar y simplificar la escritura de aplicaciones y proporcionar un punto de entrada al cluster
un poco m
as mundano. Pig esta basado en Sawzall [40], creado por Google, y
con una naturaleza similar: la manipulacion de datos dentro del cluster.
Tanto en Pig como en Sawzall se establecen un conjunto de operadores
para filtrar y manipular los datos de forma declarativa. Como indica uno de los
autores de Pig [18], el caso de uso fundamental es la transformacion de grandes
vol
umenes de datos:
In my experience, Pig Latin use cases tend to fall into three separate
categories: traditional extract transform load (ETL), data pipelines,
research on raw data, and iterative processing.
A continuaci
on se muestra un peque
no fragmento en Pig Latin que obtiene
las m
aximas temperaturas por a
no. Al ejecutar el trabajo, Pig transformara el
script en una secuencia de operaciones MapReduce dentro del cluster, que combinar
an y agrupar
an los datos de temperatura de los diferentes a
nos para obtener
un listado de temperaturas m
aximas:
records = LOAD data.txt AS (year:chararray, temperature:int, quality:int);
filtered = FILTER records BY temperature != 9999 AND (quality == 0);
grouped = GROUP filtered BY year;
max_temp = FOREACH grouped GENERATE group, MAX(filtered.temperature);
DUMP max_temp;

En el captulo 5 veremos una descripcion de otros trabajos similares como


Pig, Sawzall o Dryad [26] y estableceremos comparaciones con la funcionalidad
proporcionada por Kai.

1.3.
1.3.1.

Qu
e es Kai
Prop
osito y vista general

Kai es la implementaci
on de referencia de una propuesta de dise
no realizada
con el objetivo de salvar algunas limitaciones fundamentales de otros sistemas
similares, estableciendo una base solida para futuros trabajos dedicados la descripci
on de flujos de procesado de datos en sistemas distribuidos heterogeneos.
Dicha implementaci
on se plantea como una biblioteca escrita en Scala, que
inicialmente permitir
a ejecutar trabajos de procesado de datos dentro de Hadoop.
Para ello definiremos un conjunto fundamental de operaciones extradas de
conceptos cl
asicos de la programacion funcional (map, filter, fold ) sobre los que
implementaremos operaciones mas complejas (join, group, zip y otras), intentando reducir todas las operaciones a secuencias de MapReduce [13].
El uso de Kai es sencillo y se basa en definir la aplicacion de una o varias
funciones sobre un origen de datos. Por ejemplo, supongamos que tenemos en
5

HDFS los datos de poblaci


on de los municipios de Espa
na y queremos obtener
el total de habitantes de la provincia de Valencia. Lo primero que haremos
ser
a definir en Scala una estructura de datos denominada Municipio con la
siguiente forma:
class
val
val
val

Municipio(
nombre: String,
habitantes: Int,
codigoPostal: String)

Y posteriormente definiremos las operaciones necesarias para poder extraer


el dato requerido:
val total = new DataFlow[Municipio]
.filter { m => m.codigoPostal.startsWith("46") }
.sum { m => m.habitantes }

Como vemos se est


an definiendo dos operaciones simples que inicialmente
filtrar
an el conjunto de datos y posteriormente realizaran la agregacion mediante
el operador sum. La variable total representa una secuencia de operaciones a
ejecutar, que Kai tendr
a que transformar en distintos mappers y reducers de
Hadoop antes de distribuir el trabajo en el cluster. Kai proporciona no solo la
transformaci
on a trabajos de Hadoop sino tambien:
La distribuci
on del c
odigo que define nuestro flujo de datos. Tomando
como ejemplo el caso filter, es posible definir cualquier tipo de funcion en
Scala que consideremos adecuada, por lo que sera necesario distribuir todo
ese c
odigo a cada nodo del cluster.
La serialiaci
on de las entidades desde HDFS a la clase Municipio que
hemos definido para poder manipular los datos utilizando tipos de datos
de Scala, y que por tanto puede beneficiarse de todas las comprobaciones
de tipos que realiza el compilador.
La aplicaci
on de optimizaciones. Dado que las operaciones se traducen a
secuencias de MapReduce, en Kai se introduce un planificador que permite
aplicar una serie de optimizaciones para reducir la cantidad de trabajo a
realizar. Veremos m
as acerca del planificador en la seccion 3.4.
En el captulo 4 veremos con detalle muchos mas ejemplos de uso de la
biblioteca. Para consultar los detalles concretos de implementacion de Kai puede
consultarse el captulo 3.

1.3.2.

Operaciones soportadas

Como ya hemos mencionado antes, se han definido tres operaciones principales filter, transform y fold y un conjunto de operaciones mas complejas que
est
an definidas en terminos de esas tres operaciones basicas. Podemos enumerar
brevemente las operaciones soportadas por Kai:
Operaciones sobre conjuntos: distinct y union
Cuantificadores: all, any, contains
Orden: sort

Particionado: take y skip


Join: join, concat y shuffle
Agrupaci
on: group, zip
Agregaci
on: sum, min, max y count
Generaci
on: range y repeat
La elecci
on de estas operaciones no es casual, pues perseguimos entre otras
cosas definir una base s
olida sobre la que poder expresar operaciones de procesado de datos dentro de un entorno distribuido.
El hecho de haber elegido y absorbido algunos de estos conceptos para el
trabajo proviene de la idea fundamental sobre la que se asienta la programacion
funcional: manejar la computacion como la simple evaluacion de un conjunto de
funciones matem
aticas sin estado y cuyos datos no son mutables.
Veamos un peque
no ejemplo para poder construir algunos conceptos a partir
del mismo. Supongamos que queremos ejecutar, de forma distribuida o no, la
siguiente funci
on que divide dos n
umeros entre si:
float divide(int numerador, int denominador) { ... }

Para poder realizar la division la funcion declara los dos parametros de


entrada como n
umeros enteros y la salida como un n
umero decimal con una
determinada precisi
on. Sin embargo, la declaracion de la funcion no expresa
todos los posibles resultados que podran producirse. Al dividir un n
umero por
cero obtenemos un resultado que no es el esperado: una excepci
on al flujo normal
de ejecuci
on. Sin embargo, cualquier matematico podra darnos una definicion
un poco m
as precisa que cubra todos los posibles resultados de la funcion:

a
a, b, c Z : b c = a
si b 6= 0
= c =
'

si b = 0
b
La definici
on de funciones desde las matematicas tiende a ser mucho mas
estricta y formal. Nuestra u
nica forma de igualar en honestidad al matematico
es declarar los dos posibles resultados de nuestra funcion: En la mayora de los
casos devolveremos el resultado y, eventualmente, devolveremos cualquier otra
cosa como resultado de no poder realizar el calculo.
Podemos leer acerca de las nociones basicas de la programacion funcional en
el excelente artculo de John Huges [24], pero antes imaginemos que queremos
buscar una forma de ejecutar la primera de las definiciones de division de forma
paralela c
omo vamos a encontrar una forma optima de realizar esa operacion
si la propia declaraci
on de intenciones expresada en la firma del metodo no esta
siendo honesta?
Casi cualquier programador buscara metodos cercanos al hardware para resolver de forma eficiente dicha operacion. En este trabajo intentaremos aplicar
un enfoque distinto: Si disponemos de los bloques de construccion necesarios
para poder definir las operaciones a ejecutar de forma precisa antes de ejecutarlas, tal vez podremos aplicar tecnicas que puedan traducir esa secuencia de
operaciones en c
odigo cercano al hardware que produzca una ejecucion eficiente.
Por lo tanto este trabajo comienza un largo camino definiendo formalmente
algunas de esas operaciones y haciendo una primera traduccion a mappers y

reduccers de Hadoop. Tal vez alg


un da podamos realizar transformaciones mas
complejas que conduzcan a sistemas de computo paralelo mas sencillos, robustos
y eficientes.
Adicionalmente a esa definicion formal se ha desarrollado una biblioteca en
Scala con el prop
osito de tener una implementacion de referencia sobre la que
poder trabajar algunos conceptos. La biblioteca no solo trata de simplificar la
escritura de trabajos sobre MapReduce, sino tambien proporcionar operaciones
m
as complejas que permitan abordar problemas como el filtrado y la validacion
de datos, la transformaci
on de estructuras o la extraccion de informacion.
A corto plazo la intenci
on del autor es completar dicha implementacion y
liberar el c
odigo bajo licencia BSD, lo que situara a la biblioteca en la categora
de otros trabajos similares como Sawzall [40], Pig [38] [19] o Dryad [50].

1.4.

C
omo est
a organizada la memoria

La memoria se organiza en 7 captulos y va construyendo la propuesta a


lo largo de los mismos, desde una definicion formal de las operaciones hasta
detalles concretos de la planificacion y la ejecucion de trabajos. As mismo se
realiza una comparativa con otros trabajos similares y ya en las conclusiones se
esbozan algunos trabajos complementarios que podran realizarse.
Captulo 2 Fundamentos
Abordaremos la definici
on teorica de los tres operadores basicos Map, Filter y Fold haciendo uso de algunos conceptos provenientes de la programaci
on funcional, mediante los cuales definiremos el resto de operaciones
disponibles en Kai. Asimismo introduciremos algunos conceptos necesarios para facilitar la lectura y describiremos la notacion empleada en el
texto.
Captulo 3 Implementaci
on
Se ofrecer
a un repaso general sobre como se ha realizado la implementaci
on de referencia de los operadores disponibles en Kai, haciendo especial
incapie en la traducci
on directa a trabajos MapReduce.
Captulo 4 Uso de la Biblioteca
Describiremos c
omo hacer uso de la biblioteca para implementar trabajos
de manipulaci
on de datos, aportando peque
nos ejemplos y mostrando el
conjunto general de caractersticas disponibles en Kai. Tambien esbozaremos algunos ejemplos practicos donde se justifique el uso de la biblioteca
y aportaremos algunos ejemplos sobre como abordar su utilizacion.
Captulo 5 Trabajos relacionados
En este captulo se describiran con detalle tres trabajos similares: Pig,
Sawzall y Dryad, intentando establecer Kai como una propuesta alternativa a las mismas. Tambien daremos un repaso rapido por otras bibliotecas
menores como Cascading o FlumeJava.
Captulo 6 Conclusiones y futuros trabajos
Estableceremos algunas conclusiones extradas del trabajo aqu realizado,
planteando algunas opciones futuras donde poder ampliar el ambito de
uso y la implementaci
on de Kai.

Captulo 2

Fundamentos
Are the houses and doors and
churches in Flatland to be altered
in order to accommodate such
monsters?
Edwin A. Abbott, Flatland

En este captulo definiremos de forma te


orica las operaciones soportadas
por Kai, comenzando por introducir tres primitivas b
asicas que ser
an utilizadas
para definir el resto de operaciones. Asimismo introduciremos algunos conceptos
necesarios para facilitar la lectura y describiremos la notaci
on empleada en el
texto.

2.1.

Notaci
on y convenciones

Para la definici
on de las operaciones soportadas por Kai hemos optado por
notaci
on matem
atica b
asica para definir funciones [23] [46], de forma que una
funci
on f que establece la correspondencia entre dos conjuntos de valores A y
B se denota como:
f :AB

(2.1)

Al conjunto A se le denomina dominio de la funcion f y representa los


posibles valores aceptados por la funcion. De igual forma, al conjunto B se le
denomina codominio de la funcion f y representa los posibles valores que pueden
obtenerse de la funci
on. En el caso de funciones con m
ultiples parametros el
dominio de la funci
on se denota como un producto cartesiano P Q:
f : (P Q) T

(2.2)

Trasladando esto a un lenguaje de programacion, f representa una funcion


que toma dos par
ametros, uno de tipo A y de tipo Q y devuelve un elemento
de tipo B. La firma de esta funcion en Scala queda de la forma:
def f[P,Q,T](p: P, q: Q): T

Adicionalmente a esta definicion necesitamos establecer la correspondencia


entre los conjuntos A y B de la definicion 2.1. Supongamos que queremos definir
una funci
on que multiplica un n
umero entero a Z por dos:
f :ZZ

(2.3)

f (a) = a 2

(2.4)

La especificaci
on de la funcion f (a) del tem 2.4 tambien se puede expresar
utilizando la forma a 7 a 2.
Nuevamente, la traducci
on a Scala es directa:
def f(a: Int): Int = a * 2

Para los casos en los que una funcion acepte o devuelva otra funcion, emplearemos una notaci
on similar. Por ejemplo, definamos en Scala una funcion que
realiza un descuento determinado sobre un artculo evitando los casos donde el
importe es negativo y se paga dinero al cliente por una compra:
def descuento(importe: Double, descuento: Double => Double): Double = {
val total = importe - descuento(importe)
if (total >= 0) total
else 0
}

La definici
on de la funci
on descuento representada por g y siendo fd la
funci
on utilizada para aplicar el descuento, tenemos que:
g : (Q fd ) A

(2.5)

g(i, fd ) = a a A, a 0, a = i fd (i)

(2.6)

En ocasiones tambien expresaremos g utilizando una funcion anonima para


definir la aplicaci
on de fd :
(i, fd ) 7 (y 7 i fd (y))

(2.7)

Como caso especial, utilizaremos una letra may


uscula entre corchetes para
indicar que un determinado parametro de la funcion es una lista de valores de un
tipo determinado. As, [T ] representa una lista de valores de tipo T (o tambien,
una lista de valores pertenecientes al conjunto T ).
Puede ampliarse la informacion acerca de la notacion y las convenciones
sobre definici
on de funciones en [2].

2.2.

Funciones de orden superior

Kai est
a basado en la aplicacion de funciones para realizar transformaciones
en los datos. La definici
on de esas funciones se realiza en terminos de otras
funciones y con bastante frecuencia veremos como algunos parametros de esas
transformaciones son, a su vez, otras funciones.
Las funciones que aceptan otras funciones como parametros y que a su vez
devuelven funciones se denominan funciones de orden superior [45] [36].
Veamos un ejemplo sencillo de este uso de funciones como parametros. Imaginemos que tenemos una funci
on en que recorre una lista de elementos eliminando
los n
umeros impares. Por simplicidad escribiremos el ejemplo en Python:
10

def filtrar_impares(lista):
for n in lista:
if (n % 2) == 0:
yield return n
lista_1 = [1, 34, 12, 21]
lista_2 = filtrar_impares(lista_1)

La parte interesante del ejemplo es que puede dividirse el funcionamiento en


dos aspectos: por una parte se recorre cada elemento de la lista y por otra se filtra
individualmente cada n
umero par. Podramos intentar crear una version mas
generica de ese filtrado y dejar que el usuario proporcione sus propias funciones
de filtrado mediante un par
ametro en la funcion?
def filtrar(lista, filtro):
for n in lista:
if filtro:
yield return n
def es_par(elemento):
return (elemento % 2) == 0
lista_1 = [1, 34, 12, 21]
lista_2 = filtrar(lista_1, es_par)

De esta forma hemos generalizado la funcion para que acepte otra funcion
como par
ametro, separando la tarea de recorrer la lista de la de eliminar elementos.
En otros casos podra interesarnos devolver una funcion en lugar del propio
resultado. Imaginemos una funcion f que multiplica dos n
umeros entre si:
f : Z2 Z

(2.8)

f (x, y) = x y

(2.9)

La funci
on toma dos n
umeros enteros y devuelve otro n
umero entero con el
resultado de la multiplicaci
on. Imaginemos ahora que queremos crear la funcion
g que multiplica un entero por dos, definiendo g en terminos de f :
g : Z2 Z

(2.10)

h(x) = y 7 f (x, y)

(2.11)

g(y) = h(2) g(y) = y 7 f (2, y)

(2.12)

El hecho de que g devuelva una funcion nos proporciona otro aspecto interesante de las funciones de
orden superior, pues disponemos ahora de la posibilidad
de generar y construir funciones mediante otras funciones. Tanto en la notacion
formal como en la mayor parte del codigo, funcion h suele no declararse de forma
explcita, denomin
andose funcion lambda, funcion anonima o simplemente . La
teora formal detr
as del c
alculo lambda puede introducirse mediante el artculo
inicial de Church [8] o en el mucho mas asequible texto de Barendsen [1].
La tecnica concreta del ejemplo anterior se denomina currying, y permite
entre otras cosas transformar una funcion con m
ultiples parametros en una
sucesi
on de llamadas a funciones con un u
nico parametro. Pueden obtenerse
m
as detalles en el texto de Schonfinkel [43] y en el libro de H. Curry [11].
Adicionalmente puede consultarse el artculo de Rosser para obtener una vision
hist
orica sobre el c
alculo lambda [41].
11

2.3.

Primitivas b
asicas: Map, Filter y Fold

Todas las operaciones de Kai se definen en base a tres funciones de orden


superior bien conocidas en el ambito de la programacion funcional: map, filter
y fold. Internamente solo existe implementacion explcita para estas tres operaciones, el resto de operaciones se definen en terminos de esta base y no existe
una implementaci
on especfica.
La idea de definir operaciones en base a un conjunto de primitivas no es nueva, y ya Graham Hutton [25] introdujo un patron recursivo para procesar listas a
partir del operador fold. Pese a que podramos nosotros tambien haber basado
todo el funcionamiento u
nicamente en fold, hemos optado por a
nadir tambien
map y filter a nuestra base de operaciones con el proposito de simplificar la
definici
on del resto de operaciones y su implementacion.
El prop
osito fundamental de haber escogido esta aproximacion basada en
funciones es poder realizar una transformacion correcta a un programa que sea
capaz de ejecutarse en un entorno distribuido.
Cualquier programa ejecutado sobre Kai pasa previamente por una fase de
an
alisis donde se construye un arbol de ejecucion que expresa todas las acciones
a realizar sobre uno o varios conjuntos de datos. Posteriormente se transforman
todas las operaciones complejas en terminos de map, filter y fold y se envan los
trabajos al cluster.
Por simplicidad se ha optado por Hadoop y MapReduce como el entorno donde se ejecutar
a el c
odigo de forma distribuida, aunque se deja abierta la puerta
a trabajos futuros para trasladar dicha implementacion a otras plataformas.
En el captulo 3 detallaremos como se realiza la traduccion de dichas operaciones en una implementaci
on concreta y ejecutable sobre Hadoop, as como las
posibles optimizaciones futuras que podramos introducir en este punto.

2.3.1.

Filter: Filtrado de secuencias

Empezaremos por definir la operacion mas basica de las tres. Su comportamiento es sencillo, filter es una funcion que recibe una secuencia y una funcion
que determina si un elemento de la lista pertenece o no a la lista de salida. La
definici
on de la funci
on es muy intuitiva:
filter : (f [T ]) [T ]

(2.13)

f : T Bool

(2.14)

filter (f, [t]) = [t ] {[t ] [t] (a [t ] : f (a) = true)}

(2.15)

La funci
on anterior recibe una secuencia de elementos con tipo a, denotada
como [a], y aplica la funci
on f : T Bool a cada uno de los elementos. Dependiendo del resultado de la aplicacion incluye el elemento o no en una copia de
la secuencia.
Por ejemplo, la funci
on filter aplicada sobre la lista t1 = (2, 9, 3, 5) y con
la funci
on de filtro t 7 t 5 devolvera la lista t2 = (9, 5).
Es importante destacar que el tipo de datos de los elementos de la lista no
cambia ni muta durante la ejecucion. De igual forma, Kai no garantiza el orden
original de los elementos a la salida.

12

2.3.2.

Map: Transformaci
on de secuencias

La siguiente operaci
on que tendremos en cuenta es map, que utilizaremos
para transformar una secuencia en otra de igual longitud, pero distinto tipo
de datos. La operaci
on recibe una lista de elementos de tipo A y una funcion
f : A B, aplicando f a cada elemento de [A]. El resultado es una secuencia
de elementos de tipo B.
map : (f [A]) [B]

(2.16)

map(f, [a]) = {a A, b B : f (a) = b}

(2.17)

Pondremos un ejemplo sencillo. Si tenemos la lista t1 = (2, 9, 3, 5) de tipo


I y la funci
on f : I R, a 7 a/2 al aplicar map(f, t1 ) obtendramos la lista
t2 = (1, 4.5, 1.5, 2.5) de tipo R.
Es f
acil observar c
omo la firma de la funcion es practicamente similar a
la de Filter, pese a que la implementacion es sustancialmente distinta. De
cualquier forma hemos mantenido la implementacion de la funcion Filter por
conveniencia y simplicidad en la implementacion.

2.3.3.

Fold: Plegado de secuencias

Fold es una funci


on que recibe una funcion f , un valor inicial a A y una
lista de elementos de tipo B y devuelve un elemento de tipo A que contiene una
agregaci
on de resultados de la aplicacion de f sobre los elementos de [B].
fold : (f A [B]) A

(2.18)

f : (A B) A

(2.19)

fold (f, a, [b]) = an donde tenemos que


a1 = f (a0 , a1 ), a2 = f (a1 , a2 ), . . . , an = f (an1 , an )

(2.20)

La funci
on tiene m
ultiples aplicaciones y podra por ejemplo en su caso mas
simple utilizarse para sumar los elementos de una lista. Teniendo (x, i) 7 x + i,
el valor inicial a = 0 y una lista b = (2, 9, 3, 5), la ejecucion de cada paso hasta
obtener el resultado en r sera:
f(0,2) = 0 +
f(2,9) = 2 +
f(11,3) = 11
f(14,5) = 14

2
9
+
+

=
=
3
5

2
11
= 14
= 19

a
a
a
a

=
=
=
=

0, a+1 = 2, r = 2
2, a+1 = 9, r = 11
11, a+1 = 3, r = 14
14, a+1 = 5, r = 19

Es interesante ver como podramos generar tambien el propio operador de


map mediante la funci
on fold, puede consultarse el texto de G. Hutton [25]
para m
as informaci
on. A nivel de implementacion hemos mantenido un mapper
concreto para dar soporte a la funcion map y reducir el n
umero de optimizaciones
a aplicar dentro del planificador. En futuras versiones se podra intentar una
implementaci
on m
as generica basada exclusivamente en fold siempre que se
ampliara la definici
on para permitir plegar m
ultiples orgenes de datos.
En muchos textos de programacion funcional se definen dos tipos de operaciones de plegado, dependiendo si la lista se recorre empezando por la izquierda
o por la derecha. La definici
on e implementacion de este texto corresponde con
la implementaci
on por la izquierda (foldl) por el modo en que Hadoop tiene
de leer los archivos de datos dentro de MapReduce. A pesar de esta limitaci
on, es posible realizar una implementacion de foldr invirtiendo el orden de la
secuencia con una ronda de MapReduce adicional para ordenar los datos.
13

2.4.

Operaciones adicionales

Complementando las operaciones basicas map, filter y fold se definen en


este punto una serie de operaciones complementarias establecidas con el objetivo
de ampliar la funcionalidad basica de Kai. Estas operaciones estan definidas
haciendo uso u
nicamente de las tres primitivas antes mencionadas y por tanto
es f
acil observar que no requeriran de una implementacion adicional en terminos
de MapReduce.
El criterio para la elecci
on de estas operaciones adicionales esta basado en
los trabajos realizados sobre LINQ [3], el lenguaje SQL [15] y otras operaciones
del prelude de Haskell [33].

2.4.1.

Identidad

Representado como ida , es una funcion que no altera la secuencia de entrada.


La funci
on no existe como implementacion en el codigo, se utiliza para poder
definir otras funciones de este apartado.

2.4.2.

id a : A A

(2.21)

a 7 a, a A

(2.22)

Orden y mezcla

Sort. Realiza la ordenaci


on de una secuencia de datos mediante un criterio
fc especificado. Intuitivamente el criterio fc es una funcion que indica si un
elemento es igual, menor o mayor a otro elmento de la secuencia, funcion que
ha de comprobarse para todos los pares de la secuciencia.
La definici
on de la funci
on empleara la fase de shuffle-and-sort de Hadoop
para realizar la ordenaci
on, por lo que es posible definirla en terminos de map
y la funci
on identidad ida . Igualmente, debido a que el shuffle-and-sort de la
definici
on 2.76 depende de los tipos especificados a la salida del mapper podemos
omitir la especificaci
on de fc .
sort : (fc [A] [A])

(2.23)

sort([a]) = map(id a , [a])

(2.24)

Reverse. Dada una secuencia en un order arbitrario, reverse produce la


misma secuencia pero en orden inverso. Los datos iniciales no hace falta que
esten ordenados. Para realizar la implementacion hemos utlizado la misma aproximaci
on que con sort. Se utiliza un a funcion map para pasar los datos a traves
de un mapper y configuramos el job de Hadoop para utilizar una funcion que
ordena los datos en descendentemente.
reverse : (fc [A] [A])

(2.25)

reverse([a]) = map(id a , [a])

(2.26)

Shuffle. Mezcla aleatoriamente un conjunto de datos dado. Al igual que


con sort y reverse, podemos utilizar una funcion de orden como configuracion
del job de Hadoop de forma que los elementos a la salida del mapper queden
completamente desordenados. La funcion tambien se establece a partir de emph.

14

2.4.3.

Filtros

Distinct. Elimina elementos duplicados de la secuencia de entrada. La operaci


on se compone en base a fold y sort. Teniendo una secuencia de elementos
ordenados podemos recorrerla para establecer si un elemento b pertenece a la
secuencia y agregarlo cuando corresponda.

2.4.4.

distinct : [A] [A]

(2.27)

distinct([a]) = fold (f, , sort([a]))



[a]0 : b [a]0 si b
/ [a]
f ([a], b) =
[a]
si b [a]

(2.28)
(2.29)

Cuantificadores

All. La operaci
on all comprueba que todos los elementos de una secuencia
cumplen una condici
on dada, o tambien a [a] : f (a) = 1. La operacion se
compone en base a filter y count.
all : (f [A]) Bool

(2.30)

f : a Bool

(2.31)

all(f, [a]) = g(count(filter (f , [a])))



0 si x =
6 0
g : I Bool, g(x) =
1 si x = 0

(2.32)
(2.33)

Any. La operaci
on all comprueba que al menos un elemento de la secuencia
cumple una condici
on dada, de forma que a [a] : f (a) = 1. La operacion se
compone en base a filter y count, su descripcion es practicamente igual a all
pero modificando ligeramente la funcion g.
any : (f [A]) Bool

(2.34)

f : a Bool

(2.35)

any(f, [a]) = g(count(filter (f, [a])))



0 si x = 0
g : I Bool, g(x) =
1 si x 1

(2.36)
(2.37)

Contains. Establece que un elemento a A pertenece a una lista determinada. La operaci


on hace uso de any.
contains : (A [A]) Bool

(2.38)

contains(a, [a]) = any(a 7 g(a), [a])



0 si x = 0
g : A Bool, g(x) =
1 si x 1

(2.39)

15

(2.40)

2.4.5.

Particionado

Take. Recoge los n primeros elementos de una secuencia. Si la secuencia


est
a vaca, devuelve una secuencia vaca.
take : ([A] i) A

(2.41)

take([a], i) = f old(f, (, i), [a])



(a, i 1) si i > 0
f ((i, a), (i, [a])) =
(a, i 1) si i 0

(2.42)
(2.43)

Skip. Ignora los n primeros elementos de una secuencia. Si n es mayor que


la longitud de [a] entonces devuelve una secuencia vaca.

2.4.6.

skip : ([A] i) A

(2.44)

skip([a], i) = f old(f, (, i), [a])

(2.45)

f (i, a) = (, i 1) si i > 0 sino a

(2.46)

Agrupaci
on

Group. Agrupa un conjunto de datos en funcion de una clave determinada


perteneciente a la estructura de datos de un elemento del conjunto. La clave de
ordenaci
on se establece mediante una funcion g que extrae el valor a partir de
un elemento del conjunto.
group : ([A] g) [B]

(2.47)

group([a], g) = fold (g, [], [a])

(2.48)

La funci
on se implementa haciendo uso de fold, de forma que podamos
aprovechar la emisi
on de claves desde el mapper para que a cada reducer le
llegue un conjunto perteneciente a dicha clave. El mapper de referencia asociado
a fold se configurar
a para instanciar la funcion g y aplicar la extraccion de la
clave de agrupaci
on.
La funci
on f es una funci
on que a
nade los elementos de [a] a la lista una vez
llegan al reducer. El valor de salida B es una tupla que contiene la clave que ha
extrado la funci
on g y una lista con los elementos asociados a esa clave, de tipo
A.
Flatten. La operaci
on tiene un comportamiento opuesto al de group. Dada
una lista formada por listas de tipo T , genera una u
nica lista de tipo T .

2.4.7.

flatten : [[A]] [A]

(2.49)

flatten([[a1 ], [a2 ], . . . , [an ]]) = [a]

(2.50)

a [[a1 ], [a2 ], . . . , [an ]] a [a]0

(2.51)

Agregaci
on

Sum. Realiza una suma de todos los elementos de la secuencia.


sum : [A] i, i R

(2.52)

sum([a]) = fold (f, 0, [a])

(2.53)

f : A A, f (r, a) = r + a

(2.54)

16

Min. Obtiene el menor elemento de una secuencia dada. La operacion requiere simplemente ordenar todos los elementos de la lista y se obtener el primero
de ellos.
min : [A] i, i R

(2.55)

min([a], i) = take(sort([a]), 1)

(2.56)

Max. Obtiene el mayor elemento de una lista, siguiendo un orden natural.


La definici
on es an
aloga a min, pero ordenando la lista descendientemente.
max : [A] i, i R

(2.57)

max ([a], i) = take(reverse(sort([a])), 1)

(2.58)

Count. Obtiene una cuenta del n


umero de elementos que contiene una lista.
count : [A] i

2.4.8.

(2.59)

count([a], i) = fold (f, 0, [a])

(2.60)

f : A A, f (r, a) = r + 1

(2.61)

Generaci
on

Range. Genera una secuencia consecutiva de n


umeros enteros desde i hasta
j, siendo i > j. En caso contrario devuelve una lista vacia.
range : Z2 [Z]

(2.62)

range(i, j) = map(i 7 f (i, j), )

(2.63)

f : [Z], f (i, j) = [i, i + 1, . . . , j 1, j]

(2.64)

Repeat. Esta funci


on tambien genera una secuencia, pero en lugar de utilizar un intervalo, utiliza un valor definido que se repite tantas veces como
especifiquemos.

2.4.9.

repeat : A Z [A]

(2.65)

repeat(a, i) = map(i 7 f (i, a), )

(2.66)

f : [Z], f (i, A) = [A1 , A2 , . . . , Ai ]

(2.67)

Combinaci
on (Experimental)

Existen tres operaciones, Union, Zip y Join que combinan dos orgenes de
datos distintos para producir una u
nica secuencia de salida. Por su naturaleza y
por la propia implementaci
on de Hadoop dichas operaciones requieren algunos
ajustes especiales dentro de la generacion de mappers y reducers. En las tres
definiciones siguientes comentaremos esos ajustes a nivel de Job de Hadoop,
para posteriormente ampliar los detalles cuando comentemos la implementacion
interna. Ambas tres utilizan exclusivamente la operacion map.
Union. Realiza la concatenacion de dos listas. No se garantiza el original
orden de los elementos en cada una de las listas. La operacion no requiere una
configuraci
on especial del job.
union : ([A] [A]) [A]

(2.68)

union([x], [y]) = [c] map(ida , [c])

(2.69)

17

Zip. La funci
on toma dos orgenes de datos distintos y devuelve una lista
combinando uno a uno los elementos de ambas listas en la tupla (a, b) C,
donde a A y b B. Tal y como hemos mencionado previamente la funcion
requiere una configuraci
on especial en el Job que permita utilizar dos orgenes
de datos en una estructura combinada.
zip : ([A] [B]) [C]

(2.70)

zip([a], [b]) = [c] map(ida , [c])

(2.71)

Join. Permite unir dos orgenes de datos distintos [a] A y [b] B utilizando una funci
on que establece la correspondencia entre ambos conjuntos a
nivel de elemento individual. La funcion sigue el mismo comportamiento que
una inner join de SQL92. Para permitir la implementacion se ha optado por
emplear una tecnica denominada map-side join.

2.5.

join : ([A] [B]) [C]

(2.72)

join([a], [b]) = [c] map(ida , [c])

(2.73)

Hadoop y MapReduce

El contexto donde se ejecutan las operaciones de Kai es Hadoop, cuya definici


on vamos a sacar de la p
agina web del proyecto:
Apache Hadoop es una biblioteca que permite el procesado distribuido
de grandes conjuntos de datos sobre cl
usteres de computadores utilizando modelos de programaci
on simples. Est
a dise
nado para escalar
desde servidores individuales a miles de m
aquinas, ofreciendo almacenamiento local y computaci
on. En lugar de apoyarse en el hardware
para ofrecer alta disponibilidad, la biblioteca est
a dise
nada para detectar y manejar fallos a nivel de aplicaci
on, permitiendo establecer
servicios de alta disponibilidad encima de cl
usteres de computadores,
asumiendo fallos en cualquiera de los nodos implicados.
Hadoop permite ejecutar aplicaciones distribuidas dentro de un marco denominado MapReduce, que divide la ejecucion en dos fases diferenciadas. El
conjunto de datos a procesar se divide en una secuencia de pares que contiene
una clave u
nica identificando cada registro y el propio valor del registro asociado a dicha clave. El intercambio de informacion entre fases de map y de reduce
se establece en torno a estos pares de claves, introduciendo una fase intermedia denominada shuffle-and-sort que garantiza la localidad de claves semejantes
dentro del mismo nodo y tambien la ordenacion de los datos.
En la secci
on actual modelaremos de forma teorica las operaciones de MapReduce y estableceremos una correspondencia directa con nuestras operaciones
primitivas filter, map y fold. El objetivo es clarificar la posterior implementaci
on y garantizar que si esas tres primitivas son directamente traducibles a
MapReduce, entonces el resto de operaciones soportadas por Kai tambien se
pueden expresar en terminos de filter, map y fold y por tanto ser ejecutadas
como una secuencia de trabajos estandares de Hadoop.

18

2.5.1.

MapReduce

La operativa de Hadoop se establece entorno a una tecnica desarrollada por


Google para sus aplicaciones internas denominada MapReduce [13]. En los programas organizados utilizando las bibliotecas MapReduce de Hadoop es necesario extender dos clases, correspondientes a cada una de las fases de ejecucion:
Mapper y Reducer.
La clase Mapper que definamos debe aceptar una clave y un valor asociado
a esa clave y devolver un par de valores tambien en forma de clave y valor.
Ese par de clave/valor a la salida del mapper se utilizara para distribuir los
datos a distintos reducers en funcion de la clave elegida. En cualquier programa
MapReduce existir
a un n
umero indeterminado de mappers y reducers que se
ejecutar
an en distintos nodos fsicos. La entrada de cada reducer esta ligada a
la salida del mapper en funci
on de las claves escogidas.
Veamos un ejemplo para aclarar conceptos. Imaginemos que tenemos una
lista con los datos de deuda p
ublica por municipio, y queremos crear un programa en MapReduce que extraiga el total por cada una de las provincias. El
conjunto de datos guardado en HDFS en CSV tiene la siguiente forma:
...
Valencia,Burjassot,150
Alicante,Torrevieja,710
Valencia,Rocafort,231
Valencia,Foios,142
Alicante,Javea,341
..

Definiremos ahora un mapper, en pseudo-codigo, que recibira cada lnea por


separado dentro de la funci
on map y emitira un par clave/valor con la provincia
como clave y el gasto como valor.
class GastoPorProvinciaMapper extends Mapper<Long,Text,Text,Integer> {
map(Long posicion, Text linea, Context context) {
String[] registro = linea.split(",")
context.write(new registro[0], registro[3]);
}
}

Cada lnea del archivo de entrada se distribuira a una instancia de la clase


distinta. No hay garanta alguna de cuantas instancias habra en tiempo ejecucion
y se asume que la funci
on map es thread-safe.
El reducer asociado al mapper anterior recibira una lista de valores por cada
clave que ha emitido el mapper. Como la clave de salida del mapper es el nombre
de la provincia, cada instancia del reducer recibira una lista con todos los valores
correspondientes a los distintos municipios de la provincia.
class GastoPorProvinciaReducer extends Reducer<Text,Integer,Text,Integer> {
reduce(Text provincia, Iterable<Integer> gastosMunicipios) {
int total = 0;
for (Integer gasto : gastosMunicipio)
total = total + gasto;
context.write(provincia, total);
}
}

La salida de cada reducer corresponde a los datos agregados para una u


nica
provincia. Entre la ejecuci
on del mapper y la del reducer Hadoop ha agrupado
los datos por cada una de las provincias encontradas. Existe garanta de que
19

todos los datos de una misma provincia terminaran en la misma instancia de


la clase reducer. Esta fase de intercambio de datos entre mappers y reducers se
denomina shuffle-and-sort.
Alicante,Torrevieja,710
Valencia,Rocafort,231

Mapper 2

Valencia,Burjassot,150

Mapper 3

Valencia,Foios,142
Alicante,Javea,341

Mapper 1

Clave

Valor

Valencia

142

Alicante

341

Reducer 1
Clave

Valor

Valencia

192

Reducer 6

Clave

Valor

Valencia

142

Valencia

150

Shue & Sort

Reducer 3

Figura 2.1: Pasos en la ejecucion en MapReduce

2.5.2.

Modelo te
orico de MapReduce

Aunque en un principio podra parecer que las operaciones de map y fold se


corresponden uno a uno con el paradigma MapReduce de Hadoop, no parece ser
esta la realidad cuando las examinamos con detalle. El artculo de R. Lammel
[29] contiene un desarrollo mucho mas exacto de estas diferencias, en la seccion
actual esbozaremos las lneas generales de por que ambos modelos son diferentes
y c
omo la implementaci
on de MapReduce de Hadoop difiere de las operaciones
tradicionales de Map y Reduce de la programacion funcional.
Las clases Mapper y Reducer de las bibliotecas cliente de Hadoop contienen
firmas de metodo que pueden darnos una idea de como podramos estructurar
el modelo te
orico. Se omite parte del codigo por simplicidad:
class Mapper<K1, V1, K2, V2> {
void map(K key, V value, Context context)
class Context extends MapContext<K1, V1, K2, V2>
}
class Reducer<K2, V2, K3, V3> {
abstract void reduce(K2 key, Iterable<K2> values)
class Context extends ReducerContext<K2, V2, K3, V3>
}

Por otro lado, una definici


on informal de MapReduce en terminos de funciones [49] queda de la siguiente manera:
map : (1 , 1 ) [(2 , 2 )]

(2.74)

reduce : (2 , [2 ]) [(3 , 3 )]

(2.75)

Como puede verse los tipos de datos coinciden entre la salida del mapper
y la entrada del reducer, aunque lo normal es que tanto la entrada como la
salida del procedimiento difiera. Es observable tambien que la funcion de map
no se puede componer con la de reduce, dado que la salida del mapper y la
20

entrada del reducer difieren. Internamente en Hadoop existe un procedimiento


llamado shuffle que se ejecuta justo entre el mapper y el reducer, agrupando
[(2 , 2 )] usando las distintas claves 2 generadas en el mapper. De esta forma
se garantiza que el el reducer siempre recibe todos los valores generados 2
independientemente del mapper que los emitio.
Por conveniencia definiremos la funcion shuffle de forma que podamos componer las funciones map y reduce, quedando las firmas de las operaciones como:
shuffle : [(2 , 2 )] (2 , [2 ])

(2.76)

mapreduce : (1 , 1 ) [(3 , 3 )]

(2.77)

Para poder definir la funci


on mapreduce en terminos de map, shuffle y reduce
necesitamos encadenar dichas funciones mediante un operador de composicion
cuya definici
on hemos extraido de [31]. La funcion de composicion f g, leido
como f compuesto de g, se define como:
f g = x f (g x)

(2.78)

Llegamos pues a la definicion de la funcion mapreduce que usaremos posteriormente para definir las primitivas basicas de Kai:

2.5.3.

shuffle map = x shuffle(map x)

(2.79)

mapreduce = x reduce((shuffle map)(x))

(2.80)

mapreduce = reduce shuffle map

(2.81)

Correspondencia del modelo con la implementaci


on
interna de Hadoop

El modelo anteriormente descrito puede llevarse a la implementacion para


ver si existen otros detalles que puedan establecer differencias significativas. Un
repaso r
apido a la implementacion muestra que la clase mapper define en Java
una funci
on que no devuelve nada, pero que presumiblemente cambia el estado
interno del contexto con el fin de producir resultados a la salida del mapper.
public void map(K key, V value, Context context) {
...
context.write(outKey, outValue);
}

La definici
on 2.3.2 de map que hemos introducido para Kai establece una
funci
on que comprende un u
nico tipo de entrada y un tipo de salida. Podramos
agrupar el par clave/valor para simular un solo tipo de datos y ajustarnos a
la definici
on marcada por 2.16, pero comprobamos inmediatamente que debido
a que la escritura en el contexto se realiza de forma manual, y no como parte implcita en el retorno de la funcion, podramos producir una funcion que
devolviera un n
umero distinto de elementos de la secuencia de entrada.
De igual forma, la funci
on que da pie al reducer requiere definir una funcion
de la forma:
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context) {
...
context.write(outKey, outValue)
}

21

Los datos de entrada de esta funcion son completamente dependientes de


la clave de salida del mapper y como ya hemos visto en el modelo planteado
en 2.76 el dominio de los valores aceptados viene determinado por el mapper.
Las diferencias con la funci
on fold de Kai a nivel de firma de la funcion y
tipos de datos son notables, acrecentandose esta diferencia al examinar los tipos
de retorno de la funci
on. Nuevamente, y al igual que con los mappers, no hay
garantia de compromiso alguno por parte de reducer en cuando a la estructura
de datos devuelta1 .
En cuanto a los datos de entrada, Hadoop permite hacer uso de multiples
inputs como entrada de los mappers y m
ultiples outputs del proceso si as se
configura para que sea el caso. Si bien esto es una caracterstica bien recibida en
terminos de funcionalidad aportada, con ello introduccimos toda una serie de
efectos colaterales que pueden darse y que la propia definicion de los mappers y
reducers no evita.
Al poder combinar distintas fuentes de datos de entrada nada nos previene
de tener listas con tipos completamente heterogeneos, por lo que aunque podamos escribir c
odigo de forma defensiva para prevenir errores, nos interesara
mucho m
as una aproximaci
on donde el compilador pudiera forzar la correcion
del programa.

1 La funci
on puede igualmente lanzar excepciones en cualquier momento aunque estas no
est
an incluidas dentro de la definici
on. Dejamos el control de excepciones de las funciones de
Kai para un trabajo posterior donde puedan aplicarse un comportamiento mon
adico[30] que
encapsulen la gesti
on de errores como parte del funcionamiento de la funci
on.

22

Captulo 3

Implementaci
on
Winter is comming.
Ned Stark, Game of Thrones

Hemos visto en el captulo anterior el conjunto de operaciones soportado por


Kai y una definici
on medianamente formal de como expresar todas las operaciones en funci
on de filter, map y fold. A lo largo de este captulo haremos un
repaso interno por la implementaci
on de esas tres primitivas y veremos c
omo es
posible construir el resto de operadores sin necesidad de escribir nuevos mappers
y reducers.

3.1.
3.1.1.

Vista General
Hadoop, HDFS y MapReduce

Kai se ejecuta sobre la infraestructura basica de Hadoop. En lneas generales, Hadoop no es m


as que un framework para ejecutar aplicaciones en grandes
cl
usteres construidos a partir de hardware estandar que puede encontrarse en
estaciones de trabajo o servidores de gama baja (tambien conocido como comodity hardware). El framework proporciona dos piezas basicas que hacen posible
la ejecuci
on de Kai: La primera de ellas es HDFS, que es un sistema distribuido
consistente y con tolerancia a particiones implementado en Java con algunas
extensiones de bajo nivel. La segunda pieza que se proporciona es MapReduce,
que es un marco para la creacion de aplicaciones de procesado distribuido basado en propagar pares de clave y valor en dos fases claramente diferenciadas:
una de segregaci
on de datos y otra de recoleccion de resultados.
HDFS se apoya en dos servicios para garantizar consistencia y tolerancia a
particiones. Existe un servicio denominado namenode, tpicamente representado
por una m
aquina fsica en el sistema, que guarda un mapa de las entradas del
sistema de archivos y coordina las operaciones para que el cliente obtenga una
vista consistente de los datos. El segundo servicio, denominado datanode suele
comprender desde unas pocas a cientos de maquinas cuya responsabilidad es
mantener varias copias de los datos del sistema de archivos. Cuando una de las
replicas falla, el nodo es retirado por el sistema sin que ello suponga parar las
tareas en ejecuci
on o perder datos. Despues de una caida, el cluster se rebalancea
23

para seguir garantizando la disponibilidad de los datos. La entrada de nuevos


nodos de datos en el cluster es un proceso que puede hacerse practicamente en
caliente y no requiere de reinicios de las maquinas o paros en el servicio.
El hecho de disponer de diversas replicas de los datos es un factor crucial para MapReduce, ya que debido a esta distribucion de los datos se minimiza en lo
posible la cantidad de datos que viajan por la red. Cada trabajo de MapReduce
implica varias fases y requiere que el software se distribuya a los distintos nodos
implicados en su ejecuci
on. Para coordinar estos trabajos, existen dos servicios
denominados tasktracker y jobtracker. Si bien el sistema admite distintas formas de configuraci
on de los servicios, el despliegue estandar establece un u
nico
jobtracker encargado de la coordinacion de trabajos entre nodos. Igualmente,
existen tantos tasktracker como nodos de trabajo en el sistema y lo habitual
es disponer en cada uno de los nodos tanto un servicio de datanode como un
tasktracker. Cada tasktracker se configura con un n
umero determinado de tareas
simultaneas de Map o de Reduce que pueden ejecutarse simultaneamente. Este
n
umero de slots disponibles por nodo se consume de forma distinta dependiendo
de si la tarea es un Map o un Reduce.
Application Layer
MapReduce
Nodo 1

Nodo 2

TaskTracker

TaskTracker

DataNode

DataNode

...

Nodo N

SPF

TaskTracker

JobTracker

DataNode

NameNode

HDFS

Figura 3.1: Distribucion de componentes de Hadoop


La comunicaci
on entre los cuatro servicios se realiza mediante RCP estandar
con un protocolo especfico.
Una de las crticas m
as fuertes a Hadoop parte de la idea de que para la
coordinaci
on de tareas y operaciones de entrada/salida se requiere de un sistema
central. Tanto el namenode como el jobtracker son sistemas cruciales para el
funcionamiento del cluster y la caida de cualquiera de los dos nodos provoca
la perdida total de los datos del cluster o la perdida de las tareas en ejecucion.
Debido a esto, en versiones mas recientes de Hadoop se ha trabajado en la
alta disponibilidad del sistema para tolerar caidas del namenode sin producir
cat
astrofes.
Para obtener m
as informacion sobre la infraestructura de Hadoop y la operativa interna puede consultarse el libro de Eric Sammers [42].

24

3.1.2.

El papel de Kai en Hadoop

Kai se ha dise
nado con el objetivo de simplificar la escritura de trabajos
MapReduce que se ejecutar
an dentro de la infraestructura de Hadoop. Cualquier
flujo de ejecuci
on que se define en Kai resulta en la aplicacion de uno o mas
trabajos MapReduce en el cluster. Los trabajos enviados son completamente
est
andares y dependiendo del tipo de operacion a ejecutar requeriran distinto
c
odigo a nivel de mapper o reducer. Por ejemplo, para el caso de filter la
traducci
on involucra un solo trabajo con un u
nico mapper, no existe reducer.
Para ello se han dise
nado una sere de mappers y reducers canonicos que
reciben tanto los datos como las operaciones a ejecutar. Por ejemplo, para el caso
de filter recibiremos un u
nico registro de datos a procesar y una funcion que
decide si ese registro en particular cumple con el filtro o no. De esta forma, con
cada ejecuci
on de trabajos en Kai distribuiremos no solo los datos a manipular
sino tambien las funciones definidas en cada caso para esas manipulaciones que
se han establecido como parte del flujo. En casi la totalidad de los casos, los
resultados intermedios tambien se transfieren durante la ejecucion.
Kai Operations
Sort

Max

Take

Range

Any

Join

GroupBy

...

Filter

Transform

Fold

Kai Controller & Scheduler


FilterMapper

TransformMapper

FoldMapper
FoldReducer

FilterJobFact.

TransformJobFact.

FoldJobFactory

Hadoop MapReduce API

Figura 3.2: Componentes basicos que intervienen en Kai


La transferencia de los programas a los distintos nodos del cluster se realiza
usando los procedimientos habituales de Hadoop e involucra el envo a cada
nodo participante en el proceso de un u
nico JAR conteniendo tanto Kai como
las propias funciones a ejecutar. En cuanto a los datos es importante se
nalar
de nuevo que a diferencia de otros sistemas, se parte de la idea inicial de que
cada mapper o cada reducer consumira los datos que esten disponibles en cada
momento dentro del nodo de datos.
Kai usa Apache Avro como mecanismo de serializacion y transferencia de
datos. Avro es un formato de serializacion binario dise
nado con una serie de
propiedades que lo hacen id
oneo para su uso en sistemas distribuidos, tal y como
veremos en la secci
on 3.5.1. La eleccion de Avro como formato de transferencia
se fundamenta en dos puntos principales:
A diferencia de otros formatos como Thrift o Protocol Buffers, Avro per25

mite obtener la estructura fsica de los datos examinando las propiedades


de una clase de Java, sin necesidad de definir previamente el esquema a
emplear. El proceso contrario es tambien posible debido a que el empaquetado de los datos incluye metadatos que permiten reconstruir la estructura
correctamente.
Los dise
nadores de Avro estan detras de Hadoop y la perspectiva es dar
mejor soporte al formato en futuras versiones. Si bien el formato por excelencia de Hadoop es el texto plano (registros separados por lneas), Avro
se encuentra en muy buena posicion para mejorar la situacion.
Uno de los puntos fuertes de Kai es que la biblioteca se ha dise
nado con el
prop
osito de poder migrarla a otros sistemas de procesamiento sin un esfuerzo
excesivo. La definici
on que se ha realizado de las operaciones esta completamente
desligada de la infraestructura de Hadoop y por tanto no se descarta buscar otras
plataformas en futuras versiones.

3.2.

Definici
on de trabajos

Kai est
a implementado en Scala con algunas clases de soporte en Java. Scala
es un lenguaje de programaci
on funcional que se ejecuta sobre la JVM, por lo
que es posible combinar ambos dentro de un mismo programa. Se escogio Scala
como lenguaje de implementacion no solo por su caracter funcional, sino tambien
por el excelente sistema de tipos que tiene [34]. Debido tambien a que Hadoop
est
a escrito en Java, Scala se encuentra en posicion de poder interoperar de
forma nativa con las bibliotecas de cliente, ya que no existe distincion alguna
entre el c
odigo generado por el compilador de Java y el de Scala. Inicialmente se
consideraron otros lenguajes como Groovy o Clojure, pero se descartaron bien
por tener un sistema de tipos y estructuras de datos mas primitivo o bien por
que la velocidad de ejecuci
on y carga del codigo es excesivamente lenta.

3.2.1.

Flujos de ejecuci
on

Kai proporciona todo un conjunto de operaciones para manipular y transformar conjuntos de datos. Los trabajos de manipulacion en Kai se organizan en
torno a secuencias de operaciones sobre un flujo de datos, internamente llamado
DataFlow. Todos los flujos de ejecucion que se definen en Kai se descomponen
en uno o varios trabajos de MapReduce listos para ser enviados al cluster.
Tomemos como ejemplo el siguiente flujo de ejecucion para tener una idea
m
as precisa de c
omo funciona internamente:
class Fund(val name: String, val country: String, nav: Double)
val flow = new DataFlow[Fund]
val result: DataFlow[Double] = flow
.filter { f2 => f2.country == "US" }
.transform { f => f.nav }
.filter { i => i > 1000 }
.fold(0) { (r, i) => r + i }

26

La secuencia se compone de un filtrado por el codigo del pas, una transformaci


on de todo el objeto Fund a u
nicamente el precio, un nuevo filtrado para
seleccionar precios > 1,000 y por u
ltimo la suma de todos los precios obtenidos.
Podra parecer l
ogico pensar que la primera operacion que Kai ejecutara sera cargar los datos dentro del objeto DataFlow instanciado, pero nada mas lejos de
la realidad. Es muy importante se
nalar que el flujo de datos anterior no son
instrucciones imperativas que van ejecutandose una tras otra, sino que u
nicamente describen una secuencia de transformaciones y operaciones a realizar en
el cluster. Una secuencia de operaciones futuras a ejecutar.
En el ejemplo anterios vemos como el usuario ha definido un flujo de datos
de tipo DataFlow, parametrizando con un tipo T que en este caso es una clase
llamada Fund que define un contenedor de datos y su estructura interna: name,
country y nav (el precio del fondo de inversion).
Observamos tambien que el tipo de datos de salida result es una contenedor completamente distinto del original y que conforme vamos encadenando
distintas primitivas el tipo de datos va mutando. Podemos reescribir el flujo
anterior para poder ver dichas transformaciones:
val
val
val
val
...

entrada
paso_1:
paso_2:
paso_3:

= new DataFlow[Fund]
DataFlow[Fund] = entrada.filter(f2 => f2.country == "US")
DataFlow[Double] = paso_1.transform(f => f.nav)
DataFlow[Double] = paso_2.filter(i => i > 1000)

Si desglos
aramos el paso 3 para poder ver las estructuras internas, omitiendo
algunos detalles podemos observar que tipos de datos guarda la clase DataFlow:
(FilterFunc@49ff0dde, (class playground.Fund, class playground.Fund))
(TransformFunc@73901437, (class playground.Fund, double))
(FilterFunc@781f6226, (double, double))

Parece que el c
odigo toma algo mas de claridad, conforme se van a
nadiendo
funciones a la ejecuci
on se va componiendo una secuencia que define la transformaci
on de datos a aplicar sobre el conjunto inicial. La salida del paso anterior n1
puede usarse en el paso siguiente n2 , siempre que el sistema de tipos lo permita.
La ventaja fundamental de usar un sistema de tipos como el de Scala para
definir estas operaciones es que el compilador nos avisara de cualquier posible
error que comentamos al combinar operaciones. Si los tipos de datos del flujo de
ejecuci
on no son compatibles entre si, o la estructura de datos fuera incorrecta
(pongamos, intentamos acceder a country en un objeto sin ese campo) nuestro
programa no compilar
a correctamente y no sera necesario lanzar el trabajo en
el cluster para comprobarlo.
Resumiendo, en cualquier flujo de datos que define un usuario se obtiene
como resultado:
Una secuencia de operaciones a ejecutar
Una lista de par
ametros a esas operaciones, donde habitualmente dichos
par
ametros son a su vez otras funciones En el paso 1 del ejemplo, la funcion
filter est
a aceptando otra funcion anonima f 2 para filtrar el pas.
Una secuencia de estructuras intermedias que se obtienen de la transformaciones producidas.

27

Con toda esa informaci


on Kai ya puede transformar y crear distintos trabajos
en Hadoop, en forma de jobs de MapReduce y devolver el resultado.
A continuaci
on veremos c
omo es posible encadenar las operaciones y como
estas se traducen a mappers y reducers.

3.2.2.

Piezas b
asicas: DataFlow y Mutable

Hay dos tipos fundamentales definidos en el sistema: la clase DataFlow[T]


y el trait Mutator. Ambos dos representan las operaciones y los datos dentro
del flujo de datos. La clase DataFlow[T] contiene internamente una lista de
operaciones a ejecutar. Se ha sobrecargado el constructor para poder encadenar
operaciones como veremos m
as adelante. La definicion de la clase es bastante
directa y no lleva a error, se omiten algunas partes por brevedad:
trait Mutable
class DataFlow[T] {
var operations: List[Mutable] = List()
def this(flow: DataFlow[_], mutable: Mutable) = {
this()
operations = operations ++ (flow.operations :+ mutable)
}
}

El trait Mutator act


ua como marcador y u
nicamente existe para poder
manipular otras operaciones (filter, map y fold ) sin necesidad de conocer su
tipo de datos concreto. List[Mutable] es por tanto1 una lista heterogenea
que contiene operaciones derivadas. El paso final del flujo de ejecucion, que es
habitualmente el que se enva al cluster, contiene toda la informacion necesaria
para poder realizar la transformacion a distintos trabajos de MapReduce seg
un
convenga.

3.2.3.

Construcci
on del flujo de operaciones

Como se construye y define pues el flujo de operaciones? Por simplicidad,


vamos a estudiar la operaci
on filter y veremos que clases son necesarias.
Supongamos que se define el siguiente flujo:
var flow: DataFlow[Fund]
val paso1 = flow.filter { f => f.country == "US" }

En lugar de atacar directamente a flow.filter intentaremos describir la


operaci
on desde dentro. En el n
ucleo de todo se define la clase FilterFunc que
u
nicamente se encarga de mantener una funcion lambda con el mismo tipo que
la definida en 2.3.1. La clase se ha parametrizado con un tipo T para poder hacer
uso de objetos concretos dentro de la lambda en nuestro ejemplo aqui presente
la funci
on lambda que se provee como argumento de filter es la definicion:
f => f.country == "US"

En este caso podemos hacer uso de la inferencia de tipos de Scala, donde


f se interpretar
a como un fondo de inversion, y donde el cuerpo de la propia
funci
on es un tipo booleano.
1 Aunque

resultar
a obvio para los lectores que conocen Scala, ampliaremos la explicaci
on
para el resto de forma que vayamos introduciendo conceptos clave necesarios para posteriores
explicaciones.

28

FilterFunc se define entonces como:


class FilterFunc[T](val predicate: T => Boolean) extends Mutator

En este punto ya tenemos la clase DataFund[T] para encapsular la definicion


de los datos de entrada y clase FilterFunc[T] para definir la funcion de filtrado.
Ambas dos est
an parametrizadas, pero nada garantiza como pueden combinarse
o cual es la funci
on de aplicacion que garantice tipos compatibles. Definiremos
entonces un objeto Filter con una funcion de aplicacion que permita realizar
esa combinaci
on:
object Filter {
def apply[T](flow: DataFlow[T], predicate: (T => Boolean)): DataFlow[T] = {
new DataFlow[T](flow, new FilterFunc(predicate))
}
}

Este tipo de objetos, denominados extractor objects [37] se pueden instanciar


en Scala de la siguiente manera:
val input = new DataFlow[Fund]()
val filtro: DataFlow[Fund] = Filter(input, (f: Fund) => f.country == "US")

Subsecuentes ejecuciones sobre la misma secuencia preservaran el orden de


ejecuci
on, y los tipos, del objeto DataFlow[T] resultante.
Solo nos queda pues simplificar la sintaxis de la funcion anterior proporcionando una conversi
on implcita que permita aplicar la funcion Filter como si se
tratara de un miembro de la clase DataFlow.
object Implicits {
implicit def dataFlowFilter[T](flow: DataFlow[T]) = new {
def filter(predicate: T => Boolean) = Filter(flow, predicate)
}
}

Para map 2 y fold pueden consultarse las mismas funciones implementadas


siguiendo con la idea en los anexos a este trabajo.

3.2.4.

Seguimiento de tipos

Para poder serializar y deserializar correctamente los tipos de datos definidos en el flujo de trabajo es necesario desplegar la estructura de datos y obtener
una lista de tipos de datos implicados en cada paso. Dicha informacion se utilizar
a dentro de los mappers y reducers para en tiempo de ejecucion cargar
los datos, ejecutar la transformacion y devolver los datos como resultado del
proceso.
Vamos a describir un flujo de datos que contenga tres operaciones: dos filtrados y una transformaci
on de datos.
package playground
import es.luisbelloch.kai._
object demo2 {
def main(args: Array[String]) {
val input = new DataFlow[ShareClass]()
2 map

se denomina transform en el c
odigo

29

val flow = input


.filter(s => s.currency == "USD")
.filter(s => s.name.startsWith("A"))
.transform { s => s.price * 2 }
val scheduler: Scheduler = new BasicScheduler
val plan = scheduler.plan(flow)
plan map println
flow.operations zip flow.typeSequence map println
}
}

En el programa anterior se han utilizado las mismas clases que internamente


utiliza Kai para planificar las tareas, con el objetivo de poder examinar el plan
de ejecuci
on devuelto.
Fnc: FilterFunc, Ptr: playground.demo2$$anonfun$1, Junction: (0,1),
Tag: (class playground.ShareClass,class playground.ShareClass), Args: List()
Fnc: FilterFunc, Ptr: playground.demo2$$anonfun$2, Junction: (1,2),
Tag: (class playground.ShareClass,class playground.ShareClass), Args: List()
Fnc: TransformFunc, Ptr: playground.demo2$$anonfun$3, Junction: (2,3),
Tag: (class playground.ShareClass,double), Args: List()

El planificador devuelve tres trabajos a ejecutar, dos filtros y una transformaci


on. Como veremos m
as adelante en la seccion 3.4, cada fase de la ejecucion
necesita conocer los tipos de datos implicados, en la salida anterior se marca
como Tag. El u
ltimo paso que representa una transformacion establece la clase
ShareClass como tipo de entrada y el tipo double como salida. Hasta aqu nada
extra
no.
El problema principal es que la JVM no tiene soporte interno para tipos
genericos y durante la compilacion realiza lo que se denomina type erasure, eliminado cualquier rastro de tipos en clases y funciones parametrizadas. Incluso
aunque Scala es un lenguaje fuertemente tipado esta traduccion a bytecode debe
cumplir con las exigencias de la maquina virtual y por tanto elimina tambien
dicha informaci
on. Recordemos que nuestra clase DataFlow[T] esta parametrizada con un tipo T que u
nicamente existe en el mundo de ficcion del compilador,
pero que luego es completamente simplificado a DataFlow[Object] durante la
ejecuci
on. C
omo podemos recuperar un objeto en un mapper si no sabemos su
tipo de datos?
En otros lenguajes y plataformas, como en C# y el CLR, afortunadamente
se dispone de un sistema de tipos mucho mas rico a nivel de la maquina virtual y
durante la ejecuci
on se realizan todo tipo de comprobaciones de tipos y asumpciones sobre la varianza y covarianza de las operaciones. En el caso de la JVM
para salvar esta limitaci
on se doto de algunas clases que permiten examinar los
tipos mediante reflexi
on durante la ejecucion, aunque su soporte es bastante
limitado. En particular, y por suerte, Scala a partir de la version 2.7.2 incluye
soporte del compilador para poder capturar esa informacion durante la fase de
compilaci
on para que este disponible en tiempo de ejecucion. lo que se denomina
reified types. De esta forma, aunque la JVM no tenga informacion alguna sobre
genericos, nosotros podemos hacer el trabajo de forma medianamente segura.
Para obtener y guardar inforamcion sobre tipos genericos empleados en clases
o funciones es necesario hacer uso de las classes ClassTag y TypeTag, as como

30

de una car
acterstica de Scala que permite a
nadir un parametro a una funcion
de forma implicita, lo que aprovecharemos para obtener informacion del tipo de
datos utilizado.
El ejemplo m
as sencillo lo podemos obtener de la funcion filter vista previamente, funci
on que hemos extendido del ejemplo anterior para a
nadir el
par
ametro implicito denominado tag con informacion del tipo de dato involucrado en la operaci
on:
implicit def dataFlowFilter[T](f: DataFlow[T])(implicit tag: ClassTag[T]) ...
def filter(predicate: T => Boolean)
= Filter(flow, predicate, tag.runtimeClass)
}

De esta forma, cuando construimos la operacion de filtro el u


nico parametro
que interviene en la definici
on es el predicado de la funcion. Implicitamente el
compilador analizar
a y guardara en tag la informacion de tipos que contiene
DataFlow[T] para poder hacer uso de ella mas tarde en tiempo de compilacion.
En la secci
on 3.5 hay m
as informacion sobre como se realiza la deserializacion
y serializaci
on de datos en un formato binario y compacto, pero con tolerancia
a la reflexi
on de tipos.

3.2.5.

Implementaci
on de operaciones en base a primitivas

C
omo vimos en la secci
on 2.4, Kai define una serie de operaciones adicionales
implementadas u
nicamente en funcion de transform, filter y fold.
La implementaci
on de estas operaciones puede encontrarse en dos archivos
distintos. Por una parte, existe una definicion dentro del paquete de clases que
nos permite declarar Scala (en package.scala), para que al importar el paquete principal es.luisbelloch.kai. obtengamos acceso a las declaraciones
implcitas sobre DataFlow.
El caso m
as sencillo lo podemos encontrar en la funcion identidad, definida
en 2.4.1, donde dado un valor de entrada t devolvemos el mismo valor a la
salida. Recordemos que utilizaremos esta definicion de identidad en algunas
funciones para poder implementar la ordenacion de secuencias aprovechando la
fase shuffle-and-sort de Hadoop.
def identity() =
Transform[T,T](flow, t => t, inTag.runtimeClass, inTag.runtimeClass)

Para construir la operaci


on de map, se utiliza directamente Transform en
lugar del acceso flow.transform, principalmente para evitar una llamada recursiva y adicionalmente para evitar el type-erasure. Este modo de definicion lo
veremos tambien en otras operaciones declaradas dentro del package object de
Kai.
Otras operaciones se han implementado fuera del package object para evitar
los mismos supuestos. En el archivo Support.scala podemos encontrar algunas
de estas definiciones, como la de size, que implementa la operacion count de
2.4.7.
def size[T](flow: DataFlow[T])(implicit inTag: ClassTag[T]): DataFlow[Int] = {
Fold[Int,T](flow, (a,b) => a + 1, 0, inTag.runtimeClass, classOf[Int])
}

Observando la declaraci
on anterior vemos que no sera posible utilizar size
como si fuera un metodo declarado en DataFlow[T], aunque si que se permite
31

recibirlo como par


ametro de la funcion. Para permitir el encadenamiento de
operaciones tenemos definido un alias con el nombre count.
def count(): DataFlow[Int] = size(flow)

El uso de ambas dos definiciones produce el mismo resultado; una operacion


de fold sobre la secuencia. Mientras que la funcion size recibe un DataFlow
como par
ametro, la funci
on count permite usar una sintaxis mas conveniente
para el encadenado de operaciones, por lo que las dos declaraciones f1 y f2 son
completamente equivalentes:
val flow = DataFlow[String].filter { s => s.startsWith("A") }
val f1 = size(flow)
val f2 = flow.count()

Otro caso interesante de comentar es el de las operaciones min y max. Si


observamos el c
odigo del package object podemos comprobar que en todas las
operaciones la clase DataFlow se parametriza con un tipo T. Al inicio esta la
declaraci
on dentro de la funci
on implcita op
implicit def op[T,Q](flow: DataFlow[T])(implicit inTag: ClassTag[T]) = new {
def filter(predicate: T => Boolean) =
Filter(flow, predicate, inTag.runtimeClass)
...

Esto provoca que todas las operaciones definidas por el tipo anonimo de
retorno en op esten parametrizadas con un tipo generico T. El problema de
esta definici
on es que es demasiado generica. Como vamos a comprobar si un
elemento es mayor que otro para min y max? Son comparables entre si los tipos
incluidos en todo el universo de tipos en T?
Para solventar el problema hemos creado la clase Sortables, que incluye una
definici
on m
as ajustada de T: tipos genericos que ademas son ordenables. En Scala esta propiedad de ser ordenable se establece mediante el trait Ordered[T].
object Sortables {
class SortableDataFlow[T < % Ordered[T] : Numeric](flow: DataFlow[T]) {
def min()(implicit inTag: ClassTag[T]): DataFlow[T] = {
Support.numericReduce(flow, (a,b) => if (a < b) a else b, ...)
}
def max()(implicit inTag: ClassTag[T]): DataFlow[T] = {
Support.numericReduce(flow, (a,b) => if (a > b) a else b, ...)
}
}
implicit def flowToSortableFlow[T < % Ordered[T]:Numeric](f:DataFlow[T]) =
new SortableDataFlow[T](f)
}

Seg
un la declaraci
on de T anterior, las operaciones max y min aceptan tipos
genericos siempre que puedan ordenarse, o lo que el lo mismo, que implementen el trait Ordered[T] estableciendo el orden entre dos del mismo tipo. En
nuestro caso para max y min esta implementacion viene a traves de Numeric, se
deja la puerta abierta a una definicion mas generica que acepte cualquier otro
tipo.

32

3.3.

Transformaci
on en mappers y reducers

Una vez hemos definido una estructura de datos que nos permite definir el
flujo de ejecuci
on, conteniendo tanto datos como funciones, nos disponemos a
traducir esa estructura en una secuencia de mappers y reducers de Hadoop.
Como hemos visto anteriormente, las primitivas basicas se pueden traducir
de forma directa a funciones map y reduce aunque existen detalles de implementaci
on que requieren algo de esfuerzo para poder conservar intacta la definicion
de los metodos. En caso contrario no podramos componer correctamente el
resto de operaciones definidas en base a las primitivas basicas.
Para ello hemos implementado en Java unos mappers y reducers de referencia que se utilizaran para cargar las distintas primitivas previamente definidas.
En funci
on de los tres tipos de primitivas se han utilizado distintas clases que
implementan las transformaciones requeridas. Por ejemplo, para ejecutar operaciones filter, definidas sobre FilterFunc hemos creado la clase FilterMapper
que contiene toda la funcionalidad necesaria para poder aplicar la funcion de
filtrado dentro del mapper de Hadoop.
Una vez se enva una secuencia de operaciones al cluster, Kai compone una
lista que contiene un conjunto de jobs a lanzar. Cada uno de esos jobs representa
una operaci
on dentro de la secuencia, y a su vez cada uno de los jobs configura
distintos mappers y reducers en funcion de las necesidades. Por ejemplo, como
hemos visto antes la operaci
on filter no requiere de ning
un reducer y u
nicamente instancia un job con un mapper. El n
umero de mappers en ejecucion
depender
a de la topologa del cluster y el tama
no del problema.
Es f
acil observar que, si bien Kai facilita la escritura de trabajos, probablemente lanzar un job por cada una de las operaciones de la secuencia no es la
forma m
as optima de tratar el cluster. Si tomamos como ejemplo sencillo un
par de operaciones de filtrado que se ejecuten secuencialmente observaremos
que probablemente sea posible combinar ambas dos en un u
nico job.
M
as adelante en la secci
on 3.4 veremos algunas opciones para planificar los
trabajos de forma optima, dejando el camino abierto a futuros trabajos que
puedan hacer mejoras en esta lnea.

3.3.1.

Mappers y Reducers de referencia

Todas las operaciones de Kai estan implementadas haciendo uso de cuatro


clases que implementan las clases de Hadoop Mapper y Reducer. Las operaciones
de filter y map se han podido implementar haciendo uso exclusivo del mapper y
obviando el reducer, por lo que el formato de salida del mapper coincide con el
del trabajo. El el caso de la operacion fold se cuenta con mapper y un reducer
especficos.
En los fragmentos de c
odigo que mostraremos a continuacion se omiten algunos detalles de implementacion por brevedad, sobre todo relacionados con
la serializaci
on de objetos, el control de errores y la infraestructura com
un de
Hadoop. El c
odigo fuente completo de Kai puede consultarse en el soporte que
va anexo a esta memoria.

33

Filter
Recordemos que la operacion aceptaba una lista de elementos de tipo T y
por cada uno de ellos aplica una determinada funcion f . El mapper generico
definido, en este caso FilterMapper recibira un elemento arbitrario de la lista
de entrada y aplicar
a la funci
on f . Si el resultado de la funcion es positivo, se
escribe el elemento en el contexto y en caso contrario simplemente se descarta.
protected void map(AvroKey key, NullWritable ignore, Context context)
throws IOException, InterruptedException {
final Object dataEntity = key.datum();
Function1 filter = (Function1)filterClass.newInstance();
Boolean result = (Boolean)filter.apply(dataEntity);
if (result)
context.write(new AvroKey(dataEntity), NullWritable.get());
}

Map
En el caso de la funci
on map, tambien se recibe una lista de elementos de
tipo T y se aplica una funci
on que transforma el elemento T en otro elemento de
tipo Q. Nuevamente podemos prescindir de los valores y utilizar u
nicamente las
claves como par
ametro del Mapper definido. En el codigo la clase que define la
funci
on map se denomina TransformMapper.
protected void map(AvroKey key, NullWritable ignore, Context context) {
final Object dataEntity = key.datum();
Function1 transform = (Function1)transformClass.newInstance();
Object result = transform.apply(dataEntity);
context.write(new AvroKey(result), NullWritable.get());
}

Fold
En el caso de fold la operacion es un poco mas compleja, pues requiere
definir tanto un mapper como un reducer para permitir la ejecucion. Recordemos
que fold recibe una lista de elementos de tipo T, un valor inicial del mismo tipo
que el resultado final Q y una funcion que recibe un elemento del tipo T y
devuelve Q.
La versi
on can
onica de fold la hemos planteado de manera que la salida del
mapper no produce ning
un tipo de agrupacion a la entrada del reducer. En su
versi
on b
asica, podemos hablar que la funcion de shuffle del modelo formal se
puede reemplazar por una funcion identidad id a .
Expresaremos en pseudo c
odigo el mapper y el reducer para fold3 :
map(T, null)
emit(null, T)
shuffle(null, T)
emit(null, List[T] + T)

reduce(null, List[T])
acc = null
for e in List[T]
acc = predicate(acc, e)
emit(null, acc)

Observando el pseudo-c
odigo anterior comprobamos como la operacion de
fold puede producir un resultado completamente distinto en funcion de los
par
ametros especificados y de la gestion de las claves entre el mapper y el reducer.
3 Puede consultarse la implementaci
on completa de las clases que implementan fold en
FoldMapper.java y FoldReducer.java.

34

Veamos tres situaciones adicionales que pueden producirse y que explotaremos para producir algunas operaciones adicionales a partir de fold:
1. Si el mapper emite una serie de claves determinadas en lugar de un valor
nulo, los datos se distribuyen a distintos reducers y por lo tanto es posible
producir una salida agrupada.
2. Si el reducer recibe una lista vaca no se ejecutara la funcion de acumulaci
on. En combinaci
on con la primera de las situaciones es posible agregar
los resultados por cada uno de los grupos o simplemente generar una secuencia de salida con distintos grupos.
3. Si el reducer recibe una lista con un u
nico valor desde la salida del mapper,
pero el valor inicial es una lista vaca, podemos producir la operacion
inversa a la agrupaci
on conocida como unwrap o flatten.
La decisi
on de estas modificaciones esta delegada al planificador, dentro de
la funci
on inferArgs pues es necesario transferir esa configuracion a las clases
FoldMapper y FoldReducer en tiempo de ejecucion. Cada una de las funciones que requieran alterar esas condiciones pueden especificar dicha informacion
como parte de la clase FoldFunc.

3.3.2.

Distribuci
on del programa en el cluster

El primer detalle tecnico que tenemos que resolver es como distribuir el flujo
que ha definido el usuario a cada uno de los mappers y reducers.
Partimos de la base que hemos escrito un mapper y un reducer que salvo
excepciones ejecuta siempre de la siguiente forma:
La primera operaci
on que ha de realizarse es la de deserializar el objeto
que llega al mapper o al reducer en forma de un par de clave y valor
(k, v1 ). Para ello se cede al usuario la posibilidad de implementar una clase
Persister[v1 ,R] que transforma el objeto de entrada en una instancia
de tipo v1 . El detalle de esto se vera en el punto 3.5.
Seguidamente es necesario desempaquetar la funcion que ha de ejecutarse
como parte del mapper o del reducer. En funcion de la primitiva a ejecutar
puede variar la forma de aplicar la funcion que ha utilizado el usuario
en la definici
on del flujo de trabajo.
Durante la ejecuci
on del codigo del usuario pueden producirse excepciones
o situaciones que provoquen la cada del proceso4 , por lo que hemos de
asegurar que nunca se lanza una excepcion crtica que nos haga perder el
control del job. Recordemos que Kai tiene enfasis en la semantica de errores
y que es posible que el usuario quiera inyectar condiciones de validacion
o estructuras de error dentro de la lambda que ha utilizado para expresar
la primitiva.
4 El tratamiento de errores en el cluster de Hadoop en relaci
on a los jobs no se ha alterado
y preserva la misma sem
antica.

35

Los tipos que emite tanto el mapper, como el reducer o como el propio job
han de ser compatibles entre si. Los tipos de datos requeridos en cada paso
en concreto han sido previamente calculados por el planificador. Dentro
de cada mapper y reducer la serializacion corre a cargo de Avro.
Los datos que puedan producirse como salida de trabajos persisten a
HDFS de forma normal, tal y como hara cualquier otro trabajo de Hadoop.
El desempaquetado de la funcion del usuario introducida en el dise
no del flujo
de datos requiere un poco de atencion. Si echamos un vistazo a la clase Mapper
en Hadoop vemos que define un constructor vaco, lo que obliga a cualquier clase
derivada a implementar un constructor vaco. Y nuestra clase base que hemos
creado para dar soporte a las primitivas no es una excepcion. La existencia de
ese constructor ya nos hace pensar que Hadoop necesita un constructor vaco
para poder instanciar cada uno de los mappers en los diferentes nodos, de otra
forma no sabra que par
ametros introducir para crear la instancia.
Esta restricci
on nos devuelve a la idea de que, dentro del contexto de ejecuci
on de jobs en Hadoop no tenemos control sobre el ciclo de vida de la clase que
implementa Mapper (o su an
alogo Reducer). Pensemos que cuando se ejecuta
un job en Hadoop el c
odigo se distribuye a los distintos nodos y cada jobtracker
levanta una JVM distinta para ejecutar los mappers. Si no podemos controlar, ni
inyectar, c
odigo dentro del constructor del mapper no podemos tampoco definir
una dependencia de la clase definida al crear el flujo de datos.
Ante esto se estudiaron diversas opciones, decantandose inicialmente por dos
implementaciones que no llegaron a funcionar:
Especializar la clase base que hemos creado para que implemente una
funci
on abstracta que devuelva el codigo a ejecutar por el cliente. Es decir, que probablemente FilterFunc extendera el mapper de referencia
FilterMapper. El problema fundamental estriba que no es posible declarar clases en tiempo de ejecucion5 y no queremos que el usuario tenga que
escribir las suyas propias.
Utilizar macros higienicas [4] [27] para generar tipos en tiempo de compilaci
on que puedan ser instanciables desde el mapper. El inconveniente
principal es que la opci
on no esta disponible en la version actual de Scala
y la biblioteca de soporte es extremadamente inestable.
Finalmente se emple
o un sistema mucho mas simple basado en una nocion
sobre c
omo compila Scala las funciones lambda. Al no existir soporte en la JVM
para funciones an
onimas que no esten ligadas a clases, el compilador de Scala traduce cualquier funci
on anonima a un conjunto de clases que heredan de
Function. La idea fundamental de esta aproximacion estriba en que teniendo
una clase referenciable por nombre, desde el mapper podemos instanciarla mediante la funci
on ClassForName del class loader y llamarla mediante la funcion
apply de la clase Function.
Existen tipos distintos de clases Function en funcion del n
umero de parametros y el tipo de retorno; el compilador suele prefijarlas con un n
umero para indicar el n
umero de par
ametros de entrada, se asume siempre un tipo de retorno.
5 La asunci
on no es del todo cierta, ya que existen bibliotecas como javassist o ASM que
son capaces de emitir bitecode en tiempo de ejecuci
on.

36

Veamos un ejemplo haciendo uso del repl de Scala:


scala> var f1: (Int => Int) = a => a * 2
f1: Int => Int = <function1>
scala> f1.getClass.getName
res0: String = $anonfun$1
scala> val c1: Class[_] = Class.forName(f1.getClass.getName)
c1: Class[_] = class $anonfun$1
scala> var f1m = c1.getMethod("apply", classOf[Object])
f1m: java.lang.reflect.Method = public final java.lang.Object
$anonfun$1.apply(java.lang.Object)
scala> var obj_f1 = c1.newInstance
obj_f1: Any = <function1>
scala> f1m.invoke(obj_f1, 4: java.lang.Integer)
res2: Object = 8

La clase Function define una funcion apply con un n


umero variable de
par
ametros dependiendo del tipo de clase, por lo que conociendo los tipos de las
lambdas que se utilizan dentro del flujo de trabajo podemos instanciar clases
concretas y ejecutarlas dentro del mapper o del reducer seg
un corresponda.
Recordemos que Hadoop ya dispone de la infraestructura necesaria para
desplegar nuestro programa en cada uno de los nodos del cluster. Si combinamos esta idea con la cache distribuida de Hadoop podemos distribuir el codigo
definido por el cliente junto con las clases base de referencia que haran la instanciaci
on. Dentro de los mappers y reducers creados se ha definido el codigo
necesario para recuperar la funcion lambda que se ha utilizado al definir uno de
los pasos en concreto. Esta instanciacion se realiza una vez por cada nodo del
cluster y dicha funci
on setup tiene la siguiente en, por ejemplo, el mapper de
la funci
on filter:
protected void setup(Context context)
throws IOException, InterruptedException {
Configuration cfg = context.getConfiguration();
String filterDescriptor = cfg.get(FILTER_FNC_PTR);
filterClass = cfg.getClassByNameOrNull(filterDescriptor);
if (filterClass == null)
logger.error("Unable to find filter class: " + filterDescriptor);
}

Cuando Kai crea un Job de Hadoop para enviarlo al cluster, establece entre
otros par
ametros el nombre de la clase a instanciar dentro del mapper o del
reducer correspondiente; step.ptr() es una funcion creada por el planificador
que devuelve la localizaci
on exacta de la clase Function1 dentro del espacio de
clases del class loader.
cfg.set(descriptorName, step.ptr());

Para asegurar que Hadoop es capaz de encontrar las clases del flujo de trabajo
definido, y tambien las clases de soporte de Kai, debemos establecer la ruta
dentro de la variable de entorno HADOOP CLASSPATH dentro del nodo que lanza
el trabajo, o bien usar la opci
on de configuracion libjars del programa cliente
de Hadoop.

37

3.3.3.

Infraestructura para lanzar trabajos

Kai provee no solo las transformaciones necesarias para convertir las operaciones que define en secuencias de trabajos de MapReduce, sino tambien la
gesti
on de los trabajos involucrados en el flujo de trabajo.
Para dar soporte a esta ejecucion se ha creado la clase BasicApplication
que extiende la funcionalidad provista por Configured y Tool que ofrece Hadoop. As, para definir un flujo de trabajo podemos extender esta clase comodamente:
package playground
import es.luisbelloch.kai._
class FluffyResult(val name: String, val constant: String)
object demo3 extends BasicApplication[FluffyResult] {
val name: String = "demo3"
val flow = new DataFlow[ShareClass]
.filter(s => s.currency == "USD")
.transform { s => new FluffyResult(s.name, "Hello") }
}

El c
odigo anterior representa lo mnimo necesario para que Kai procese, planifique y enve diversos trabajos al cluster. El flujo de trabajo anterior compilar
a a una clase visible en la JVM con un metodo main que acepta los parametros
est
andar de Hadoop y adicionalmente un directorio de trabajo en HDFS donde
residen los datos con los que trabajar. En un momento inicial, Kai preparara una
configuraci
on general del trabajo y lo enviara al cluster para su ejecucion:
val cfg = new FlowConfig(name, base, getConf)
val result = Kai.submit(flow, cfg)

Existe un objeto est


atico para orquestar el envo de trabajos, denominado
Kai. El objeto no tiene mucho codigo y delega la responsabilidad a otras dos
clases para realizar dos pasos importantes dentro de la ejecucion. La primera de
las clases, definida por el trait Scheduler, se encarga de traducir el programa a
una secuencia bien definida y coherente de pasos a ejecutar. Las optimizaciones
se aplican en este paso. La segunda de las clases que intervienen es el controlador
de trabajos de Kai, encargado de transformar la salida del Scheduler en un
conjunto de jobs de Hadoop y posteriormente ejecutarlos.
object Kai extends Controller {
val scheduler: Scheduler = new BasicScheduler
val controller = new DefaultJobController(scheduler)
def submit(df: DataFlow[_], cfg: FlowConfig) = controller.submit(df, cfg)
}

El metodo submit definido en el trait Controller realiza este segundo paso.


La implementaci
on de referencia esta contenida dentro del objeto DefaultJobController y realiza las dos acciones anteriormente mencionadas: delegar la
planificaci
on del flujo de trabajo al Scheduler y enviar cada uno de los trabajos
resultantes al cluster.
def submit(df: DataFlow[_], cfg: FlowConfig): FlowResult = {
val plan = scheduler.plan(df)
val results = plan map { e => submitOne(e, cfg, plan.length) }
new FlowResult(succeeded = results.last)
}

38

Dando un vistazo r
apido al objeto estatico HadoopJobFactory podemos
observar como dependiendo del tipo de funcion primitiva se configura una u
otra factora de trabajos. Existen tres factoras distintas que corresponden con
las tres operaciones primitivas soportadas, todas ellas implementan la interfaz
JobFactory que en base a un paso de un trabajo de Kai, representado por la
clase FlowStep, son capaces de devolver un Job de Hadoop listo para enviar al
cluster.
public interface JobFactory {
Job create(FlowStep step, String input, String output, Configuration cfg)
throws IOException;
}

Las factoras para las operaciones de filter y transform se han abstrado


a una clase m
as generica que puede utilizarse para lanzar trabajos que no contengan fase de reducci
on, denominada MapOnlyJobFactory. La implementacion
del metodo create contiene cuatro aspectos fundamentales:
Extraer los par
ametros de la operacion de Kai a ejecutar y configurar la
cache distribuida de Hadoop para que al instanciar los mappers se pueda
recuperar las funciones a ejecutar.
Establecer las clases correspondientes que asumiran las funciones de MapReduce.
Configurar Avro para que dependiendo del tipo de entidad de entrada
y salida pueda realizarse correctamente la serializacion de datos binarios
comprimidos a objetos concretos de la JVM para su manipulacion por la
funci
on anterior.
Establecer la localizaci
on en HDFS tanto de la entrada como de la salida
del Job, previamente calculada por el Scheduler para mantener las rutas
intermedias coherentes entre si.
Mostramos un fragmento a continuacion de MapOnlyJobFactory para ilustrar estos aspectos:
cfg.set(descriptorName, step.ptr());
final Job job = Job.getInstance(cfg, getClass().getName() + "/" + step.ptr());
job.setJarByClass(mapper);
FileInputFormat.setInputPaths(job, new Path(input));
FileOutputFormat.setOutputPath(job, new Path(output));
job.setInputFormatClass(AvroKeyInputFormat.class);
job.setOutputFormatClass(AvroKeyOutputFormat.class);
Schema inputSchema = ReflectData.get().getSchema(step.io()._1());
Schema outputSchema = ReflectData.get().getSchema(step.io()._2());
AvroJob.setInputKeySchema(job, inputSchema);
AvroJob.setOutputKeySchema(job, outputSchema);
logger.debug("Input schema: " + inputSchema.toString());
logger.debug("Output schema: " + outputSchema.toString());
AvroJob.setMapOutputKeySchema(job, outputSchema);
job.setMapOutputValueClass(NullWritable.class);

39

job.setMapperClass(mapper);
job.setNumReduceTasks(0);
return job;

El dise
no anteriormente mostrado permite separar cuatro responsabilidades
b
asicas en cuanto al lanzamiento de trabajos se refiere:
La transformaci
on de un flujo de trabajo a una secuencia coherente de
MapReduce.
La generaci
on de Jobs de Hadoop en base a esa secuencia.
La coordinaci
on y el lanzamiento de esos trabajos.
El enlace necesario para que Avro pueda cargar y descargar datos desde
HDFS en base a clases de la JVM contenidas en el flujo de ejecucion.
La separaci
on de estas responsabilidades permite poder reemplazar componentes en un futuro, con distintas intenciones como pueda ser el uso de otros
sistemas de serializaci
on distintos a Avro, el cambio de un planificador distinto
para probar optimizaciones, la generacion de Jobs para otro sistema distinto al
MapReduce de Hadoop, etc.

3.4.

Planificaci
on y optimizaci
on

Las tareas de planificaci


on dentro de Kai estan definidas mediante el trait
Scheduler, que define una u
nica operacion que recibe el un flujo de datos especificado por DataFlow[T] y devuelve una secuencia de operaciones a ejecutar.
trait Scheduler {
def plan(flow: DataFlow[_]): List[FlowStep]
}

Recordemos que al componer las distintas operaciones dentro del flujo de datos, estas se van acumulando dentro de la estructura interna de DataFlow[T], de
manera que la u
ltima asignacion contiene todas las operaciones que involucran
la definici
on y una secuencia con los tipos de datos de entrada y salida para
cada paso.
La implementaci
on b
asica del Scheduler se define mediante la clase BasicScheduler. Dicha clase no contiene ninguna optimizacion y u
nicamente se encarga de generar los distintos pasos en crudo a ejecutar, representados por
FlowStep:
class
val
val
val
val
val

FlowStep(
op: Mutator,
ptr: String,
junction: Pair[Int, Int],
io: (Class[_], Class[_]),
args: List[(String,String)] = null)

La clase no es m
as que una estructura que contiene algunos campos interesantes:

40

OP. Contiene una tipo abstracto de la clase de Scala que genero la operacion.
Por ejemplo, si la operacion fue generada por fold, contendra el valor de
la funci
on primitiva utilizada para definirla, FoldFunc.
PTR. Es una representaci
on en forma de cadena que apunta a la funcion lambda
utilizada como argumento principal en la llamada a la funcion. La generaci
on de esta representacion se lleva a cabo mediante la funcion getName
de la clase Class<T> de la JVM.
JUNCTION. Representa los n
umeros de secuencia de la operacion para establecer
el destino de los resultados intermedios. La operacion n contendra una
tupla con los valores n y n + 1.
IO. Contiene una tupla con los tipos de datos de entrada y de salida del paso
de ejecuci
on. Como hemos visto anteriormente, este tipo se ha representado utilizando Class<?> debido al type erasure de la JVM, aunque es
completamente referenciable en tiempo de ejecucion.
ARGS. Guarda una lista de argumentos adicionales a la funcion a ejecutar. En
el caso de fold por ejemplo, la lista guarda el valor del elemento inicial.
En el caso de map o filter dicha lista viene vaca.
El planificador por defecto representado por BasicScheduler u
nicamente genera una secuencia de operaciones a ejecutar por Hadoop, en forma de
FlowStep. Cada uno de los pasos generados se corresponde con una ronda de
MapReduce, por lo que es en este punto donde podemos aplicar optimizaciones
para combinar entre si algunas de las operaciones.
Para este prop
osito existe otra implementacion del trait Scheduler que
aplica una serie de optimizaciones justo despues de convertir la secuencia de
operaciones a FlowStep. La implementacion de OptimizedScheduler esta completamente asentada sobre BasicScheduler:
class OptimizedScheduler(optimizers: Optimizer*)
extends BasicScheduler {
override def plan(flow: DataFlow[_]): List[FlowStep] = {
val original = super.plan(flow)
optimizers.foldLeft(original)((f,opt) => opt.solve(f)).toList
}
}

Esta lista se aplica de forma secuencial, de forma que el resultado de aplicar


la primera optimizaci
on es transferido al siguiente optimizador.

3.4.1.

El pipeline de optimizaciones

Uno de los puntos fuertes de Kai es la posibilidad de crear nuevas optimizaciones para la generaci
on de trabajos. Como la aplicacion de las optimizaciones
se hace a traves de una secuencia ordenada, podemos intercambiar el orden de
las optimizaciones escogidas produciendo distintos resultados.
Cada optimizador debe ser capaz de recoger una secuencia de operaciones y
devolver una nueva secuencia que contin
ue representando la misma transformaci
on de datos. El trait Optimizer define esta funcionalidad:
trait Optimizer {
def solve(original: List[FlowStep]): List[FlowStep]
}

41

Como ejemplo de optimizacion vamos a introducir una optimizacion sencilla basada en la idea de que si existen dos o mas operaciones consecutivas
de tipo filter o transform, estas se pueden colapsar en una u
nica operaci
on que realice la transformacion pedida. La implementacion esta en la clase
MergeConsecutive.
El optimizador explota la transitividad de las operaciones de transform y
de filter, o de otra forma si tenemos una funcion f : A B y otra funcion g :
B C tenemos que (g f ) : A C y por lo tanto g(f (a)) = (g f )(a), a A.
Es decir, que teniendo dos operaciones consecutivas de tipo filter:
class Articulo(val sku: Int, val stock: Int)
class StockEnFecha(a: Articulo, val fecha: Date)
extends Articulo(a.sku, a.stock)
val flow = DataFlow[Articulo]
.transform[StockEnFecha](a => new StockEnFecha(a, new Date()))
.transform[Int](s => s.sku)
.transform[String](s => s"ES-$s")

Estas traducibles por el planificador, sin aplicar ninguna optimizacion, en


tres FlowSteps distintos:
F: TransformFunc(demo2$$anonfun$1), IO: (0, Articulo) to (1, StockEnFecha)
F: TransformFunc(demo2$$anonfun$2), IO: (1, StockEnFecha) to (2, int)
F: TransformFunc(demo2$$anonfun$3), IO: (2, int) to (3, String)

Podemos ahora realizar la misma ejecucion, pero introduciendo en el optimizador la clase MergeConsecutives, que colapsa las operaciones tal y como
antes habamos descrito:
F: TransformFunc(scala.Function1$$anonfun$compose$1),
IO: (0, Articulo) to (3, String)

Vemos que se ha introducido una u


nica funcion con un nuevo predicado. El
predicado es el resultado de la composicion de los predicados anteriores. Como
Scala es un lenguaje funcional, esta composicion es muy sencilla de realizar.
Para facilitar la implementacion de optimizadores de operaciones se facilita tambien la funci
on compress, que permite establecer un criterio donde dos
operaciones se pueden combinar y una funcion de mezcla:
object Optimizer {
def compress[T](l : List[T], eq: (T,T) => Boolean, merge: (T,T) => T)
: List[T] = l match {
case head::next::tail if eq(head,next) =>
compress(merge(head,next)::tail, eq, merge)
case head::tail => head::compress(tail, eq, merge)
case nil => List()
}
}

La implementaci
on de MergeConsecutive y la definicion del trait Optimizer
est
a definida en el archivo Optimizer.scala. De igual forma, tanto el planificador b
asico como la implementacion con una secuencia de optimizadores puede
encontrarse en Scheduler.scala.

42

3.5.

Serializaci
on de Entidades: Apache Avro

Para manejar datos dentro de los trabajos definidos en Kai se ha optado por
una biblioteca de serializaci
on de datos llamada Avro.
Avro esta bajo el paraguas de la Apache Foundation y ha sido creada por
algunos de los autores de Hadoop. La disposicion de los datos dentro de un
contenedor de Avro [12] permite definir esquemas complejos en arbol, no solo
orientados a una lista de registros individuales. El formato de datos es completamente binario, acepta compresion basada en Deflate [14] o Snappy [22] [16]
y la definici
on de los datos del contenedor esta basada en un esquema externo,
aunque tiene posibilidad de inferir el esquema en tiempo de ejecucion.
Esta u
ltima fue la raz
on principal de escoger Avro en lugar de otras bibliotecas. Inicialmente se pens
o en simplificar la implementacion aceptando CSV
como formato de almacenamiento, orientado a registro por fila, lo que requera
que la definici
on del trabajo en Kai incorporara un traductor entre las clases
de la JVM y las columnas del archivo de datos. El hecho de que Avro permita
definir una clase en Scala o en Java e inferir el esquema de almacenamiento
de datos es crucial para que los datos intermedios generados por cada uno de
los mappers y reducers de los trabajos lanzados por Kai sean facilmente almacenables en el cluster. Adicionalmente, y como podemos comprobar en algunos
artculos [32] [17], el rendimiento de Avro es bastante conveniente para este tipo
carga de trabajo.

3.5.1.

Utilizaci
on de Avro

En Avro los contenedores de datos se definen utilizando una representacion


en JSON de la propia estructura que queremos generar:
{"namespace": "es.luisbelloch.kai.samples.avro",
"type": "record",
"name": "Persona",
"fields": [
{"name": "nombre", "type": "string"},
{"name": "edad", "type": "int"},
{"name": "email", "type": ["string", "null"]}
]
}

A partir de esta estructura, y en tiempo de compilacion, Avro genera un


conjunto de clases que permiten guardar los datos y cargar una clase con datos
previamente guardados. Adicionalmente, tambien puede generar una definicion
de diversos servicios que puedan comunicarse entre si mediante RCP.
Si bien este lenguaje intermedio puede ser u
til para generar codigo compatible entre varias plataformas, nos interesa especialmente comprobar como Avro
es capaz de inferir el formato de almacenamiento a partir de clases en Java o en
Scala.
Por ejemplo, la siguiente clase en Scala define una clase Persona con algunos
campos:
class
var
var
var
}

ShareClass() {
nombre: String = _
edad: Int = _
email: String = _

43

Es importante se
nalar que las clases deben contener un constructor p
ublico
sin par
ametros para que puedan ser instanciables por el codigo de Avro. A
partir de la clase anterior podemos serializar o deserializar entidades haciendo
u
nicamente uso de la propia definicion de la clase.
ByteArrayOutputStream os = new ByteArrayOutputStream();
Encoder encoder = EncoderFactory.get().binaryEncoder(os, null);
Persona persona = new Persona("Ana", 33, "ana@google.com");
DatumWriter writer = new ReflectDatumWriter(persona.getClass());
writer.write(persona, encoder);
encoder.flush();

La misma tecnica es la que se utiliza en los mappers y reducers que hemos


implementado para realizar la carga. En la clase donde se realiza la configuracion
del Job de Hadoop obtenemos el esquema de entrada y de salida de cada paso
del flujo de datos:
Schema inputSchema = ReflectData.get().getSchema(step.io()._1());
Schema outputSchema = ReflectData.get().getSchema(step.io()._2());

Para poder utilizar Avro desde MapReduce anteriormente a Hadoop 2.0


nuestro c
odigo deba heredar de una serie de clases que realizaban esa conversi
on. Con la introducci
on del nuevo API se elimino esta restriccion y se proveen
distintas clases para configurar el Job. Desde los mappers y reducers es posible
utilizar distintas clases que act
uan de contenedores para nuestros datos. En funci
on de la intenci
on, es posible utilizar AvroKey<T> o AvroValue<T> en diversas
combinaciones.
@Override
protected void reduce(AvroKey<Localidad> kloc, Iterable<AvroValue<Temp>> vtemp,
Context context) throws IOException, InterruptedException {
double maxima = 0;
for (AvroValue<Temp> tmp : vtemp)
maxima = Math.max(maxima, tmp.datum().mediaDiaria());
context.write(kloc, new AvroValue<Double>(maxima));
}

3.5.2.

Comparativa contra otros formatos

En el momento de estudiar diversos formatos que pudieran ser empleados en


Kai se elabor
o una lista que contena Protocol Buffers, Thrift, Message Pack,
ASN.1, Hessian, CSV y el propio Avro. Sin embargo no pudimos encontrar
referencias bibliogr
aficas o publicaciones que establecieran alg
un tipo de comparativa6 entre los distintos formatos, por lo que se ha optado por establecer
una peque
na comparativa cualitativa en base a funcionalidad ofrecida sobre los
dos m
as populares: Protocol Buffers y Thrift.
Protocol Buffers
Protocol Buffers [21] es un formato binario y portable, publicado bajo licencia BDS por Google en Julio de 2008 y utilizado en muchos de los sistemas
6 Un posible trabajo interesante desde mi punto de vista ser
a la comparaci
on de distintos
contenedores de datos para su uso con MapReduce, HDFS y/o HBase, no solo a nivel de
caractersticas, sino tambi
en centrado en el rendimiento, uso de memoria, utilizaci
on de espacio
efectivo en almacenamiento secundario, etc.

44

internos de dicha empresa desde muchos a


nos atras. El formato se basa en la
definici
on de un esquema en un lenguaje intermedio que posteriormente se traduce a Java, C++ o Python. Dicho lenguaje establece el contenedor principal
como un mensaje, al que pueden a
nadirse distintos campos:
message Alumno {
required string nombre = 1;
optional int32 edad = 2;
}

Cuando se serializa una estructura de datos, Protocol Buffers empaqueta los


distintos campos utilizando el orden establecido en la definicion inicial, internamente los campos se disponen de forma continua en el archivo, uno seguido
del otro. El primer byte de cada campo indica el n
umero de orden establecido
en el IDL y el siguiente byte la longitud del campo. Esta disposicion permite
generar mensajes muy compactos, pero introduce la limitacion de que no es posible recomponer el mensaje si no se conoce el esquema en tiempo de ejecucion.
Alternativamente la propia definicion del mensaje se puede codificar usando el
IDL de Protocol Buffers, aunque esto requiere el envo de un mensaje adicional.
A diferencia de Avro o de Thrift, Protocol Buffers no incluye la generacion de
c
odigo para comunicaci
on RCP y delega estas tareas a otros frameworks en ese
sentido. En la documentaci
on de Google no se recomienda utilizar mensajes de
mas de un MB, el formato no se ha dise
nado para manejar mensajes individuales
grandes.
Actualmente el mantenimiento del proyecto corre a cargo de Google, tanto
la documentaci
on como las distintas versiones se mantienen frecuentemente.
Thrift
Thrift [44] es un formato dise
nado en el 2007 por ex-ingenieros de Google
mientras trabajaban en Facebook. El proyecto se mantiene en fase de incubacion
por la fundaci
on Apache y se usa tanto en los sistemas internos de Facebook
como en otros sistemas distribuidos como Cassandra.
En contraposici
on a Protocol Buffers, Thrift tiene soporte para una considerable variedad de lenguajes y plataformas, entre las que se incluye Java.
As mismo es capaz de generar el codigo necesario para habilitar comunicacion
directa mediante RCP, en este sentido es un proyecto mucho mas complejo que
Avro o Protocol Buffers. Los archivos se definen en un lenguaje propio utilizando estructuras de datos con una semantica mucho mas rica que la de Protocol
Buffers:
struct Alumno {
1: string nombre,
2: optional i32 edad
}
service ServicioAlumnado {
void almacenarExpediente(1: Alumno alumnno)
}

El lenguaje es pr
acticamente identico al de Protocol Buffers, aunque la
codificaci
on interna ocupa mucho mas espacio. El formato de almacenamiento
no tiene porque ser estrictamente binario y se aceptan otras codificaciones como
JSON, XML o texto plano.

45

La documentaci
on es escasa y el proyecto no recibe especial atencion por
parte de la comunidad de desarrolladores de software libre.
Ventajas de Avro frente a Thrift o Protocol Buffers
La funcionalidad que nos llevo a escoger Avro frente a los otros formatos se
establece en los siguientes puntos:
Avro no requiere generacion de codigo a partir de una definicion previa
del esquema de datos. Esto permite que desde los mappers y los reducers podamos instanciar funciones que aceptan tipos no conocidos, pero
existentes en tiempo de ejecucion. El propio contenedor de datos de Avro
puede guardar una copia del esquema que contiene.
Aunque el tama
no del mensaje parece algo superior al que obtendramos
con Protocol Buffers, con Avro no necesitamos especificar ni el orden ni el
tama
no de cada registro interno, por lo que en lineas generales podemos
asumir un tama
no de archivo inferior.
Parece existir un consenso dentro de la comunidad de desarrolladores de
Hadoop a mantener Avro como formato estandar para Hadoop.

46

Captulo 4

Uso de la Biblioteca
En este captulo explicaremos c
omo utilizar Kai desde una perspectiva externa y mostraremos algunos ejemplos que hagan uso de las operaciones definidas
en captulos anteriores. As mismo documentaremos las clases necesarias para
definir una aplicaci
on en Kai, su compilaci
on y el modo de enviar trabajos a un
cluster de Hadoop. Tambien incluimos algunas directrices sobre la extensibilidad de Kai y c
omo es posible extender la funcionalidad principal implementando
nuevas operaciones o haciendo cambios al planificador. Al final del captulo incluiremos un repaso por algunos casos de uso donde Kai es aplicable.

4.1.
4.1.1.

Primeros Pasos
Definiendo transformaciones de datos

Como ya hemos visto anteriormente, la unidad basica que define nuestro


flujo de transformaci
on de datos esta representada por la clase DataFlow[T].
Dicha clase es u
nicamente una representacion de la secuencia de operaciones a
ejecutar.
Sobre la clase DataFlow[T] se han implementado todas las operaciones del
captulo 2. Para tener acceso a las operaciones por defecto incluidas en Kai,
podemos importar el paquete completo:
import es.luisbelloch.kai._

A partir de este momento podemos definir el trabajo encadenando operaciones sucesivas sobre origen inicial. Por ejemplo, teniendo un fichero con los datos
de personas, podramos describir un trabajo filtrando las personas mayores de
edad, y extrayendo u
nicamente el nombre:
val flow = DataFlow[Persona]
.filter { p => p.edad > 18 }
.transform { p => p.nombre }

En Kai se asume que todo flujo de datos se compone por una lista de registros
con una determinada estructura. Todos los registros son del mismo tipo, aunque
este puede variar entre ejecuciones. Por ejemplo, en el caso anterior Kai leera de
HDFS una lista de personas y terminara escribiendo una lista de cadenas en
HDFS. Por cada uno de los registros individuales, Kai aplicara las funciones

47

especificadas. Tanto la distribucion de los datos a los distintos nodos como la


propia distribuci
on del programa son completamente transparentes.
La funci
on filter acepta un predicado que hemos expresado con una funcion
como p => p.edad > 18, que recibe una persona p y devuelve una expresion
booleana. Una caracterstica interesante de Scala es que nos permite simplificar
este tipo de expresiones usando la forma resumida .filter { _.edad > 18 }.
Tambien podemos cambiar las llaves por parentesis, como es de esperar el
uso de llaves nos permite expresar cuerpos de metodo complejos. Cabe recordar
de nuevo que la definici
on de DataFlow no realiza ejecucion alguna, y solo define
las operaciones futuras sobre el cluster. Por lo tanto, los predicados que pasemos
a las distintas funciones ser
an distribuidos a todos los nodos en el momento de
la ejecuci
on del trabajo.
A partir de este momento ya estamos listos para que Kai enve el trabajo
al cluster. Antes del envo Kai descompondra cada operacion en una serie de
mappers y reducers y configurara los trabajos para que puedan procesar datos
de entrada y salida en HDFS.
package playground
import es.luisbelloch.kai._
class Persona {
var nombre: String = _
var edad: Int = _
def this(nombre: String, edad: Int) {
this()
this.nombre = nombre
this.edad = edad
}
}
object MayoresDeEdad extends BasicApplication {
val name = "Nombres de personas mayores de edad"
val flow = DataFlow[Persona]
.filter { p => p.edad > 18 }
.transform { p => p.nombre }
}

Como vemos se ha definido una clase que define la estructura de datos de


una persona y un objeto MayoresDeEdad que extiende de BasicApplication.
Este u
ltimo es una clase que provee Kai que recoge el trabajo de la variable
flow, lo transforma y lo enva al cluster.
Los flujos de datos que se definan son completamente reciclables y se pueden componer entre si. Supongamos que necesitamos comprobar si la lista de
personas mayores de edad del ejemplo anterior disponen de direcciones de correo electr
onico validas. Como los objetos y clases de Kai son completamente
est
andares, nada nos impide construir nuestra propia biblioteca de funciones:
object Utilidades {
val EMAIL_PATTERN: String = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*" +
"@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"
val pattern = Pattern.compile(EMAIL_PATTERN)
def soloEmailsValidos(flow: DataFlow[String]): DataFlow[String] = flow
.filter(email => {
val m = pattern.matcher(email)
if (m.matches()) true

48

else false
})
}

Y reutilizar dichas funciones cuando consideremos conveniente:


object MayoresDeEdad extends BasicApplication {
val name = "Nombres de personas mayores de edad"
val mayores = DataFlow[Persona]
.filter { _.edad > 18 }
.transform { _.nombre }
var flow = Utilidades.soloEmailsValidos(flow)
}

4.1.2.

Avro, el formato de datos de Kai

Kai utiliza Avro como contenedor de datos, por lo que tanto la entrada como
la salida del proceso utilizar
an dicho formato. El proceso de carga desde HDFS
a la clase Persona en cada uno de los nodos de procesado corre a cargo de Kai.
Lo u
nico que debemos definir es una estructura, en forma de clase, con la misma
forma que el formato binario de Avro en HDFS.
Hay muchas formas de generar o transformar datos al formato binario de
Avro, pero quiz
a la m
as popular sea utilizar la herramienta avrotool.jar disponible con la distribuci
on de Avro. Por ejemplo, dado un archivo de personas
podramos comprobar cu
al sera el esquema del mismo:
$ java -jar avro-tools-1.7.4.jar getschema personas.avro
{
"type" : "record",
"name" : "Persona",
"namespace" : "playground",
"fields" : [ {
"name" : "nombre",
"type" : "string"
}, {
"name" : "edad",
"type" : "int"
} ]
}

Y tambien podramos convertir datos de formatos como json o texto plano:


$ java -jar avro-tools-1.7.4.jar tojson personas.avro
{"nombre":"Mike Wazowski","edad":27}
{"nombre":"Jack Sparrow","edad":33}

El esquema de datos de Avro no hace falta definirlo, ya que se usa inferencia


de tipos para extraerlo de la clase en tiempo de ejecucion. As, cuando Kai
configura los trabajos de Hadoop para que se ejecuten, comprueba las clases
de entrada y salida del trabajo y genera los esquemas correspondientes que
ser
an utilizados durante la ejecucion del trabajo. La u
nica limitacion de esta
aproximaci
on estriba en que es necesario crear un constructor vaco para que
Avro pueda instanciar la clase antes de rellenarla.

4.1.3.

Compilar y enviar trabajos al cluster

Ahora que ya sabemos el ABC de la creacion de trabajos podemos enviarlo


al cluster. El envo del trabajo sigue las mismas normas que cualquier job de
49

Hadoop y el programa principal se ha preparado para que acepte un directorio


donde estar
an los datos de entrada.
$ hadoop playground.MayoresDeEdad hdfs://itaca:9000/user/luis/data/demo3/

Como es habitual en este tipo de trabajos, se pueden disponer distintos ficheros dentro del directorio especificado y Kai los procesara dejando el resultado
en la carpeta output. Los resultados intermedios de cada mapper y reducer individual se vuelcan dentro de la carpeta tmp y son descartados tras la ejecucion.
$ hadoop fs -ls -R data/demo3
-rw-r--r-- 1 luis supergroup 293 data/demo3/input
drwxr-xr-x - luis supergroup 0 data/demo3/output
-rw-r--r-- 1 luis supergroup 0 data/demo3/output/_SUCCESS
-rw-r--r-- 1 luis supergroup 276 data/demo3/output/part-m-00000.avro

En el envo de trabajos aplican los mismos requisitos que en cualquier job


de Hadoop:
Se asume que existen permisos de lectura y escritura sobre la carpeta
elegida.
El classpath debe estar bien configurado y apuntar a las rutas donde se
encuentra tanto Kai como la biblioteca de tiempo de ejecucion de Scala.
Dentro de la carpeta elegida con los datos se creara una carpeta denominada output que se limpiara al inicio del trabajo.
En el archivo de que realiza la compilacion y empaquetado de Kai, escrito para Gradle, se ha incluido una tarea para volcar todas las dependencias
requeridas a una carpeta lib:
task exportDependencies(type: Copy) {
into "$buildDir/libs/lib"
from configurations.runtime
}

Puede revisarse el mismo archivo para obtener una lista completa de dependencias. A nivel de proyecto, cualquier proyecto cliente no debera depender de
ning
un jar a parte del propio de Kai y las bibliotecas de Scala.

4.1.4.

Depurando la salida del planificador

Los objetos que devuelve y manipula Kai, como la clase DataFlow, son completamente manipulables y puede hacerse introspeccion del encadenamiento de
operaciones en cualquier momento. Utilizando el ejemplo anterior, podemos extraer dos listas: una de operaciones y otra de tipos de datos en cada paso de la
ejecuci
on:
object ExaminandoDataFlow extends App {
val flow = MayoresDeEdad.flow
flow.operations zip flow.typeSequence map println
}

Obteniendo una salida por consola con la definicion de tipos y operaciones:


(FilterFunc@6648,(class Persona,class Persona))
(TransformFunc@6e811,(class Persona,class java.lang.String))
(FilterFunc@5e785,(class java.lang.String,class java.lang.String))

50

Si quisieramos comprobar que salida produce el planificador, u


nicamente necesitamos instanciar una de las dos clases disponibles para planificar y examinar
la salida:
val scheduler = new BasicScheduler
val plan = scheduler.plan(flow)
plan map println

La salida describe las acciones calculadas por el planificador para la ejecucion del
trabajo, cada una de ellas correspondiente a una ronda de MapReduce dentro
de Hadoop:
Fnc: FilterFunc, Ptr: playground.casos.MayoresDeEdad$$anonfun$1,
Junction: (0,1), Tag: (class playground.Persona,class playground.Persona)
Fnc: TransformFunc, Ptr: playground.casos.MayoresDeEdad$$anonfun$2,
Junction: (1,2), Tag: (class playground.Persona,class java.lang.String)
Fnc: FilterFunc, Ptr: playground.casos.Utilidades$$anonfun$soloEmailsValidos$1,
Junction: (2,3), Tag: (class java.lang.String,class java.lang.String)

4.2.

Extendiendo Kai

Una de las principales razones de la existencia de Kai, y que guio parte del
dise
no, fue la imposibilidad de otras herramientas similares de proveer puntos
de extensi
on apropiados. La mayora de ellas u
nicamente ofrecen la posibilidad
de a
nadir peque
nas funciones y ninguna permite modificar cuestiones basicas
como la sem
antica de las operaciones, las propias operaciones o el planificador.
En Kai los trabajos de datos se definen en Scala, y por tanto podemos hacer
uso de todas las funcionalidad del lenguaje y de la plataforma. Kai es extensible
por dise
no.
Existen tres
areas principales donde podemos extender Kai:
Implementando nuevas operaciones sobre un DataFlow.
Cambiando la forma en que se generan los trabajos para Hadoop, reemplazando el controlador de trabajos por defecto.
Creando nuevas optimizaciones.

4.2.1.

Extendiendo el conjunto de operaciones de Kai

Dado que todas las operaciones en Kai estan construidas en base a transform,
filter y fold, y que la biblioteca se ha dise
nado para permitir definir unas operaciones en base a otras, la va para la creacion de nuevas operaciones esta completamente abierta.
Remarcaremos de nuevo que el propio objeto DataFlow[T] es un mero contenedor de datos que describe la secuencia de operaciones a aplicar, y que por
tanto no existe ninguna operacion directamente implementada como metodo de
la clase. Lo que parece una forma de azucarar la sintaxis en Kai se convierte en
el mecanismo principal de extension para a
nadir nuevas operaciones.
La clave del mecanismo de extension reside en que Scala permite definir
conversiones implcitas entre tipos. Por ejemplo, podemos asignar un entero a
una cadena siempre que en el contexto actual exista definida una conversion
implcita para realizar la transformacion. Veamos a traves del repl de Scala
c
omo definirla:
51

scala> object Impl { implicit def intToString(i: Int): String = i.toString }


defined module Impl
scala> val a: Int = 4
a: Int = 4
scala> val b: String = a
<console>:8: error: type mismatch;
found : Int
required: String
val b: String = a
^
scala> import Impl._
import Impl._
scala> val b: String = a
b: String = 4

El mismo concepto lo podemos aplicar para transformar cualquier objeto de


creaci
on propia en una nueva operacion de Kai. Podemos ver el funcionamiento
de esto en algunas de las propias operaciones de Kai, como en flatten, donde
hay una conversi
on implcita que permite convertir de un DataFlow[List[T]] a
un DataFlow[T] y por tanto a
nadir la funcion requerida, u
nicamente accesible
cuando el objeto al que deseamos aplicarlo es de tipo DataFlow[List[T]].
class FlattableDataFlow[T](flow: DataFlow[List[T]]) {
def flatten(): DataFlow[T] = ...
}
implicit def dataFlowTravelsToFlatland[E](flow: DataFlow[List[E]]) =
new FlattableDataFlow(flow)

4.2.2.

Extendiendo la generaci
on de trabajos

El controlador b
asico de Kai tiene una implementacion muy sencilla, tal

y como hemos visto en el captulo 3 . Unicamente


se encarga de traducir las
operaciones de transform, filter y fold a sus homologos en MapReduce. El
resto de operaciones de Kai estan construidos en base a estas operaciones, por
lo que no se requiere modificacion adicional alguna.
No obstante, si quisieramos realizar una implementacion alternativa de Kai
sobre otro sistema distinto de Hadoop u
nicamente tenemos que implementar el
trait Controller y reemplazar la asignacion en el objeto Kai.
Vamos a realizar una implementacion de un controlador que no enva ning
un
trabajo de procesado, pero imprime por la salida estandar el trabajo antes y
despues de la optimizaci
on:
class ControladorParaInspeccion extends Controller {
def submit(df: DataFlow[_], cfg: FlowConfig): FlowResult = {
df.operations zip df.typeSequence foreach(o => {
println(s"Entrada: ${o._2._1.getName}\n" +
s"Operacion: ${o._1.getClass.getSimpleName}, Predicado: ${o._1.reflect()}\n" +
s"Salida: ${o._2._2.getName}\n")
})
return new FlowResult(true)
}
}

52

El uso de dicho controlador es inmediato, solo necesitamos asignarlo al objeto


global Kai y enviar el trabajo. En lugar de realizarse el envo a Hadoop se
imprimir
an por pantalla la secuencia de operaciones generadas.
object demo extends App {
Kai.controller = new ControladorParaInspeccion
val name = "extctrl"
val flow = DataFlow[Contenedor]
.filter(_.destino == "VLC")
.filter(_.peso > 500)
.transform[Date](c => c.llegada)
.any(_.after(new Date("2010/03/05")))
Kai.submit(flow, null)
}

4.2.3.

Extendiendo el planificador

Tal y como hemos visto en la seccion 3.4.1 el planificador es tambien uno de


los elementos de Kai dise
nados para poder ser extendido. Existen dos puntos
donde podemos extender el planificador:
A
nadiendo una nueva optimizacion a la lista manejada por OptimizedScheduler u ordenando las entradas de esta lista para producir distintos
resultados. Esta variaci
on no es estatica y pueden cambiar en cada uno de
los flujos de trabajo que definamos.
Extendiendo, reemplazando o modificando cualquiera de los dos traits envueltos en la generaci
on de trabajos: Scheduler y Controller.
La implementaci
on de optimizadores de trabajos es una tarea compleja y
podra abarcar un trabajo separado. Podemos dar aqu sin embargo una serie de
ideas sobre d
onde podran aplicarse algunas mejoras con el objetivo de mejorar
la eficiencia de los flujos de transformacion de datos enviados al cluster.
Las operaciones de filter y transform pueden implementarse en base a
fold [25], o al menos incorporar ambas dos dentro de la fase de mapping
de fold.
La transitividad entre operaciones fold es tambien explotable, siempre
y cuando los tipos de entrada y salida coincidan. Un caso tpico donde
se produce esta situaci
on es cuando utilizamos flatten y seguidamente
realizamos cualquier otra operacion con fold. La fase de reducci
on de
flatten y la fase de mapping del siguiente fold se pueden colapsar con
tipos compatibles.
En ocasiones los flujos de ejecucion pueden ser optimizados eliminando
zonas de los contenedores de datos que no se usan. Para ello sera necesario
inspeccionar los predicados de la secuencia para ver que propiedades de
una entidad se utilizar
an durante el trabajo. Una vez hemos recolectado
esas propiedades clave podemos introducir una operacion de transform
para eliminar los datos sobrantes justo al inicio del proceso.
Aquellas operaciones destinadas a devolver un u
nico resultado, como any
o all se pueden colapsar dentro de la fase de reduccion de fold.

53

4.3.

Algunos ejemplos pr
acticos

Kai ofrece distintos operadores aplicables a un flujo de datos, en funcion de


las operaciones que queramos realizar. Podemos hacer aqu un repaso al uso de
las distintas funciones m
as relevantes poniendo distintos ejemplos que combinen
varias de ellas.

4.3.1.

Comprobar si existen elementos

Imaginemos un escenario donde en una estacion logstica se reciben cientos


de registros de contenedores cada hora y al finalizar el da queremos determinar
si ma
nana existe alguna entrega para Valencia con pesos mayores de 500kg.
class Contenedor(val uid: String, val peso: Int,
val destino: String, val llegada: Date)
val flow = DataFlow[Contenedor]
.filter(_.destino == "VLC")
.filter(_.peso > 500)
.transform[Date](c => c.llegada)
.any(_.after(new Date("2010/03/05")))

La funci
on any nos devuelve un u
nico resultado con la confirmacion. El
operador de transform podra haber sido omitido, pero su utilizacion permite
que por el cluster no circulen todos los datos del contenedor y solo utilicemos
la fecha de llegada.

4.3.2.

Ranking de documentos

El siguiente ejemplo recoge una lista de documentos y por cada uno de


ellos genera una lista de palabras con el n
umero de apariciones en el texto.
La puntuaci
on est
a tambien normalizada, siendo el resultado final una lista de
identificadores de palabra que tienen asignados una lista de puntuaciones por
documento.
class Documento(val id: Int, val titulo: String,
val abstr: String, val cuerpo: String)
class Termino(val id: Int, val valor: String, var puntuacion: Double)
def normalizar(terms: List[Termino]): List[Termino] = {
val tmax = terms.map(_.puntuacion).max
val max = if (tmax == 0) 0.0001d else tmax
terms foreach { s =>
s.puntuacion = (s.puntuacion / max)
}
terms
}
def ranking() = {
DataFlow[Documento]
.transform(fancyTermFrecuencyExtractor)
.flatten()
.groupBy(_.valor)
.transform[(String,List[Termino])](g => (g._1, normalizar(g._2)))
}

Podramos descartar algunos resultados en la u


ltima transformacion que
hemos realizado, ya que no nos interesan todos los resultados, tan solo el identificador del documento Termino.id y las puntuaciones obtenidas.
54

4.3.3.

Paginaci
on de resultados

El ejemplo actual usa las funciones take y skip para obtener un subconjunto continuo del total de registros, de forma que en cada ejecucion la funcion
ventana obtenga n elementos a partir del elemento de en posicion p.
def ventana(inicio: Int, elementosPorPagina: Int) =
DataFlow[Contenedor]
.skip(inicio)
.take(elementosPorPagina)

La funci
on anterior podramos reutilizarla en otros trabajos, por cada llamada a la funci
on se creara un trabajo distinto apuntando a una seccion del
conjunto de datos determinada.

4.3.4.

Obtener contenedores m
as pesados por destino

La funci
on topElemByGroup agrupa una lista de contenedores por su destino
y extrae los n contenedores m
as pesados por cada uno de los destinos.
Como puede observarse en esta funcion hemos combinado parte del proceso
en Scala para que sea realizado en la fase de reduccion justo despues de establecer
la agrupaci
on.
class Contenedor(val uid: String, val peso: Int,
val destino: String, val llegada: Date)
def topElemByGroup(topN: Int) = {
DataFlow[Contenedor]
.groupBy(_.destino)
.transform[List[Contenedor]](group => {
group._2.sortBy(_.peso).reverse.take(topN)
})
}

La lambda que figura dentro de la funcion de transform se ejecuta dentro


en una fase de mapping disponiendo los datos de forma local a cada nodo, por
lo que no es necesario ejecutar trabajos adicionales. Si group._2 representa la
lista de elementos agrupados, group._1 es la clave de agrupacion.

4.3.5.

Factora para la aplicaci


on de reglas de negocio

Kai puede utilizarse para validar los datos de entrada a otros sistemas, aplicando distintas reglas de negocio a diversas entidades. Para ello, vamos a crear
una clase Factoria que contendra una lista de reglas de negocio a aplicar. Por
cada una de las reglas generaremos una funcion transform que ejecutara la regla
sobre el conjunto de entidades y marcara cada registro como valido o invalido.
trait Entidad {
var id: Int = _
var esValida: Boolean = _
}
trait Regla[T <: Entidad] {
def marcar(entidad: T): T = {
entidad.esValida = validar(entidad)
entidad
}
def validar(entidad: T): Boolean

55

}
class FactoriaDeReglas[+T >: Entidad] {
def crearTrabajo(reglas: List[Regla[T]]): DataFlow[T] = {
reglas.foldLeft(DataFlow[T])((flow, r) =>
flow.transform[T](e => r.marcar(e)))
}
}
class PagoElectronico(val importe: Double, val descuento: Double)
extends Entidad
object EvitarDescuentosNegativos extends Regla[PagoElectronico] {
def validar(entidad: PagoElectronico): Boolean = entidad.descuento >= 0
}
object BizApp extends BasicApplication {
var name = "BizApp"
val factoria = new FactoriaDeReglas[PagoElectronico]
var flow = factoria.crearTrabajo(List(EvitarDescuentosNegativos))
}

Durante el texto hemos mostrado diversas definiciones de trabajos que han sido realizadas de forma explcita, encadenando distintas operaciones a un DataFlow.
En este ejemplo mostramos tambien como Kai puede ser integrado como una
biblioteca m
as del ecosistema Java, permitiendo escribir peque
nos programas
que generen a su vez otros programas.

56

Captulo 5

Trabajos relacionados
En este captulo veremos otras tres libreras similares a Kai con el objetivo
de describir la funcionalidad que aportan y establecer un comparativa sobre las
distintas aproximaciones utilizadas. Estas tres libreras, Pig, Sawzall y Dryad
fueron dise
nadas e implementadas con un proposito similar: El procesado de
vol
umenes de datos en sistemas distribuidos.
Adicionalmente y ya al final del captulo haremos un peque
no repaso a otras
libreras de menor tama
no que tambien merecen ser incluidas en este trabajo.

5.1.

Sawzall

Sawzall [40] es una librera escrita en C++ y creada por Google para el desarrollo de peque
nos programas para la extracion y el procesado de datos. La
librera se cre
o para facilitar la tarea de creacion de trabajos en los cl
usters de
Google, originalmente desarrollados en C y C++.
Existe un compilador de Sawzall liberado por Google denominado Szl [10],
que permite ejecutar trabajos escritos en Sawzall, aunque a diferencia de Kai o
Pig el entorno de ejecuci
on es completamente local, ya que no se dispone de la
infraestructura de servidores interna de Google.
Sawzall est
a asentado en dos observaciones fundamentales:
Que las operaciones de consulta de datos son completamente conmutativas, es decir, que no importa el orden en que se ejecuten y por tanto es
posible recorrer el conjunto de datos siguiendo un orden arbitrario. Esta
asumpci
on tambien se cumple en Hadoop, y por tanto en Kai.
Que las si las operaciones de agregacion son conmutativas, el orden en el
cual se emiten los valores a procesar no importa. En Hadoop este planteamiento tambien se explota, pues el orden de recepcion de valores dentro
de la fase de reducci
on es completamente arbitrario e intercambiable.
Es f
acil observar como estas dos observaciones establecen practicamente la
base de trabajo de MapReduce y, cabe recordar, Hadoop se inicio como un mero
clon del sistema MapReduce empleado internamente por Google.

57

5.1.1.

Funcionamiento b
asico

Sawzall dispone de una sintaxis propia para definir los trabajos de procesado.
En dichos programas se asume que la entrada es un u
nico registro del conjunto
a procesar, y que la salida del trabajo es un resultado intermedio que esta listo
para ser agregado en una fase de reduccion. A diferencia de Kai, podemos hablar
que Sawzall es un lenguaje que u
nicamente dispone de soporte para la fase
de mapping y deja la fase de reduccion a otras implementaciones o sistemas
separados.
El cl
asico programa que cuenta las palabras de un texto, conocido como
word-count, puede ser expresado de la siguiente manera:
topwords: table top(3) of word: string weight count: int;
fields: array of bytes = splitcsvline(input);
w: string = string(fields[0]);
c: int = int(string(fields[1]), 10);
if (c != 0) {
emit topwords <- w weight c;
}

Tal y como vemos, en Sawzall existe una declaracion de tipos de datos anunciada al declarar la variable. La sintaxis es muy similar a C y hereda muchas
de las construcciones disponibles en el lenguaje como bucles for y while, expresiones condicionales con if, etc. Sawzall soporta tipos de datos elementales
como cadenas, n
umeros enteros, en punto flotante y similares, ampliando dichos
tipos a estructuras mas complejas como arrays, maps y tuples. Como era de
esperar, el sistema de serializacion de Sawzall tiene soporte para Protocol Buffers. Pese a que no existe indicio alguno, en principio Sawzall podra ejecutarse
sobre otros sistemas distintos a GFS [20], ya que la propia implementacion de
Szl corre sobre sistemas de archivos convencionales.
En cuanto a las operaciones soportadas, Sawzall permite distintas formas de
manipulaci
on de los datos, haciendo especial incapie en aquellas que producen
agregaci
on de datos. La forma mas simple de recoger datos de una entrada
determinada es expresar la entrada como una colleccion de datos:
c: table collection of string;

Y partiendo de esta base se da soporte a otras operaciones de carga que producen distintos niveles de agregacion en funcion de la operacion elegida: sample,
sum, maximum, quantile, top y unique. Salvo sample y quantile, todas estas
operaciones est
an soportadas por Kai y, llegado el caso, la implentancion de las
restantes puede ser creada sin que suponga un esfuerzo considerable.

5.1.2.

Rendimiento

En cuanto al rendimiento se refiere solo hemos encontrado dos referencias a


estudios relativos y ambas han sido producidas por Google. El estudio inicial
proviene del artculo original de Sawzall [40] y compara el procesado del mismo
conjunto de datos con otros lenguajes como Python, Ruby or Perl, obteniendo
resultados muy favorables para Sawzall, aunque es dificil establecer una comparaci
on si no se usa un entorno de ejecucion similar. La comparativa para trabajos
similares utilizando Java o C++ se reduce a una u
nica assercion en el artculo:
Sawzall is about 1.6 times slower than interpreted Java, 21 times slower than
compiled Java, and 51 times slower than compiled C++. Dado que Sawzall corre
58

sobre la infraestructura MapReduce de Google, asumimos que han establecido


la comparaci
on sobre los mismos programas en el mismo entorno.

5.1.3.

Resumen de caractersticas

Frente a Kai resaltaramos las siguientes diferencias notables:


Sawzall est
a basado en un lenguaje cerrado de implementacion propia,
con tipado est
atico.
No se han encontrado indicios de que el lenguaje o las operaciones soportadas puedan ser extendidas, aunque asumimos que es posible recompilar los
fuentes de Sawzall con una implementacion de las extensiones requeridas
en C++.
Sawzall est
a limitado a generar secuencias a ejecutar dentro de un mapper,
dejando la agregaci
on de resultados a terceros.
El conjunto de operaciones soportado [39] no incluye mucha de la funcionalidad aportada por Kai, aunque es posible simularla mediante las
estructuras de control que Sawzall provee. La aproximacion de Sawzall en
este sentido se basa en proveer estructuras de control clasicas de programaci
on estructurada1 .
Debido a que el sistema de Google para MapReduce es cerrado, Sawzall y
su compilador Szw es necesario ejecutarlos utilizando codigo para adaptar
su funcionamiento al sistema requerido. En el caso de Hadoop, podramos
ejecutarlo mediante Streaming con algunas modificaciones sustanciales.

5.2.

Pig

Apache Pig [38] es un lenguaje creado para dar soporte en Hadoop a la


escritura de programas MapReduce fuera de la infraestructura Java. El lenguaje
nacio dentro de Yahoo como lenguaje de scripting, com
unmente denominado Pig
Latin, para ejecutar peque
nos trabajos dentro del cluster, aunque actualmente
tiene soporte para tareas m
as complejas.
Pig es una librera similar a Kai que basa su ejecucion en los mismos principios: Se define un programa que establece las manipulaciones de datos necesarias
para su ejecuci
on, traduciendolo posteriormente a secuencias de trabajos MapReduce. Pig tambien se ejecuta sobre Hadoop, y aunque siempre se ha abierto
la posibilidad de ejecutarlo en otros sistemas, no existen muchas evidencias de
casos de exito fuera de Hadoop. En el libro de Programming Pig [18] pueden
encontrarse los principios de dise
no que guiaron la implementacion:
Pigs eat anything, haciendo referencia a dise
nar un lenguaje que permita
operar con estructuras de datos muy diversas, con o sin esquemas.
Pigs live anywhere. El proposito de Pig es el procesado de datos y su dise
no
no debera estar ligado a Hadoop, aunque su implementacion inicial as lo
sea [19].
1 Hecho que no es de extra
nar sabiendo que Rob Pike, con notables contribuciones a Plan9
y Go est
a detr
as de Sawzall

59

Pigs are domestic animals. El dise


no de la librera ha puesto especial incapie en poder permitir su extensibilidad a partir de la construccion de
funciones definidas por el usuario (UDFs). Estas funciones es posible implementarlas en Java, Python, Ruby o Javascript, siendo Java el lenguaje
que mejor soporte tiene para la escritura de UDFs.
Pigs fly. Al realizar una traduccion directa de programas escritos en Pig
Latin a una serie de trabajos MapReduce, Pig ha introducido un optimizador capaz de reducir el n
umero de funciones a ejecutar y la carga de
datos en el cluster.

5.2.1.

Funcionamiento b
asico

La estructura de datos fundamental de Pig es la tupla, pues toda la ejecucion


se basa en dicho concepto. Una tupla no es mas que un conjunto de valores que
permite anidaci
on, de forma que es posible crear construcciones complejas. Pig
soporta tipos de datos habituales como n
umeros y cadenas, y cabe destacar la
incorporaci
on de dos estructuras adicionales que resultan interesantes: bag y
map. La primera de ellas es un conjunto que permite elementos duplicados y la
segunda es un ndice clave / valor, tpica tabla hash.
Otra interesante caracterstica es que las funciones para cargar datos en Pig
est
an preparadas para inferir el esquema a partir de los datos de entrada. De
esta forma, si configuramos la entrada para leer archivos CSV (lo mas habitual),
Pig analizar
a las primeras lneas del archivo para determinar el tipo de datos
a utilizar. El lenguaje de por si tiene un marcado caracter dinamico y, aunque
internamente en Java las tuplas conserven el tipo de los datos, este puede mutar
conforme evoluciona la ejecucion.
Podramos destacar tambien el hecho de poder construir flujos de datos no
lineales mediante el operador SPLIT, que divide la secuencia en dos partes con
ejecuciones completamente separadas. Al margen de dicho operador tambien es
posible cargar diversos orgenes de datos en un mismo script, por lo que podemos
hablar de que Pig no sigue un modelo de ejecucion completamente lineal.
Siguiendo el ejemplo anterior mostrado en Sawzall podemos escribir en Pig
Latin un programa para contar palabras de un texto:
input = load data as (line);
words = foreach input generate flatten(TOKENIZE(line)) as word;
cntd = foreach grpd generate group, COUNT(words);
dump cntd;

5.2.2.

Extensibilidad

Pig est
a implementado en Java y como tal cuenta con un excelente soporte
de extensiones. Es posible escribir funciones definidas por el usuario (UDF ) en
Java y hacerlas posteriormente visibles desde Pig. Existen dos tipos posibles
de UDFs: que evaluan condiciones booleanas y que permiten transformar datos
concretos. Por ejemplo, supongamos que queremos incorporar funcionalidad en
Java dentro de nuestro script en Pig para convertir una cadena a mayusculas.
El siguiente ejemplo est
a sacado de la documentacion de Pig:
REGISTER myudfs.jar;
A = LOAD student_data AS (name: chararray, age: int, gpa: float);

60

B = FOREACH A GENERATE myudfs.UPPER(name);


DUMP B;

La definici
on de la UDF UPPER quedara en Java de la siguiente manera:
public class UPPER extends EvalFunc<String> {
public String exec(Tuple input) throws IOException {
if (input == null || input.size() == 0)
return null;
try {
String str = (String)input.get(0);
return str.toUpperCase();
} catch(Exception e) {
throw new IOException("Caught exception processing row ", e);
}
}
}

Igual no puede apreciarse en el ejemplo anterior, pero en el se esboza una


limitaci
on que introduce bastantes errores en los programas en Pig. El objeto
Tuple es una estructura que puede manejar distintos tipos de datos y como
tal no tiene una forma fija. Al combinar esta idea con un lenguaje como Java,
donde internamente se maneja constantemente los tipos de los objetos y sus
propiedades, observamos que es necesario realizar todo tipo de comprobaciones
sobre el tipo de datos de la tupla y de los propios elementos que lo componen.

5.2.3.

Resumen de car
actersticas

No negaremos aqu que el dise


no de Kai esta inspirado en Pig y en la cantidad de horas en las que hemos podido observar como fallos en la eleccion del
paradigma que gobierna una librera se traducen en estar expuestos a errores
no f
acilmente entendibles o reproducibles.
Pig proporciona un lenguaje de scripting con un mecanismo de enlace a
traves de funciones de usuario bien definido, pero limitado. Kai esta completamente integrado en la maquina virtual y su extension es mas directa
a traves de Scala, Java o cualquier otro lenguaje de la JVM.
Pig soporta distintos sistemas de serializacion, siendo CSV el metodo empleado por defecto en la mayora de los usos y aceptando tambien Avro
como formato.
El planificador de Pig tiene una implementacion prematura y en determinados casos es necesario desactivar algunas optimizaciones para evitar
errores o resultados incorrectos en la ejecucion de los trabajos. Hemos detectado que la ejecuci
on del planificador no es determinista y en ocasiones
genera planes distintos con ligeros errores. A
un as, el conjunto de optimizaciones que Pig provee solo hemos podido observarlas en otros sistemas
como Hive o Impala; y fuera de Hadoop en la librera FlumeJava [6] de
uso interno en Google.
Algunas estructuras de control y operaciones en Pig siguen la semantica
de SQL en relaci
on a los valores nulos, aunque no todas. Vease por ejemplo
el operador split y el keyword otherwise o el tratamiento de clausulas
dentro de filter. En este sentido la semantica de Kai es mucho mas
estricta y menos propensa a errores.
61

La killer-feature de Pig es la posibilidad de describir trabajos de manipulaci


on de datos no lineales y cuya ejecucion se bifurca en un momento
dado con posibilidad de agregar los resultados en cualquier momento.
Pig no tiene soporte para reutilizar, al menos de forma sencilla, scripts o
trozos de scripts en funciones. Se ha introducido la posibilidad de escribir
macros no higienicas, pero la escritura de programas grandes se vuelve
ciertamente compleja.

5.3.

Dryad

Dryad [26] es una librera desarrollada por Microsoft Research basada en


el concepto de modelar la ejecucion de un programa como un grafo acclico
dirigido. El modelo computacional de Dryan establece los vertices del grafo
como unidades de proceso y las aristas representan el flujo de datos establecido
entre ambos.
Uno de los problemas que intenta abordar Dryad, frente al enfoque tradicional de MapReduce, es el intentar reducir el espacio de almacenamiento requerido que se produce al intentar encadenar trabajos. En MapReduce, cuando
enlazamos dos jobs distintos, la salida del primer reducer ha de persistir a disco
completamente antes de poder ser empleada en el siguiente mapper. Otra de las
crticas de Dryad a MapReduce es el uso que se hace de operaciones de join
o trabajos que tienen m
ultiples orgenes de datos: como ya hemos visto, son
difciles y tediosos de programar y en ocasiones se delega su ejecucion a otras
DSLs de m
as alto nivel como Pig.
El modo de funcionamiento, o al menos los principios de dise
no de Dryad
son similares a los de otros sistemas homologos a Hadoop como Yahoo S4 [35]
o Storm [48].
En octubre de 2011 Microsoft descontinuo su desarrollo, por lo que u
nicamente es posible recuperar algunas ideas de dise
no para utilizar en futuras
implementaciones de Kai. La limitacion principal del sistema estriba en que la
plataforma de ejecuci
on est
a basada en Windows, lo que incrementa considerable el precio por las licencias de los clusters de produccion.
Para expresar trabajos sobre Dryad existen dos alternativas: una basada en
LINQ y C# y la otra utilizando un lenguaje de scripting denominado SCOPE.

5.3.1.

DryadLINQ

Para expresar trabajos en Dryad se ha creado una librera basada en LINQ


[3] denominada DryadLINQ [50] que permite definir trabajos dentro de la plataforma .NET. La librera la definen de la siguiente manera:
DryadLINQ is a system and a set of language extensions that enable
a new programming model for large scale distributed computing. It
generalizes previous execution environments such as SQL, MapReduce, and Dryad in two ways: by adopting an expressive data model of
strongly typed .NET objects; and by supporting general-purpose imperative and declarative operations on datasets within a traditional
high-level programming language.

62

El lenguaje de implementacion de trabajos en DryadLINQ es C# y las transformaciones de datos se establecen haciendo uso de los operadores soportados
dentro de LINQ. Dentro de estos operadores se cubre practicamente cualquier
transformaci
on de datos que queramos hacer, incluidas agrupaciones, joins, agregaciones complejas, etc.
Un peque
no ejemplo de uso de la librera se muestra a continuacion:
var adjustedScoreTriples =
from d in scoreTriples
join r in staticRank on d.docID equals r.key
select new QueryScoreDocIDTriple(d, r);
var rankedQueries =
from s in adjustedScoreTriples
group s by s.query into g
select TakeTopQueryResults(g);

Las construcciones de DryadLINQ tienen un modo de ejecucion diferido


dictado por el planificador de Dryad. La secuencia de operaciones se descompone
en un conjunto de operaciones basicas que se envian a los distntos nodos. En
el caso de MapReduce se compone un plan de ejecucion denominado EPG,
con la peculiaridad de que a diferencia de Pig o de Sawzall, es necesario crear
c
odigo en tiempo de ejecuci
on para poder absorber correctamente la ejecucion
desde los nodos en terminos de serializacion de datos y en el caso concreto de
subexpressiones en LINQ.

5.3.2.

SCOPE

Adicionalmente a DryadLINQ existe un peque


no proyecto denomininado
SCOPE [5] que introduce un lenguaje de scripting para el procesado de datos
dentro de Dryad. El lenguaje asemeja muchas de sus caractersticas a SQL,
principalmente en cuanto a consultas se refiere. La ejecucion de SCOPE tambien
deriva en un conjunto de programas a ser ejecutados sobre Dryad y al igual que
Pig se provee todo un conjunto de operadores que describen la secuencia de
modificaciones sobre los datos a ejecutar.
El cl
asico ejemplo de word-count puede expresarse en un script de SCOPE
mediante:
SELECT query, COUNT(*) AS count
FROM "search.log" USING LogExtractor
GROUP BY query
HAVING count > 1000
ORDER BY count DESC;
OUTPUT TO "qcount.result";

Como podemos observar la sintaxis es muy similar a la de SQL. Adicionalmente SCOPE permite extender la funcionalidad de las operaciones soportadas
por defecto mediante c
odigo especfico escrito en C#.

5.4.

Otras libreras similares

A parte de las tres libreras anteriormente mencionadas, existen otras libreras que merecen una mencion en este trabajo por las caractersticas que
aportan o por la aproximaci
on que toman para facilitar el procesado de datos.

63

La primera de ellas es Cascading [9], que es una librera escrita en Java que
introduce el concepto de pipes de ejecucion con las que es posible declarar un
flujo de trabajo bien delimitado. En Cascading se han implementado y abstraido
muchas de las operaciones que son comunes en Hadoop, como el enlazado de
trabajos, la agrupaci
on de datos, etc.
Sobre Cascading existen diversas implementaciones creadas debido a que el
API de uso no parece ser de muy facil acceso. La mas popular es Scalding [47],
creada internamente en Twitter. Scalding define un conjunto de operadores por
encima de Cascading, vale la pena mencionar las operaciones para manipulacion
de matrices que existen Al margen de algunas operaciones dentro de Apache
Mahout no hemos visto nada parecido. A diferencia de Kai, la implementacion
de Scalding no estaba basada en la estructura de tipos de Scala y utilizaba
smbolos para definir los datos de los trabajos. Existe un intento de transformar
el API de Scalding para que pueda utilizar tipos seguros.
Google public
o un artculo sobre una librera interna denominada FlumeJava
[6], destinada tambien a simplificar la escritura de trabajos MapReduce de uso
interno. Aunque no se tiene acceso a la implementacion, del artculo podemos
destacar la abstracci
on que realizan de MapReduce (incluyendo la fase de shuffle) mediante la funci
on parellelDo. Al igual que Pig la librera permite lanzar
trabajos con flujos de ejecuci
on no lineales y la ejecucion de trabajos tambien
comprende un planificador en diferido con una fase de optimizacion.

64

Captulo 6

Conclusiones
A lo largo de este texto hemos podido ver como Kai simplifica la escritura de
trabajos de manipulaci
on de datos. Hemos provisto un conjunto bien definido
y estructurado de operaciones que pueden combinarse entre s, con interfaces
claras de entrada y salida, y con un sistema de tipos estricto que evita, desde
la compilaci
on, los errores m
as frecuentes en este tipo de procesos.
El cuidado en la elecci
on de tres operaciones fundamentales filter, transform y fold nos ha permitido definir muchas otras operaciones en base a las
anteriores, y de igual forma mantener una implementacion contenida en cuatro
mappers y un reducer genericos para Hadoop.
La diferencia significativa que introduce Kai frente a otras libreras similares es el dise
no de la extensibilidad para poder manipular tanto la generacion
de trabajos como las optimizaciones aplicadas. Siendo que el comportamiento
de los trabajos ejecutados en Hadoop es muy dependiente de estas modificaciones, creemos que estamos aportando valor a otras propuestas como Pig o
Sazwall donde las opciones de extension de la librera son ciertamente mucho
m
as limitadas.
En el momento de escribir estas lineas Apache Pig, un proyecto liberado en
octubre de 2007, tiene en su haber 370K lineas de codigo Java y 28 desarrolladores de distintas empresas han contribuido al codigo. Solo en el u
ltimo a
no se
produjeron 409 cambios o mejoras en el codigo. Tanto el alcance como la estabilidad de Pig distan mucho de lo que modestamente podamos realizar desde
aqu con Kai.
Sin embargo, eso no impide que podamos ofrecer un dise
no alternativo, coherente y s
olido para Kai, adjuntando una implementacion de referencia para esbozar muchas de los conceptos que Kai introduce, que a su vez puede servir de
gua para futuros trabajos dedicados a estabilizar la base de codigo.
La elecci
on de Hadoop como plataforma es tambien una limitacion inherente
de este tipo de sistemas. Si bien Hadoop ofrece la posibilidad de ampliar el tama
no del cluster sin necesidad de modificar el codigo involucrado en los mappers
y reducers, la latencia que introduce en el lanzamiento de trabajos y en alguno
de los componentes de entrada/salida parece dejarnos en un escenario propenso
a las perdidas de eficiencia. Existen algunos problemas que no son faciles de resolver en Hadoop debido a la naturaleza de MapReduce. Algoritmos iterativos
o de recorrido de estructuras de grafos no se comportan de forma eficiente sobre
Hadoop, forzando implementaciones poco naturales.
65

En este sentido Kai se ha preparado para traducir una secuencia de operaciones en diferentes rondas de MapReduce, para ser ejecutadas sobre Hadoop.
El efecto de lanzar diversos trabajos en secuencia provoca dos perdidas significativas de eficiencia bien determinadas: por una parte, cada fase de MapReduce
requiere persistir a hdfs parte de los resultados parciales y este intercambio
tambien se realiza para intercambiar resultados entre trabajos. Por otra, el lanzamiento de trabajos es completamente lineal y no permite solapar fases de
ejecuci
on.
Futuros trabajos pueden intentar solventar esta limitacion, pues nada impide
modificar el planificador para introducir trabajos no lineales o incluso reemplazar la generaci
on de trabajos de Hadoop por otro tipo de sistemas como Storm
o S4. Tambien sera interesante eliminar MapReduce por completo de la infraestructura Hadoop e introducir un modelo distinto basado exclusivamente en
fold. Para no perder parte de las propiedades de Hadoop podramos basar el
trabajo de gesti
on de recursos y coordinacion encapsulando las transformaciones
en una aplicaci
on YARN.
El dise
no de la librera facilita que emerjan ideas muy distintas como el
uso de CUDA u OpenCL como backend, e incluso proponer una generacion de
programas nativos utilizando LLVM. Kai introduce un dise
no de operaciones
que pueden componerse unas con otras, donde el resultado esperado por cada
funci
on es completamente determinista y acotado a un conjunto definido por el
tipo de datos usado. Esto permite que las secuencias de trabajo de Kai puedan
traducirse a c
odigo nativo de forma practicamente directa. Aquellas operaciones definidas en terminos de primitivas son completamente independientes del
entorno de ejecuci
on final.
En lo referente al uso de lenguajes funcionales para definir transformaciones
de datos, y en concreto al uso que le hemos dado a Scala, estamos seguros de
que debe seguirse trabajando en esta direccion. Scala ha introducido muchas
caractersticas que han sido crticas para el dise
no de Kai y que han procurado
un control ferreo a traves del compilador. No solamente eso, sino que cuantas
m
as restricciones aplicamos al uso de los tipos de datos, mas controlada es la
transformaci
on de las operaciones a trabajos de Hadoop. El propio planificador
trabaja sobre una estructura de datos, estable y bien definida, y en ning
un
momento hemos tenido que bajar al bytecode de la JVM para producir distintos
programas.
A pesar de ello, Scala est
a asentado sobre la JVM y aunque esto nos permite
utilizar Hadoop de forma c
omoda, la propia JVM no honora en absoluto el
sistema de tipos de Scala. Futuros trabajos pueden considerar el uso de macros
higienicas para acotar estas limitaciones, generando en tiempo de compilacion
y no de ejecuci
on c
odigo fuertemente tipado que evite un entorno laxo
y facilite la aplicaci
on
optima y segura de transformaciones. Otras opciones
involucran radicalizar el uso del lenguaje y derivar a lenguajes como Haskell
esta transformaci
on.
Si el objetivo es la generacion de programas que ejecuten transformaciones
de datos, de forma correcta y optimizable, alternativamente puede explorarse
el espacio diametralmente opuesto a los sistemas estrictos de tipos y emplear
lenguajes como Lisp o Scheme. Dos caractersticas de esta aproximacion nos
interesan: por una parte, la representacion de programas como una simple estructura de datos en lista puede facilitar una traduccion no solo correcta, sino
tambien
optima. Por otra, la posibilidad de definir macros como fundamento
66

del lenguaje nos abre un espacio que permitira simplificar considerablemente


la escritura de transformaciones.
La b
usqueda del santo grial de sistemas paralelos y distribuidos ha centrado
incontables intentos de automatizar la generacion de algoritmos. Aunque existen
notables aportaciones, como las de Nancy Lynch a la definicion de sistemas distribuidos, muchas lneas de investigacion siguen obviando el uso de los lenguajes
usados en la implementaci
on. En el espacio donde se definen algoritmos de calculo numerico, por ejemplo, existen a
nos de investigacion dedicados a acotar las
estructuras matem
aticas necesarias para dotar de formalidad y correccion a muchos algoritmos empleados. Esta formalidad parece mezclarse, y muchas veces
obviarse, al introducir lenguajes de programacion con entornos mucho menos
restrictivos que la matem
atica formal empleada en el estudio de la solucion.
Existen problemas de generacion automatica que son imposibles de resolver si
no proveemos bloques de construccion correctos que permitan aplicar reglas y
definir escenarios coherentes con la teora que acompa
na a estos problemas.
Es por ello que, si bien desde este texto no hemos proporcionado solucion a
dichos problemas fundamentales, si creemos que el dise
no de Kai esta alineado
en esta direcci
on para conseguir una definicion de transformaciones de datos
mucho m
as correcta y optimizable que otras soluciones similares.

67

Agradecimientos
Me gustara dedicar unas lneas a Adela para agradecerle las largas tardes y
noches dedicadas a nuestro hijo mientras yo redactaba este texto. Vicente y
Amparo, mis padres, tambien fueron de inestimable ayuda. Paciencia infinita.
Igualmente el director de este trabajo, Jordi Bataller, pudo estructurar muchas
de las ideas que bamos plasmando en papel apuntando siempre en la direccion
correcta.

68

Bibliografa
[1] Henk Barendregt Erik Barendsen. Introduction to lambda calculus. 1994.
[2] Michael Barr and Charles Wells. Category Theory for Computing Science, chapter 1.2, page 3 (21). Department of Mathematics, Case Western
Reserve University, 1998.
[3] Don Box and Anders Hejlsberg. Linq: .net language-integrated query.
MSDN Developer Centre, page 89, 2007.
[4] Eugene Burmako and Martin Odersky. Scala macros, a technical report.
In Third International Valentin Turchin Workshop on Metacomputation,
page 23, 2012.
[5] Ronnie Chaiken, Bob Jenkins, Per Ake Larson, Bill Ramsey, Darren Shakib,
Simon Weaver, and Jingren Zhou. Scope: easy and efficient parallel processing of massive data sets. Proc. VLDB Endow., 1(2):12651276, August
2008.
[6] Craig Chambers, Ashish Raniwala, Frances Perry, Stephen Adams, Robert R. Henry, Robert Bradshaw, and Nathan Weizenbaum. Flumejava:
easy, efficient data-parallel pipelines. In Proceedings of the 2010 ACM
SIGPLAN conference on Programming language design and implementation, PLDI 10, pages 363375, New York, NY, USA, 2010. ACM.
[7] Fay Chang, Jeffrey Dean, Sanjay Ghemawat, Wilson C Hsieh, Deborah A
Wallach, Mike Burrows, Tushar Chandra, Andrew Fikes, and Robert E
Gruber. Bigtable: A distributed storage system for structured data. ACM
Transactions on Computer Systems (TOCS), 26(2):4, 2008.
[8] Alonzo Church. A Set of Postulates for the Foundation of Logic. Annals
of Mathematics, 2(33):346366, 1932.
[9] Concurrent Inc. Cascading. http://www.cascading.org/.
[10] Coogle Inc. Szl - a compiler and runtime for the sawzall language.
https://code.google.com/p/szl/, Julio 2010.
[11] Haskell Brooks Curry, Robert Feys, William Craig, J Roger Hindley, and
Jonathan P Seldin. Combinatory logic, volume 2. North-Holland Amsterdam, 1972.
[12] Doug Cutting. Apache Avro 1.7.4 Specification. The Apache Foundation,
Febrero 2013.
69

[13] Jeffrey Dean and Sanjay Ghemawat. Mapreduce: simplified data processing
on large clusters. In Proceedings of the 6th conference on Symposium on
Opearting Systems Design & Implementation - Volume 6, OSDI04, pages
1010, Berkeley, CA, USA, 2004. USENIX Association.
[14] P. Deutsch. Rfc-1951 deflate compressed data format specification version
1.3. http://www.isi.edu/in-notes/rfc1951.txt, May 1996.
[15] Digital Equipment Corporation. Database Language SQL. ISO/IEC, Julio
1992.
[16] James A Edwards and Uzi Vishkin. Empirical speedup study of truly
parallel data compression. 2013.
[17] Avrilia Floratou, Jignesh M. Patel, Eugene J. Shekita, and Sandeep Tata.
Column-oriented storage techniques for mapreduce. Proc. VLDB Endow.,
4(7):419429, April 2011.
[18] Alan Gates. Programming Pig. OReilly Media, 2011.
[19] Alan F Gates, Olga Natkovich, Shubham Chopra, Pradeep Kamath, Shravan M Narayanamurthy, Christopher Olston, Benjamin Reed, Santhosh
Srinivasan, and Utkarsh Srivastava. Building a high-level dataflow system
on top of map-reduce: the pig experience. Proceedings of the VLDB Endowment, 2(2):14141425, 2009.
[20] Sanjay Ghemawat, Howard Gobioff, and Shun-Tak Leung. The google file
system. In ACM SIGOPS Operating Systems Review, volume 37, pages
2943. ACM, 2003.
[21] Google
Inc.
Protocol
buffers
https://developers.google.com/protocol-buffers/.

developer

[22] Google Inc.


Snappy: A fast compressor/decompressor
https://code.google.com/p/snappy/, Marzo 2011.

guide.
library.

[23] Timothy Gowers. The language and grammar of mathematics. Departament of Pure Mathematics and Mathematical Statistics. University of
Cambridge, October 2004.
[24] John Hughes. Why functional programming matters. The computer journal, 32(2):98107, 1989.
[25] Graham Hutton. A Tutorial on the Universality and Expressiveness of Fold.
Journal of Functional Programming, 9(4):355372, July 1999.
[26] Michael Isard, Mihai Budiu, Yuan Yu, Andrew Birrell, and Dennis Fetterly.
Dryad: distributed data-parallel programs from sequential building blocks.
ACM SIGOPS Operating Systems Review, 41(3):5972, 2007.
[27] Eugene Kohlbecker, Daniel P Friedman, Matthias Felleisen, and Bruce Duba. Hygienic macro expansion. In Proceedings of the 1986 ACM conference
on LISP and functional programming, pages 151161. ACM, 1986.

70

[28] Avinash Lakshman and Prashant Malik. Cassandra: a decentralized structured storage system. SIGOPS Oper. Syst. Rev., 44(2):3540, April 2010.
[29] Ralf L
ammel. Googles mapreduce programming model revisited. Science
of Computer Programming, 70(1):130, 1 2008.
[30] Sheng Liang, Paul Hudak, and Mark Jones. Monad transformers and modular interpreters. In Proceedings of the 22nd ACM SIGPLAN-SIGACT
symposium on Principles of programming languages, POPL 95, pages 333
343, New York, NY, USA, 1995. ACM.
[31] Miran Lipovaca. Learn You a Haskell for Great Good!: A Beginners Guide.
No Starch Press, San Francisco, CA, USA, 1st edition, 2011.
[32] K. Maeda. Performance evaluation of object serialization libraries in xml,
json and binary formats. In Digital Information and Communication Technology and its Applications (DICTAP), 2012 Second International Conference on, pages 177182, 2012.
[33] Simon Marlow et al. Haskell 2010 language report, chapter 9. 2010.
[34] Adriaan Moors, Frank Piessens, and Martin Odersky. Generics of a higher
kind. SIGPLAN Not., 43(10):423438, October 2008.
[35] L. Neumeyer, B. Robbins, A. Nair, and A. Kesari. S4: Distributed stream
computing platform. In Data Mining Workshops (ICDMW), 2010 IEEE
International Conference on, pages 170177, 2010.
[36] Kurt Nrmark. Functional Programming in Scheme, chapter 14. Department of Computer Science, Aalborg University, Denmark, 2003.
[37] Martin Odersky, Lex Spoon, and Bill Venners. Programming in Scala: A
Comprehensive Step-by-Step Guide, 2nd Edition. Artima Incorporation,
USA, 2nd edition, 2011.
[38] Christopher Olston, Benjamin Reed, Utkarsh Srivastava, Ravi Kumar, and
Andrew Tomkins. Pig latin: a not-so-foreign language for data processing.
In Proceedings of the 2008 ACM SIGMOD international conference on Management of data, pages 10991110. ACM, 2008.
[39] Rob
Pike.
Sawzall
lenguage
specification.
http://szl.googlecode.com/svn/doc/sawzall-spec.html, Noviembre 2010.
[40] Rob Pike, Sean Dorward, Robert Griesemer, and Sean Quinlan. Interpreting the data: Parallel analysis with sawzall. Sci. Program., 13(4):277298,
October 2005.
[41] J. Barkley Rosser. Highlights of the history of the lambda-calculus. In
Proceedings of the 1982 ACM symposium on LISP and functional programming, LFP 82, pages 216225, New York, NY, USA, 1982. ACM.
[42] Eric Sammer. Hadoop Operations. OReilly Media, Inc., first edition, September 2012.

71


[43] M. Sch
onfinkel. Uber
die bausteine der mathematischen logik. Mathematische Annalen, 92(3-4):305316, 1924.
[44] Mark Slee, Aditya Agarwal, and Marc Kwiatkowski. Thrift: Scalable crosslanguage services implementation. Facebook White Paper, 5, 2007.
[45] Gerry Sussman, Hal Abelson, and Julie Sussman. Structure and interpretation of computer programs, chapter 1, page 56. The MIT Press, second
edition edition, 1985.
[46] Patrick W. Thompson. Students, functions and the undergraduate curriculum. In Research in Collegiate Mathematics Education, volume 4, pages
2144. American Mathematical Society, 1994.
[47] Twitter Inc. Scalding. https://github.com/twitter/scalding.
[48] Twitter Inc. Storm: Distributed and fault-tolerant realtime computation.
http://storm-project.net/.
[49] Tom White. Hadoop, The Definitive Guide, chapter 7, page 249. OReilly
Media, Inc., 3rd edition, May 2012.

[50] Yuan Yu, Michael Isard, Dennis Fetterly, Mihai Budiu, Ulfar
Erlingsson,
Pradeep Kumar Gunda, and Jon Currey. Dryadlinq: A system for generalpurpose distributed data-parallel computing using a high-level language.
In Proceedings of the 8th USENIX conference on Operating systems design
and implementation, pages 114, 2008.

72

S-ar putea să vă placă și