Replication: High-Availability Architecture
Replica Sets are the cornerstone of MongoDB high availability—mastering them allows you to build production clusters with 99.99% availability.
1. What You'll Learn
- Replica Set Concept (Primary / Secondary / Arbiter)
- Election mechanism (based on Raft)
- Data Synchronization (initial sync / oplog tailing)
- Read-Write Separation (readPreference)
- Deploying a 3-node replica set with Docker
- Failover and Recovery
2. Replica Set Architecture
Concept Overview: A Replica Set is the foundation of MongoDB’s high-availability architecture. It consists of multiple mongod processes—1 Primary node + N Secondary nodes + an optional Arbiter node. The Primary node accepts all write operations, which are asynchronously replicated to the Secondary nodes via the oplog, thereby ensuring data redundancy and automatic failover.
How It Works: The Primary records every write operation to the oplog (a fixed-size capped collection), while the Secondary continuously pulls and replays write operations by tailing the oplog to maintain data consistency with the Primary. When the Primary fails, the remaining nodes elect a new Primary using an election protocol (based on Raft), and the application automatically reconnects, achieving 99.99% availability.
The Raft Protocol and Election Mechanism: MongoDB’s election process is based on a variant of the Raft protocol, with the core rule being “majority rule”—only a candidate who receives votes from more than half of the nodes can become the Primary. This ensures that there is at most one Primary at any given time (to prevent split-brain) and that the new Primary possesses the most complete data.
graph TB
App[Applications] --> P[Primary<br/>Master Node<br/>Accept all posts]
P -.Copy oplog.-> S1[Secondary 1<br/>From node<br/>Readable+Backup]
P -.Copy oplog.-> S2[Secondary 2<br/>From node<br/>Readable+Backup]
A[Arbiter<br/>Arbitration Node<br/>Vote Only]
subgraph "Data Flow"
W[Write] --> P
P -->|oplog| S1
P -->|oplog| S2
end
subgraph "Election"
P -->|Heartbeat| S1
P -->|Heartbeat| S2
P -->|Heartbeat| A
end
style P fill:#d4edda
style S1 fill:#cce5ff
style S2 fill:#cce5ff
style A fill:#fff3cd
Comparison of Node Roles:
| Node | Responsibilities | Stores Data | Readable | Participates in Elections | Priority |
|---|---|---|---|---|---|
| Primary | Accepts all write operations | ✅ | ✅ | ✅ | Default 1 (can be increased) |
| Secondary | Copy Primary data | ✅ | ✅ (requires configuration) | ✅ | Default 1 |
| Arbiter | Participates only in election voting | ❌ | ❌ | ✅ | 0 (cannot be elected) |
Replica Sets vs. Single Nodes:
| Dimension | Single Node | Replica Set |
|---|---|---|
| Availability | Single Point of Failure | 99.99% (automatic failover) |
| Data Security | Disk Failure Means Data Loss | Multi-Copy Redundancy |
| Read Expansion | None | Secondary Read Load Balancing |
| Transaction Support | ❌ | ✅ (Requires a replica set) |
| Operational Complexity | Low | Medium |
3. Start the replica set
Concept Explanation: Starting a replica set involves two steps: 1) Start the mongod process with the --replSet parameter; 2) Execute rs.initiate() in mongosh to initialize the replica set configuration. During initialization, specify the addresses of all members, and MongoDB will automatically elect a Primary.
How It Works: rs.initiate() writes the replica set configuration to each node’s local database, triggering the election protocol. The first node to initialize typically becomes the Primary; the other nodes automatically synchronize the configuration and begin tailing the oplog.
# === Start the first node(Raid Mode)===
mongod --replSet rs0 --port 27017 --dbpath /data/db1 --bind_ip localhost
# === mongosh Initialization ===
mongosh
> rs.initiate({
_id: 'rs0',
members: [
{ _id: 0, host: 'localhost:27017' },
{ _id: 1, host: 'localhost:27018' },
{ _id: 2, host: 'localhost:27019' }
]
});
Initialization Configuration Parameters:
| Parameter | Description | Example |
|---|---|---|
_id |
Instance set name; must match --replSet |
'rs0' |
members |
Member List | [{_id: 0, host: '...'}] |
members._id |
Member ID (unique) | 0, 1, 2 |
members.host |
Member Address | 'localhost:27017' |
members.priority |
Election priority (0 = ineligible) | Default 1 |
settings.electionTimeoutMillis |
Heartbeat timeout | Default 10000ms |
4. Adding/Removing Nodes
Concept Explanation: Replica sets support adding and removing nodes online without requiring a service outage. After a node is added, the new node automatically performs an initial sync (full sync) from the Primary, and then switches to oplog tailing (incremental sync).
Data Synchronization Process:
sequenceDiagram
participant New as New Node
participant P as Primary
participant Oplog as oplog
New->>P: Request to Join a Raid Group
P-->>New: Confirm + Current Configuration
Note over New,P: Phase 1: Initial Sync(Full Synchronization)
New->>P: Request Full Data
P-->>New: All Aggregated Data + Index
Note over New,P: Phase 2: Oplog Tailing(Incremental Synchronization)
loop Continuous
New->>Oplog: Get the latest oplog Entry
Oplog-->>New: Incremental Operations
New->>New: Replay oplog Operation
end
Note over New: Once synchronization is complete, you can participate in the election.
| Operation | Command | Description |
|---|---|---|
| Add Data Node | rs.add('host:port') |
Automatic Initial Sync |
| Add Arbitration Node | rs.addArb('host:port') |
Vote only, do not store data |
| Remove Node | rs.remove('host:port') |
Automatic Node Downgrade |
| View Status | rs.status() |
All Node Status + Health |
| View Configuration | rs.conf() |
Replica Set Configuration Details |
// === Add a New Node ===
rs.add('localhost:27020');
// === Add Arbiter(Do not save data)===
rs.addArb('localhost:27021');
// === Remove Node ===
rs.remove('localhost:27020');
// === View Replica Set Status ===
rs.status();
Key Points Analysis:
- During the initial sync, new nodes do not participate in elections or read services; once synchronization is complete, they automatically become Secondary nodes.
- The initial sync of large datasets takes a long time (100 GB may take several hours), so it is recommended to add them during off-peak hours.
- Arbiter does not store data; it is used solely to ensure an odd number of nodes participate in voting, and its priority is 0.
5. Election Mechanism (Based on Raft)
Concept Explanation: The election mechanism is central to the high availability of a replica set—when the Primary fails, the Secondary automatically initiates an election to select a new Primary. MongoDB’s election is based on a variant of the Raft protocol, with the core guarantees being “at most one Primary at any given time” (to prevent split-brain) and “the new Primary has the most complete data.”
Raft Election Process:
- Fault Detection: The Secondary did not receive a heartbeat from the Primary within the electionTimeout (default 10 seconds)
- Initiate an election: The available Secondary with the highest priority becomes a candidate, and its term is incremented.
- Request a Vote: The candidate sends a vote request to all nodes
- Voting Rules: Each node casts only one vote per term, for the candidate with the most up-to-date data.
- Majority Election: The candidate who receives more than half of the votes (> N/2) becomes the new Primary.
- Application Reconnection: The driver automatically detects the new primary and switches transparently
sequenceDiagram
participant S1 as Secondary 1<br/>(Candidates)
participant S2 as Secondary 2
participant A as Arbiter
Note over S1: Primary Heartbeat Timeout 10s<br/>Call for an Election
S1->>S1: Auto-increment term = 2<br/>Vote for yourself
S1->>S2: RequestVote(term=2, lastOplogTime)
S1->>A: RequestVote(term=2, lastOplogTime)
S2->>S1: GrantVote ✅<br/>(The data is recent enough)
A->>S1: GrantVote ✅
Note over S1: receive a majority of the votes (2/3)<br/>Become a New Primary
S1->>S2: Heartbeat(term=2, role=PRIMARY)
S1->>A: Heartbeat(term=2, role=PRIMARY)
Note over S1,S2: The election is over,App Auto-Reconnect
Key Election Parameters:
| Election Rules | Description | Default Value |
|---|---|---|
| Trigger | Primary has been unreachable for longer than electionTimeout | 10 seconds |
| Candidate | All Secondary + Arbiter | — |
| Vote | Must receive a majority of votes (> half) to become the Primary | — |
| Priority | Nodes with higher priority are elected first | 1 |
| Data Integrity | Voters vote only for candidates with updated oplogs | — |
| Election Cooldown | Prevent Frequent Elections | 30 seconds |
graph LR
A[Primary Failure] --> B[Secondary Testing]
B --> C{receive a majority of the votes?}
C -->|Yes| D[Promoted to Primary]
C -->|No| E[Waiting]
D --> F[App Reconnection]
style D fill:#d4edda
| Election Rules | Explanation |
|---|---|
| Trigger | Primary has been unreachable for longer than electionTimeout |
| Candidate | All Secondary + Arbiter |
| Vote | Must receive a majority of votes (> half) to become the Primary |
| Priority | Nodes with higher priority are elected first |
Why an Odd Number of Nodes Is Needed: A system with 3 nodes can tolerate 1 failure (2/3 majority vote); a system with 4 nodes can also tolerate 1 failure (3/4 majority vote); and a system with 5 nodes can tolerate 2 failures (3/5 majority vote). An even number of nodes does not increase fault tolerance but does increase costs, so an odd number of nodes is recommended.
▶ Example 1: Election Priority Configuration
// TechCorp:Give priority to the more powerful server Primary
rs.reconfig({
_id: 'rs0',
members: [
{ _id: 0, host: 'mongo1:27017', priority: 3 }, // The Strongest Server,Elected by a landslide
{ _id: 1, host: 'mongo2:27017', priority: 2 }, // Second choice
{ _id: 2, host: 'mongo3:27017', priority: 1 }, // Lowest Priority
{ _id: 3, host: 'mongo4:27017', priority: 0 } // Never Elected(Cold Standby)
]
});
// priority: 0 That node will never be elected Primary,Suitable for use as a cold standby or reporting server
// Adjustment priority This will trigger a re-election(If the current Primary No longer the best option)
6. Separation of Read and Write Operations
Concept Explanation: Replica sets inherently support read-write separation—write operations are routed to the Primary, while read operations can be distributed to the Secondary. By configuring the read routing policy via readPreference, the load on the Primary can be significantly reduced in read-intensive scenarios.
How It Works: The Mongoose/Mongo driver uses the readPreference parameter to determine which node a read operation is sent to. primary ensures strong consistency, while secondary reduces the load on the Primary node but introduces replication latency.
readPreference Details:
| Pattern | Behavior | Consistency | Latency | Use Cases |
|---|---|---|---|---|
primary |
Read-only primary node | Highest | Lowest | Transactions, critical data |
primaryPreferred |
Primary first; if unavailable, fallback to secondary | Strong | Low | General scenarios |
secondary |
Read-only secondary node | Weak | High | Reports/Analytics |
secondaryPreferred |
Use this first; if unavailable, use the primary | Weaker | Lower | Read-heavy, write-light |
nearest |
Lowest network latency | Unknown | Lowest | Geographically distributed |
Consistency vs. Performance Trade-off:
graph LR
A[primary<br/>Strong consistency<br/>High latency] --> B[primaryPreferred<br/>Strong consensus]
B --> C[secondaryPreferred<br/>Weak consensus]
C --> D[secondary<br/>Weak consistency<br/>Low latency]
D --> E[nearest<br/>Not sure<br/>Lowest Latency]
style A fill:#d4edda
style D fill:#fff3cd
// === Default:All read and write operations take place in Primary ===
const user = await User.findById(userId);
// === Read from a child node(Reduce Primary Pressure)===
const products = await Product.find().read('secondary');
// === Read Preference Options ===
await Product.find().read('primary'); // Master node only
await Product.find().read('primaryPreferred'); // Preferred Primary Node
await Product.find().read('secondary'); // From the node alone
await Product.find().read('secondaryPreferred'); // Priority Node
await Product.find().read('nearest'); // Recent Online Trends
Key Points Analysis:
- Reading from a node may involve a replication delay (typically < 1 second, though it may be longer in the event of network issues).
- In
secondarymode, if all Secondary nodes are unavailable, the query will return an error. - For scenarios where consistency is not a major concern, such as reports and search index building, we recommend
secondaryorsecondaryPreferred.
7. Deploying Docker on 3 Nodes
Concept Explanation: Docker Compose is the most convenient way to develop and test replica sets locally. A 3-node configuration (1 Primary + 2 Secondary) is the minimum recommended production configuration, providing high availability and automatic failover.
Deployment Architecture:
graph TB
subgraph "Docker Compose"
M1[mongo1:27017<br/>Primary/Secondary]
M2[mongo2:27017<br/>Primary/Secondary]
M3[mongo3:27017<br/>Primary/Secondary]
end
App[Applications] --> M1
App --> M2
App --> M3
M1 <--> M2
M2 <--> M3
M1 <--> M3
style M1 fill:#d4edda
style M2 fill:#cce5ff
style M3 fill:#cce5ff
# docker-compose.yml
version: '3.8'
services:
mongo1:
image: mongo:7.0
command: mongod --replSet rs0 --port 27017
ports:
- "27017:27017"
mongo2:
image: mongo:7.0
command: mongod --replSet rs0 --port 27017
ports:
- "27018:27017"
mongo3:
image: mongo:7.0
command: mongod --replSet rs0 --port 27017
ports:
- "27019:27017"
// === Initialize the replica set ===
rs.initiate({
_id: 'rs0',
members: [
{ _id: 0, host: 'mongo1:27017' },
{ _id: 1, host: 'mongo2:27017' },
{ _id: 2, host: 'mongo3:27017' }
]
});
Deployment Steps:
| Step | Command | Description |
|---|---|---|
| 1 | docker-compose up -d |
Start 3 containers |
| 2 | mongosh --port 27017 |
Connect to the first node |
| 3 | rs.initiate({...}) |
Initialize Replica Set |
| 4 | rs.status() |
Verification Status |
| 5 | rs.conf() |
View Settings |
8. Failover
Concept Explanation: Failover is the most critical capability of a replica set—when the Primary node fails, the remaining nodes automatically elect a new Primary, and the application reconnects transparently. The entire process typically completes within 10–30 seconds; during this time, writes are unavailable, but reads can be degraded to the Secondary node.
Failover Process:
sequenceDiagram
participant App as Applications
participant P as Primary (mongo1)
participant S1 as Secondary (mongo2)
participant S2 as Secondary (mongo3)
Note over P: mongo1 Failure
P-xApp: Heartbeat Disconnected
P-xS1: Heartbeat Disconnected
P-xS2: Heartbeat Disconnected
Note over S1,S2: 10Not received yetPrimaryHeartbeat
S1->>S2: Call for an Election
S2->>S1: Vote in favor
S1->>S1: receive a majority of the votes(2/3)
Note over S1: mongo2 Become a New Primary
App->>S1: Automatic Reconnection → Writing has returned to normal
App->>S2: Continue reading(secondaryPreferred)
Note over App,S1: Total downtime: 10-30 seconds
| Failure Scenario | Impact | Recovery Time |
|---|---|---|
| Primary Outage | Write Interruption 10–30 Seconds | Automatic Election Recovery |
| Secondary Outage | No Impact | Automatic Synchronization After Restart |
| Majority of nodes down | Replica set is read-only | Must restore the majority of nodes |
| Network Partition | Not writable on the minority side | Automatically merge after network recovery |
# === Primary Downtime Simulation ===
# kill mongo1 Container
docker stop mongo1
# === Automatic Election(30 Completed in seconds)===
# mongo2 or mongo3 Automatically promoted to Primary
# === App Auto-Reconnect(mongoose Layout)===
mongoose.connect('mongodb://mongo1,mongo2,mongo3/shopdb?replicaSet=rs0');
Application Layer Troubleshooting:
| Setting | Description | Recommended Value |
|---|---|---|
| Connection String | List All Nodes | mongodb://m1,m2,m3/db?replicaSet=rs0 |
| Retry Write | Automatically Retry Network Errors | retryWrites=true |
| Read Preferences | Read Degradation During Failures | secondaryPreferred |
| Connection Timeout | Connection Timeout Duration | connectTimeoutMS=5000 |
| Server selection timeout | Node selection timeout | serverSelectionTimeoutMS=30000 |
9. Best Practices for Replica Sets
Concept Overview: Best practices for replica sets cover aspects such as the number of nodes, security configurations, and monitoring and alerts. Following these practices can help prevent common replica set failures in production environments.
Key Practices:
| Practice | Description | Reason |
|---|---|---|
| 3-node setup | 1 Primary + 2 Secondary | Minimum fault-tolerant configuration, tolerating 1 node failure |
| Odd number of nodes | Election requires a majority vote: 3/5/7 | An even number of nodes does not increase fault tolerance |
| writeConcern: majority | Write confirmed on a majority of nodes | No data loss (even if the Primary fails) |
| readPreference primaryPreferred | Default read primary node | Guarantees consistency; if the primary is unavailable, a secondary node is promoted |
| Monitor the oplog window | Oplog rollover can cause data loss | Keep the window > 24 hours |
| Reasonable priority configuration | The strong server has a higher priority | After a failure is recovered, the strong server is re-elected |
| Use Arbiter with caution | Use only when cost is a concern | Arbiter does not store data and cannot be restored |
writeConcern Comparison:
| writeConcern | Data Safety | Write Latency | Use Cases |
|---|---|---|---|
w: 1 |
Primary confirmation only | Fastest | Logs, non-critical data |
w: majority |
Confirmed by most nodes | Medium | General business data |
w: majority, j: true |
Most confirmations + journal written to disk | Slowest | Finance, critical data |
Monitoring Checklist:
| Monitoring Item | Command | Health Value | Alert Threshold |
|---|---|---|---|
| Replica Set Status | rs.status() |
1 PRIMARY + N SECONDARY | No PRIMARY |
| Copy Delay | rs.status().members[n].optimeDate |
< 1 second | > 10 seconds |
| oplog window | rs.printReplicationInfo() |
> 72 hours | < 24 hours |
| Number of Connections | db.serverStatus().connections |
< 1000 | > 8000 |
| Number of Elections | rs.status().electionMetrics |
Few | Frequent Elections |
▶ Example: Docker Deployment of a 3-Node Replica Set + Failover Testing
# === 1. docker-compose.yml ===
# version: '3.8'
# services:
# mongo1:
# image: mongo:7.0
# command: mongod --replSet rs0 --bind_ip_all --port 27017
# ports: ["27017:27017"]
# networks: [mongo-net]
#
# mongo2:
# image: mongo:7.0
# command: mongod --replSet rs0 --bind_ip_all --port 27017
# ports: ["27018:27017"]
# networks: [mongo-net]
#
# mongo3:
# image: mongo:7.0
# command: mongod --replSet rs0 --bind_ip_all --port 27017
# ports: ["27019:27017"]
# networks: [mongo-net]
#
# networks:
# mongo-net:
# driver: bridge
# Start 3 Node
docker-compose up -d
# === 2. Initialize the replica set ===
mongosh --port 27017 --eval '
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "localhost:27017" },
{ _id: 1, host: "localhost:27018" },
{ _id: 2, host: "localhost:27019" }
]
});
'
# === 3. Verify the replica set status ===
mongosh --port 27017 --eval 'rs.status();'
# Output:
# {
# set: 'rs0',
# members: [
# { name: 'localhost:27017', stateStr: 'PRIMARY' },
# { name: 'localhost:27018', stateStr: 'SECONDARY' },
# { name: 'localhost:27019', stateStr: 'SECONDARY' }
# ]
# }
# === 4. Writing Test Data(Automatically copy to Secondary)===
mongosh --port 27017 --eval '
db.products.insertOne({ sku: "PHONE-001", title: "Smartphone X", price: 599 });
'
# === 5. Verify Data Replication ===
mongosh --port 27018 --eval '
db.setSecondaryOk(); // Read Permitted Secondary
db.products.findOne({ sku: "PHONE-001" });
'
# Return to the same document,Proof that the copy was successful
# === 6. Failover Testing ===
# Stop Primary
docker stop mongo1
# View on mongo2
mongosh --port 27018 --eval 'rs.status();'
# Output:mongo2 Automatically promoted to PRIMARY(30 Completed in seconds)
# === 7. App Auto-Reconnect(mongoose)===
// mongoose The chain automatically discovers all nodes
const mongoose = require('mongoose');
await mongoose.connect(
'mongodb://localhost:27017,localhost:27018,localhost:27019/shopdb?replicaSet=rs0'
);
// Even if Primary fails, App automatically connects to new Primary
const products = await Product.find(); // Automatic Retry,No errors
// === 8. Read-Write Separation Configuration ===
// Write operation complete Primary
await Product.create({ sku: 'PHONE-002', title: 'Phone 2' });
// Read operations can proceed Secondary(Reduce Primary Pressure)
const products = await Product.find().read('secondaryPreferred');
// === 9. Restore a Failed Node ===
docker start mongo1
# mongo1 Automatically set to Secondary Join a Raid Group,Start Over Primary Synchronize Data
Output: A 3-node replica set provides high availability; in the event of a primary failure, a failover occurs automatically within 30 seconds, with no impact on the application.
❓ FAQ
📖 Summary
- Replica Set: Primary + Secondary + Arbiter
- Election mechanism: Based on Raft; requires a majority vote
- Data Synchronization: initial sync + oplog tailing
- Read-write separation: readPreference
- Docker Deployment on 3 Nodes
- Failover: Automatic election + application reconnection
📝 Exercises
- Basic Question (⭐): Deploy a 3-node replica set using Docker Compose.
- Basic Question (⭐): Initialize the replica set and verify its status (rs.status()).
- Advanced Exercise (⭐⭐): Test read-write separation (readPreference: secondary).
- Advanced Exercise (⭐⭐): Simulate a Primary failure and observe the automatic election process.
- Challenge (⭐⭐⭐): Deploy a complete replica set (3 nodes + replica set monitoring + failure recovery testing).



