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 passer directement à la question. Désolé si c'est long.
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 les tests, 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.
+--------+----------+------------+------+
| 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 |
+--------+----------+------------+------+
J'utilise cette requête pour obtenir les 5 profils les plus consultés depuis le 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!
Passons maintenant au monde de MongoDB. J'ai configuré un environnement fragmenté en utilisant 3 serveurs. Serveurs M, S1 et S2. J'ai utilisé les commandes suivantes pour configurer l'installation (Remarque: j'ai masqué les adresses 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
Une fois ceux-ci opérationnels, j'ai sauté sur le serveur M et lancé mongo. J'ai émis 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 });
J'ai ensuite 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 les pommes de terre ici ... Ma carte et les fonctions de réduction. De retour sur le serveur M dans le Shell, j'installe 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 où j'ai rencontré des problèmes. Cette requête a duré plus de 15 minutes! La requête MySQL a pris moins d'une minute. Voici la sortie:
{
"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 il a fallu une éternité pour fonctionner, mais les résultats ne semblent même pas corrects.
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 du fragment, ce qui devrait augmenter les performances. J'ai attendu que Mongo ait fini de distribuer les documents entre les deux serveurs de tessons 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 pour autant que je sache, cela a été fait automatiquement par MongoDB.
extraits du guide définitif MongoDB d'O'Reilly:
Le prix de l'utilisation de MapReduce est la vitesse: le groupe n'est pas particulièrement rapide, mais MapReduce est plus lent et n'est pas censé être utilisé en "temps réel". Vous exécutez MapReduce en tant que tâche d'arrière-plan, 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.
Je suis peut-être trop tard, mais ...
Tout d'abord, vous interrogez la collection pour remplir le MapReduce sans index. Vous devez créer un index le "jour".
MongoDB MapReduce est un thread unique sur un seul serveur, mais parallélise sur des fragments. Les données des fragments de mongo sont conservées ensemble dans des morceaux contigus triés par clé de partitionnement.
Comme votre clé de partitionnement est "jour" et que vous la questionnez, vous n'utilisez probablement qu'un seul de vos trois serveurs. La clé de partitionnement est uniquement utilisée pour diffuser les données. Map Reduce interrogera en utilisant l'index "jour" sur chaque fragment, et sera très rapide.
Ajoutez quelque chose devant la clé du jour pour diffuser 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 ça:
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 égaler la vitesse de MySQL, encore plus rapidement.
Aussi, mieux vaut ne pas l'utiliser en temps réel. Si vos données n'ont pas besoin d'être "minutieusement" précises, planifiez une tâche de réduction de carte de temps en temps et utilisez la collection de résultats.
Vous ne faites rien de mal. (En plus de trier sur la mauvaise valeur comme vous l'avez déjà remarqué dans vos commentaires.)
Les performances de mappage/réduction de MongoDB ne sont tout simplement pas géniales. 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.
Cependant, vous pouvez spécifier un nom de collection de sortie permanent avec l'argument out
de l'appel mapReduce
. Une fois le M/R terminé, la collection temporaire sera renommée atomiquement en nom permanent. De cette façon, vous pouvez planifier vos mises à jour de statistiques et interroger la collection de sorties M/R en temps réel.
Avez-vous déjà essayé d'utiliser le connecteur hadoop pour mongodb?
Regardez ce lien ici: http://docs.mongodb.org/ecosystem/tutorial/getting-started-with-hadoop/
Puisque vous n'utilisez que 3 fragments, je ne sais pas si cette approche améliorerait votre cas.