基数级别 | 取值数量 | 适用性评估 |
极低基数 | < 100 | 禁止使用(必然热点) |
低基数 | 100 ~ 10,000 | 谨慎使用(需评估分布) |
高基数 | 10,000 ~ 1,000,000 | 推荐使用 |
极高基数 | > 1,000,000 | 理想选择 |
city(城市)字段作为分片键,全国仅50个城市。orderId 或 customerId)作为分片键,这类字段的取值范围可达百万级甚至更高,能够确保数据和读写负载极其均匀地分散到集群内的所有分片中,充分释放分布式的水平扩展能力。// ==========================================// 案例 A:评估 customer_id 字段的基数// ==========================================// 注意:大表执行此命令会触发全表扫描,请在测试环境进行验证,线上环境谨慎操作!db.t_orders.aggregate([{ $group: { _id: "$customer_id" } },{ $count: "cardinality" }], { allowDiskUse: true }) // 建议加上 allowDiskUse,防止内存超限报错// 执行结果:{ "cardinality": 2500000 }// 结论评估:高基数字段,具备极佳的数据离散度,适合作为分片键。// ==========================================// 案例 B:评估 status 字段的基数// ==========================================// 注意:大表执行此命令会触发全表扫描,请在测试环境进行验证,线上环境谨慎操作!db.t_orders.aggregate([{ $group: { _id: "$status" } },{ $count: "cardinality" }])// 执行结果:{ "cardinality": 6 }// 结论评估:极低基数字段,极易引发 Jumbo Chunk 与数据倾斜,【禁止】单独作为分片键!
ObjectId、自增 ID、时间戳)作为范围分片键。时间线 ─────────────────────────────────────────────────────►Chunk 1 Chunk 2 Chunk 3 Chunk 4[2024-01-01] [2024-02-01] [2024-03-01] [2024-04-01]↑ ↑ ↑ ↑冷数据 冷数据 冷数据 ←── 所有写入集中在此(热点)
特性 | 范围分片(Range) | 哈希分片(Hash) | Zone Sharding |
写入分布 | 可能热点(单调字段) | 均匀分布 | 按 Zone 规则分布 |
范围查询 | 高效(定向查询) | 低效(全分片扫描) | Zone 内高效 |
等值查询 | 高效 | 高效 | Zone 内高效 |
数据定位 | 按值范围 | 按哈希值 | 按 Zone 标签 |
适用场景 | 时间范围查询、区间统计 | 高并发写入、等值查询 | 按 Zone 标签 |
createTime(时间戳)作为范围分片键。_id 的哈希分片,写入均匀分布到所有分片,单分片压力下降80%。// 错误设计:对递增键使用范围分片,必然导致单一节点的写入热点sh.shardCollection("mydb.t_orders", { _id: 1 })sh.shardCollection("mydb.t_orders", { createTime: 1 })// 规范动作:点查比较多的场景,建议使用 Hash 分片,强制打散数据sh.shardCollection("mydb.t_orders", { _id: "hashed" })// 规范动作:范围查询比较多的场景,建议采用 Hash 分片以追求均衡sh.shardCollection("mydb.t_orders", { orderNo: "hashed" })
路由类型 | 说明 | 性能影响 |
定向查询(Targeted) | 查询条件包含分片键,直接路由到目标分片 | 低延迟 |
广播查询(Scatter-Gather) | 查询条件不含分片键,广播到所有分片 | 高延迟 |
_id 哈希分片(写入均匀),但99%的查询按 user_id 过滤。{user_id: 1} 作为分片键,查询直接路由到目标分片,延迟恢复至15ms。// 分片键:{ user_id: 1 }// 定向查询:条件包含分片键,直接路由到目标分片db.t_messages.find({ user_id: "U001", create_time: { $gt: ISODate("2024-01-01") } })// 广播查询:条件不含分片键,广播到所有分片db.t_messages.find({ create_time: { $gt: ISODate("2024-01-01") } })
tenant_id 过滤,但部分大租户数据量是小租户的1000倍。tenant_id 分片导致大租户数据集中在少数分片,存储和性能严重倾斜。{tenant_id: 1, _id: "hashed"},同租户数据按 _id 哈希进一步打散,既支持租户定向查询,又避免大租户热点。// 模式一:前缀定向 + 后缀打散// 适用:查询总是按某字段过滤,但该字段基数不够高sh.shardCollection("db.t_orders", { customer_id: 1, order_id: "hashed" })// 模式二:时间范围 + 随机打散// 适用:需要时间范围查询,但写入要均匀sh.shardCollection("db.t_logs", { month: 1, order_id: "hashed" })// month 字段为 "2024-01" 格式,每月一个范围,月内哈希打散。
┌─────────────┐│ 应用程序 │└──────┬──────┘│┌──────▼──────┐│ mongos │ ◄── 必须通过 mongos 访问└──────┬──────┘┌───────────────┼───────────────┐│ │ │┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐│ Shard 1 │ │ Shard 2 │ │ Shard 3 ││ (副本集) │ │ (副本集) │ │ (副本集) │└─────────────┘ └─────────────┘ └─────────────┘↑ ↑ ↑└───────────────┴───────────────┘❌ 禁止直连
# 正确:通过 mongos 连接mongodb://mongouser:password@10.0.0.1:27017,10.0.0.2:27017/admin# 错误:直连 shard 节点mongodb://mongouser:password@shard1-node1:27018/admin
# 正确:连接串中包含所有 mongos 地址uri = "mongodb://user:pass@mongos1:27017,mongos2:27017,mongos3:27017/admin"# 错误:仅配置单个 mongosuri = "mongodb://user:pass@mongos1:27017/admin"
// 设置 Balancer 运行时间窗口(凌晨 2:00 - 6:00)use configdb.settings.updateOne({ _id: "balancer" },{$set: {activeWindow: {start: "02:00",stop: "06:00"}}},{ upsert: true })// 大促期间临时停止 Balancersh.stopBalancer()// 确认 Balancer 已停止sh.isBalancerRunning() // 应返回 false// 大促结束后启动 Balancersh.startBalancer()
// 关闭某个集合的 Balancer(其他集合不受影响)sh.disableBalancing("mydb.t_orders")// 打开某个集合的 Balancersh.enableBalancing("mydb.t_orders")// 查看某个集合是否开启了 Balancerdb.getSiblingDB("config").collections.findOne({_id: "mydb.t_orders"}).noBalance// 返回 true 表示已关闭,返回 false 或无返回值表示已开启
// 查找所有 Jumbo Chunkuse configdb.chunks.find({ jumbo: true }).forEach(function(chunk) {print("Jumbo Chunk: " + chunk.ns + " [" +tojson(chunk.min) + " -> " + tojson(chunk.max) + "]")})// 统计各分片的 Chunk 数量(检查是否均衡)db.chunks.aggregate([{ $match: { ns: "mydb.mycollection" } },{ $group: { _id: "$shard", count: { $sum: 1 } } },{ $sort: { count: -1 } }])
email 字段创建唯一索引,但分片键是 user_id。can't shard collection with unique index on { email: 1 }。每个分片只能保证本分片内 email 唯一,无法跨分片保证全局唯一。{user_id: 1, email: 1} 复合索引,或在应用层通过去重逻辑保证全局唯一性。// 分片键:{ user_id: 1 }// 错误:唯一索引不包含分片键db.t_users.createIndex({ email: 1 }, { unique: true })// 报错:can't shard collection with unique index on { email: 1 }// 正确:唯一索引包含分片键db.t_users.createIndex({ user_id: 1, email: 1 }, { unique: true })// 正确:_id 索引天然唯一(每个分片独立)// 注意:分片集合的 _id 仅在分片内唯一,跨分片可能重复
// 哈希分片预分片:指定预创建 Chunk 数量sh.shardCollection("mydb.t_orders",{ order_id: "hashed" },false, // unique{ numInitialChunks: 64 } // 预创建 64 个 Chunk)// Chunk 会自动均匀分布到各分片// 范围分片预分片:手动划分 Chunk 边界sh.shardCollection("mydb.t_logs", { month: 1, _id: 1 })// 手动划分 Chunk 边界sh.splitAt("mydb.t_logs", { month: "2024-01", _id: MinKey })sh.splitAt("mydb.t_logs", { month: "2024-02", _id: MinKey })sh.splitAt("mydb.t_logs", { month: "2024-03", _id: MinKey })// 手动迁移 Chunk 到指定分片(可选,Balancer 也会自动均衡)sh.moveChunk("mydb.t_logs", { month: "2024-01", _id: MinKey }, "shard1")sh.moveChunk("mydb.t_logs", { month: "2024-02", _id: MinKey }, "shard2")
检查项 | 验证方法 | 通过标准 |
基数是否足够高 | `db.collection.distinct("field").length` 注意: 大表会触发全表扫描,请在低峰期或从节点执行 | > 10,000 |
写入是否均匀 | 观察各分片写入 QPS | 偏差 < 20% |
是否覆盖主要查询 | 分析 TOP 10 查询语句 | > 80% 定向查询 |
是否为单调递增 | 分析字段特征 | 非时间戳/自增 ID |
是否考虑了数据倾斜 | 分析字段值分布 | 无明显热点值 |
检查项 | 验证方法 | 操作建议 |
各分片 Chunk 数量 | `sh.status()` | 偏差超过20%需排查 |
Jumbo Chunk 数量 | `db.chunks.find({ jumbo: true })` | 存在 Jumbo Chunk 需处理 |
分片存储均衡 | 各分片磁盘使用量 | 偏差超过30%需排查 |
场景 | 推荐架构 | 分片键建议 |
数据量 < 500GB,QPS < 10,000 | 副本集 | 无需分片 |
高并发写入,等值查询为主 | 分片集群 + 哈希分片 | 业务主键哈希 |
时间序列数据,范围查询为主 | 分片集群 + 复合分片 | `{time_bucket: 1, _id: "hashed"}` |
多租户 SaaS | 分片集群 + 复合分片 | `{tenant_id: 1, _id: "hashed"}` |
地理分布数据 | Zone Sharding | `{region: 1, _id: 1}` |
_id 字段,否则您可以更新文档的分片键值。 若要更新,请使用以下方式来更新文档的分片键值:命令 | 方法 |
db.collection.update() with multi: false | |
- | 说明: 如果分片键修改导致将文档移动到另一个分片,则在批量操作中不能指定多个分片键修改;即批量大小为 1。 如果分片键修改不会导致将文档移动到另一个分片,则可以在批量操作中指定多个分片键修改。 |
sh.stopBalancer()// 验证状态是否已成功关闭sh.getBalancerState() // 预期返回 false
sh.startBalancer()
文档反馈