Nous avons récemment atteint plus de 2 millions de disques pour l'une de nos principales collections et nous commençons maintenant à souffrir de problèmes de performances majeurs sur cette collection.
Les documents de la collection contiennent environ 8 champs que vous pouvez filtrer à l'aide de l'interface utilisateur et les résultats sont supposés être triés en fonction du champ d'horodatage dans lequel l'enregistrement a été traité.
J'ai ajouté plusieurs index composés avec les champs filtrés et l'horodatage par exemple:
db.events.ensureIndex({somefield: 1, timestamp:-1})
J'ai également ajouté quelques index permettant d'utiliser plusieurs filtres à la fois afin d'améliorer les performances. Mais certains filtres mettent encore énormément de temps à fonctionner.
Je me suis assuré que l'utilisation expliquait que les requêtes utilisaient les index que j'ai créés, mais que les performances n'étaient toujours pas suffisantes.
Je me demandais si le sharding était la voie à suivre maintenant .. mais nous allons bientôt commencer à avoir environ 1 million de nouveaux disques par jour dans cette collection .. donc je ne suis pas sûr que ça se répande bien ..
EDIT: exemple pour une requête:
> db.audit.find({'userAgent.deviceType': 'MOBILE', 'user.userName': {$in: ['[email protected]']}}).sort({timestamp: -1}).limit(25).explain()
{
"cursor" : "BtreeCursor user.userName_1_timestamp_-1",
"isMultiKey" : false,
"n" : 0,
"nscannedObjects" : 30060,
"nscanned" : 30060,
"nscannedObjectsAllPlans" : 120241,
"nscannedAllPlans" : 120241,
"scanAndOrder" : false,
"indexOnly" : false,
"nYields" : 1,
"nChunkSkips" : 0,
"millis" : 26495,
"indexBounds" : {
"user.userName" : [
[
"[email protected]",
"[email protected]"
]
],
"timestamp" : [
[
{
"$maxElement" : 1
},
{
"$minElement" : 1
}
]
]
},
"server" : "yarin:27017"
}
veuillez noter que deviceType n'a que 2 valeurs dans ma collection.
Ceci est la recherche de l'aiguille dans une botte de foin. Nous aurions besoin de la sortie de explain()
pour les requêtes qui ne fonctionnent pas bien. Malheureusement, même cela ne résoudrait le problème que pour cette requête particulière, voici donc une stratégie pour vous aider:
db.setProfilingLevel(1, timeout)
où timeout
est le seuil du nombre de millisecondes que prend la requête ou la commande, tout ce qui est plus lent sera consigné)db.system.profile
et exécutez-les manuellement à l'aide de explain()
.explain()
, telles que scanAndOrder
ou les grandes nscanned
, etc.Un problème clé est que vous autorisez apparemment vos utilisateurs à combiner des filtres à leur guise. Sans intersection d'index, le nombre d'index requis augmente considérablement.
Également, lancer aveuglément un index sur toutes les requêtes possibles est une très mauvaise stratégie. Il est important de structurer les requêtes et de vous assurer que les champs indexés ont une sélectivité suffisante .
Supposons que vous avez une requête pour tous les utilisateurs avec status
"active" et d'autres critères. Mais sur les 5 millions d'utilisateurs, 3 millions sont actifs et 2 millions ne le sont pas. Ainsi, sur 5 millions d'entrées, il n'y a que deux valeurs différentes. Un tel index n'aide généralement pas. Il est préférable de rechercher d’abord les autres critères, puis d’analyser les résultats. En moyenne, pour renvoyer 100 documents, vous devrez numériser 167 documents, ce qui ne nuira pas trop aux performances. Mais ce n'est pas si simple. Si le critère principal est la date joined_at
de l'utilisateur et que les utilisateurs risquent fort de ne plus l'utiliser avec le temps, vous devrez peut-être numériser des milliers de documents avant trouver cent allumettes.
L’optimisation dépend donc beaucoup des données (non seulement de leur structure , mais également des données elles-mêmes ), ses corrélations internes et vos modèles de requête .
La situation empire lorsque les données sont trop volumineuses pour la RAM, car disposer d'un index est un avantage, mais analyser (ou même renvoyer) les résultats peut nécessiter l'extraction aléatoire de nombreuses données d'un disque, ce qui prend beaucoup de temps.
Le meilleur moyen de contrôler cela consiste à limiter le nombre de types de requête différents, à interdire les requêtes sur des informations de faible sélectivité et à empêcher un accès aléatoire à d'anciennes données.
Si tout échoue et si vous avez vraiment besoin de beaucoup de flexibilité dans les filtres, il pourrait être intéressant de considérer une base de données de recherche distincte prenant en charge les intersections d'index, de rechercher les identifiants mongo à partir de là, puis d'obtenir les résultats de mongo en utilisant $in
. Mais cela est lourd de ses propres périls.
-- MODIFIER --
L’explication que vous avez publiée est un bel exemple du problème posé par la numérisation des champs de faible sélectivité. Apparemment, il y a beaucoup de documents pour "[email protected]". Désormais, trouver ces documents et les classer par ordre chronologique décroissant est assez rapide car il est pris en charge par des index à haute sélectivité. Malheureusement, comme il n'y a que deux types de périphériques, Mongo doit numériser 30060 documents pour trouver le premier qui correspond à "mobile".
Je suppose que ceci est une sorte de suivi Web et que le modèle d'utilisation de l'utilisateur ralentit la requête (s'il basculait tous les jours sur le Web et sur le mobile, la requête serait rapide).
Pour accélérer cette requête, vous pouvez utiliser un index composé contenant le type de périphérique, par exemple. en utilisant
a) ensureIndex({'username': 1, 'userAgent.deviceType' : 1, 'timestamp' :-1})
ou
b) ensureIndex({'userAgent.deviceType' : 1, 'username' : 1, 'timestamp' :-1})
Malheureusement, cela signifie que des requêtes telles que find({"username" : "foo"}).sort({"timestamp" : -1});
ne pouvant plus utiliser le même index , ainsi, comme décrit, le nombre d'index augmentera très rapidement.
Je crains qu'il n'y ait pas de très bonne solution pour cela en utilisant mongodb pour le moment.
Si vous utilisez $ in, mongodb n'utilise jamais INDEX. Modifiez votre requête en supprimant ce $ dans. Il devrait utiliser index et donnerait de meilleures performances que ce que vous aviez auparavant.
Mongo n'utilise qu'un index par requête . Donc, si vous souhaitez filtrer sur deux champs, Mongo utilisera l'index avec l'un des champs, mais doit néanmoins analyser l'ensemble du sous-ensemble.
Cela signifie que vous aurez besoin d'un index pour chaque type de requête afin d'obtenir les meilleures performances.
Selon vos données, il n’est peut-être pas mauvais d’avoir une requête par champ et de traiter les résultats dans votre application . De cette façon, vous n’auriez besoin que d’index sur chaque champ, processus.