mercredi 1 septembre 2010

MongoDB : scalabilité, réplication et failover grâce au sharding et aux replica set

Deux des fonctionnalités les plus attendues de MongoDB arrivent à maturation avec la sortie de la version 1.6 : le sharding et les replica set.

Le sharding, ou partitionnement, permet de rendre MongoDB parfaitement "horizontally scalable".
Les replica set permettent eux de répliquer les données entre des instances MongoDb, c'est une amélioration du mode master/slave existant, en ajoutant le failover automatique et la récupération automatiques des noeuds.

Mettre en place le Replica Set

Il est possible d'avoir autant de membres que voulu dans un replica set, et les données existeront sur chacun des noeuds du set. Cela permet de répartir les serveurs entre différents datacenters, et ainsi d'assurer une redondance totale. Un seul serveur est "primaire" et peut recevoir des lectures et des écriture, les autres sont "secondaires" et ne peuvent recevoir que des lectures.
Si le noeud primaire tombe, un autre noeud prendra le relai automatiquement.
Le changement de master se fait via un système d'élection, ou chaque noeud actif du set vote pour élire un nouveau master. Un noeud "arbitre", qui appartient au set mais ne reçoit ou n'envoie aucune données peut être ajouté. L'ajout de cet arbitre est obligatoire dans le cas où le set ne comporte que 2 noeuds : en effet si le master tombe, il ne reste plus qu'un noeud qui votera pour lui même, il aura donc 1 voix sur 2, ce qui est insuffisant pour qu'il soit élu.

On va donc commencer par lancer 2 serveurs mongod + l'arbitre (avec l'option --shardsvr pour préparer la suite), en indiquant qu'ils appartiennent à un Replica Set.

mongod --port=10001 --shardsvr --replSet=replset --logpath=${path}/nodes/node1/logs/node.log --logappend --dbpath=${path}/nodes/node1/data/ --fork --rest
mongod --port=10002 --shardsvr --replSet=replset --logpath=${path}/nodes/node2/logs/node.log --logappend --dbpath=${path}/nodes/node2/data/ --fork --rest
mongod --port=10009 --shardsvr --replSet=replset --logpath=${path}/nodes/arbiter/logs/node.log --logappend --dbpath=${path}/nodes/arbiter/data/ --fork --rest

Un petit coup d'oeil aux logs du premier noeud :
[initandlisten] ******
[websvr] web admin interface listening on port 11001
[initandlisten] connection accepted from 127.0.0.1:53323 #1
[startReplSets] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

Les 3 noeuds sont maintenant lancés, on va pouvoir configurer la réplication
mongo --port 10001
cfg = { _id: "replset", members: [{_id: 0, host: "ubuntu:10001"},{_id: 1, host: "ubuntu:10002"},{_id: 2, host: "ubuntu:10009", arbiterOnly: true }]}
rs.initiate(cfg)

On va pouvoir regarder les logs pour vérifier que tout se passe bien :

tail nodes/node1/logs/node.log

[conn1] replSet replSetInitiate admin command received from client
[conn1] replSet replSetInitiate config object parses ok, 3 members specified
[initandlisten] connection accepted from 127.0.1.1:60557 #3
[conn1] replSet replSetInitiate all members seem up
[conn1] replSet info saving a newer config version to local.system.replset
[conn1] replSet replSetInitiate config now saved locally.  Should come online in about a minute.
[conn1] end connection 127.0.0.1:53512
[rs Manager] replSet can't see a majority, will not try to elect self
[initandlisten] connection accepted from 127.0.1.1:49305 #4
[initandlisten] connection accepted from 127.0.1.1:49306 #5
[ReplSetHealthPollTask] replSet info ubuntu:10002 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10009 is now up
[rs Manager] replSet info electSelf 0
[rs Manager] replSet PRIMARY
[initandlisten] connection accepted from 127.0.1.1:49308 #6

tail nodes/node2/logs/node.log

[startReplSets] replSet got config version 1 from a remote, saving locally
[startReplSets] replSet info saving a newer config version to local.system.replset
[rs Manager] replSet can't see a majority, will not try to elect self
[conn2] replSet info voting yea for 0
[ReplSetHealthPollTask] replSet info ubuntu:10001 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10009 is now up
[rs_sync] replSet initial sync pending
[rs_sync] building new index on { _id: 1 } for local.me
[rs_sync] Buildindex local.me idxNo:0 { name: "_id_", ns: "local.me", key: { _id: 1 } }
[rs_sync] done for 0 records 0.001secs
[initandlisten] connection accepted from 127.0.1.1:37633 #3
[rs_sync] replSet initial sync drop all databases
[rs_sync] dropAllDatabasesExceptLocal 1
[rs_sync] replSet initial sync cloning db: admin
[rs_sync] replSet initial sync query minValid
[rs_sync] replSet initial sync copy+apply oplog
[rs_sync] replSet initial sync finishing up
[rs_sync] replSet set minValid=4c72a31e:1
[rs_sync] building new index on { _id: 1 } for local.replset.minvalid
[rs_sync] Buildindex local.replset.minvalid idxNo:0 { name: "_id_", ns: "local.replset.minvalid", key: { _id: 1 } }
[rs_sync] done for 0 records 0secs
[rs_sync] replSet initial sync done
[rs_sync] replSet SECONDARY

tail nodes/arbiter/logs/node.log

[startReplSets] replSet got config version 1 from a remote, saving locally
[startReplSets] replSet info saving a newer config version to local.system.replset
[initandlisten] connection accepted from 127.0.1.1:51923 #3
[ReplSetHealthPollTask] replSet info ubuntu:10001 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10002 is now up

On peut donc voir que node1 a été élu PRIMARY, node2 est donc SECONDARY, et arbiter ne fait pas parti du groupe.

On va maintenant essayé d'ajouter un noeud a chaud :
mongod --port=10003 --shardsvr --replSet=replset/localhost:10001 --logpath=${path}/nodes/node3/logs/node.log --logappend --dbpath=${path}/nodes/node3/data/ --fork --rest

mongo --port 10001

rs.add("ubuntu:10003")

Encore une fois on jete un coup d'oeil aux logs :

tail nodes/node3/logs/node.log

[startReplSets] replSet got config version 2 from a remote, saving locally
[startReplSets] replSet info saving a newer config version to local.system.replset
[rs Manager] replSet warning total number of votes is even - considering giving one member an extra vote
[rs Manager] replSet can't see a majority, will not try to elect self
[ReplSetHealthPollTask] replSet info ubuntu:10001 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10002 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10009 is now up
[rs_sync] replSet initial sync pending
[rs_sync] building new index on { _id: 1 } for local.me
[rs_sync] Buildindex local.me idxNo:0 { name: "_id_", ns: "local.me", key: { _id: 1 } }
[rs_sync] done for 0 records 0.007secs
[rs_sync] replSet initial sync drop all databases
[rs_sync] dropAllDatabasesExceptLocal 1
[rs_sync] replSet initial sync cloning db: admin
[rs_sync] replSet initial sync query minValid
[rs_sync] replSet initial sync copy+apply oplog
[rs_sync] replSet initial sync finishing up
[rs_sync] replSet set minValid=4c72a3fe:1
[rs_sync] building new index on { _id: 1 } for local.replset.minvalid
[rs_sync] Buildindex local.replset.minvalid idxNo:0 { name: "_id_", ns: "local.replset.minvalid", key: { _id: 1 } }
[rs_sync] done for 0 records 0secs
[rs_sync] replSet initial sync done
[rs_sync] replSet SECONDARY

tail nodes/node2/logs/node.log

[rs Manager] replset msgReceivedNewConfig version: version: 2
[rs Manager] replSet info saving a newer config version to local.system.replset
[rs Manager] replSet replSetReconfig new config saved locally
[rs Manager] replSet warning total number of votes is even - considering giving one member an extra vote
[rs Manager] replSet can't see a majority, will not try to elect self
[ReplSetHealthPollTask] replSet info ubuntu:10001 is now up
[ReplSetHealthPollTask] replSet info ubuntu:10009 is now up
[rs Manager] replSet info electSelf 1
[rs Manager] replSet PRIMARY
[ReplSetHealthPollTask] replSet info ubuntu:10003 is now up

L'arrivée de node3 a donc entrainé un nouveau vote pour élire le master, et c'est maintenant node2 le nouveau PRIMARY.

Si on essaye maintenent de tuer le master, on va voir qu'un nouveau noeud est élu pour prendre son relai

kill -9 6486

[rs_sync] replSet syncThread: 10278 dbclient error communicating with server
[conn2] end connection 127.0.0.1:55409
[ReplSetHealthPollTask] replSet info ubuntu:10002 is now down (or slow to respond)
[rs Manager] replSet info electSelf 2
[rs Manager] replSet PRIMARY

Voilà, c'est aussi simple que ça, on a maintenant 3 serveurs MongoDB répliqués, en master/slave, avec élection automatique du master en cas de problème.

Passons maintenant au sharding

Mise en place du sharding

Maintenant qu'on a 3 serveurs MongoDB répliqués, on va pouvoir y ajouter le partitionnement horizontal, ou sharding.

On va donc passer à une architecture à 9 serveurs, répartis en 3 replica set.
Il y a 3 éléments à mettre en place pour que le sharding fonctionne :
  • Les Shard servers : ou instances mongod, c'est ce que l'on vient de faire.
  • Les Config servers : Serveur de configuration qui va stocker les metadat du shard. La documentation conseille au moins 3 serveurs de configuration dans un environnement de production, on va içi se limiter à un seul.
  • Le mongos : Sert de routeur vers les différents shards.



Nous allons maintenant activer le sharding sur notre replica set. Pour cela, on va commencer par créer nos replica set :
mongod --port=10011 --shardsvr --replSet=rs1 --logpath=${path}/nodes/node1-1/logs/node.log --logappend --dbpath=${path}/nodes/node1-1/data/ --fork --rest
mongod --port=10012 --shardsvr --replSet=rs1 --logpath=${path}/nodes/node1-2/logs/node.log --logappend --dbpath=${path}/nodes/node1-2/data/ --fork --rest
mongod --port=10013 --shardsvr --replSet=rs1 --logpath=${path}/nodes/node1-3/logs/node.log --logappend --dbpath=${path}/nodes/node1-3/data/ --fork --rest

mongo 127.0.0.1:10011/admin
> cfg = { _id: "rs1", members: [{_id: 0, host: "ubuntu:10011"},{_id: 1, host: "ubuntu:10012"},{_id: 2, host: "ubuntu:10013"}]};
> rs.initiate(cfg);

mongod --port=10021 --shardsvr --replSet=rs2 --logpath=${path}/nodes/node2-1/logs/node.log --logappend --dbpath=${path}/nodes/node2-1/data/ --fork --rest
mongod --port=10022 --shardsvr --replSet=rs2 --logpath=${path}/nodes/node2-2/logs/node.log --logappend --dbpath=${path}/nodes/node2-2/data/ --fork --rest
mongod --port=10023 --shardsvr --replSet=rs2 --logpath=${path}/nodes/node2-3/logs/node.log --logappend --dbpath=${path}/nodes/node2-3/data/ --fork --rest

mongo 127.0.0.1:10021/admin
> cfg = { _id: "rs2", members: [{_id: 0, host: "ubuntu:10021"},{_id: 1, host: "ubuntu:10022"},{_id: 2, host: "ubuntu:10023"}]};
> rs.initiate(cfg);

mongod --port=10031 --shardsvr --replSet=rs3 --logpath=${path}/nodes/node3-1/logs/node.log --logappend --dbpath=${path}/nodes/node3-1/data/ --fork --rest
mongod --port=10032 --shardsvr --replSet=rs3 --logpath=${path}/nodes/node3-2/logs/node.log --logappend --dbpath=${path}/nodes/node3-2/data/ --fork --rest
mongod --port=10033 --shardsvr --replSet=rs3 --logpath=${path}/nodes/node3-3/logs/node.log --logappend --dbpath=${path}/nodes/node3-3/data/ --fork --rest

