2010-10-16 10 views
42

Tengo una larga historia con bases de datos relacionales, pero soy nuevo en MongoDB y MapReduce, así que estoy casi seguro de que debo estar haciendo algo mal. Iré directo a la pregunta. Lo siento si es largo.MongoDB: Terrible rendimiento de MapReduce

Tengo una tabla de base de datos en MySQL que rastrea el número de vistas de perfil de miembro para cada día. Para la prueba tiene 10,000,000 filas.

CREATE TABLE `profile_views` (
    `id` int(10) unsigned NOT NULL auto_increment, 
    `username` varchar(20) NOT NULL, 
    `day` date NOT NULL, 
    `views` int(10) unsigned default '0', 
    PRIMARY KEY (`id`), 
    UNIQUE KEY `username` (`username`,`day`), 
    KEY `day` (`day`) 
) ENGINE=InnoDB; 

Los datos típicos pueden verse así.

+--------+----------+------------+------+ 
| id  | username | day  | hits | 
+--------+----------+------------+------+ 
| 650001 | Joe  | 2010-07-10 | 1 | 
| 650002 | Jane  | 2010-07-10 | 2 | 
| 650003 | Jack  | 2010-07-10 | 3 | 
| 650004 | Jerry | 2010-07-10 | 4 | 
+--------+----------+------------+------+ 

Utilizo esta consulta para obtener los 5 perfiles más vistos desde 2010-07-16.

SELECT username, SUM(hits) 
FROM profile_views 
WHERE day > '2010-07-16' 
GROUP BY username 
ORDER BY hits DESC 
LIMIT 5\G 

Esta consulta se completa en menos de un minuto. ¡No está mal!

Ahora avanzando hacia el mundo de MongoDB. Configuré un entorno fragmentado usando 3 servidores. Servidores M, S1 y S2. Usé los siguientes comandos para configurar el equipo (Nota: he oscurecido los dispositivos adicionales de IP).

S1 => 127.20.90.1 
./mongod --fork --shardsvr --port 10000 --dbpath=/data/db --logpath=/data/log 

S2 => 127.20.90.7 
./mongod --fork --shardsvr --port 10000 --dbpath=/data/db --logpath=/data/log 

M => 127.20.4.1 
./mongod --fork --configsvr --dbpath=/data/db --logpath=/data/log 
./mongos --fork --configdb 127.20.4.1 --chunkSize 1 --logpath=/data/slog 

Una vez que estaban en funcionamiento, salté al servidor M y lancé mongo. Emití los siguientes comandos:

use admin 
db.runCommand({ addshard : "127.20.90.1:10000", name: "M1" }); 
db.runCommand({ addshard : "127.20.90.7:10000", name: "M2" }); 
db.runCommand({ enablesharding : "profiles" }); 
db.runCommand({ shardcollection : "profiles.views", key : {day : 1} }); 
use profiles 
db.views.ensureIndex({ hits: -1 }); 

entonces me importan las mismas 10.000.000 filas de MySQL, que me dio los documentos que se ven así:

{ 
    "_id" : ObjectId("4cb8fc285582125055295600"), 
    "username" : "Joe", 
    "day" : "Fri May 21 2010 00:00:00 GMT-0400 (EDT)", 
    "hits" : 16 
} 

Ahora viene la verdadera carne y patatas aquí ... Mi mapa y reducir funciones. De vuelta en el servidor M en el shell configuro la consulta y la ejecuto así.

use profiles; 
var start = new Date(2010, 7, 16); 
var map = function() { 
    emit(this.username, this.hits); 
} 
var reduce = function(key, values) { 
    var sum = 0; 
    for(var i in values) sum += values[i]; 
    return sum; 
} 
res = db.views.mapReduce(
    map, 
    reduce, 
    { 
     query : { day: { $gt: start }} 
    } 
); 

Y aquí estaba me encontré con problemas. ¡Esta consulta tomó más de 15 minutos en completarse! La consulta de MySQL tomó menos de un minuto. Aquí está la salida:

{ 
     "result" : "tmp.mr.mapreduce_1287207199_6", 
     "shardCounts" : { 
       "127.20.90.7:10000" : { 
         "input" : 4917653, 
         "emit" : 4917653, 
         "output" : 1105648 
       }, 
       "127.20.90.1:10000" : { 
         "input" : 5082347, 
         "emit" : 5082347, 
         "output" : 1150547 
       } 
     }, 
     "counts" : { 
       "emit" : NumberLong(10000000), 
       "input" : NumberLong(10000000), 
       "output" : NumberLong(2256195) 
     }, 
     "ok" : 1, 
     "timeMillis" : 811207, 
     "timing" : { 
       "shards" : 651467, 
       "final" : 159740 
     }, 
} 

No sólo le tomó por siempre a correr, pero los resultados ni siquiera parecen ser correctas.

db[res.result].find().sort({ hits: -1 }).limit(5); 
{ "_id" : "Joe", "value" : 128 } 
{ "_id" : "Jane", "value" : 2 } 
{ "_id" : "Jerry", "value" : 2 } 
{ "_id" : "Jack", "value" : 2 } 
{ "_id" : "Jessy", "value" : 3 } 

Sé que los números de valores deberían ser mucho más altos.

Mi comprensión de todo el paradigma de MapReduce es que la tarea de realizar esta consulta debe dividirse entre todos los miembros del shard, lo que debería aumentar el rendimiento. Esperé a que Mongo terminara de distribuir los documentos entre los dos servidores de fragmentos después de la importación. Cada uno tenía casi exactamente 5,000,000 de documentos cuando comencé esta consulta.

Así que debo estar haciendo algo mal. ¿Alguien puede darme algún consejo?

