2010-10-16 18 views
42

J'ai une longue histoire avec les bases de données relationnelles, mais je suis nouveau sur MongoDB et MapReduce, donc je suis presque certain que je dois faire quelque chose de mal. Je vais sauter directement dans la question. Désolé si c'est long.MongoDB: Terrible MapReduce Performance

J'ai une table de base de données dans MySQL qui suit le nombre de vues de profil de membre pour chaque jour. Pour tester il a 10 000 000 lignes.

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; 

Les données typiques peuvent ressembler à ceci. J'utilise cette requête pour obtenir les 5 profils les plus consultés depuis 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 

Cette requête se termine en moins d'une minute. Pas mal!

Maintenant, passer à l'univers de MongoDB. Je configure un environnement fragmenté en utilisant 3 serveurs. Serveurs M, S1 et S2. J'ai utilisé les commandes suivantes pour régler le gréement (Note: j'ai obscurci les adresses IP). Une fois que ceux-ci étaient opérationnels, j'ai sauté sur le serveur M et lancé mongo. J'ai donné les commandes suivantes:

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 }); 

Je puis importé les mêmes 10.000.000 lignes de MySQL, ce qui m'a donné des documents qui ressemblent à ceci:

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

Maintenant vient la vraie viande et pommes de terre ici ... Ma carte et réduire les fonctions. De retour sur le serveur M dans le shell, je configure la requête et l'exécute comme ceci.

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 }} 
    } 
); 

Et voici, j'ai eu des problèmes. Cette requête a duré plus de 15 minutes! La requête MySQL a pris moins d'une minute. Voici le résultat:

{ 
     "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 
     }, 
} 

Non seulement at-il fallu pour toujours courir, mais les résultats ne semblent même pas être correct.

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 } 

Je sais que ces valeurs devraient être beaucoup plus élevées.

Ma compréhension de l'ensemble du paradigme MapReduce est que la tâche d'effectuer cette requête doit être répartie entre tous les membres de fragment, ce qui devrait augmenter les performances. J'ai attendu que Mongo ait fini de distribuer les documents entre les deux serveurs de partition après l'importation. Chacun avait presque exactement 5 000 000 de documents lorsque j'ai commencé cette requête.

Je dois donc faire quelque chose de mal. Quelqu'un peut-il me donner des indications? Edit: Quelqu'un sur IRC a mentionné l'ajout d'un index sur le champ jour, mais autant que je sache, cela a été fait automatiquement par MongoDB.

+0

Gah .. Juste réalisé une raison pour laquelle les résultats sont incorrects. J'aurais dû trier sur "valeur" plutôt que "hits". – mellowsoon

+2

Un problème est que lorsque vous importez vos données dans Mongo, la valeur 'day' est une chaîne géante, mais dans mysql, c'est une date (entier).Lorsque vous placez vos données dans mongo, veillez à les stocker en tant que type de date. – Clint

+0

vous pouvez également séparer le champ de date et heure, et stocker la date sous forme de chaîne "20110101" ou entier 20110101 et l'index basé sur la date –

Répondre

53

extraits de MongoDB Definitive Guide de O'Reilly:

Le prix de l'utilisation MapReduce est la vitesse: groupe est pas particulièrement rapide, mais MapReduce est plus lent et n'est pas censé être utilisé dans « Vous exécutez MapReduce comme un arrière-plan travail, il crée une collection de résultats, puis vous pouvez interroger cette collection en temps réel.

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

Je pense que j'ai mal compris le but de MapReduce. Je pensais qu'il était utilisé pour traiter une grande quantité de données plus rapidement que les alternatives. Je pense que je vois maintenant qu'il s'agit plus de la capacité de traiter ** d'énormes quantités ** de données qui seraient autrement impossibles à traiter sur une seule machine, et la vitesse n'est pas un facteur. – mellowsoon

+6

@mellowsoon, bien sûr, le but de mapreduce est de traiter rapidement une grande quantité de données. C'est juste l'implémentation de MongoDB qui n'est pas très rapide. – TTT

+0

@TTT - Merci! À l'heure actuelle, je pense que mongodb est toujours le bon choix pour le type de données que nous essayons d'enregistrer, mais il semblerait que je devrais utiliser d'autres technologies mapreduce pour réellement croquer les données. – mellowsoon

6

Vous ne faites rien de mal. (En plus de trier sur la mauvaise valeur que vous avez déjà remarqué dans vos commentaires.

La carte MongoDB/réduire les performances n'est tout simplement pas géniale. C'est un problème connu; voir par exemple http://jira.mongodb.org/browse/SERVER-1197 où une approche naïve est ~ 350x plus rapide que M/R.

Un avantage est que vous pouvez spécifier un nom de collection de sortie permanente avec l'argument out de l'appel mapReduce. Une fois le M/R terminé, la collection temporaire sera renommée en nom permanent atomiquement. De cette façon, vous pouvez planifier vos mises à jour statistiques et interroger la collection de sortie M/R en temps réel.

+0

Merci pour la réponse. Je vais laisser la question sans réponse un peu plus longtemps pour voir si quelqu'un d'autre a quelque chose à dire. C'est vraiment décevant. Je me demande où est le goulot de la bouteille? Peut-être parce que MongoDB est un seul thread, donc le serveur coordonnant tous les fragments ne peut aller si vite? Je suis également curieux des résultats. Il apparaît que tous les 10 millions de documents ont été mappés, alors que la plupart auraient dû être exclus par la requête. – mellowsoon

+0

@mellowsoon: Vérifiez votre requête en effectuant un décompte sur la collection avec les mêmes arguments (et rappelez-vous que le mois d'un objet Date JS est indexé en base zéro). –

+0

Merci, je le fais maintenant. J'ai fait une nouvelle installation complète de Mongo sur les 3 serveurs, et j'importe les données maintenant. Une fois cela fait, je vais regarder comment les données sont réparties entre les fragments, et choisir une plage de dates qui devrait mettre la moitié des documents correspondants sur chaque fragment. – mellowsoon

27

Peut-être que je suis trop tard, mais ...

D'abord, vous interrogez la collection pour remplir le MapReduce sans index. Vous devriez créer un index sur "jour". MongoDB MapReduce est un thread unique sur un serveur unique, mais parallélisé sur les partitions. Les données contenues dans les fragments mongo sont conservées dans des blocs contigus triés par clé de partition.

Comme votre clé de partition est "day", et que vous interrogez, vous n'utilisez probablement que l'un de vos trois serveurs. La clé de sharding est seulement utilisée pour étaler les données. Map Reduce interrogera en utilisant l'index "day" sur chaque fragment, et sera très rapide. Ajoutez un élément devant la clé du jour pour répartir les données. Le nom d'utilisateur peut être un bon choix.

De cette façon, la réduction de la carte sera lancée sur tous les serveurs et, espérons-le, réduira le temps de trois.

Quelque chose comme ceci:

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 }); 

Je pense qu'avec ces ajouts, vous pouvez faire correspondre la vitesse MySQL, encore plus rapide.

Aussi, mieux vaut ne pas l'utiliser en temps réel. Si vos données n'ont pas besoin d'être "minutieusement" précises, lancez une carte réduisez la tâche de temps en temps et utilisez la collection de résultats.

+1

Aussi, une dernière chose à signaler est que MongoDB vous demande de vous assurer que vos index peut être gardé en mémoire; L'exécution de db.views.stats() vous indique la taille de l'index. C'est ce qui vous aide à optimiser et maximiser les performances. – Krynble