Formation MongoDB M101N: semaine 4 – Performance

Fonctionnement d’un index

Une table est rangée comme une liste de valeurs rangée les unes à la suite des autres. Lorsqu’on effectue une requête sur cette liste, si il n’y a pas d’index, on va parcourir toute la liste pour trouver les valeurs. Le parcours de toute la liste correspond à un “table scan” pour une base relationnelle et à une “collection scan” pour une base MongoDB.

Pour remédier à ce problème, on utilise un index qui est un annuaire vers les valeurs en base. Si on crée un index sur une valeur triable d’une collection alors on pourra construire un arbre (B tree) de valeurs classées par ordre alphabétiques qui vont pointer vers la valeur dans la collection. Le parcours de l’arbre est rapide et donc on peut trouver plus rapidement les valeurs dans la collection.

Si on crée un index composite sur 3 valeurs de la collection alors on crée un annuaire pour la valeur 1. Pour chaque valeur 1, on crée un 2ème annuaire de valeurs 2. Et pour chaque valeur 2, on crée un 3ème annuaire de valeurs 3. Ainsi si on fait une recherche sur la valeur 1 seulement, il suffit de consulter le 1er annuaire. En revanche si on fait une recherche sur la valeur 2 seulement, on ne pourra pas utiliser l’index puisque les annuaires de valeurs 2 dépendent de celui de la valeur 1.

Ainsi si on considère un index sur les valeurs "a", "b" et "c" d’une collection et si on effectue une requête sur:
"a" seul alors l’index sera utilisé
"b" seul ou "c" seul, l’index ne sera pas utilisé.
"a" et "b", l’index sera utilisé.
"a", "b" et "c" dans cet ordre alors l’index sera utilisé.
"a" et "c" on ne pourra utiliser l’index que pour "a".

Remarques

– Les requêtes "find", "findOne", "update", "remove" et "sort" utilisent un index.
– Par défaut, pour toutes les tables un index est créé sur "_id".
– Si on crée un index sur des valeurs dans le sens ascendant, dans le cas d’une requête dans le sens descendant, l’index ne sera pas utilisé.

Remarques:

Pluggable storage engines

Il est possible de paramétrer un “storage engine” différent de celui par défaut (à partir de la version 3.0):

Driver ⇔ MongoDB server ⇔ Storage engine ⇔ Disks

Plusieurs “storage engine” sont supportés:
– MMAP
– WiredTiger

Les “storage engines” affectent le format du fichier de données et le format des index.

Le storage engine n’affecte pas la communication entre plusieurs serveurs MongoDB ni l’API à disposition des programmeurs.

MMAPv1

MMAP permet de gérer les données qui sont chargées dans la mémoire virtuelle. Il se base sur le système de fichier MMAP qui mappe les fichiers en mémoire.

Ainsi un fichier de données de grande taille (supérieure à 100GB) sur le disque sera chargé de façon paginée. Les pages seront chargées en mémoire virtuelle en fonction de leur utilisation: à la première utilisation, ils sont chargés.

Collection Level locking

MMAP permet de gérer la concurrence des collections en faisant du “collection level locking”. Si 2 opérations d’écriture sont effectuées au même moment dans une même collection, l’une d’entre elles doit attendre que l’autre a terminée, il peut y avoir plusieurs lecteurs mais un seul écrivain à la fois. Si des collections différentes sont affectées alors l’écriture peut se faire au même moment.

Inplace Updates

Une autre fonctionnalité de MMAP est de bouger les collections d’une page à l’autre en mémoire virtuelle en fonction de leur taille. Ainsi si une page est trop petite pour une collection alors la collection sera déplacée vers une page plus grande.

Power of two sized allocations

Pour éviter de déplacer les collections quand elles grossissent, on place les collections dans des pages plus grandes.

On utilise des puissances de 2 pour savoir la taille des pages:

Par exemple une collection de 3gb sera stockée dans une page de 4gb; une collection 19gb sera stockée dans une page de 32gb.

Plan d’exécution

Si il existe 3 index sur une collection, quand on va effectuer une requête sur cette collection et qu’il est possible d’utiliser les 3 index, MongoDB va exécuter utiliser séparément les 3 index pour effectuer la requête. MongoDB gardera ensuite en memoire l’index qui a permis l’exécution la plus rapide de la requête. Ainsi pour les exécutions suivantes du même type de requête, MongoDB choisira systématiquement l’index qui permet l’exécution la plus rapide.

