MapReduce es la manera que tenemos en MongoDB de “ejecutar código en el lado del servidor”, podriamos compararlo con los GROUP BY de las Bases de Datos Relacionales.

Nos sirve para hacer operaciones de agregación sin necesidad de bajarnos todos los datos al cliente.

Lo que hacemos es definir funciones en JavaScript, una en la que definiremos los datos que queremos usar (map) y otra como queremos tratarlos (reduce), y le mandamos esas dos funciones al servidor definiéndole el flitro de datos y donde queremos que nos deje el resultado.

Lo mejor es verlo con un ejemplo, en este caso lo pondré en Ruby que es en lo que tenemos hecho nuestra aplicación de logs.

Nosotros estamos guardando en una colección de MongoDB datos desde el log de nuestro servidores web guardando servidor, fecha y hora de cuando recibimos la petición, el tiempo que ha tardado, etc. quitando los datos que no interesan al ejemplo, el documento es algo así:

 
{
  "_id" : ObjectId("4fa101ee78754332a4004cc6"),
  "datetime" : new Date("Wed, 02 May 2012 09:43:15 GMT +02:00"),
  "servername" : "SERVICESTMP2",
  "timetaken" : "436",
  "uri" : "/dingus/tazzy/hotelservice.asmx",
  "useragent" : "Mozilla/4.0+(compatible;+MSIE+6.0;+MS+Web+Services+Client+Protocol+2.0.50727.5448)",
  "status" : "200"
}

En nuestro caso esta colección se incrementa en varios millones de registros cada dia, así que sacar informes bajando todos los datos al cliente es una locura, aquí es donde es imprescindible el uso del MapReduce.

Por ejemplo, queremos saber cuantas peticiones estamos devolviendo cada hora por rango de tiempo en la respuesta.

Lo primero, definir la función map, en la que hacemos lo siguiente:

  • Descomponemos la fecha para quitar los minutos y segundos para tener el Año Mes Dia Hora como unidad de agregación
  • Definimos que valores serán clave en la agregación, en este caso el Año Mes Dia Hora y el nombre del servidor
  • Definimos que valores serán los agregados, count, fijo a 1 para contar registros, y timetaken para agregar por rango de tiempo de respuesta:
map = "function Map() { 
	var day = new Date(this.datetime.getFullYear(), 
		this.datetime.getMonth(), 
		this.datetime.getDate(), 
		this.datetime.getHours()); 
	emit({day : day, servername: this.servername}, {count: 1, timetaken: this.timetaken}); 
		}"

Después definimos la función reduce, en la que hacemos lo siguiente:

  • Definimos la estructura del resultado en result, el contador y las peticiones que tardan mas de 2, 5, 10 y 20 segundos
  • Nos recorremos los registros y vamos asignado el resultado a result, incrementando el count e incrementado los countmore según el timetaken
reduce = "function Reduce(key, arr_values) { 
            var result = {count: 0, countmore2: 0, countmore5: 0, countmore10: 0, countmore20: 0};
 
            for(var i in arr_values) { 
            	result.count += arr_values[i].count;
 
	    	if(arr_values[i].timetaken > 20000) 
			result.countmore20 += arr_values[i].count; 
		else if(arr_values[i].timetaken > 10000) 
                        result.countmore10 += arr_values[i].count; 
                else if(arr_values[i].timetaken > 5000) 
                        result.countmore5 += arr_values[i].count; 
                else if(arr_values[i].timetaken > 2000) 
                        result.countmore2 += arr_values[i].count; 
            }
            return result; 
            }"

Con las dos funciones listas solo hay que mandárselas al servidores con el filtro de datos que nos interese y decirle donde queremos los resultados.

Puedes pedir los resultados en línea o que te los deje en una colección.

Las ventaja de tener los datos en una colección es que puedes hacer agregación sobre esa colección, ósea procesar este MapReduce cada hora y hacer reduce sobre los datos existentes en la colección, tambien puedes hacer un replace, un merge …

Con mongoid y haciendo reduce para agregar datos a la colección seria algo así:

  • Hacemos el MapReduce y agregamos los resultados a la colección “requestbyday”
  • Pedimos los datos de la colección.
WebLog.collection.map_reduce(map, reduce,
	{
        	:query =>  { :datetime => {'$gte' => datefilterfrom},
			     :datetime => {'$lte' => datefilterto}} ,
                :out => { reduce: "requestbyday"}
        })
 
@weblogs = WebLog.db.collection("requestbyday").find

las otras opciones de salida serian:

  • replace: Si no exista la colección la crea y si existe elimina todos los datos y mete los nuevos
  • merge: Si la key no existe la inserta, si existe la reemplaza por la nueva
  • reduce: Lo que he explicado antes.
  • inline: Devuelve los datos directamente, sin guardarlos en una colección.

Espero que sea de ayuda, cualquier comentario es bienvenido.

Categories: , , , , ,

Leave a Reply


*

PUBLICIDAD

EMAIL





posts recientes

MapReduce con MongoDB

Posted on may - 18 - 2012

0 Comment

Habilitar la compresión de...

Posted on abr - 25 - 2012

0 Comment

Bamboo, MSBuild y referencias...

Posted on abr - 17 - 2012

2 Comments

Sponsors

  • Etooltech
  • Dingus Services
  • Etooltech
  • Dingus Services