Aller au contenu

Réplication Dans MongoDB


Définition

La réplication est une technique commune aux systèmes NoSQL pour assurer la sécurité et la reprise après les pannes. Elle consiste à créer des copies des données sur des serveurs différents pouvant remplacer le serveur en cas de panne.

Principe du Replica Set

  • Un Replica Set est un groupe de serveurs mongod qui gèrent les mêmes données.
  • Il est composé d'un unique serveur primaire (maître) et de plusieurs serveurs secondaires (esclaves).
  • Le serveur primaire répond aux demandes du (lecture/écriture) client (driver). Le client peut également lire à partir des serveurs secondaires.
  • Les serveurs secondaires reproduisent les mêmes opérations (écritures) réalisées au niveau du serveur primaire.
  • Si le serveur primaire est indisponible, un serveur secondaire est promu en primaire après une procédure d'élection.
  • Si le nombre de serveurs est pair, un serveur arbitre est ajouté.
  • Le nombre minimal de serveurs est de 3 et ils peuvent atteindre 50.
  • 7 serveurs au maximum peuvent participer à une élection.

Mise en place d'un Replica Set avec Docker

La création d'un cluster de 3 noeuds avec Docker consiste à réaliser les 3 étapes suivantes :

  1. Créer un réseau Docker pour interconnecter les containers
  2. Démarrer 3 instances de MongoDB
  3. Initier le Replica Set

1⃣ Créer un réseau docker

docker network create mongoCluster
2⃣ Démarrer les 3 containers MongoDB

docker run -d -p 27017:27017 --name mongo1 --network mongoCluster mongo:6.0.1 mongod --replSet rs1 --bind_ip localhost,mongo1
docker run -d -p 27018:27017 --name mongo2 --network mongoCluster mongo:6.0.1 mongod --replSet rs1 --bind_ip localhost,mongo2
docker run -d -p 27019:27017 --name mongo3 --network mongoCluster mongo:6.0.1 mongod --replSet rs1 --bind_ip localhost,mongo3

3⃣ Initier le Replica Set

  • Se connecter à mongo1 avec Compass (mongodb://localhost:27017) ou avec la commande docker exec -it mongo1 mongosh

  • Exécuter depuis le shell

    > rs.initiate({ _id: "rs1", members: [
       {_id: 0, host: "mongo1"},
       {_id: 1, host: "mongo2"},
       {_id: 2, host: "mongo3"}
     ]
    })
    

  • Vérifier le replica

    > rs.status()
    
    La réponse est similaire à ce qui suit :

{
  set: 'rs1',
  date: 2022-10-12T09:56:20.947Z,
  myState: 1,
  term: Long("1"),
  syncSourceHost: '',
  syncSourceId: -1,
  heartbeatIntervalMillis: Long("2000"),
  majorityVoteCount: 2,
  writeMajorityCount: 2,
  votingMembersCount: 3,
  writableVotingMembersCount: 3,
  optimes: {
    lastCommittedOpTime: { ts: Timestamp({ t: 1665568580, i: 7 }), t: Long("1") },
    lastCommittedWallTime: 2022-10-12T09:56:20.382Z,
    readConcernMajorityOpTime: { ts: Timestamp({ t: 1665568580, i: 7 }), t: Long("1") },
    appliedOpTime: { ts: Timestamp({ t: 1665568580, i: 7 }), t: Long("1") },
    durableOpTime: { ts: Timestamp({ t: 1665568580, i: 7 }), t: Long("1") },
    lastAppliedWallTime: 2022-10-12T09:56:20.382Z,
    lastDurableWallTime: 2022-10-12T09:56:20.382Z
  },
  lastStableRecoveryTimestamp: Timestamp({ t: 1665568522, i: 1 }),
  electionCandidateMetrics: {
    lastElectionReason: 'electionTimeout',
    lastElectionDate: 2022-10-12T09:52:42.586Z,
    electionTerm: Long("1"),
    lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 1665568351, i: 1 }), t: Long("-1") },
    lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1665568351, i: 1 }), t: Long("-1") },
    numVotesNeeded: 2,
    priorityAtElection: 1,
    electionTimeoutMillis: Long("10000"),
    numCatchUpOps: Long("0"),
    newTermStartDate: 2022-10-12T09:52:42.779Z,
    wMajorityWriteAvailabilityDate: 2022-10-12T09:52:44.189Z
  },
  members: [
    {
      _id: 0,
      name: 'mongo1:27017',
      health: 1,
      state: 1,
      stateStr: 'PRIMARY',
      uptime: 310,
      optime: [Object],
      optimeDate: 2022-10-12T09:56:20.000Z,
      lastAppliedWallTime: 2022-10-12T09:56:20.382Z,
      lastDurableWallTime: 2022-10-12T09:56:20.382Z,
      syncSourceHost: '',
      syncSourceId: -1,
      infoMessage: '',
      electionTime: Timestamp({ t: 1665568362, i: 1 }),
      electionDate: 2022-10-12T09:52:42.000Z,
      configVersion: 1,
      configTerm: 1,
      self: true,
      lastHeartbeatMessage: ''
    },
    {
      _id: 1,
      name: 'mongo2:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 229,
      optime: [Object],
      optimeDurable: [Object],
      optimeDate: 2022-10-12T09:56:20.000Z,
      optimeDurableDate: 2022-10-12T09:56:20.000Z,
      lastAppliedWallTime: 2022-10-12T09:56:20.382Z,
      lastDurableWallTime: 2022-10-12T09:56:20.382Z,
      lastHeartbeat: 2022-10-12T09:56:20.778Z,
      lastHeartbeatRecv: 2022-10-12T09:56:20.316Z,
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo1:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    },
    {
      _id: 2,
      name: 'mongo3:27017',
      health: 1,
      state: 2,
      stateStr: 'SECONDARY',
      uptime: 229,
      optime: [Object],
      optimeDurable: [Object],
      optimeDate: 2022-10-12T09:56:20.000Z,
      optimeDurableDate: 2022-10-12T09:56:20.000Z,
      lastAppliedWallTime: 2022-10-12T09:56:20.382Z,
      lastDurableWallTime: 2022-10-12T09:56:20.382Z,
      lastHeartbeat: 2022-10-12T09:56:20.778Z,
      lastHeartbeatRecv: 2022-10-12T09:56:20.317Z,
      pingMs: Long("0"),
      lastHeartbeatMessage: '',
      syncSourceHost: 'mongo1:27017',
      syncSourceId: 0,
      infoMessage: '',
      configVersion: 1,
      configTerm: 1
    }
  ],
  ok: 1,
  '$clusterTime': {
    clusterTime: Timestamp({ t: 1665568580, i: 7 }),
    signature: {
      hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
      keyId: Long("0")
    }
  },
  operationTime: Timestamp({ t: 1665568580, i: 7 })
}