Manipulations des index

Création d’un index sur une table

db.table.createIndex({ a: 1, b:1,c:1})

Permet de créer un index sur 3 colonnes d’une table.

Le "1" indique que l’on veut construire l’index dans le sens ascendant.

Obtenir le plan d’exécution d’une requête

QueryPlanner

[requete].explain()

Les valeurs affichées sont:
cursor: permet d’indiquer le type de curseur utilisée pour afficher les résultats. "BasicCursor" signifie qu’aucun curseur n’a été utilisé pour afficher le résultat. Dans le cas où un curseur est utilisé, cette donnée indique le nom du curseur utilisé.
millis: nombre de millisecondes nécessaires pour exécuter la requête.
n: indique le nombre de documents retournés.
nScannesObjects: nombre de documents scannés pour afficher le résultat
nScanned: nombre de lignes de l’index scannées et le nombre de documents scannés pour afficher le résultat.
indexBounds: intervalles de l’index qui ont été parcourus.
indexOnly: booléen indiquant si l’index seul a suffit pour afficher les résultats. Si faux alors il a été nécessaire de parcourir les documents pour trouver le résultat.
winningPlan: permet d’indiquer quel est le plan qui a permis de retourner les résultats.

Il existe d’autres modes: "executionStats" et "allPlansExecution".

ExecutionStats

[requete].explain("executionStats")

On peut voir les options du "queryPlanner" mode.
On a en plus quelques statistiques “executionStats”:
"nReturned": nombre de documents retournés.
– Différentes étapes d’exécution de la requète “stage”.
"executionTimeMillis": temps d’exécution.

Les étapes permettent de savoir précisemment les index qui sont utilisés.

AllPlansExecution

[requete].explain("allPlansExecution")

Permet d’exécuter tous les plans d’exécutions en utilisant les différents index pour savoir quel est celui qui est le plus rapide.

Les informations spécifiques à ce mode sont dans “allPlansExecution”.

Avec ce mode, certains plans peuvent ne pas retourner de résultat en fonction de l’index qui est utilisé.

Lister les indexes sur un document

db.table.getIndexes()

Lister tous les indexes d’une base

db.system.indexes.find()

Supprimer un index sur une table

