Documente Academic
Documente Profesional
Documente Cultură
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
1.2.
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
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
Municipio(
nombre: String,
habitantes: Int,
codigoPostal: String)
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
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
1.4.
C
omo est
a organizada la memoria
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
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)
(2.2)
(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)
(2.7)
2.2.
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)
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)
(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
2.3.1.
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)
(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)
(2.17)
2.3.3.
(2.18)
f : (A B) A
(2.19)
(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
2.4.
Operaciones adicionales
2.4.1.
Identidad
2.4.2.
id a : A A
(2.21)
a 7 a, a A
(2.22)
Orden y mezcla
(2.23)
(2.24)
(2.25)
(2.26)
14
2.4.3.
Filtros
2.4.4.
(2.27)
(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)
(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)
(2.36)
(2.37)
(2.38)
(2.39)
15
(2.40)
2.4.5.
Particionado
(2.41)
(2.42)
(2.43)
2.4.6.
skip : ([A] i) A
(2.44)
(2.45)
(2.46)
Agrupaci
on
(2.47)
(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.
(2.49)
(2.50)
(2.51)
Agregaci
on
(2.52)
(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)
(2.57)
(2.58)
2.4.8.
(2.59)
(2.60)
f : A A, f (r, a) = r + 1
(2.61)
Generaci
on
(2.62)
(2.63)
(2.64)
2.4.9.
repeat : A Z [A]
(2.65)
(2.66)
(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)
(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)
(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.
(2.72)
(2.73)
Hadoop y MapReduce
18
2.5.1.
MapReduce
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
Reducer 3
2.5.2.
Modelo te
orico de MapReduce
(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
(2.76)
mapreduce : (1 , 1 ) [(3 , 3 )]
(2.77)
(2.78)
Llegamos pues a la definicion de la funcion mapreduce que usaremos posteriormente para definir las primitivas basicas de Kai:
2.5.3.
(2.79)
(2.80)
(2.81)
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
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
3.1.
3.1.1.
Vista General
Hadoop, HDFS y MapReduce
Nodo 2
TaskTracker
TaskTracker
DataNode
DataNode
...
Nodo N
SPF
TaskTracker
JobTracker
DataNode
NameNode
HDFS
24
3.1.2.
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
TransformMapper
FoldMapper
FoldReducer
FilterJobFact.
TransformJobFact.
FoldJobFactory
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
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
3.2.2.
Piezas b
asicas: DataFlow y Mutable
3.2.3.
Construcci
on del flujo de operaciones
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
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
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)
}
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)
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
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.
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
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.
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)
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;
}
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
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
}
}
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")
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)
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
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();
3.5.2.
44
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
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
48
else false
})
}
4.1.2.
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"
} ]
}
4.1.3.
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
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.
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
}
50
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.
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
4.2.2.
Extendiendo la generaci
on de trabajos
El controlador b
asico de Kai tiene una implementacion muy sencilla, tal
52
4.2.3.
Extendiendo el planificador
53
4.3.
Algunos ejemplos pr
acticos
4.3.1.
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
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)
})
}
4.3.5.
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
5.1.3.
Resumen de caractersticas
5.2.
Pig
59
5.2.1.
Funcionamiento b
asico
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
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);
}
}
}
5.2.3.
Resumen de car
actersticas
5.3.
Dryad
5.3.1.
DryadLINQ
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);
5.3.2.
SCOPE
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.
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
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
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