Formation MongoDB M101N: semaine 6 – Conception des applications

Durabilité des écritures

Dans un contexte classique, on a:

Application ⇔ driver ⇔ mongod ⇔ mongo Shell

Des opérations d’écritures sont faites par l’application et par le mongo shell. Pour obtenir les erreurs:
– dans le cas du mongo Shell, on les a tout de suite,
– dans le cas de l’application, ces erreurs sont gérées par le driver.

On peut aussi obtenir les erreurs en appelant "getLastError".

"getLastError" comporte 2 paramètres:
w: permet d’indiquer qu’on souhaite recevoir un acknowladgement quand on a écrit en base. Cette acknowladgement ne certifie pas que l’écriture a été effectuée sur le disque mais qu’elle se trouve en mémoire. En cas de coupure d’alimentation l’écriture peut être perdue.
j: indique que l’écriture a été rajoutée dans le journal de log. Dans le cas de cette valeur, on est sûr que l’écriture s’est bien passée et qu’elle se trouve en base.

Différents cas sont possibles:

w=0 et j=0: mode “fire and forget”: on a aucune assurance que l’écriture s’est bien passée. C’était la valeur par défaut jusqu’à la version 3.0.
w=1 et j=0: mode “safe”, dans la plupart des cas l’écriture s’est bien passée. C’est la mode par défaut à partir de la version 3.
w=1 et j=1: mode “commit to journal”: l’écriture est garantie.
w=0 et j=1: même comportement que mode précédent.

Tous ces modes s’appellent le “Write concern” et sont gérés par le driver.

Problèmes réseau

Même dans le mode “commit to journal”, on peut ne pas recevoir l’indication de l’écriture dans le journal en cas de problème réseau. Dans ce cas, l’écriture peut avoir été faite en base mais on ne reçoit pas le signal indiquant l’écriture dans le journal.

Réplication

L’architecture classique de mongo DB est le “replica set”. Il est composé de minimum 3 bases de données autonomes. L’un des nœuds est le primaire “primary” et les autres sont secondaires “secondary”. L’application via le driver effectue les opérations d’écriture systématiquement sur le primaire.

Si le primaire n’est plus accessible, les 2 autres nœuds élisent un primaire qui recevra les nouvelles opérations d’écriture. Si la nœud défaillant redevient opérationnel, il deviendra un secondaire.

Il faut un minimum de 3 nœuds pour permettre l’élection d’un nouveau primaire.

Types de nœud dans un “replica set”

4 types différents:
Regular: nœud normal pouvant être primaire ou secondaire et ayant un droit de vote.
Arbitrer: il ne contient pas de données mais dans le cas où on a que 2 machines, il permet d’avoir un nombre impair pour effectuer les votes. Il peut donc être hébergé sur la même machine qu’un “regular”.
Delayed/regular: il s’agit d’une machine n’ayant pas les mêmes performances qu’un “regular”. Il peut servir à dépanner un “regular” défaillant. Il contient des données et a un droit de vote mais il ne peut pas devenir primaire. Sa priorité est nulle.
Hidden: il contient des données aussi mais il ne peut pas devenir primaire. Il possède un droit de vote. Sa priorité est nulle.

Consistance en écriture

Par défaut l’application effectue des opérations d’écriture et de lecture sur le nœud primaire seulement. Ceci permet d’avoir une forte consistance des données après l’écriture. Ainsi si on lit les données tout de suite après l’écriture, les valeurs correspondront à l’écriture.

Il est possible de permettre les lectures sur les nœuds secondaires mais sachant que la réplication des données est asynchrone, une lecture immédiate après l’écriture ne certifie pas d’avoir des données consistantes.

Création d’un “replica set”

A partir du mongo Shell:

mongod --replSet rs1 --logPath "1.log" --dbPath /data/rs1 --port 27017 --fork

"rs1" est juste le nom du “replica set”.

On indique un port dans ce cas pour différencier les serveurs dans le cas où le “replica set” se trouve sur la même machine.

"fork" permet d’afficher certaines informations sur le mongo Shell.

Pour créer les autres serveurs, il suffit de renouveler la commande avec des ports différents:

mongod --replSet rs1 --logPath "2.log" --dbPath /data/rs1 --port 27018 --fork 
mongod --replSet rs1 --logPath "3.log" --dbPath /data/rs1 --port 27019 --fork

A ce moment les serveurs ne sont pas connectés entre eux.

Pour connecter les serveurs entre eux:

config = { _id : "rs1", members : [ 
{ _id : 0, host : "machine:27017", priority: 0 } 
{ _id : 1, host : "machine:27018" } 
{ _id : 2, host : "machine:27019" } 
]}

rs.initiate(config) 
rs.status()

Le nœud "0" ne pourra pas devenir primaire à cause de "priority:0".

"rs.status()" n’est pas nécessaire pour la création du “replica set” mais affiche seulement le statut.

Pour passer d’un serveur à l’autre, il suffit d’écrire:

mongo --port 27017

