MongoDB: Replication in action

Damien Cosset - Aug 20 '17 - - Dev Community

This article takes up right where I left off in Setting up a replica set in MongoDB. Please make sure to read this one if you want to follow along.

Introduction

We have our replica set configured. We have each node running with the following commands:

mongod --replSet introRep --dbpath ~/data/rs1 --port 27017 
mongod --replSet introRep --dbpath ~/data/rs2 --port 27018 
mongod --replSet introRep --dbpath ~/data/rs3 --port 27019 
Enter fullscreen mode Exit fullscreen mode

You can connect to each instance with mongo --port portNumber

For now, you'll need to connect to your primary node. If you have followed the previous article, you should see a introRep:PRIMARY> at the beginning of each line in your shell. In my case, the primary is on port 27017, but it could be a different one for you.

Playing around

Let's write some documents to our primary. We will write 10000 simple documents, like so:

introRep:PRIMARY> use test
switched to db test

introRep:PRIMARY>  for( i = 0; i < 10000; i++){ db.example.insertOne({count: i})}
{
    "acknowledged" : true,
    "insertedId" : ObjectId("599994478d005c672de000ed")
}
introRep:PRIMARY> db.example.count()
10000

Enter fullscreen mode Exit fullscreen mode

Great, we now have our primary populated. The concept of replication means that your data is replicated identically in your secondaries. Let's connect to one of our secondaries. First, let's run isMaster(). This command acts as a condensed rs.status(). You will see informations such as which node is your primary and on which node you're connected:

introRep:PRIMARY> db.isMaster()
{
    "hosts" : [
        "localhost:27017",
        "localhost:27018",
        "localhost:27019"
    ],
    "setName" : "introRep",
    "setVersion" : 1,
    "ismaster" : true,
    "secondary" : false,
    "primary" : "localhost:27017",
    "me" : "localhost:27017",
    "electionId" : ObjectId("7fffffff0000000000000002"),
    "lastWrite" : {
        "opTime" : {
            "ts" : Timestamp(1503237420, 1),
            "t" : NumberLong(2)
        },
        "lastWriteDate" : ISODate("2017-08-20T13:57:00Z")
    },
    "maxBsonObjectSize" : 16777216,
    "maxMessageSizeBytes" : 48000000,
    "maxWriteBatchSize" : 1000,
    "localTime" : ISODate("2017-08-20T13:57:08.312Z"),
    "maxWireVersion" : 5,
    "minWireVersion" : 0,
    "readOnly" : false,
    "ok" : 1
}
Enter fullscreen mode Exit fullscreen mode

I know I'm not on a secondary, and I know where I am. Let's create a connection on localhost:27018:

introRep:PRIMARY> secondary = new Mongo("localhost:27018")
connection to localhost:27018

introRep:PRIMARY> secondaryDB = secondary.getDB("test")
test

introRep:PRIMARY> secondaryDB.example.find()
Error: error: {
    "ok" : 0,
    "errmsg" : "not master and slaveOk=false",
    "code" : 13435,
    "codeName" : "NotMasterNoSlaveOk"
}
Enter fullscreen mode Exit fullscreen mode

We use the Mongo() constructor to instantiate a connection. Then, we store our database test in a variable to query it. But, as you can see, we end up with the error "not master and slaveOk=false".

Why is this? Well, secondaries may not have the most up to date data when you query it. Therefore, by default, read requests will be refused. If you accidentally connect to one of your secondaries, this will protect your application from having outdated data.

To allow queries on a secondary, we must tell Mongo that we are okay with reading from the secondary, like so :

introRep:PRIMARY> secondary.setSlaveOk()

Careful: The method is used on the connection, not the database. Let's try to query it again:

introRep:PRIMARY> secondaryDB.example.find()
{ "_id" : ObjectId("5999943d8d005c672ddfd9de"), "count" : 0 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9df"), "count" : 1 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9ec"), "count" : 14 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9eb"), "count" : 13 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9e1"), "count" : 3 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9f1"), "count" : 19 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9e4"), "count" : 6 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9e0"), "count" : 2 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9e9"), "count" : 11 }
{ "_id" : ObjectId("5999943d8d005c672ddfd9ea"), "count" : 12 }

...
Enter fullscreen mode Exit fullscreen mode

Let's try to write to our secondary now:

introRep:PRIMARY> secondaryDB.example.insert({"count": 10001})
WriteResult({ "writeError" : { "code" : 10107, "errmsg" : "not master" } })
introRep:PRIMARY> secondaryDB.example.count()
10000
Enter fullscreen mode Exit fullscreen mode

We have an error and the write failed. The secondary only accepts writes that it gets through replication.

New primary

One last interesting thing about replication. If your primary goes down, a secondary is elected as the new primary. Let's see it in action by stopping our primary:

db.adminCommand({"shutdown": 1})

You'll see some error messages because the instance we were connected to lost its connection. But don't worry, the shell won't crash. Now, run the isMaster() method we use before on the secondary:

>secondaryDB.isMaster()
{
    "hosts" : [
        "localhost:27017",
        "localhost:27018",
        "localhost:27019"
    ],
    "setName" : "introRep",
    "setVersion" : 1,
    "ismaster" : false,
    "secondary" : true,
    "primary" : "localhost:27019",
    "me" : "localhost:27018",
    "lastWrite" : {
        "opTime" : {
            "ts" : Timestamp(1503238614, 1),
            "t" : NumberLong(3)
        },
        "lastWriteDate" : ISODate("2017-08-20T14:16:54Z")
    },
    "maxBsonObjectSize" : 16777216,
    "maxMessageSizeBytes" : 48000000,
    "maxWriteBatchSize" : 1000,
    "localTime" : ISODate("2017-08-20T14:16:54.375Z"),
    "maxWireVersion" : 5,
    "minWireVersion" : 0,
    "readOnly" : false,
    "ok" : 1
}
Enter fullscreen mode Exit fullscreen mode

Look at that! Our primary is no longer localhost:27017, but localhost:27019. Now, I can write directly to the new primary.

Conclusion

A few things to remember about replication:

  • Clients ( your app for example ) can send a primary node the same operations you could send on a standalone server. You can write, read, build indexes ...

  • By default, you can't read from secondaries. This is a security check. MongoDB can't promise you that the data on your secondaries will be up-to-date with the primary. If you don't care about your data being the most up-to-date, you can use setSlaveOk() to read from the secondary.

  • Clients can't write to secondaries. Secondaries only accept write through replication.

Source: MongoDB: The Definitive Guide, 3rd Edition

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .