MongoDB 单节点在数据量超过内存容量后查询性能急剧下降,副本集虽提供高可用但无法突破单机存储上限。分片集群(Sharded Cluster)通过水平拆分将数据分布到多个 Shard 节点,实现存储和计算的线性扩展。本文从集群规划到分片键选型,给出 MongoDB 分片集群完整搭建方案。
MongoDB分片集群架构组件
分片集群由三类节点组成:
Shard(分片服务器):实际存储数据分片的 mongod 进程,每个 Shard 可以是单节点也可以是副本集。生产环境强制使用副本集部署,保证单个 Shard 内部的高可用。
Config Server(配置服务器):存储集群元数据,包括 Shard 信息、chunk 分布和分片键范围映射。Config Server 采用 3 节点副本集部署,元数据变更通过副本集共识协议保证一致性。
Mongos(路由服务器):无状态查询路由进程,客户端连接 Mongos 而非直连 Shard。Mongos 从 Config Server 获取元数据,将查询路由到对应的 Shard 并合并结果。
一个典型的 3-Shard 集群需要 3 个 Config Server、3 个 Mongos 和 3 个 Shard 副本集(每个 3 节点),共 15 个 mongod 进程加 3 个 mongos 进程。
集群搭建:Config Server与Shard副本集初始化
使用 Docker Compose 快速搭建测试集群。生产环境建议物理机或独立虚拟机部署,避免容器网络延迟影响副本集心跳:
# docker-compose.yml
version: '3.8'
services:
# Config Server 副本集
cfgsvr1:
image: mongo:7.0
command: mongod --configsvr --replSet cfgReplSet --port 27019 --bind_ip_all
volumes:
- cfgsvr1_data:/data/db
cfgsvr2:
image: mongo:7.0
command: mongod --configsvr --replSet cfgReplSet --port 27019 --bind_ip_all
volumes:
- cfgsvr2_data:/data/db
cfgsvr3:
image: mongo:7.0
command: mongod --configsvr --replSet cfgReplSet --port 27019 --bind_ip_all
volumes:
- cfgsvr3_data:/data/db
# Shard 1 副本集
shard1a:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018 --bind_ip_all
volumes:
- shard1a_data:/data/db
shard1b:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018 --bind_ip_all
volumes:
- shard1b_data:/data/db
shard1c:
image: mongo:7.0
command: mongod --shardsvr --replSet shard1ReplSet --port 27018 --bind_ip_all
volumes:
- shard1c_data:/data/db
# Shard 2 副本集
shard2a:
image: mongo:7.0
command: mongod --shardsvr --replSet shard2ReplSet --port 27018 --bind_ip_all
volumes:
- shard2a_data:/data/db
shard2b:
image: mongo:7.0
command: mongod --shardsvr --replSet shard2ReplSet --port 27018 --bind_ip_all
volumes:
- shard2b_data:/data/db
shard2c:
image: mongo:7.0
command: mongod --shardsvr --replSet shard2ReplSet --port 27018 --bind_ip_all
volumes:
- shard2c_data:/data/db
# Mongos 路由
mongos1:
image: mongo:7.0
command: mongos --configdb cfgReplSet/cfgsvr1:27019,cfgsvr2:27019,cfgsvr3:27019 --port 27017 --bind_ip_all
depends_on:
- cfgsvr1
- cfgsvr2
- cfgsvr3
mongos2:
image: mongo:7.0
command: mongos --configdb cfgReplSet/cfgsvr1:27019,cfgsvr2:27019,cfgsvr3:27019 --port 27017 --bind_ip_all
volumes:
cfgsvr1_data:
cfgsvr2_data:
cfgsvr3_data:
shard1a_data:
shard1b_data:
shard1c_data:
shard2a_data:
shard2b_data:
shard2c_data:
启动后依次初始化各副本集:
# 初始化 Config Server 副本集
docker exec -it cfgsvr1 mongosh --port 27019 --eval '
rs.initiate({
_id: "cfgReplSet",
configsvr: true,
members: [
{ _id: 0, host: "cfgsvr1:27019" },
{ _id: 1, host: "cfgsvr2:27019" },
{ _id: 2, host: "cfgsvr3:27019" }
]
})
'
# 初始化 Shard 1 副本集
docker exec -it shard1a mongosh --port 27018 --eval '
rs.initiate({
_id: "shard1ReplSet",
members: [
{ _id: 0, host: "shard1a:27018" },
{ _id: 1, host: "shard1b:27018" },
{ _id: 2, host: "shard1c:27018" }
]
})
'
# 初始化 Shard 2 副本集
docker exec -it shard2a mongosh --port 27018 --eval '
rs.initiate({
_id: "shard2ReplSet",
members: [
{ _id: 0, host: "shard2a:27018" },
{ _id: 1, host: "shard2b:27018" },
{ _id: 2, host: "shard2c:27018" }
]
})
'
# 通过 Mongos 添加 Shard 到集群
docker exec -it mongos1 mongosh --port 27017 --eval '
sh.addShard("shard1ReplSet/shard1a:27018,shard1b:27018,shard1c:27018")
sh.addShard("shard2ReplSet/shard2a:27018,shard2b:27018,shard2c:27018")
'
# 查看集群状态
docker exec -it mongos1 mongosh --port 27017 --eval 'sh.status()'
分片键选型策略与注意事项
分片键决定数据在 Shard 间的分布方式,一旦设置不可更改(MongoDB 4.4 起支持 refineCollectionShardKey 追加字段,但核心字段不可移除)。选型原则:
1. 高基数(High Cardinality):分片键的取值范围要广。使用 status 字段(仅 5 个枚举值)作为分片键会导致大部分数据集中在少数 chunk 中,无法均匀分布。
2. 低频率(Low Frequency):单个分片键值的文档数量不宜过多。使用 userId 作为分片键,若某用户产生百万条日志,该用户的全部数据会集中在一个 chunk 中成为 jumbo chunk。
3. 非单调递增(Non-Monotonic):使用 ObjectId 或时间戳等单调递增字段作为分片键,新数据始终插入到最后一个 Shard,导致写入热点。应使用哈希分片打散顺序写入。
// 连接 Mongos 启用数据库分片
sh.enableSharding("ecommerce")
// 方式1:范围分片(适合范围查询)
sh.shardCollection("ecommerce.orders", { "userId": 1, "createdAt": 1 })
// 方式2:哈希分片(适合均匀写入分布)
sh.shardCollection("ecommerce.events", { "userId": "hashed" })
// 方式3:复合分片键(兼顾范围查询和分布均匀)
sh.shardCollection("ecommerce.user_logs", { "userId": "hashed", "date": 1 })
哈希分片将分片键值通过哈希函数映射到 chunk 范围,数据均匀分布但无法支持基于分片键的范围查询。范围分片支持高效范围查询但存在热点写入风险。userId: "hashed" + date: 1 的复合键在保证写入均匀的同时,支持按 userId 的精确查询和按 date 的范围查询。
平衡器配置与chunk迁移调优
平衡器(Balancer)自动在 Shard 间迁移 chunk 以保持数据均匀分布。默认配置下,当 Shard 间 chunk 数差异超过 2 时触发迁移。对于写入密集型集群,迁移过程会占用带宽和 CPU,需要调优:
// 查看平衡器状态
sh.getBalancerState()
// 在指定时间窗口禁用平衡器(低峰期迁移)
sh.setBalancerState(false) // 手动关闭
sh.startBalancer() // 手动开启
// 设置自动平衡窗口(凌晨 2-6 点)
sh.setBalancerState(true)
db.settings.update(
{ _id: "balancer" },
{ $set: { activeWindow: { start: "02:00", stop: "06:00" } } },
{ upsert: true }
)
// 调整 chunk 迁移参数
db.settings.update(
{ _id: "chunkMigrationConcurrency" },
{ $set: { value: 2 } }, // 并发迁移数(默认 1)
{ upsert: true }
)
// 设置单个集合的 chunk 大小(默认 128MB)
db.settings.update(
{ _id: "chunksize" },
{ $set: { value: 64 } }, // 64MB
{ upsert: true }
)
chunksize 影响迁移频率和元数据规模。较小的 chunk 大小使数据分布更均匀但迁移更频繁,较大的 chunk 减少迁移但可能导致分布不均匀。对于 TB 级数据,建议保持默认 128MB 或增大到 256MB 以减少 chunk 数量。
分片集群监控与故障排查
// 查看各 Shard 数据分布
db.getSiblingDB("config").chunks.aggregate([
{ $group: {
_id: "$shard",
chunkCount: { $sum: 1 },
minKey: { $min: "$min" },
maxKey: { $max: "$max" }
}}
])
// 查看是否存在 jumbo chunk(过大无法迁移的 chunk)
db.getSiblingDB("config").chunks.find({ "jumbo": true })
// 标记 jumbo chunk 以强制分裂
sh.splitAt("ecommerce.events", { userId: "user_99999" })
// 查看 Mongos 连接分布
db.getSiblingDB("config").mongos.find().sort({ ping: -1 })
// 查看迁移历史
db.getSiblingDB("config").chunks.find({}).sort({ "lastmod": -1 }).limit(10)
Jumbo chunk 通常由分片键选型不当导致——某个分片键值对应的文档量超过 chunk 大小限制且无法分裂。解决方案是优化分片键设计,或使用 sh.splitAt() 在数据内部寻找分裂点。若分片键值单一无法分裂,需要考虑 refineCollectionShardKey 追加辅助字段或重建集合。
生产环境建议使用 MongoDB Atlas 或 Ops Manager 进行集群监控,关注指标包括:各 Shard 的存储利用率、chunk 迁移队列长度、Mongos 连接数、Config Server 延迟。当 Config Server 延迟超过 100ms 时,集群元数据操作会显著变慢,影响所有路由查询。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mongodb-fen-pian-ji-qun-da-jian-shi-zhan-shard-fen-bu-yu/