MongoDB: 分片集群Sharding:水平扩展
最后更新:2026-08-26
分片集群是 MongoDB 水平扩展的终极方案——掌握它能支撑 PB 级数据。
1. 你将学到
- 分片概念(horizontal scaling)
- 分片键(shard key)选择策略
- Chunk 拆分与迁移
- Range vs Hashed 分片
- Config Server + Mongos + Shard 架构
- Docker 分片集群搭建
2. 分片架构
概念说明:分片(Sharding)是 MongoDB 水平扩展的方案——将数据分散存储在多个分片(Shard)上,每个分片是独立的副本集。应用通过 Mongos 路由透明访问,无需感知数据分布。当单机容量或写入达到瓶颈时,分片是唯一的扩展途径。
工作原理:Mongos 作为查询路由,接收应用请求后,从 Config Server 获取分片元数据(哪些 Chunk 在哪个 Shard 上),将请求路由到目标 Shard 执行,合并结果返回。应用代码与单机 MongoDB 完全一致——分片对应用透明。
三大组件:
graph TB
App[应用] --> M[Mongos<br/>查询路由<br/>无状态]
M --> CS[Config Server<br/>配置信息<br/>3节点副本集]
M --> S1[Shard 1<br/>分片节点 1<br/>副本集]
M --> S2[Shard 2<br/>分片节点 2<br/>副本集]
M --> S3[Shard 3<br/>分片节点 3<br/>副本集]
subgraph "数据流"
Q[查询请求] --> M
M -->|元数据查询| CS
M -->|数据查询| S1
M -->|数据查询| S2
M -->|数据查询| S3
end
style M fill:#cce5ff
style CS fill:#fff3cd
style S1 fill:#d4edda
style S2 fill:#d4edda
style S3 fill:#d4edda
| 组件 | 职责 | 部署要求 |
|---|---|---|
| Mongos | 查询路由、应用透明 | 无状态,可多实例 |
| Config Server | 存储分片元数据(集群配置) | 必须副本集(3 节点) |
| Shard | 存储实际数据(每个分片是副本集) | 副本集,至少 3 节点 |
分片 vs 副本集:
| 维度 | 副本集 | 分片集群 |
|---|---|---|
| 目标 | 高可用(HA) | 水平扩展 + HA |
| 数据 | 每个节点存全量数据 | 数据分散到多个 Shard |
| 写入扩展 | 无(所有写走 Primary) | ✅ 写入分散到多个 Shard |
| 读取扩展 | Secondary 分担读 | ✅ 多 Shard 并行查询 |
| 运维复杂度 | 中 | 高 |
| 适用规模 | < 1TB | > 1TB / > 10K ops/s |
3. 分片键选择策略
概念说明:分片键(Shard Key)是决定数据如何分布到各分片的字段——它直接影响查询性能、数据均衡和扩展能力。分片键一旦设定无法修改,选择错误的分片键是最严重的架构决策失误。
工作原理:MongoDB 根据分片键值将数据划分为 Chunk(默认 64MB),每个 Chunk 分配到特定 Shard。Range 分片按分片键值范围划分,Hashed 分片按分片键哈希值划分。查询时,Mongos 根据分片键定位目标 Chunk 所在的 Shard,避免广播查询。
分片键选择 ESRT 规则:
| 顺序 | 类型 | 说明 | 示例 |
|---|---|---|---|
| Equality | 等值过滤 | 首选 userId, _id | find({userId: 'u001'}) |
| Sort | 排序字段 | createdAt 等 | sort({createdAt: -1}) |
| Range | 范围查询 | 频繁范围则选 | {price: {$gte: 100}} |
| Time | 时间趋势 | 时序数据 | {createdAt: 1} |
好分片键的四个特征:
| 特征 | 说明 | 原因 |
|---|---|---|
| 高基数 | 大量唯一值 | 数据可细粒度划分到更多 Chunk |
| 低频更新 | 值不常变化 | 分片键变更需迁移 Chunk,开销大 |
| 查询命中 | 查询条件包含分片键 | Mongos 可定向路由,避免广播 |
| 均匀分布 | 值分布均匀 | 避免热点 Shard |
(1) Range 分片(范围分片)
概念说明:Range 分片按分片键值的自然顺序划分 Chunk——连续值范围放在同一 Chunk。适合范围查询和排序,但容易产生数据倾斜(热点)。
原理:Range 分片将分片键的值域划分为连续区间 [min, splitPoint1), [splitPoint1, splitPoint2), ...,每个区间对应一个 Chunk。值相邻的文档在同一 Chunk → 同一 Shard。
// === 启用分片 ===
sh.enableSharding('shopdb');
// === 选择分片键:用户 ID 范围 ===
sh.shardCollection('shopdb.orders', { userId: 1 });
// userId 1-1000 → Shard 1
// userId 1001-2000 → Shard 2
| 维度 | Range 分片 | 说明 |
|---|---|---|
| 范围查询 | ✅ 高效 | 连续数据在同一 Shard |
| 排序 | ✅ 高效 | 索引天然有序 |
| 数据分布 | ⚠️ 可能倾斜 | 热点 key 集中在单 Shard |
| 适合场景 | 时间序列、ID 范围 | createdAt, userId |
(2) Hashed 分片(哈希分片)
概念说明:Hashed 分片对分片键值计算哈希,按哈希值范围划分 Chunk。数据均匀分布,无热点,但范围查询需要广播到所有 Shard。
// === 哈希分片 ===
sh.shardCollection('shopdb.products', { sku: 'hashed' });
// sku 哈希值均匀分布到各分片
| 维度 | Hashed 分片 | 说明 |
|---|---|---|
| 数据分布 | ✅ 均匀 | 哈希函数天然打散 |
| 写入分布 | ✅ 均匀 | 无热点 |
| 范围查询 | ❌ 需广播 | 相邻值在不同 Shard |
| 适合场景 | 等值查询为主 | sku, email, userId |
(3) Range vs Hashed 对比
| 维度 | Range | Hashed |
|---|---|---|
| 数据分布 | 可能倾斜 | 均匀 |
| 范围查询 | ✅ 目标路由 | ❌ 广播所有 Shard |
| 等值查询 | ✅ | ✅ |
| 排序 | ✅ 索引有序 | ❌ |
| 写入热点 | ⚠️ 单点热点 | ✅ 均匀 |
| 复合分片键 | ✅ 支持 | ❌ 仅单字段 |
(4) 选择最佳分片键
// ✅ 好分片键:高基数、低频更新、查询命中
sh.shardCollection('shopdb.orders', { userId: 1, createdAt: -1 });
// 复合分片键,userId 范围 + 时间排序
// ❌ 坏分片键:低基数
sh.shardCollection('shopdb.products', { category: 1 });
// category 只有 5 个值,会严重倾斜
分片键反模式:
| 反模式 | 后果 | 正确做法 |
|---|---|---|
| 低基数字段(如 category) | 数据严重倾斜 | 选高基数字段(userId) |
| 单调递增字段(如 ObjectId) | 所有新数据写入同一 Shard | 用 Hashed 或复合分片键 |
| 高频更新字段 | 频繁 Chunk 迁移 | 选低频更新字段 |
| 不在查询条件中 | 所有查询广播 | 选高频查询字段 |
4. Chunk 拆分与迁移
概念说明:Chunk 是分片数据管理的最小单位——默认 64MB,包含分片键值在特定范围内的所有文档。当 Chunk 超过阈值时自动拆分,当分片间 Chunk 数量不均衡时自动迁移。这是 MongoDB 自动均衡的核心机制。
工作原理:
- 拆分(Split):Chunk 超过 64MB(或文档数超过配置阈值)时,Mongos 找到中位数分片键值,将 Chunk 一分为二
- 迁移(Migration):均衡器(Balancer)定期检查各 Shard 的 Chunk 数量,从多的 Shard 迁移 Chunk 到少的 Shard,直到均衡
均衡器原理:
graph TB
subgraph "均衡器工作流程"
A[检查各Shard<br/>Chunk数量] --> B{差距>8?}
B -->|是| C[选择迁移源Shard<br/>(Chunk最多的)]
C --> D[选择迁移目标Shard<br/>(Chunk最少的)]
D --> E[迁移Chunk]
E --> F[更新Config Server<br/>元数据]
F --> A
B -->|否| G[等待下次检查<br/>默认10秒]
end
style E fill:#cce5ff
style F fill:#d4edda
graph LR
A[Chunk 1<br/>1-1000] -->|拆分| B[Chunk 1a<br/>1-500]
A -->|拆分| C[Chunk 1b<br/>501-1000]
C -->|迁移| D[Shard 2]
style B fill:#d4edda
style D fill:#fff3cd
Chunk 操作命令:
| 操作 | 命令 | 说明 |
|---|---|---|
| 手动拆分 | sh.splitAt(ns, key) |
在指定键值处拆分 |
| 查看分布 | db.col.getShardDistribution() |
各 Shard 数据量 |
| 集群状态 | sh.status() |
Chunk 分布详情 |
| 均衡器状态 | sh.isBalancerRunning() |
是否正在均衡 |
| 启停均衡器 | sh.startBalancer() / sh.stopBalancer() |
维护窗口可暂停 |
| 修改 Chunk 大小 | db.settings.save({_id:'chunksize', value: 128}) |
单位 MB |
// === 手动拆分 Chunk ===
sh.splitAt('shopdb.orders', { userId: 5000 });
// === 查看 Chunk 分布 ===
db.orders.getShardDistribution();
// === 平衡器状态 ===
sh.status();
sh.isBalancerRunning();
要点解析:
- Chunk 拆分是逻辑操作(仅修改元数据),不移动数据;Chunk 迁移是物理操作(实际复制数据)
- 均衡器在后台运行,迁移期间不影响读写(使用双写保证一致性)
- 维护窗口(如批量导入)建议暂停均衡器,避免迁移影响导入性能
▶ 示例 1: 均衡器管理与 Chunk 手动拆分
// ShopHub:大批量导入前暂停均衡器,导入后恢复
// 1. 暂停均衡器
sh.stopBalancer();
// 2. 批量导入数据
for (let i = 0; i < 1000000; i++) {
db.orders.insertOne({ userId: 'user_' + (i % 500), total: Math.random() * 1000, createdAt: new Date() });
}
// 3. 手动拆分热点 Chunk
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
sh.splitAt('shopdb.orders', { userId: 'user_300' });
// 4. 恢复均衡器
sh.startBalancer();
// 5. 验证分布
db.orders.getShardDistribution();
输出:
// 执行成功
5. Docker 分片集群搭建
概念说明:分片集群的部署比副本集复杂——需要 Config Server 副本集、多个 Shard 副本集和 Mongos 路由。Docker Compose 可以一键编排所有组件,适合开发测试环境。
部署架构:
graph TB
subgraph "Config Server 副本集"
CS1[config1:27019]
end
subgraph "Shard 1 副本集"
S1A[shard1a:27018]
end
subgraph "Shard 2 副本集"
S2A[shard2a:27020]
end
subgraph "Mongos 路由"
MS[mongos:27017]
end
App[应用] --> MS
MS --> CS1
MS --> S1A
MS --> S2A
style MS fill:#cce5ff
style CS1 fill:#fff3cd
style S1A fill:#d4edda
style S2A fill:#d4edda
生产 vs 开发配置对比:
| 组件 | 生产环境 | 开发环境 |
|---|---|---|
| Config Server | 3 节点副本集 | 1 节点(仅测试) |
| 每个 Shard | 3 节点副本集 | 1 节点 |
| Mongos | 多实例(负载均衡) | 1 实例 |
| 总 mongod 数 | 3 + 3×3 = 12+ | 1 + 2 + 1 = 4 |
# docker-compose-sharding.yml
version: '3.8'
services:
# Config Server(副本集)
config1:
image: mongo:7.0
command: mongod --configsvr --replSet configReplSet --port 27019
# Shard 1(副本集)
shard1a:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018
# Mongos 路由
mongos:
image: mongo:7.0
command: mongos --configdb configReplSet/config1:27019 --port 27017
ports:
- "27017:27017"
初始化步骤:
| 步骤 | 命令 | 说明 |
|---|---|---|
| 1 | rs.initiate() on config1 |
初始化 Config Server 副本集 |
| 2 | rs.initiate() on shard1a |
初始化 Shard 1 副本集 |
| 3 | sh.addShard() on mongos |
添加 Shard 到集群 |
| 4 | sh.enableSharding() |
启用数据库分片 |
| 5 | sh.shardCollection() |
选择分片键 |
# === 初始化分片集群 ===
# 1. 初始化 Config Server 副本集
mongosh --port 27019 --eval 'rs.initiate({_id: "configReplSet", members: [{_id: 0, host: "config1:27019"}]})'
# 2. 初始化 Shard 1 副本集
mongosh --port 27018 --eval 'rs.initiate({_id: "shard1ReplSet", members: [{_id: 0, host: "shard1a:27018"}]})'
# 3. 通过 Mongos 添加分片
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1 });
'
6. 分片集群监控
概念说明:分片集群的监控比单机/副本集复杂——需要监控各组件健康、数据分布均衡性、Chunk 迁移状态等。合理的监控能及时发现数据倾斜、热点 Shard 和配置问题。
监控维度:
| 维度 | 命令 | 关注指标 |
|---|---|---|
| 集群总览 | sh.status() |
Shard 数、Chunk 分布、均衡器状态 |
| 数据分布 | db.col.getShardDistribution() |
各 Shard 数据量是否均衡 |
| 均衡器 | sh.isBalancerRunning() |
是否在迁移、迁移进度 |
| 配置信息 | db.settings.find() |
Chunk 大小、均衡器配置 |
| Shard 健康 | sh.status().shards |
各 Shard 是否可达 |
// === 查看分片状态 ===
sh.status();
// === 数据分布 ===
db.orders.getShardDistribution();
// === 平衡器管理 ===
sh.startBalancer();
sh.stopBalancer();
sh.setBalancerState(true);
// === 配置 ===
db.settings.find();
常见问题诊断:
| 问题 | 现象 | 诊断 | 解决 |
|---|---|---|---|
| 数据倾斜 | 某 Shard 数据量远大于其他 | getShardDistribution() |
手动拆分热点 Chunk |
| Jumbo Chunk | Chunk 超过 64MB 不可拆分 | sh.status() 显示 jumbo |
提高 chunkSize 或拆分键 |
| 均衡器卡住 | 迁移不进行 | sh.isBalancerRunning() |
检查 Config Server 健康 |
| 查询广播 | 所有 Shard 都被查询 | explain() 显示 SHARD_MERGE |
查询条件包含分片键 |
▶ 示例 2: 分片监控与问题诊断
// Bob 的 DataFlow 系统发现查询变慢,诊断分片问题
// 1. 查看集群状态
sh.status();
// 发现 Shard1: 8000 chunks, Shard2: 2000 chunks → 严重倾斜
// 2. 查看数据分布
db.orders.getShardDistribution();
// Shard 1: 80GB (80%), Shard 2: 20GB (20%)
// 3. 检查是否有 Jumbo Chunk
db.config.chunks.find({ jumbo: true }).count();
// 3 个 Jumbo Chunk
// 4. 手动拆分热点 Chunk
sh.splitAt('shopdb.orders', { userId: 'user_100' });
sh.splitAt('shopdb.orders', { userId: 'user_200' });
// 5. 确认均衡器在运行
sh.startBalancer();
sh.isBalancerRunning(); // true
// 6. 等待均衡完成,再次检查
db.orders.getShardDistribution();
// Shard 1: 50GB (50%), Shard 2: 50GB (50%) → 均衡
输出:
// mongoose 操作成功执行
// 数据库查询/更新结果
7. 何时使用分片?
概念说明:分片不是越早越好——它带来扩展能力的同时也带来运维复杂度。过早分片会增加不必要的运维成本;过晚分片则面临单机瓶颈和数据迁移困难。需要根据数据量、写入吞吐和增长趋势综合判断。
决策框架:
graph TB
A{数据量?} -->|< 100GB| B[❌ 单机 + 副本集]
A -->|100GB-1TB| C{写入QPS?}
A -->|> 1TB| D[✅ 分片]
C -->|< 10K ops/s| E[⚠️ 副本集<br/>监控增长趋势]
C -->|> 10K ops/s| D
B --> F[优化索引 + 查询]
E --> G[预规划分片键]
D --> H[选分片键 + 部署分片集群]
style B fill:#d4edda
style D fill:#cce5ff
style E fill:#fff3cd
| 场景 | 是否分片 | 原因 |
|---|---|---|
| 数据量 < 100GB | ❌ 单机足够 | 分片增加复杂度无收益 |
| 数据量 100GB - 1TB | ⚠️ 视情况 | 评估写入QPS和增长速度 |
| 数据量 > 1TB | ✅ 推荐 | 单机容量和IO瓶颈 |
| 写入 > 10K ops/s | ✅ 推荐 | 单 Primary 写入瓶颈 |
| 数据量持续增长 | ✅ 推荐 | 提前规划避免被动分片 |
分片前准备清单:
| 准备项 | 说明 |
|---|---|
| 确认无法通过索引优化 | 先用 explain() 排除查询问题 |
| 确认无法通过垂直扩展 | 升级 CPU/内存/SSD 是否足够 |
| 选好分片键 | 高基数、低频更新、查询命中 |
| 部署副本集 | 分片的基础,每个 Shard 都是副本集 |
| 规划容量 | 预估数据增长,确定 Shard 数量 |
| 应用兼容性测试 | 确保查询包含分片键,unique 索引包含分片键 |
▶ 示例:Docker 部署分片集群 + 数据分片实战
# === 1. 启动分片集群(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. 初始化 Config Server 副本集 ===
mongosh --port 27019 --eval '
rs.initiate({
_id: "configReplSet",
members: [{ _id: 0, host: "config1:27019" }]
});
'
# === 3. 初始化 Shard 副本集 ===
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. 通过 Mongos 添加分片 ===
mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018");
sh.addShard("shard2ReplSet/shard2a:27020");
'
# === 5. 启用数据库分片 ===
mongosh --port 27017 --eval '
sh.enableSharding("shopdb");
sh.shardCollection("shopdb.orders", { userId: 1, createdAt: -1 }); // 复合分片键
'
# === 6. 插入测试数据(自动分布到不同分片)===
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. 查看分片分布 ===
mongosh --port 27017 --eval 'sh.status();'
# 输出:
# shards:
# { _id: 'shard1ReplSet', count: 5012 }
# { _id: 'shard2ReplSet', count: 4988 }
# 数据均匀分布到 2 个分片
# === 8. 查询时 Mongos 自动路由 ===
mongosh --port 27017 --eval '
db.orders.find({ userId: "user_50" }).count();
'
# Mongos 自动定位到对应分片(基于 userId 范围)
输出:
CONTAINER ID IMAGE STATUS
abc123 latest Up 2 hours
// === 9. 应用连接 Mongos(应用透明,无需感知分片)===
const mongoose = require('mongoose');
await mongoose.connect('mongodb://localhost:27017/shopdb');
// 应用代码无需修改,与单机 MongoDB 完全相同
await Order.create({
userId: 'user_001',
total: 599,
items: [{ sku: 'PHONE-001', qty: 1 }]
});
// Mongos 自动路由到合适的分片
// === 10. Hashed 分片(数据均匀分布)===
mongosh --port 27017 --eval '
sh.shardCollection("shopdb.products", { sku: "hashed" });
'
// sku 哈希后均匀分布,避免热点
输出:10000 条订单自动分布到 2 个分片(5012 + 4988),应用通过 Mongos 连接无需感知分片细节。
❓ 常见问题
📖 小节
- 分片架构:Mongos + Config Server + Shard
- 分片键选择:ESRT(Equality / Sort / Range / Time)
- Range vs Hashed 分片
- Chunk 自动拆分与平衡
- Docker 分片集群搭建
- 何时分片:> 1TB 数据 / > 10K ops/s
📝 作业
- 基础题(⭐):理解分片架构与组件(Mongo / Config Server / Shard)。
- 基础题(⭐):分析你的业务场景,判断是否需要分片。
- 进阶题(⭐⭐):用 Docker Compose 部署 1 Config + 2 Shard 分片集群。
- 进阶题(⭐⭐):测试分片键选择(userId vs sku vs compound)。
- 挑战题(⭐⭐⭐):完整分片集群部署(Config 副本集 + 2 Shard 副本集 + Mongos + 监控)。