db.table.dropIndex({ "student_id": 1}

Le nom de l’index doit exactement celui affiché avec la valeur "key" quand on écrit db.table.getIndexes().

Unique index

Permet de rajouter une contrainte d’unicité sur les valeurs de l’index. Dans le cas où un unique index est appliqué, toute insertion de valeurs déjà présentes dans la base conduira à une erreur correspondant à une clé dupliquée.

Création d’un unique index

db.table.ensureIndex( { name: 1}, { unique: true })

Afficher les index de la table

db.table.getIndexes()

On verra que l’unique index comporte une valeur "unique" : true. Cette valeur permet d’indiquer que l’index contraint l’unicité.

Remarque

Pour le champ "_id" l’index n’est pas affiché comme étant unique pourtant il contraint bien le champ "_id" à être unique.

Index multiclé (multikey indexes)

Les index composites (c’est-à-dire composés de plusieurs champs) ne peuvent pas contenir plusieurs champs contenant des valeurs sous forme de tableaux. Il est seulement possible d’avoir un champ qui comporte des valeurs sous forme de tableau.

Ainsi une collection possède un seul document avec les champs:

Person: 
    Names [andrew, benson] 
    Hobby: [photo, hiking, golf] 
    Location: NY 
    Favorite_Color:

Il sera possible de créer un index sur Names et Location ou Hobby et Favorite_Color mais il sera impossible de créer un index sur Names et Hobby.

Si on crée un index sur hobby et location, les valeurs dans l’index seront:

    photo et NY 
    hiking et NY  
    golf et NY 

Ainsi on fait un produit cartésien entre les valeurs (c’est la raison pour laquelle les index multiclé avec des tableaux pour toutes les clés sont interdits puisqu’il y aurait trop de valeurs possibles dans le produit cartésion).

Si un index composite existe déjà pour names et location et si on ajoute un document avec location sous forme de tableau, on aura un message d’erreur indiquant qu’il n’est pas possible d’ajouter la valeur à cause de l’index.

On peut voir qu’un index est multiclé en utilisant "explain". Le champ utilisé est "isMultiKey".

Index background

2 façons de créer des index "foreground" et "background":

Création "foreground":
– Méthode par défaut,
– Rapide,
– Bloque les lecteurs et les écrivains qui essaient d’accéder à la base,
– Ne pas utiliser en production

Création "background":
– Lente,
– Ne bloque pas les lecteurs et écrivains lors de la création.

Dans le cas d’un “replica set” composé de plusieurs serveurs, il est possible de créer l’index successivement sur tous les serveurs. Par exemple, sur le serveur sur lequel on veut créer l’index, on l’enlève du “replica set”; on répartit la charge sur les autres serveurs; on crée l’index; on réintégre le serveur dans le “replica set”. Et ainsi de suite pour les autres serveurs.

Création d’un index “background”

db.students.createIndex({ name:1 }, { background : true })

Covered query

Requête dont le résultat utilise exclusivement un index.

Ainsi si on regarde le plan d’exécution d’une requête et qu’on s’aperçoit que le nombre de documents parcourus "totalDocsExamined" > 0 alors il ne s’agit pas d’une “covered query”.

Remarque

L’utilisation totale d’un index dépend des champs qui sont affichés par la requête. Si on affiche des champs non utilisés par un index alors MongoDB est obligé d’aller scanner la collection pour obtenir les autres champs.

Taille des index

Il est possible de voir la taille des index utilisés pour une collection en faisant:

db.table.stats()

Les champs "totalIndexSize" et "indexSize" permettent de donner la mémoire utilisée pour les index.

Il est important que les index puissent être chargés dans le “memory set” (partie chargée dans la mémoire plutôt que sur le disque) pour éviter trop d’aller-retour avec le disque.

Remarque

“WiredTiger” compresse les données et ainsi il est possible de réduire la taille des index en mémoire.

Cardinalité

Comparaison entre le nombre de valeurs dans l’index et le nombre de valeurs des champs:
– Regular index: 1:1
– Sparse index: ≤ documents
– Index multikey: > documents

Hint

Permet de forcer l’utilisation d’un index en particulier. On va l’utiliser à ajoutant ".hint" à la suite d’une requête et en indiquant le nom de l’index:

Par exemple:

db.table.find({ a :500, b:500}).hint({a:1})
Attention

Dans le "hint" on indique bien le nom de l’index tel qu’il apparaît avec "getIndexes".

Si on ne veut pas utiliser d’index particulier et qu’on souhaite effectuer un scan de la collection:
On utilisera la fonction "$natural":

db.table.find({ a :500, b:500}).hint({$natural:1})

Sparse index

A la création d’un index, il est possible d’indiquer que l’on souhaite que cette index porte exclusivement sur les documents ou la clé de l’index existe. Ainsi tous les documents ne contenant pas la valeur, ne seront pas indexés.

La création de ce type d’index se fait comme suit:

db.table.createIndex( { name: 1}, { sparse: true })

Ainsi si on utilise “hint” sur cet index, alors les résultats ne comporteront que des documents où le champ "name" existe. Évidement, la même requête sans la fonction "hint" retournerait des résultats.

Full text search index

Permet d’indexer un document contenant un champ contenant avec une chaine de caractères de grande taille.

Création d’un index “full text search”:

db.table.createIndex({ "words": "text"})

"words" est le nom du champ.

Chercher dans une chaine en utilisant un index “full text search”:

db.table.find({ $text: { $search: "chaine de caractères" } })

Même si on écrit plusieurs mots dans la chaine de caractères, la requête va retourner un résultat dès qu’il y a au moins un mot comment entre la chaine de caractères et la valeur d’un champ dans un document.

Performance des index

Pour certains opérateurs, les index se révèlent particulièrement inefficaces. Par exemple pour les opérateurs: $gt, $lt, $ne et $regex. Lorsque ces opérateurs sont utilisés dans les premières conditions d’une requête "find", il est préférable d’imposer l’utilisation d’un index sur les autres champs de la requête n’utilisant pas ces opérateurs (en utilisant "hint").

Par exemple:

db.students.find({ student_id: { $gt: 500}}, { class_id: 20})

Par défaut l’index sur "student_id" sera utilisé. Pour être plus efficace, il faudrait imposer l’utilisation de l’index sur "class_id".

L’élément le plus important dans l’efficacité d’un index est sa sélectivité c’est-à-dire sa capacité à minimiser la quantité de documents ou de clés qui ont été scannés.

D’une façon générale, il faut privilégier la sélectivité des valeurs dans cet ordre: égalité > ordonnancement (utilisation de "sort") > sélection d’un intervalle.

Profiler

Il existe 3 niveaux de profiling:
0: la fonctionnalité est désactivée,
1: seules les requêtes lentes sont logguées.
2: toutes les requêtes sont logguées.

Pour lancer le serveur dans un mode particulier de profiling:

mongod --profile 1 --slowms 2

Ce paramétrage signifie que le niveau utilisé sera "1" et que les requêtes seront logguées lorsqu’elles durent plus de 2 millisecondes.

Pour consulter les logs de profiling:

db.system.profile.find()

Les champs du résultat sont:
"ts": timespamp c’est l’heure à laquelle la requête a été effectuée.
"query": la requête qui été effectuée.
"nscanned": nombre d’objets scannés pour afficher le résultat.
"nreturned": nombre de documents retournés
"millis": temps en millisecondes de la requête.
"ns": namespace, c’est la table sur laquelle la requête a été exécutée: base et nom de la table.

Par exemple:
Pour obtenir les logs comportant un namespace particulier ordonné par timespamp:

db.system.profile.find({ ns: /test.foo/}).sort({ts:1}).pretty()

Pour obtenir les requêtes qui ont duré plus d’un certain temps:

db.system.profile.find({ millis: { $gt: 1 }}).sort({ts:1}).pretty()

Pour obtenir le niveau de profiling courant:

db.getProfilingLevel()
db.getProfilingStatus()

Permet d’obtenir un document comportant les champs:
"was": niveau de profiling
"slowms": durée à partir de laquelle les logs sont faits.

Pour paramétrer un nouveau niveau de profiling ou changer la durée:

db.setProfilingLevel(1,4)

pour niveau 1 et une durée de 4 millisecondes.

Index geospatial

C’est un type d’index qui permet de référencer des points sur un plan (espace à 2 dimensions).

Pour qu’il fonctionne, il faut:
– un champ "location" dans le document avec des coordonnées (x,y) dans un tableau: location : [ x, y]
– de créer l’index: createIndex({ "location" : '2d', type: 1 }). "2d" permet de spécifier le type d’index geospatial et type 1 permet de préciser le sens ascendant.
– d’effectuer des requêtes en utilisant l’opérateur $near:
find({ location: { $near: [x,y]}}) ce qui permettra de trouver les autres points dans la base situé à côté du point (x,y) du plus proche au plus éloigné. On pourra rajouter ".limit(20)" pour limiter le nombre de points à 20 par exemple.

Sharding

C’est une architecture qui permet de multiplier les serveurs sur lesquels une requête sera effectuée pour augmenter les performances. Ainsi l’application va communiquer avec un serveur “mongos” qui va communiquer avec plusieurs “replica sets”:

Chaque “replica set” comprend plusieurs bases qui sont répliquées cependant il s’agit d’un seul serveur “mongod”. Les “replica sets” contiennent des données différentes en fonction de la “shard key”.

Ainsi une même table peut être divisée sur plusieurs “replica sets”. Cette table comprend un champ “shard key” qui va permettre de savoir sur quel “replica set” le document sera réellement stocké, le “shard key” pourrait être la clé primaire ou le champ “_id” de la table. Le mécanisme est transparent pour l’application qui ne sait pas sur quel “replica set” une requête sera effectuée. Le serveur “mongos” exécutera la requête en fonction de la valeur du “shard key”. Si la requête ne comporte pas de “shard key” alors le serveur “mongos” effectuera la requête sur tous les “replica sets”.

Mongotop

Permet de faire du profiling pour une application. Il suffit d’écrire à la ligne de commandes:

mongotop [nombre de secondes d'exécution]

Mongostat

Permet d’avoir le nombre de "insert", "query", "update" et "delete" sur une base mongoDB pendant 1 seconde. Il suffit de taper à la ligne de commandes:

mongostat

"flushes": est le nombre d’écritures sur le disque,
"mapped" est la quantité de mémoire mappée,
"ar/aw" est le nombre de lecture/écriture,
"% used" pourcentage de mémoire utilisée,
"res": resident size.

Leave a Reply