In this blog post, we will attempt to create MongoDb
replicaset using docker compose
. A replica set in mongodb is a group of mongod instances which maintains the same set of data. This enables applications to work despite one of the db servers going down.
At every instance, there wil be only and only ONE primary node. Rest of the instances are deemed secondary. We would do write operations only via the primary node. The primary node records all the changes using the oplog
, or in other words the operations log. The secondary replicates the changes in the oplog
of primary node and replicate the data. Do note that this is an asynchronous operation.
When the primary node is unable to communicate, an election ensures another node is selected is primary. From that point onwards, the new node assumes the role of primary and would act as the entry point of data.
Implementing Mongo ReplicaSet
For this tutorial we will be using Docker for implementing mongodb. Infact, the focus would be on docker compose
and how to use it to implement replicaset using mongodb containers. We will be implementing a replicaset of size 3 (nodes). we will begin by defining the network
the containers would share and also the volume
for each of the containers.
version: '3.8'
networks:
common.network:
driver: bridge
volumes:
mongo.one.vol:
name: "mongo.one.vol"
mongo.two.vol:
name: "mongo.two.vol"
mongo.three.vol:
name: "mongo.three.vol"
It is now time to define the 3 services required. Each of the services are mapped to their respective volume and is linked via a common network.
services:
mongo.one.db:
container_name: mongo.one.db
image: mongo:latest
networks:
common.network:
ports:
- 20000:27017
volumes:
- mongo.one.vol:/data/db
# - ./rs-init.sh:/scripts/rs-init.sh
restart: always
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]
mongo.two.db:
container_name: mongo.two.db
image: mongo:latest
networks:
common.network:
ports:
- 20001:27017
depends_on:
- mongo.one.db
volumes:
- mongo.two.vol:/data/db
restart: always
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]
mongo.three.db:
container_name: mongo.three.db
image: mongo:latest
networks:
common.network:
ports:
- 20002:27017
depends_on:
- mongo.one.db
volumes:
- mongo.three.vol:/data/db
restart: always
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]
The most interesting part of the above docker compose
is the --replSet
command in entrypoint
. This specifies the replica set name for the containers. Notice that we have specified same replica set name for all three containers (dbrs
).
We are not done yet. We have specified the replicaset name, but we are yet to initiate the replica set. For this, we need to run the rs.initiate()
command from mongosh
.
we will use a batch file for the same. The batch files would initiate the docker compose
, and then run the rs.initiate()
command on one of the containers.
// startdb.bat
docker-compose up -d
timeout /t 10 /nobreak
docker exec -it mongo.one.db mongosh --eval "rs.initiate({_id:'dbrs', members: [{_id:0, host: 'mongo.one.db'},{_id:1, host: 'mongo.two.db'},{_id:2, host: 'mongo.three.db'}]})"
Run the batch files and see how the replica set is built. You could ensure the replicaset is initated properly by running the rs.status()
command on one of the containers.
docker exec -it mongo.one.db mongosh --eval "rs.status()"
If your replicaset was successfully initiated, you would recieve a response as following.
{
set: 'dbrs',
date: ISODate("2023-02-13T14:53:31.692Z"),
myState: 1,
term: Long("3"),
syncSourceHost: '',
syncSourceId: -1,
heartbeatIntervalMillis: Long("2000"),
majorityVoteCount: 2,
writeMajorityCount: 2,
votingMembersCount: 3,
writableVotingMembersCount: 3,
optimes: {
lastCommittedOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
lastCommittedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
readConcernMajorityOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
appliedOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
durableOpTime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z")
},
lastStableRecoveryTimestamp: Timestamp({ t: 1676299994, i: 1 }),
electionCandidateMetrics: {
lastElectionReason: 'electionTimeout',
lastElectionDate: ISODate("2023-02-13T14:52:34.888Z"),
electionTerm: Long("3"),
lastCommittedOpTimeAtElection: { ts: Timestamp({ t: 0, i: 0 }), t: Long("-1") },
lastSeenOpTimeAtElection: { ts: Timestamp({ t: 1676218262, i: 1 }), t: Long("2") },
numVotesNeeded: 2,
priorityAtElection: 1,
electionTimeoutMillis: Long("10000"),
numCatchUpOps: Long("0"),
newTermStartDate: ISODate("2023-02-13T14:52:34.899Z"),
wMajorityWriteAvailabilityDate: ISODate("2023-02-13T14:52:35.451Z")
},
members: [
{
_id: 0,
name: 'mongo.one.db:27017',
health: 1,
state: 1,
stateStr: 'PRIMARY',
uptime: 69,
optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
syncSourceHost: '',
syncSourceId: -1,
infoMessage: '',
electionTime: Timestamp({ t: 1676299954, i: 1 }),
electionDate: ISODate("2023-02-13T14:52:34.000Z"),
configVersion: 1,
configTerm: 3,
self: true,
lastHeartbeatMessage: ''
},
{
_id: 1,
name: 'mongo.two.db:27017',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 67,
optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
optimeDurable: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
optimeDurableDate: ISODate("2023-02-13T14:53:24.000Z"),
lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastHeartbeat: ISODate("2023-02-13T14:53:30.959Z"),
lastHeartbeatRecv: ISODate("2023-02-13T14:53:29.991Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: 'mongo.one.db:27017',
syncSourceId: 0,
infoMessage: '',
configVersion: 1,
configTerm: 3
},
{
_id: 2,
name: 'mongo.three.db:27017',
health: 1,
state: 2,
stateStr: 'SECONDARY',
uptime: 66,
optime: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
optimeDurable: { ts: Timestamp({ t: 1676300004, i: 1 }), t: Long("3") },
optimeDate: ISODate("2023-02-13T14:53:24.000Z"),
optimeDurableDate: ISODate("2023-02-13T14:53:24.000Z"),
lastAppliedWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastDurableWallTime: ISODate("2023-02-13T14:53:24.382Z"),
lastHeartbeat: ISODate("2023-02-13T14:53:30.959Z"),
lastHeartbeatRecv: ISODate("2023-02-13T14:53:29.991Z"),
pingMs: Long("0"),
lastHeartbeatMessage: '',
syncSourceHost: 'mongo.one.db:27017',
syncSourceId: 0,
infoMessage: '',
configVersion: 1,
configTerm: 3
}
],
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1676300004, i: 1 }),
signature: {
hash: Binary(Buffer.from("0000000000000000000000000000000000000000", "hex"), 0),
keyId: Long("0")
}
},
operationTime: Timestamp({ t: 1676300004, i: 1 })
}
Notice the stateStr
property of containers. One (and ONLY ONE) of the containers have the property set as PRIMARY
, while rest of the members are marked as SECONDARY
. In the above output, we can notice that the instance mongo.one.db
has been currently elected as the primary node.
So how do one test this ? You could do the following.
- Connect the current primary instance and create a document (create db and collection if it doesn’t exists).
- Check if entry has been created in the other instances as well.
- Pause or stop the primary instance.
- Run the
rs.status()
command on one of the remaining nodes. - Notice another node has been chosen as primary.
- Switch to current primary and create another document.
- Swithc on the previosly stopped container and ensure the new changes are synced (asynchronously)
That’s all for now. 🙂