Test du Replica Set

Test des opérations de lecture/écriture

Essayer les opérations suivantes :

1⃣ Se connecter au serveur primaire et créer une base de données 'Courrier' en suivant les étapes suivantes :

> use courrier
> db.createCollection('mail')
> db.mail.insert({from:'support@mongodb.com',subject:'Replica Set Testing',body:'OK'})

2⃣ Se connecter aux serveurs secondaires et vérifier le contenu de la base :

   > use courrier
   > db.mail.find()
Read Preference

Par défaut, la lecture sur Replica Set est autorisée sur le serveur Primary. Pour éviter cette erreur, exécuter > db.getMongo().setReadperf('secondary') et réessayer.

3⃣ Vérifier si l'insertion est possible depuis un serveur secondaire.

Test de la reprise sur panne

Pour simuler une panne :

  • Se connecter au serveur primaire.
  • Arrêter le serveur et examiner le comportement du Replica Set (réponse aux requête, élection d'un nouveau serveur primaire).
> use admin
> db.shutdownServer()

Le fichier oplog

Le serveur primaire crée automatiquement une collection oplog.rs dans la base de données local pour y enregistrer toutes les opérations de mise à jour. Les serveurs secondaires lisent régulièrement le fichier oplog pour répliquer les données.

Afficher le contenu de cette collection :

> use local
> db.oplog.rs.find().pretty()
Vous pouvez lire les informations suivantes :

  • "op" : code opération
  • "ts" : date et heure
  • "ns" : collection concernée

Pour fixer la taille de ce fichier modifier le paramètre oplogSizeMB

replication:
  oplogSizeMB: <taille en MB>

Haute disponibilité et modes de lectures

Pour améliorer le temps de réponse des opérations de lecture (l'écriture est exclusivement exécutée sur le serveur primaire). Plusieurs modes d'exécution des lectures sont possibles à travers le pramètre localThresholdMS de la section replication à savoir :

  • Primary : valeur par défaut, lecture sur le serveur d’écriture.
  • PrimaryPreferred : si jamais le PRIMARY n’est plus disponible, les requêtes sont routées vers le SECONDARY.
  • Secondary : Routé uniquement sur les SECONDARY.
  • Nearest : Le serveur physique le plus proche sur le réseau (latence la plus faible) est interrogé directement par le client.

D'autres paramètres permettent aussi d'influencer la haute disponibilité comme la priorité d'élection de chaque serveurs et le paramètre readConcern.

1⃣ Se connecter au primary

docker exec -it mongo1 mongosh

2⃣ Changer le mode de lecture

use courrier;
db.getMongo().setReadPref('primary');
db.getMongo().setReadPref('secondary');
db.getMongo().setReadPref('primaryPreferred');
db.getMongo().setReadPref('secondaryPreferred');
3⃣ Se connecter à un noeud secondaire et lire avec différents modes

docker exec -it mongo2 mongosh

puis

use courrier;
db.mail.find();