mongo 127.0.0.1:10031/admin
> cfg = { _id: "rs3", members: [{_id: 0, host: "ubuntu:10031"},{_id: 1, host: "ubuntu:10032"},{_id: 2, host: "ubuntu:10033"}]};
> rs.initiate(cfg);

Nos 3 replica set sont prêts, on va pouvoir passer au sharding : en commençant par lancer les serveurs de configuration et le routeur :
mongod --port=11001 --configsvr --logpath=${path}/nodes/config1/logs/node.log --logappend --dbpath=${path}/nodes/config$i/data/ --fork
mongod --port=11002 --configsvr --logpath=${path}/nodes/config2/logs/node.log --logappend --dbpath=${path}/nodes/config$i/data/ --fork
mongod --port=11003 --configsvr --logpath=${path}/nodes/config3/logs/node.log --logappend --dbpath=${path}/nodes/config$i/data/ --fork

mongos --port=9999 --configdb=ubuntu:11001,ubuntu:11002,ubuntu:11003 --logpath=${path}/nodes/mongos/logs/node.log --logappend --fork --chunkSize=1

Par defaut, la taille minimale d'un chunk est de 50Mo, pour des raisons de facilité de test, on passe à 1Mo via l'option chunkSize.

Ensuite, on va activer le sharding sur une collection db.people :
mongo 127.0.0.1:9999/admin
> db.runCommand({addshard: "rs1/ubuntu:10011,ubuntu:10012,ubuntu:10013"})                                                  
{ "shardAdded" : "shard0000", "ok" : 1 }
> db.runCommand({addshard: "rs2/ubuntu:10021,ubuntu:10022,ubuntu:10023"})
{ "shardAdded" : "shard0001", "ok" : 1 }
> db.runCommand({addshard: "rs3/ubuntu:10031,ubuntu:10032,ubuntu:10033"})
{ "shardAdded" : "shard0002", "ok" : 1 }

> db.runCommand({enablesharding: "test"}) 
{ "ok" : 1 }
> db.runCommand({shardcollection: "test.people", key:{"_id":1}})
{ "collectionsharded" : "db.people", "ok" : 1 }

On a donc partitionner la collection "people" de la base "test" sur la clef autogénérée par MongoDb.

> db.printShardingStatus()
--- Sharding Status --- 
  sharding version: { "_id" : 1, "version" : 3 }
  shards:
      {
 "_id" : "shard0000",
 "host" : "rs1/ubuntu:10011,ubuntu:10012,ubuntu:10013"
}
      {
 "_id" : "shard0001",
 "host" : "rs2/ubuntu:10021,ubuntu:10022,ubuntu:10023"
}
      {
 "_id" : "shard0002",
 "host" : "rs3/ubuntu:10031,ubuntu:10032,ubuntu:10033"
}
  databases:
 { "_id" : "admin", "partitioned" : false, "primary" : "config" }
 { "_id" : "db", "partitioned" : true, "primary" : "shard0000" }
  db.people chunks:
   { "_id" : { $minKey : 1 } } -->> { "_id" : { $maxKey : 1 } } on : shard0000 { "t" : 1000, "i" : 0 }

Voila, le sharding est maintenant appliqué , on va pouvoir insérer un peu de données. Dans les stats, on voit bien que pour l'instant, un seul shard est utilisé par notre base, la répartition sur plusieurs shards se faisant uniquement quand le besoin s'en fait ressentir.

mongo 127.0.0.1:9999
> for (var i=1;i<=100000;i++) db.people.save({index:i, data:'Just for filling'})

Si on regarde les stats de la base immédiatement, on va se rendre compte qu'il y a plusieurs chunks, mais qu'ils sont tous sur le même shard, il faut attendre quelques minutes pour que la répartition se fasse entre les shards.

Apres 2 minutes d'insertions, on voit bien que les 3 shards sont utilisés :

