副本集把同一份数据复制到多个成员,主要解决高可用;分片把不同数据范围分布到多个 shard,主要解决单个副本集在容量或吞吐上的边界。两者不是替代关系:生产中的每个 shard 通常仍然是副本集。
分片会引入路由、均衡、元数据、跨 shard 查询、跨 shard 事务和更复杂的备份恢复。数据量“未来可能很大”并不足以成为立即分片的理由。
在决定分片前,先依次检查:
只有当证据指向单个副本集的实际边界,并且分片键能让主要请求定向路由时,分片的运维成本才有合理回报。
分片不会自动修复没有索引的查询。一个广播到所有 shard 的低效查询,可能把原来的一个问题复制成多个问题。
一个分片集群至少包含:
应用
│
▼
mongos
┌────────┼────────┐
│ │ │
▼ ▼ ▼
shard 1 shard 2 config server
副本集 副本集 副本集mongos 根据分片键和 config server 中的路由表判断请求应去一个 shard、多个 shard,还是全部 shard。这个选择直接影响延迟和资源消耗。
分片键不是“选一个看起来唯一的字段”。它会长期影响数据分布、写入热点和查询能否定向。
评估一个候选键时至少看四件事:
纸舟书店常按 customerId 查某位客户的订单。把 customerId 做哈希分片键,可以把不同客户较均匀地分散,并让完整等值查询定向到一个 shard;代价是按客户 ID 范围查询不再具有数据邻近性。
如果主要需求是按地区批量处理订单,{ region: 1, customerId: 1 } 这样的范围复合键可能更合适。分片键必须从真实请求和增长方式推导。
下面建立四个进程:
它能展示路由和数据分布,却没有生产高可用性。生产 config server replica set 和各 shard 都应按故障域规划多个成员,并启用认证、成员间认证、TLS、监控和备份。
我们使用单独的 Compose 项目名 bookstore-sharded,不会改动主课程容器。
先创建并进入本节专用目录:
mkdir paperboat-sharding-lab
cd paperboat-sharding-lab在目录中创建 compose.yaml:
services:
configsvr:
image: mongo:8.0
command:
- mongod
- --configsvr
- --replSet
- configReplSet
- --port
- "27019"
- --bind_ip_all
volumes:
- config-data:/data/configdb
shard1:
image: mongo:8.0
command:
- mongod
- --shardsvr
- --replSet
- shard1rs
- --port
- "27018"
- --bind_ip_all
volumes:
- shard1-data:/data/db
shard2:
image: mongo:8.0
command:
- mongod
- --shardsvr
- --replSet
- shard2rs
- --port
- "27018"
- --bind_ip_all
volumes:
- shard2-data:/data/db
mongos:
image: mongo:8.0
command:
- mongos
- --configdb
- configReplSet/configsvr:27019
- --bind_ip_all
ports:
- "127.0.0.1:27217:27017"
depends_on:
- configsvr
- shard1
- shard2
volumes:
config-data:
shard1-data:
shard2-data:启动四个服务:
docker compose -p bookstore-sharded up -d[+] Running 8/8
✔ Network bookstore-sharded_default Created
✔ Volume bookstore-sharded_config-data Created
✔ Volume bookstore-sharded_shard1-data Created
✔ Volume bookstore-sharded_shard2-data Created
✔ Container bookstore-sharded-configsvr-1 Started
✔ Container bookstore-sharded-shard1-1 Started
✔ Container bookstore-sharded-shard2-1 Started
✔ Container bookstore-sharded-mongos-1 Started此时进程已启动,但三个副本集尚未初始化,mongos 可能暂时等待 config server 就绪。
--configsvr 和 --shardsvr 只声明进程角色,不会自动创建副本集配置。每个副本集都需要 rs.initiate();config server 就绪后 mongos 才能稳定读取元数据。
初始化 config server:
docker compose -p bookstore-sharded exec configsvr \
mongosh --quiet --port 27019 --eval '
rs.initiate({
_id: "configReplSet",
configsvr: true,
members: [
{ _id: 0, host: "configsvr:27019" }
]
})
'{ ok: 1 }初始化两个 shard:
docker compose -p bookstore-sharded exec shard1 \
mongosh --quiet --port 27018 --eval '
rs.initiate({
_id: "shard1rs",
members: [
{ _id: 0, host: "shard1:27018" }
]
})
'{ ok: 1 }docker compose -p bookstore-sharded exec shard2 \
mongosh --quiet --port 27018 --eval '
rs.initiate({
_id: "shard2rs",
members: [
{ _id: 0, host: "shard2:27018" }
]
})
'{ ok: 1 }每组只有一个成员,初始化后都应成为 Primary。下面分别读取状态:
docker compose -p bookstore-sharded exec configsvr \
mongosh --quiet --port 27019 --eval 'print(rs.status().myState)'
docker compose -p bookstore-sharded exec shard1 \
mongosh --quiet --port 27018 --eval 'print(rs.status().myState)'
docker compose -p bookstore-sharded exec shard2 \
mongosh --quiet --port 27018 --eval 'print(rs.status().myState)'1
1
11 表示当前成员是 Primary。若连接过早,可以稍等后重试;不要用固定休眠时间代替最终状态检查。
现在通过 mongos 执行 sh.addShard()。注册字符串同时包含副本集名和成员地址:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
printjson(sh.addShard("shard1rs/shard1:27018"));
printjson(sh.addShard("shard2rs/shard2:27018"));
'{
shardAdded: 'shard1rs',
ok: 1
}
{
shardAdded: 'shard2rs',
ok: 1
}读取精简集群状态:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
printjson(db.adminCommand({ listShards: 1 }).shards.map(s => ({
id: s._id,
host: s.host,
state: s.state
})))
'[
{
id: 'shard1rs',
host: 'shard1rs/shard1:27018',
state: 1
},
{
id: 'shard2rs',
host: 'shard2rs/shard2:27018',
state: 1
}
]我们为 shard_lab.orders 选择 { customerId: "hashed" }。哈希键会牺牲范围邻近性,换取较均匀的键空间分布。
空集合可以在分片时通过 numInitialChunks 预建多个数据块,让两组 shard 在很小的数据集上也能观察分布。生产数据块数量与策略应根据规模、版本和运维计划决定,不能照搬演示值。
先启用数据库分片,再创建哈希索引和分片集合:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
sh.enableSharding("shard_lab");
const d = db.getSiblingDB("shard_lab");
d.orders.createIndex({ customerId: "hashed" });
printjson(sh.shardCollection(
"shard_lab.orders",
{ customerId: "hashed" },
false,
{ numInitialChunks: 4 }
))
'预期最后得到:
{
collectionsharded: 'shard_lab.orders',
ok: 1
}写入 2000 份可重复生成的订单。bulkWrite 分批提交,避免构造一个过大的单次请求:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
const orders = db.getSiblingDB("shard_lab").orders;
const ops = [];
for (let i = 0; i < 2000; i += 1) {
ops.push({
insertOne: {
document: {
_id: `shard-order-${String(i).padStart(4, "0")}`,
customerId: `customer-${String(i % 200).padStart(3, "0")}`,
amount: 50 + (i % 20),
createdAt: new Date("2026-01-01T00:00:00Z")
}
}
});
}
const result = orders.bulkWrite(ops, { ordered: false });
printjson({
acknowledged: result.isAcknowledged(),
insertedCount: result.insertedCount
})
'{
acknowledged: true,
insertedCount: 2000
}mongos 接收的是普通集合写入,驱动不需要为每一篇文档手动选择 shard。路由由分片键值和集群元数据决定。
“两个 shard 都有数据”只能证明发生了分布。还要确认主要查询能否定向:
customerId 的金额查询无法定位数据块,需要向多个 shard 广播。从 config 数据库读取每个 shard 的文档统计:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
db.getSiblingDB("shard_lab").orders.getShardDistribution();
'getShardDistribution() 会打印每个 shard 的数据量、文档数和数据块数,具体数字可能因哈希分布和均衡时机略有差异:
Shard shard1rs at shard1rs/shard1:27018
data : ...KiB docs : ... chunks : 2
Shard shard2rs at shard2rs/shard2:27018
data : ...KiB docs : ... chunks : 2
Totals
data : ...KiB docs : 2000 chunks : 4验收重点是总文档数为 2000、总数据块数为 4,并且两个 shard 都有数据。
下面用 explain 提取参与计划的 shard 名称。第一条带完整分片键等值:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
const exp = db.getSiblingDB("shard_lab").orders
.find({ customerId: "customer-042" })
.explain("executionStats");
printjson({
query: "customerId 等值",
shards: exp.executionStats.executionStages.shards.map(
s => s.shardName
),
returned: exp.executionStats.nReturned
})
'结果会只包含一个 shard:
{
query: 'customerId 等值',
shards: [ 'shard2rs' ],
returned: 10
}具体是 shard1rs 还是 shard2rs 由哈希结果决定。接着查询金额,不提供分片键:
docker compose -p bookstore-sharded exec mongos \
mongosh --quiet --eval '
const exp = db.getSiblingDB("shard_lab").orders
.find({ amount: { $gte: 65 } })
.explain("executionStats");
printjson({
query: "amount 范围",
shards: exp.executionStats.executionStages.shards.map(
s => s.shardName
),
returned: exp.executionStats.nReturned
})
'{
query: 'amount 范围',
shards: [ 'shard1rs', 'shard2rs' ],
returned: 500
}这就是 targeted query 与 scatter-gather query 的差别。广播查询不是一律禁止,但高频低延迟接口若长期广播,就说明分片键与访问模式可能不匹配。
复合分片键可以把两种目标组合起来,但字段顺序会决定路由能力。设计时应拿真实查询样本做 analyzeShardKey、基数与频率分析,再在接近生产分布的数据上验证。
MongoDB 支持 refine shard key 和 resharding 等演进手段,但变更仍会消耗资源并影响运维计划。不要把“以后能改”当作跳过前期分析的理由。
如果一次订单事务涉及的数据落在不同 shard,协调成本会高于单 shard 事务。好的模型会尽量让一个业务操作访问同一分片键范围,例如把客户订单操作围绕 customerId 组织。
唯一索引也受分片键约束。对分片集合创建集群范围内的唯一约束时,唯一索引通常必须以分片键为前缀,否则各 shard 无法只凭自身数据保证全局唯一。订单号、租户 ID 和客户 ID 的组合要在建模阶段一起考虑。
先确认本节的 shard_lab 数据不再需要。下面按 Compose 项目名删除四个容器、默认网络和三个数据卷:
docker compose -p bookstore-sharded down -v[+] Running 8/8
✔ Container bookstore-sharded-mongos-1 Removed
✔ Container bookstore-sharded-configsvr-1 Removed
✔ Container bookstore-sharded-shard1-1 Removed
✔ Container bookstore-sharded-shard2-1 Removed
✔ Volume bookstore-sharded_config-data Removed
✔ Volume bookstore-sharded_shard1-data Removed
✔ Volume bookstore-sharded_shard2-data Removed
✔ Network bookstore-sharded_default Removeddocker compose -p bookstore-sharded ps -aNAME IMAGE COMMAND SERVICE CREATED STATUS PORTS标题下没有服务,说明分片实验组已经删除。主课程容器 paperboat-mongo 仍由第 17 节完成最终验收和清理。
最后删除本节的 Compose 文件与空目录:
cd ..
rm -f paperboat-sharding-lab/compose.yaml
rmdir paperboat-sharding-labrmdir 失败说明目录不为空。先列出残留文件并判断用途,不要为了省一步检查而扩大删除范围。
你已经完成了最小但完整的分片链路:初始化 config server 与两个 shard,把它们注册给 mongos,创建哈希分片集合,写入 2000 篇文档,并用 explain 区分定向与广播查询。