第九章:分片集群
最后更新: 2024-01-01
作者: MongoDB Team
页面目录
第九章:分片集群
构建 MongoDB 水平扩展分片集群
9.1 分片集群概述
┌─────────────────────────────────────────────────────────────────┐
│ MongoDB 分片集群架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Router │ │
│ │ (mongos) │ │
│ └──────┬───────┘ │
│ │ │
│ ┌─────────────────────────┼─────────────────────────┐ │
│ │ │ │ │
│ │ ┌──────────────┐ │ ┌──────────────┐ │ │
│ │ │ Router │ │ │ Router │ │ │
│ │ │ (mongos) │ │ │ (mongos) │ │ │
│ │ └──────────────┘ │ └──────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────┼─────────────────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ Config Server │ │
│ │ (Config) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Shard1 │ │ Shard2 │ │ Shard3 │ │
│ │ RS │ │ RS │ │ RS │ │
│ │ (标签A) │ │ (标签B) │ │ (标签C) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
分片集群组件
| 组件 | 说明 | 必需 |
|---|---|---|
| mongos | 查询路由器,分发请求到分片 | ✓ |
| Config Server | 存储集群元数据 | ✓ |
| Shard | 分片,存储数据子集 | ✓ |
9.2 分片键
选择分片键原则
┌─────────────────────────────────────────────────────────────────┐
│ 分片键选择原则 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 高基数(Cardinality) │
│ ✓ user_id - 唯一值多 │
│ ✗ status - 只有几个值 │
│ │
│ 2. 查询局部性 │
│ ✓ 经常按 region 查询 │
│ ✓ 选择 region 作为分片键 │
│ │
│ 3. 写入分散 │
│ ✓ _id (ObjectId 包含时间戳) │
│ ✗ 单一值导致写入热点 │
│ │
│ 4. 避免单调递增/递减 │
│ ✓ hashed 分片 │
│ ✗ ObjectId 在高频写入时可能热点 │
│ │
└─────────────────────────────────────────────────────────────────┘
分片键策略
// 基于字段分片
sh.shardCollection("app.orders", { order_id: 1 })
// 复合分片键(提高基数)
sh.shardCollection("app.events", { user_id: 1, created_at: 1 })
// 哈希分片(写入分散)
sh.shardCollection("app.events", { _id: "hashed" })
// 多字段哈希分片
sh.shardCollection("app.logs", { user_id: "hashed", timestamp: 1 })
9.3 部署分片集群
Docker Compose 部署
# docker-compose.yml
version: '3.8'
services:
# Config Server 副本集
config1:
image: mongo:7.0
container_name: config1
command: mongod --configsvr --replSet csReplSet --port 27017 --dbpath /data/db
volumes:
- config1_data:/data/db
ports:
- "27017:27017"
config2:
image: mongo:7.0
container_name: config2
command: mongod --configsvr --replSet csReplSet --port 27017 --dbpath /data/db
volumes:
- config2_data:/data/db
ports:
- "27018:27017"
config3:
image: mongo:7.0
container_name: config3
command: mongod --configsvr --replSet csReplSet --port 27017 --dbpath /data/db
volumes:
- config3_data:/data/db
ports:
- "27019:27017"
# Shard 1 副本集
shard1a:
image: mongo:7.0
container_name: shard1a
command: mongod --shardsvr --replSet rs0 --port 27017 --dbpath /data/db
volumes:
- shard1a_data:/data/db
ports:
- "27217:27017"
shard1b:
image: mongo:7.0
container_name: shard1b
command: mongod --shardsvr --replSet rs0 --port 27017 --dbpath /data/db
volumes:
- shard1b_data:/data/db
ports:
- "27218:27017"
# Shard 2 副本集
shard2a:
image: mongo:7.0
container_name: shard2a
command: mongod --shardsvr --replSet rs1 --port 27017 --dbpath /data/db
volumes:
- shard2a_data:/data/db
ports:
- "27317:27017"
shard2b:
image: mongo:7.0
container_name: shard2b
command: mongod --shardsvr --replSet rs1 --port 27017 --dbpath /data/db
volumes:
- shard2b_data:/data/db
ports:
- "27318:27017"
# Router
mongos:
image: mongo:7.0
container_name: mongos
command: mongos --configdb csReplSet/config1:27017,config2:27017,config3:27017 --port 27017
depends_on:
- config1
- config2
- config3
ports:
- "27030:27017"
volumes:
config1_data:
config2_data:
config3_data:
shard1a_data:
shard1b_data:
shard2a_data:
shard2b_data:
初始化脚本
#!/bin/bash
# init-sharded-cluster.sh
# 等待服务启动
sleep 10
# 初始化 Config Server 副本集
docker exec -it config1 mongosh --eval '
rs.initiate({
_id: "csReplSet",
configsvr: true,
members: [
{ _id: 0, host: "config1:27017" },
{ _id: 1, host: "config2:27017" },
{ _id: 2, host: "config3:27017" }
]
})
'
sleep 5
# 初始化 Shard 1 副本集
docker exec -it shard1a mongosh --eval '
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "shard1a:27017" },
{ _id: 1, host: "shard1b:27017" }
]
})
'
# 初始化 Shard 2 副本集
docker exec -it shard2a mongosh --eval '
rs.initiate({
_id: "rs1",
members: [
{ _id: 0, host: "shard2a:27017" },
{ _id: 1, host: "shard2b:27017" }
]
})
'
sleep 5
# 添加分片到集群
docker exec -it mongos mongosh --eval '
sh.addShard("rs0/shard1a:27017,shard1b:27017")
sh.addShard("rs1/shard2a:27017,shard2b:27017")
'
# 启用数据库分片
docker exec -it mongos mongosh --eval '
sh.enableSharding("app")
'
# 对集合进行分片
docker exec -it mongos mongosh --eval '
sh.shardCollection("app.orders", { user_id: 1, order_id: 1 })
sh.shardCollection("app.events", { _id: "hashed" })
'
echo "分片集群初始化完成!"
9.4 分片集群管理
分片操作
// 连接到 mongos
mongosh localhost:27017/admin
// 添加分片
sh.addShard("rs0/shard1-node1:27017")
sh.addShard("rs0/shard1-node2:27017")
sh.addShard("rs1/shard2-node1:27017")
// 移除分片
sh.removeShard("rs1")
// 查看分片状态
sh.status()
db.adminCommand({ listShards: 1 })
// 查看集群配置
db.adminCommand({ getShardMap: 1 })
数据库分片
// 启用数据库分片
sh.enableSharding("mydb")
// 禁用数据库分片
sh.disableSharding("mydb")
集合分片
// 基于字段分片
sh.shardCollection("mydb.users", { user_id: 1 })
// 复合分片键
sh.shardCollection("mydb.orders", { user_id: 1, created_at: 1 })
// 哈希分片
sh.shardCollection("mydb.events", { _id: "hashed" })
// 带 tag 的分片
sh.shardCollection("mydb.users", { region: 1 })
sh.addTagRange(
"mydb.users",
{ region: "us-east" },
{ region: "us-east" },
"us-east-shard"
)
sh.addTagRange(
"mydb.users",
{ region: "us-west" },
{ region: "us-west" },
"us-west-shard"
)
9.5 块(Chunk)管理
块大小配置
// 默认块大小为 64MB
// 修改为 128MB
db.adminCommand({ splitChunk: "mydb.users" })
use config
db.settings.save({ _id: "chunksize", value: 128 })
// 查看块信息
db.chunks.find({ ns: "mydb.users" }).pretty()
手动拆分块
// 手动拆分块
sh.splitAt("mydb.users", { user_id: 1000 })
sh.splitFind("mydb.users", { user_id: 500 })
迁移块
// 迁移块到指定分片
sh.moveChunk("mydb.users", { user_id: 500 }, "shard2")
// 平衡器
sh.startBalancer() // 启动平衡器
sh.stopBalancer() // 停止平衡器
sh.isBalancerRunning() // 检查平衡器状态
// 查看平衡器状态
sh.getBalancerState()
sh.status()
9.6 分片集群查询
广播查询 vs 定向查询
// 广播查询(所有分片都要查询)
// 没有分片键的查询
db.users.find({ name: "Alice" }) // 需要查询所有分片
// 定向查询(只查询目标分片)
// 包含分片键的查询
db.users.find({ user_id: "123" }) // 只查询包含该 user_id 的分片
查询路由
// 查看查询路由信息
db.users.find({ user_id: "123" }).explain()
// 强制不适用分片
db.users.find({ name: "Alice" }).hint({ $sharded: false })
// 使用不包含分片键的索引
db.users.find({ name: "Alice" }).hint({ name: 1 })
聚合管道
// 分片聚合查询
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$user_id", total: { $sum: "$amount" } } }
])
// 使用 $geoNear (必须在分片键上)
db.places.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [73.97, 40.73] },
distanceField: "distance",
spherical: true
}
}
])
9.7 分片键策略
基于范围的分区
// 范围分片适合:查询局部性高
sh.shardCollection("app.orders", { user_id: 1 })
// 数据分布示例
// Shard 1: user_id: [min, 1000)
// Shard 2: user_id: [1000, 2000)
// Shard 3: user_id: [2000, max]
哈希分区
// 哈希分片适合:写入分散
sh.shardCollection("app.events", { _id: "hashed" })
// 数据分布均匀,但查询局部性差
// user_id 12345 -> hash -> Shard 2
// user_id 12346 -> hash -> Shard 1
复合分片键
// 复合分片键:平衡基数和查询局部性
sh.shardCollection("app.events", { region: 1, timestamp: 1 })
// 低基数字段放前面,高基数字段放后面
// 优势:
// 1. region 提供足够的基数
// 2. timestamp 提供写入分散
// 3. 同一 region 的查询可以利用区域本地性
9.8 分片集群高可用
故障转移
// Config Server 故障
// 1. 确保 Config Server 是副本集
// 2. mongos 会自动重连到备用 Config Server
// Shard 故障
// 1. mongos 检测到 Shard 不可用
// 2. 写入会返回错误(除非使用 unacknowledged)
// 3. 读取可能失败或返回部分数据
// 4. 故障恢复后自动同步
// mongos 故障
// 1. 添加多个 mongos 实现高可用
// 2. 客户端连接任意 mongos
连接字符串
// 连接到分片集群
// mongodb://mongos1:27017,mongos2:27017,mongos3:27017/?replicaSet=rsName
// MongoDB Driver 连接示例
// mongodb://mongos1:27017,mongos2:27017/?readPreference=secondaryPreferred
💡 实践提示
- 分片键选择:基于查询模式选择,考虑基数、写入分布和查询局部性
- 避免热点:单调递增的分片键会导致写入热点
- 预分片:在数据量小的时候进行分片,避免后期迁移
- 监控块分布:确保数据均匀分布在各个分片
- 使用副本集作为分片:提高单个分片的高可用性
📚 继续学习