Sharding: Horizontal Scaling
Sharded clusters are the ultimate solution for horizontal scaling in MongoDB—mastering them enables you to support petabyte-scale data.
1. What You'll Learn
- Sharding Concept (horizontal scaling)
- Shard Key Selection Strategies
- Chunk Splitting and Migration
- Range vs Hashed Sharding
- Config Server + Mongos + Shard Architecture
- Setting Up a Docker Sharded Cluster
2. Sharded Architecture
Concept Explanation: Sharding is MongoDB’s horizontal scaling solution—it distributes data across multiple shards, each of which is an independent replica set. Applications access the data transparently through Mongos routing, without needing to be aware of how the data is distributed. When a single server reaches its capacity or write throughput limit, sharding is the only way to scale.
How It Works: Mongos acts as a query router. After receiving a request from the application, it retrieves shard metadata (which chunks are on which shards) from the Config Server, routes the request to the target shard for execution, and merges the results before returning them. The application code is identical to that of a single-node MongoDB—sharding is transparent to the application.
Three Major Components:
graph TB
App[Applications] --> M[Mongos<br/>Query Route<br/>Stateless]
M --> CS[Config Server<br/>Configuration Information<br/>3Node Replica Set]
M --> S1[Shard 1<br/>Shard Node 1<br/>Dungeon Collection]
M --> S2[Shard 2<br/>Shard Node 2<br/>Dungeon Collection]
M --> S3[Shard 3<br/>Shard Node 3<br/>Dungeon Collection]
subgraph "Data Flow"
Q[Query Request] --> M
M -->|Metadata Query| CS
M -->|Data Query| S1
M -->|Data Query| S2
M -->|Data Query| S3
end
style M fill:#cce5ff
style CS fill:#fff3cd
style S1 fill:#d4edda
style S2 fill:#d4edda
style S3 fill:#d4edda
| Component | Responsibilities | Deployment Requirements |
|---|---|---|
| Mongos | Query routing, application transparency | Stateless, supports multiple instances |
| Config Server | Stores shard metadata (cluster configuration) | Must be a replica set (3 nodes) |
| Shard | Stores actual data (each shard is a replica set) | Replica set with at least 3 nodes |
Shards vs. Replica Sets:
| Dimension | Replica Set | Shard Cluster |
|---|---|---|
| Objective | High Availability (HA) | Horizontal Scaling + HA |
| Data | Each node stores the full dataset | Data is distributed across multiple shards |
| Write Scaling | None (All writes go to the Primary) | ✅ Writes distributed across multiple shards |
| Read Scaling | Secondary Read Load Balancing | ✅ Multi-Shard Parallel Queries |
| Operations Complexity | Medium | High |
| Suitable Scale | < 1 TB | > 1 TB / > 10K ops/s |
3. Sharding Key Selection Strategies
Concept Explanation: A shard key is the field that determines how data is distributed across shards—it directly affects query performance, data balance, and scalability. Once set, a shard key cannot be modified; choosing the wrong shard key is one of the most serious architectural mistakes.
How It Works: MongoDB divides data into chunks (64 MB by default) based on the shard key, with each chunk assigned to a specific shard. Range sharding divides data based on ranges of shard key values, while hashed sharding divides data based on the hash values of the shard key. During a query, Mongos uses the shard key to locate the shard containing the target chunk, thereby avoiding broadcast queries.
ESRT Rules for Sharding Key Selection:
| Order | Type | Description | Example |
|---|---|---|---|
| Equality | Equal-value filtering | Prefer userId, _id | find({userId: 'u001'}) |
| Sort | Sort Field | createdAt, etc. | sort({createdAt: -1}) |
| Range | Range Query | Select Frequent Ranges | {price: {$gte: 100}} |
| Time | Time Trend | Time Series Data | {createdAt: 1} |
Four Characteristics of a Good Partition Key:
| Feature | Description | Cause |
|---|---|---|
| High baseline | Large number of unique values | Data can be divided into more fine-grained chunks |
| Infrequent updates | Values rarely change | Changing the shard key requires migrating chunks, which is resource-intensive |
| Query Hit | Query conditions include the shard key | Mongos supports directed routing to avoid broadcast |
| Even Distribution | Values Are Evenly Distributed | Avoid Hotspot Shards |
(1) Range Sharding (Range Partitioning)
Concept Explanation: Range sharding divides chunks based on the natural order of shard key values—consecutive value ranges are placed in the same chunk. This is suitable for range queries and sorting, but can easily lead to data skew (hotspots).
Principle: Range sharding divides the value range of the shard key into contiguous intervals [min, splitPoint1), [splitPoint1, splitPoint2), ..., with each interval corresponding to a chunk. Documents with adjacent values are placed in the same chunk → the same shard.
// === Enable Sharding ===
sh.enableSharding('shopdb');
// === Selecting a Sharding Key:User ID Scope ===
sh.shardCollection('shopdb.orders', { userId: 1 });
// userId 1-1000 → Shard 1
// userId 1001-2000 → Shard 2
| Dimension | Range Shards | Description |
|---|---|---|
| Range Query | ✅ Efficient | Consecutive data in the same shard |
| Sorting | ✅ Efficient | Indexes are naturally ordered |
| Data Distribution | ⚠️ Possible Skew | Hot keys concentrated in a single shard |
| Suitable Scenarios | Time Series, ID Range | createdAt, userId |
(2) Hashed Shards
Concept Explanation: Hashed sharding calculates a hash for the shard key and divides chunks based on hash value ranges. Data is evenly distributed with no hotspots, but range queries require a broadcast to all shards.
// === Hash Sharding ===
sh.shardCollection('shopdb.products', { sku: 'hashed' });
// sku Hash values are evenly distributed across the shards
| Dimension | Hashed Shard | Description |
|---|---|---|
| Data Distribution | ✅ Uniform | Naturally scattered by the hash function |
| Write Distribution | ✅ Uniform | No Hotspots |
| Range Query | ❌ Requires Broadcast | Adjacent Values Are in Different Shards |
| Suitable Scenarios | Primarily equality queries | SKU, email, userID |
(3) Range vs Hashed Comparison
| Dimension | Range | Hashed |
|---|---|---|
| Data Distribution | Possible Skewness | Uniform |
| Range Query | ✅ Target Route | ❌ Broadcast to All Shards |
| Equivalence Query | ✅ | ✅ |
| Sorting | ✅ Indexed in order | ❌ |
| Write Hotspots | ⚠️ Single-Point Hotspots | ✅ Uniform |
| Composite Sharded Keys | ✅ Supported | ❌ Single-field only |
(4) Selecting the Best Sharding Key
// ✅ Good Sharding Key:High base figure、Infrequent Updates、Query hits
sh.shardCollection('shopdb.orders', { userId: 1, createdAt: -1 });
// Composite Segmented Key,userId Scope + Sort by Time
// ❌ Invalid Shard Key:Low base
sh.shardCollection('shopdb.products', { category: 1 });
// category Only 5 a value,It will tilt severely
Partition Key Anti-Pattern:
| Anti-Pattern | Consequences | Best Practice |
|---|---|---|
| Low-key field (e.g., category) | Severe data skew | Select high-key field (userId) |
| Monotonically increasing field (such as ObjectId) | All new data is written to the same shard | Use a hashed or composite sharding key |
| Fields with Frequent Updates | Frequent Chunk Migration | Select Fields with Infrequent Updates |
| Not in query criteria | All query broadcasts | Select high-frequency query fields |
4. Chunk Splitting and Migration
Concept Explanation: A chunk is the smallest unit of sharded data management—64 MB by default—and contains all documents whose shard keys fall within a specific range. Chunks are automatically split when they exceed a threshold, and are automatically migrated when the number of chunks is uneven across shards. This is the core mechanism behind MongoDB’s automatic load balancing.
How It Works:
- Split: When a chunk exceeds 64 MB (or the number of documents exceeds the configured threshold), Mongos finds the median shard key and splits the chunk into two.
- Migration: The Balancer periodically checks the number of chunks in each shard and migrates chunks from shards with more chunks to those with fewer, until the load is balanced.
How an Equalizer Works:
graph TB
subgraph "Equalizer Workflow"
A[Check eachShard<br/>ChunkQuantity] --> B{Gap>8?}
B -->|Yes| C[Select Migration Source Shard<br/>(Chunk with the most)]
C --> D[Select a Migration DestinationShard<br/>(ChunkThe fewest)]
D --> E[MigrationChunk]
E --> F[UpdateConfig Server<br/>Metadata]
F --> A
B -->|No| G[Wait for the next check<br/>Default 10 seconds]
end
style E fill:#cce5ff
style F fill:#d4edda
graph LR
A[Chunk 1<br/>1-1000] -->|Split| B[Chunk 1a<br/>1-500]
A -->|Split| C[Chunk 1b<br/>501-1000]
C -->|Migration| D[Shard 2]
style B fill:#d4edda
style D fill:#fff3cd
Chunk Commands:
| Operation | Command | Description |
|---|---|---|
| Split Manually | sh.splitAt(ns, key) |
Split at the specified key-value pair |
| View Distribution | db.col.getShardDistribution() |
Data Volume by Shard |
| Cluster Status | sh.status() |
Chunk Distribution Details |
| Equalizer Status | sh.isBalancerRunning() |
Is equalization in progress? |
| Start-Stop Equalizer | sh.startBalancer() / sh.stopBalancer() |
Can be paused during maintenance windows |
| Change Chunk Size | db.settings.save({_id:'chunksize', value: 128}) |
Unit: MB |
// === Manual Split Chunk ===
sh.splitAt('shopdb.orders', { userId: 5000 });
// === View Chunk Distribution ===
db.orders.getShardDistribution();
// === Balancer Status ===
sh.status();
sh.isBalancerRunning();
Key Points Analysis:
- Chunk splitting is a logical operation (it only modifies metadata) and does not move data; chunk migration is a physical operation (it actually copies data).
- The equalizer runs in the background and does not affect read/write operations during migration (using dual writes to ensure consistency).
- During maintenance windows (such as batch imports), it is recommended to pause the equalizer to prevent the migration from affecting import performance.
▶ Example 1: Equalizer Management and Manual Chunk Splitting
// ShopHub:Pause the equalizer before importing a large batch,Restore After Import
// 1. Pause Equalizer
sh.stopBalancer();
// 2. Bulk Import Data
for (let i = 0; i < 1000000; i++) {
db.orders.insertOne({ userId: 'user_' + (i % 500), total: Math.random() * 1000, createdAt: new Date() });
}
// 3. Manually Split Hotspots Chunk
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
sh.splitAt('shopdb.orders', { userId: 'user_300' });
// 4. Restore the Equalizer
sh.startBalancer();
// 5. Verification Distribution
db.orders.getShardDistribution();
5. Setting Up a Docker Sharded Cluster
Concept Overview: Deploying a sharded cluster is more complex than deploying a replica set—it requires a Config Server replica set, multiple shard replica sets, and Mongos routing. Docker Compose can orchestrate all these components with a single command, making it ideal for development and testing environments.
Deployment Architecture:
graph TB
subgraph "Config Server Dungeon Collection"
CS1[config1:27019]
end
subgraph "Shard 1 Dungeon Collection"
S1A[shard1a:27018]
end
subgraph "Shard 2 Dungeon Collection"
S2A[shard2a:27020]
end
subgraph "Mongos Routing"
MS[mongos:27017]
end
App[Applications] --> MS
MS --> CS1
MS --> S1A
MS --> S2A
style MS fill:#cce5ff
style CS1 fill:#fff3cd
style S1A fill:#d4edda
style S2A fill:#d4edda
Comparison of Production vs. Development Configurations:
| Component | Production Environment | Development Environment |
|---|---|---|
| Config Server | 3-node replica set | 1 node (for testing only) |
| Per Shard | 3-node replica set | 1 node |
| Mongos | Multiple instances (load balancing) | 1 instance |
| Total number of mongods | 3 + 3 × 3 = 12+ | 1 + 2 + 1 = 4 |
# docker-compose-sharding.yml
version: '3.8'
services:
# Config Server(Dungeon Collection)
config1:
image: mongo:7.0
command: mongod --configsvr --replSet configReplSet --port 27019
# Shard 1(Dungeon Collection)
shard1a:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018
# Mongos Routing
mongos:
image: mongo:7.0
command: mongos --configdb configReplSet/config1:27019 --port 27017
ports:
- "27017:27017"
Initialization Steps:
| Step | Command | Description |
|---|---|---|
| 1 | rs.initiate() on config1 |
Initialize Config Server Replica Set |
| 2 | rs.initiate() on shard1a |
Initializing Shard 1 replica set |
| 3 | sh.addShard() on mongos |
Add a shard to the cluster |
| 4 | sh.enableSharding() |
Enable database sharding |
| 5 | sh.shardCollection() |
Select a shard key |
# === Initialize a sharded cluster ===
# 1. Initialization Config Server Dungeon Collection
mongosh --port 27019 --eval 'rs.initiate({_id: "configReplSet", members: [{_id: 0, host: "config1:27019"}]})'
# 2. Initialization Shard 1 Dungeon Collection
mongosh --port 27018 --eval 'rs.initiate({_id: "shard1ReplSet", members: [{_id: 0, host: "shard1a:27018"}]})'
# 3. Through Mongos Add a shard
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1 });
'
6. Sharded Cluster Monitoring
Concept Explanation: Monitoring a sharded cluster is more complex than monitoring a single node or a replica set—it requires monitoring the health of each component, data distribution balance, chunk migration status, and more. Proper monitoring can help detect data skew, hot shards, and configuration issues in a timely manner.
Monitoring Dimensions:
| Dimension | Command | Key Metrics |
|---|---|---|
| Cluster Overview | sh.status() |
Number of Shards, Chunk Distribution, Balancer Status |
| Data Distribution | db.col.getShardDistribution() |
Is the data volume balanced across shards? |
| Equalizer | sh.isBalancerRunning() |
Migration status, migration progress |
| Configuration Information | db.settings.find() |
Chunk Size, Equalizer Configuration |
| Shard Health | sh.status().shards |
Shard Reachability |
// === View Shard Status ===
sh.status();
// === Data Distribution ===
db.orders.getShardDistribution();
// === Balancer Management ===
sh.startBalancer();
sh.stopBalancer();
sh.setBalancerState(true);
// === Layout ===
db.settings.find();
Troubleshooting Common Issues:
| Problem | Symptom | Diagnosis | Solution |
|---|---|---|---|
| Data Skew | Data volume in a certain shard is much larger than in others | getShardDistribution() |
Manually split hot chunks |
| Jumbo Chunk | Chunk larger than 64MB cannot be split | sh.status() Show jumbo |
Increase chunkSize or split the key |
| Equalizer Stuck | Migration Not Progressing | sh.isBalancerRunning() |
Check Config Server Health |
| Query Broadcast | All shards were queried | explain() Shows SHARD_MERGE |
Query conditions include the shard key |
▶ Example 2: Shard Monitoring and Troubleshooting
// Bob's DataFlow system has detected that queries are running slowly, diagnosing sharding issues
// 1. View Cluster Status
sh.status();
// Discovery Shard1: 8000 chunks, Shard2: 2000 chunks → Severe tilt
// 2. View Data Distribution
db.orders.getShardDistribution();
// Shard 1: 80GB (80%), Shard 2: 20GB (20%)
// 3. Check to see if there is Jumbo Chunk
db.config.chunks.find({ jumbo: true }).count();
// 3 Jumbo Chunks
// 4. Manually Split Hotspots Chunk
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
// 5. Verify that the equalizer is running
sh.startBalancer();
sh.isBalancerRunning(); // true
// 6. Waiting for the balancing process to complete,Check again
db.orders.getShardDistribution();
// Shard 1: 50GB (50%), Shard 2: 50GB (50%) → Balance
7. When Should You Use Sharding?
Concept Explanation: Sharding is not always best done as early as possible—while it provides scalability, it also increases operational complexity. Sharding too early can lead to unnecessary operational costs; sharding too late can result in single-server bottlenecks and difficulties with data migration. The decision should be based on a comprehensive assessment of data volume, write throughput, and growth trends.
Decision-Making Framework:
graph TB
A{Data volume?} -->|< 100GB| B[❌ Single-player + Dungeon Collection]
A -->|100GB-1TB| C{WriteQPS?}
A -->|> 1TB| D[✅ Sharding]
C -->|< 10K ops/s| E[⚠️ Dungeon Collection<br/>Monitor Growth Trends]
C -->|> 10K ops/s| D
B --> F[Optimize Indexes + Search]
E --> G[Pre-planned Sharding Keys]
D --> H[Select a partition key + Deploy a Sharded Cluster]
style B fill:#d4edda
style D fill:#cce5ff
style E fill:#fff3cd
| Scenario | Sharding | Reason |
|---|---|---|
| Data volume < 100 GB | ❌ A single server is sufficient | Sharding adds complexity without providing any benefits |
| Data volume: 100 GB – 1 TB | ⚠️ Depends on the situation | Evaluate write QPS and growth rate |
| Data Volume > 1 TB | ✅ Recommended | Single-server Capacity and I/O Bottlenecks |
| Write > 10K ops/s | ✅ Recommended | Single Primary Write Bottleneck |
| Continually Growing Data Volume | ✅ Recommended | Plan Ahead to Avoid Forced Sharding |
Pre-Sharding Checklist:
| Preparation Item | Description |
|---|---|
| Confirm that index optimization is not possible | First use explain() to rule out query issues |
| Confirm that vertical scaling is not possible | Will upgrading the CPU, memory, and SSD be sufficient? |
| Choose the Right Partition Key | High Cardinality, Low Update Frequency, Query Hits |
| Deploying a Replica Set | The foundation of sharding; each shard is a replica set |
| Planned Capacity | Estimate Data Growth and Determine the Number of Shards |
| Application Compatibility Testing | Ensure that queries include the shard key and that unique indexes include the shard key |
▶ Example: Hands-On Guide to Deploying a Sharded Cluster with Docker + Data Sharding
# === 1. Start the sharded cluster(docker-compose-sharding.yml)===
# services:
# config1:
# image: mongo:7.0
# command: mongod --configsvr --replSet configReplSet --bind_ip_all --port 27019
# ports: ["27019:27019"]
#
# shard1a:
# image: mongo:7.0
# command: mongod --shardsvr --replSet shard1ReplSet --bind_ip_all --port 27018
# ports: ["27018:27018"]
#
# shard2a:
# image: mongo:7.0
# command: mongod --shardsvr --replSet shard2ReplSet --bind_ip_all --port 27020
# ports: ["27020:27020"]
#
# mongos:
# image: mongo:7.0
# command: mongos --configdb configReplSet/config1:27019 --bind_ip_all --port 27017
# ports: ["27017:27017"]
# depends_on: [config1, shard1a, shard2a]
docker-compose -f docker-compose-sharding.yml up -d
# === 2. Initialization Config Server Dungeon Collection ===
mongosh --port 27019 --eval '
rs.initiate({
_id: "configReplSet",
members: [{ _id: 0, host: "config1:27019" }]
});
'
# === 3. Initialization Shard Dungeon Collection ===
mongosh --port 27018 --eval '
rs.initiate({ _id: "shard1ReplSet", members: [{ _id: 0, host: "shard1a:27018" }] });
'
mongosh --port 27020 --eval '
rs.initiate({ _id: "shard2ReplSet", members: [{ _id: 0, host: "shard2a:27020" }] });
'
# === 4. Through Mongos Add a shard ===
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.addShard("shard2ReplSet/shard2a:27020");
'
# === 5. Enable Database Sharding ===
mongosh --port 27017 --eval '
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1, createdAt: -1 }); // Composite Segmented Key
'
# === 6. Insert Test Data(Automatically distributed across different shards)===
mongosh --port 27017 --eval '
for (let i = 0; i < 10000; i++) {
db.orders.insertOne({
userId: "user_" + (i % 100),
total: Math.random() * 1000,
createdAt: new Date(),
status: "paid"
});
}
'
# === 7. View Shard Distribution ===
mongosh --port 27017 --eval 'sh.status();'
# Output:
# shards:
# { _id: 'shard1ReplSet', count: 5012 }
# { _id: 'shard2ReplSet', count: 4988 }
# The data is evenly distributed across 2 shards
# === 8. When querying Mongos Automatic Routing ===
mongosh --port 27017 --eval '
db.orders.find({ userId: "user_50" }).count();
'
# Mongos Automatically route to the corresponding shard(Based on userId Scope)
// === 9. App Connections Mongos(App Transparency,Transparent Sharding)===
const mongoose = require('mongoose');
await mongoose.connect('mongodb://localhost:27017/shopdb');
// No changes to the application code are required,Compared to a standalone system MongoDB Exactly the same
await Order.create({
userId: 'user_001',
total: 599,
items: [{ sku: 'PHONE-001', qty: 1 }]
});
// Mongos Automatically route to the appropriate shard
// === 10. Hashed Sharding(Uniform Distribution of Data)===
mongosh --port 27017 --eval '
sh.shardCollection("shopdb.products", { sku: "hashed" });
'
// sku Uniform Distribution After Hashing,Avoid Hot Spots
Output: 10,000 orders are automatically distributed across 2 shards (5,012 + 4,988); the application connects via Mongos without needing to be aware of the sharding details.
❓ FAQ
📖 Summary
- Sharding Architecture: Mongos + Config Server + Shard
- Shard key selection: ESRT (Equality / Sort / Range / Time)
- Range vs Hashed Sharding
- Automatic Chunk Splitting and Balancing
- Setting Up a Docker Sharded Cluster
- When to partition: > 1 TB of data / > 10,000 ops/s
📝 Exercises
- Basic Questions (⭐): Understand the sharding architecture and components (Mongo / Config Server / Shard).
- Basic Question (⭐): Analyze your business scenario and determine whether sharding is necessary.
- Advanced Exercise (⭐⭐): Deploy a cluster with 1 config and 2 shards using Docker Compose.
- Advanced Question (⭐⭐): Test sharding key selection (userId vs. sku vs. compound).
- Challenge (⭐⭐⭐): Deploy a complete sharded cluster (Config replica set + 2 shard replica sets + Mongos + monitoring).