Pour autoriser les opérations de lecture sur un secondaire:

rs.slaveOk()

Pour savoir si le serveur sur lequel on est connecté est le primaire:

rs.isMaster()

La réplication des modifications du primaire sur les secondaires se fait par l’intermédiaire de la table "oplog.rs" qui contient un timestamp. Ainsi les secondaires interrogent le primaire régulièrement pour savoir quelles sont les dernières modifications en fonction du timestamp, il peuvent ensuite répliquer les modifications.

Remarque

La réplication supporte des moteurs de stockage différents (storage engine) entre primaire et secondaire.

Failover et rollback

Si un primaire devient défaillant dans un “replica set”, un secondaire sera élu en tant que primaire. Durant le laps de l’élection et de la défaillance, des écritures ont pu être effectué dans le primaire initial.

Si ce primaire redevient opérationnel, il restera secondaire mais il peut contenir des opérations d’écriture que le nouveau primaire n’a pas. Dans ce cas, le primaire va effectuer un rollback et écrire ces opérations dans un fichier pour qu’elles soient éventuellement exécutées manuellement.

Durant le laps de temps du failover, les écritures méneront à des erreurs.

Driver et failover

Lorsqu’une application est connectée à un primaire, si ce primaire devient défaillant puis redevient opérationnel, il aura perdu son statut de primaire. L’application peut entraîner une exception si elle effectue une écriture sur cet ancien primaire.

Par la suite il faudra que l’application change de serveur pour continuer ces opérations d’écriture. Ces mécanismes sont gérés par le driver mais les blocs de code doivent contenir des Try...Catch pour prévenir de ces exceptions.

“Write concern” dans le cas d’un “replica set”

Il est possible de paramétrer la valeur de "w" différemment dans le cas d’un “replica set”:
w=1: on attends l’acknowledgment du primaire,
w=2: on attends l’acknowledgment d’au moins un secondaire,
w=3: on attends l’acknowledgment de tous les secondaires.

En paramétrant une valeur de "w" correspondant à la majorité, on évite la plupart des cas de rollback puisque si la valeur est écrite au moins sur 2 serveurs, on peut imaginer qu’elle ne sera pas rollbacker.

Il est possible de paramétrer une valeur de "w" au niveau de la connexion, de la collection ou du “replica set”.

Il n’est pas possible de paramétrer une valeur différente de 0 ou 1 pour "j".

Remarque

Si w=1 et j=1, on a pas la garantie que des rollbacks ne vont pas se produire car une écriture peut ne pas avoir été faite sur le secondaire lors de la défaillance du primaire.

Préférence en lecture

Au niveau du driver d’une application, il est possible d’indiquer des préférences pour définir la façon dont les données seront lues:
PRIMARY: les lectures ne se font que sur le primaire,
SECONDARY: on effectue les lectures seulement sur un secondary. Si il est indisponible, on ne pourra plus lire.
SECONDARY_PREFERRED: la lecture sera effectuée de préférence sur des secondaires.
PRIMARY_ PREFERRED: la lecture sera effectuée de préférence sur des primaires.
NEAREST: on effectuera les lectures sur le secondaire qui est le plus proche (i.e. Pong le plus court).
TAGGING: on peut ajouter des tags pour indiquer sur quel serveur primaire ou secondaire, on va effectuer les lectures.

Implication de la réplication

– Liste des nœuds: le plus souvent géré par le driver.
– Write concern: valeurs de "w", "j" et du timeout de "w".
– Préférences en lecture
– Des erreurs peuvent survenir, il faut protéger la lecture et l’écriture des données par des blocs TRY...CATCH pour anticiper les erreurs.

Remarque

Si on ne précise pas de timeout de "w", dans le cas où w > 1, on peut attendre une très longue période avant d’avoir une réponse.

Sharding

Pour augmenter les performances et la scalabilité d’une base, il est possible de diviser un environnement Mongo en plusieurs éléments: les “shards”. Chaque “shard” est un “replica set” (composé donc d’au moins 3 serveurs).

Il sera donc possible de diviser une collection en plusieurs parties et chaque partie sera hébergée sur un “shard” différent. Les parties de la collection s’appelle les “chunks”.

Lorsqu’une requête est effectuée sur un environnement composé de “shards”:
– l’application doit se connecter un serveur Mongos au lieu de se connecter à un serveur Mongod. C’est Mongos qui va savoir quel “shard” doit être requêté.
– si on utilise le mongo Shell, il devra être connecté directement au Mongos.
– les documents sont rangés dans les “shards” en fonction de la valeur du “shard key” qui correspond à la clé primaire de la collection. Mongos va se charger lui-même de repartir les documents équitablement sur chaque “shard”.
– sachant que les “shards” sont interrogés en fonction de la valeur du “shard key”, toutes les requêtes doivent comporter le “shard key” lorsqu’une collection est ainsi divisée.
– le “shard key” doit posséder un index.

Mise en œuvre d’un environnement comportant des “shards”