mongo 127.0.0.1:9999
> db.people.stats()
{
 "sharded" : true,
 "ns" : "test.people",
 "count" : 89264,
 "size" : 12854112,
 "avgObjSize" : 144.00107546155226,
 "storageSize" : 33546240,
 "nindexes" : 1,
 "nchunks" : 13,
 "shards" : {
  "shard0000" : {
   "ns" : "test.people",
   "count" : 44609,
   "size" : 6423696,
   "avgObjSize" : 144,
   "storageSize" : 11182080,
   "numExtents" : 6,
   "nindexes" : 1,
   "lastExtentSize" : 8388608,
   "paddingFactor" : 1,
   "flags" : 1,
   "totalIndexSize" : 1867776,
   "indexSizes" : {
    "_id_" : 1867776
   },
   "ok" : 1
  },
  "shard0001" : {
   "ns" : "test.people",
   "count" : 20975,
   "size" : 3020448,
   "avgObjSize" : 144.0022884386174,
   "storageSize" : 11182080,
   "numExtents" : 6,
   "nindexes" : 1,
   "lastExtentSize" : 8388608,
   "paddingFactor" : 1,
   "flags" : 1,
   "totalIndexSize" : 876544,
   "indexSizes" : {
    "_id_" : 876544
   },
   "ok" : 1
  },
   "shard0002" : {
   "ns" : "test.people",
   "count" : 26220,
   "size" : 3775728,
   "avgObjSize" : 144.00183066361555,
   "storageSize" : 11182080,
   "numExtents" : 6,
   "nindexes" : 1,
   "lastExtentSize" : 8388608,
   "paddingFactor" : 1,
   "flags" : 1,
   "totalIndexSize" : 1089536,
   "indexSizes" : {
    "_id_" : 1089536
   },
   "ok" : 1
  }
 },
 "ok" : 1
}

Conclusion

En conclusion, par rapport au sharding sans replica set, on ne gagne pas vraiment en performances, on perd en stockage (les shards sont dupliqués sur les membres du set), mais on gagne fortement en fiabilité.

Pour optimiser un peu l'utilisation des serveurs, on pourrait faire se recouper les replica set, c'est un dire qu'un serveur physique ferait partie de plusieurs replica.

Notes sur le failover

Si une instance mongod crashe, ou un serveur de configuration, il n'y aura aucun impact sur la disponibilités des données, et toutes les opérations seront disponibles.
Par contre, si 2 noeuds sur les 3 disponibles d'un même replica set tombent, ce replica set passera en ReadOnly.
De même, un bonne architecture serait d'avoir au moins 4 serveurs par replica set, repartis sur 3 datacenter.

Extension

Pour scaler notre architecture, il suffit donc de créer un nouveau replica set, et de l'ajouter au shard, les données seront réparties automatiquement par MongoDB.

4 commentaires:

  1. Merci pour cette article :)

    RépondreSupprimer
  2. Bonjour,
    Tout d'abord merci pour ton article. J'aurais une question sur la méthode pour désactiver le sharding sur une base, en effet j'ai dropé une base (+restart de mon replicaSet) et celle-ci apparaît toujours lorsque je lance "db.printShardingStatus()". Je n'ai pas trouvé de commande du type unshard ou disableSharding :(.
    Merci par avance.

    Matthieu.

    RépondreSupprimer
  3. Je spam ton blog, j'ai fini par trouver ici https://jira.mongodb.org/browse/SERVER-2253

    Désolé ;)

    RépondreSupprimer
  4. Bonjour,
    Ton poste est très intéressant et tu expliques bien.
    Par contre peux tu me dire en quoi tu améliores la sécurité des données dans ce cas précis ?
    En effet si je comprends bien te sécurise les données en la répliquant en local sur le même serveur.
    Donc si un serveur est down, dans ton cas tu perdra 1/3 des données.
    Pour ma par je vois une amélioration des performances mais une diminution de la sécurité.
    Pour plus de sécurité il faudrait mixer les replica (nodeX-Y) sur les shards

    RépondreSupprimer