Editar: Alguien en el IRC mencionó la adición de un índice en el campo del día, pero hasta donde puedo decir que MongoDB lo hizo automáticamente.

+0

Gah .. Acabo de darme cuenta de una razón por la cual los resultados son incorrectos. Debería haber estado clasificando en "valor" en lugar de "hits". – mellowsoon

+2

Un problema es que cuando importa sus datos en Mongo, el valor 'día' es una cadena gigante, pero en mysql, es una fecha (número entero).Cuando pones tus datos en mongo, asegúrate de almacenarlos como un tipo de fecha. – Clint

+0

también puede separar el campo de fecha y hora, y almacenar la fecha como la cadena "20110101" o entero 20110101 y el índice basado en la fecha –

Respuesta

53

extractos de guía definitiva MongoDB de O'Reilly:

El precio de usar MapReduce es la velocidad: grupo no es particularmente rápido, pero MapReduce es más lento y no se supone que debe ser utilizado en “ tiempo real. " Ejecuta MapReduce como un trabajo de fondo , crea una colección de resultados, y luego puede consultar esa colección en tiempo real.

options for map/reduce: 

"keeptemp" : boolean 
If the temporary result collection should be saved when the connection is closed. 

"output" : string 
Name for the output collection. Setting this option implies keeptemp : true. 
+8

Creo que malentendí el propósito de MapReduce. Pensé que se usaba para procesar una gran cantidad de datos más rápido que las alternativas. Creo que ahora veo que se trata más de la capacidad de procesar ** grandes ** cantidades de datos que de otro modo serían imposibles de procesar en una sola máquina, y la velocidad no es un factor. – mellowsoon

+6

@mellowsoon, por supuesto, el objetivo de mapreduce es procesar una cantidad grande o enorme de datos rápidamente. Es solo la implementación de MongoDB que no es muy rápida. – TTT

+0

@TTT - ¡Gracias! En este momento estoy pensando que mongodb sigue siendo la elección correcta para el tipo de datos que estamos tratando de guardar, pero parece que debería usar algunas otras tecnologías de reducción de mapas para realmente descifrar los datos. – mellowsoon

6

No está haciendo nada mal. (Además de ordenar el valor incorrecto como ya has notado en tus comentarios.)

MongoDB mapear/reducir el rendimiento simplemente no es tan bueno. Este es un problema conocido; ver por ejemplo http://jira.mongodb.org/browse/SERVER-1197 donde un enfoque ingenuo es ~ 350x más rápido que M/R.

Una ventaja es que se puede especificar un nombre de colección de salida permanente con el argumento out de la llamada mapReduce. Una vez que se completa la M/R, la colección temporal cambiará de nombre atómico al nombre permanente. De esta forma, puede programar sus actualizaciones de estadísticas y consultar la colección de resultados de M/R en tiempo real.

+0

Gracias por la respuesta. Voy a dejar la pregunta sin respuesta por un tiempo más largo para ver si alguien más tiene algo de información. Esto es realmente decepcionante. Me pregunto dónde está el cuello de la botella. Tal vez porque MongoDB tiene un solo hilo, por lo que el servidor que coordina todos los fragmentos solo puede ir tan rápido. También tengo curiosidad por los resultados. Parece que los 10 millones de documentos fueron mapeados, cuando la mayoría debería haber sido excluida por la consulta. – mellowsoon

+0

@mellowsoon: verifique su consulta contando la colección con los mismos argumentos (y recuerde que el mes para un objeto JS Date está indexado basado en cero). –

+0

Gracias, lo estoy haciendo ahora. He hecho una nueva instalación completa de Mongo en los 3 servidores, y ahora estoy importando los datos. Una vez hecho esto, veré cómo se distribuyen los datos entre los fragmentos, y elegiré un rango de fechas que debería poner la mitad de los documentos correspondientes en cada fragmento. – mellowsoon

27

Tal vez soy demasiado tarde, pero ...

En primer lugar, se está consultando la colección para llenar el MapReduce sin un índice. Debes crear un índice en "día".

MongoDB MapReduce tiene una sola hebra en un solo servidor, pero se duplica en fragmentos. Los datos en mongo shards se mantienen juntos en fragmentos contiguos ordenados por clave de fragmentación.

Como su clave de fragmentación es "día", y lo está consultando, probablemente solo esté utilizando uno de sus tres servidores. La clave de Sharding solo se usa para difundir los datos. Map Reduce consultará usando el índice "día" en cada fragmento, y será muy rápido.

Agregue algo antes de la tecla de día para difundir los datos. El nombre de usuario puede ser una buena opción.

De esta forma, la reducción del mapa se iniciará en todos los servidores y, con suerte, reducirá el tiempo en tres.

Algo como esto:

use admin 
db.runCommand({ addshard : "127.20.90.1:10000", name: "M1" }); 
db.runCommand({ addshard : "127.20.90.7:10000", name: "M2" }); 
db.runCommand({ enablesharding : "profiles" }); 
db.runCommand({ shardcollection : "profiles.views", key : {username : 1,day: 1} }); 
use profiles 
db.views.ensureIndex({ hits: -1 }); 
db.views.ensureIndex({ day: -1 }); 

Creo que con esas adiciones, puede igualar la velocidad de MySQL, aún más rápido.

Además, mejor no lo use en tiempo real. Si sus datos no necesitan ser "minuciosamente" precisos, planifique una tarea de reducción de mapa de vez en cuando y utilice la colección de resultados.

+1

Además, una última cosa que señalar es que MongoDB le pide que se asegure de que sus índices puede mantenerse en la memoria; ejecutar db.views.stats() le indica el tamaño del índice. Esto es lo que le ayuda a optimizar y maximizar el rendimiento. – Krynble

Cuestiones relacionadas