La configuration basique d’un environnement avec des “shards” doit comporter au minimum:
– 2 “shards” composé chacun d’un “replica set”
– chaque “replica set” doit être composé d’au moins 3 serveurs mongod.
– il doit y avoir au moins 3 instances de mongod pour effectuer de la configuration. Ces instances peuvent être exécutées sur la machine qu’un des “replica sets”.

Ainsi une configuration minimum comporte au moins 9 instances de mongod qui s’exécutent: 3 par “replica set” et 3 pour les instances de configuration.

Pour mettre en place un “shard”, il faut créer un “replica set” et préciser qu’il s’agit d’un “shard”:

mongod --replSet rs1 --logPath "1.log" --dbPath /data/shard0/rs0 --port 27017 --fork --shardsvr 
mongod --replSet rs1 --logPath "2.log" --dbPath /data/shard0/rs1 --port 27018 --fork --shardsvr 
mongod --replSet rs1 --logPath "3.log" --dbPath /data/shard0/rs2 --port 27019 --fork --shardsvr

config = { _id : "s0", members : [ 
{ _id : 0, host : "machine:27017" } 
{ _id : 1, host : "machine:27018" } 
{ _id : 2, host : "machine:27019" } 
]} 

rs.initiate(config)

"--shardsvr" permet d’indiquer que le replica set doit faire partie d’un “shard”.

On renouvelle l’opération pour le 2ème “shard”.

On crée ensuite les 3 serveurs de configuration:

mongod --logpath "cfg-a.log" --dbpath /data/config/config-a --port 57017 --fork --configsvr 
mongod --logpath "cfg-b.log" --dbpath /data/config/config-b --port 57018 --fork --configsvr 
mongod --logpath "cfg-c.log" --dbpath /data/config/config-c --port 57019 --fork --configsvr

"--configsvr" permet d’indiquer qu’on souhaite créer des serveurs de configuration.

On démarre le serveur mongos en incluant les serveurs de configuration:

mongos --logpath "mongos.log" --configdb machine:57017,machine:57018,machine:57019 --fork

On ajoute les “shards” et on ajoute la collection au “shards”:

db.adminCommand( { addshard: "s0/machine:37017" } ); 
db.adminCommand( { addshard: "s1/machine:47017" } );
db.adminCommand( { enableSharding: "test" } );

"test" est le nom de la base de données.

db.adminCommand( { shardCollection: "test.grades", key: { student_id : 1 }} );

"test.grades" est le nom de la collection,
"student_id" est le nom de la shard key.

Si on utilise le mongo shell, il faut ensuite se connecter au mongos et non à un serveur mongod.

Quand on effectue une requête, en utilisant ".explain()" on peut voir sur quel “shard” la requête a réellement été exécutée.

sh.status() permet d’indiquer le statut de l’environnement de “shards”.

Implications du “sharding”

– Tous les documents doivent inclure la “shard key”.
– La “shard key” est non modifiable.
– Tous les index doivent commencer par la “shard key”. Ils peuvent comporter d’autres champs mais le premier champ doit forcément être la “shard key”.
– Les requêtes d’update doivent comporter la “shard key” ou doivent être “multi”.
– Si dans une requête, on ne précise pas la “shard key”, tous les “shards” seront requêtés, ce qui est très coûteux.
– Pas d’index unique à moins qu’il comporte la “shard key” car il n’est pas possible de requêter tous les “shards” pour vérifier l’unicité.
– La “shard key” ne peut être un index “multi-key” (portant sur des tableaux par exemple).

Remarque

La “shard key” ne doit pas forcément être unique.

Sharding et réplication

Tout ce qui a été énoncé auparavant concernant les “replica sets” est valable dans un environnement comportant des “shards”:
– Par défaut, mongos effectue les requêtes sur les serveurs primaires de chaque “shard”. Il est possible d’autoriser les requêtes en lecture vers les serveurs secondaires.
– Les éléments de “write concern”: "w", "j" et le timeout "w" définissable au niveau de l’application sont aussi appliqués à chaque “shard”. Ainsi l’obtention d’un acknowladgement au niveau d’un “shard” se fera si on a l’acknowledgment au niveau du “replica set” du “shard”.
– Enfin il est possible d’utiliser plusieurs instances de mongos pour éviter les interruptions de service dans le cas de la défaillance d’une instance.

Sélection d’une “shard key”

– Sachant que la “shard key” n’est pas modifiable, il faut la choisir correctement à la crearion de l’environnement.
– La “shard key” doit comporter suffisamment de cardinalité pour permettre une bonne répartition des documents sur les différents “shards”. Si la cardinalité est insuffisante, un “chunck” pourrait comporter plus de documents qu’un autre.
Hotspotting: intervalle de valeur de la “shard key” doit rester le plus stable possible. Si cet intervalle augmente trop souvent, le “shard” contenant les valeurs maximales sera utilisé plus souvent que les autres. La réparation de charge ne sera donc pas équitable entre tous les “shards”. Ce problème peut se produire si on choisit la date par exemple.

Leave a Reply