MongoDB: 分片集群Sharding:水平扩展

最后更新:2026-08-26

分片集群是 MongoDB 水平扩展的终极方案——掌握它能支撑 PB 级数据。

1. 你将学到


2. 分片架构

概念说明:分片(Sharding)是 MongoDB 水平扩展的方案——将数据分散存储在多个分片(Shard)上,每个分片是独立的副本集。应用通过 Mongos 路由透明访问,无需感知数据分布。当单机容量或写入达到瓶颈时,分片是唯一的扩展途径。

工作原理:Mongos 作为查询路由,接收应用请求后,从 Config Server 获取分片元数据(哪些 Chunk 在哪个 Shard 上),将请求路由到目标 Shard 执行,合并结果返回。应用代码与单机 MongoDB 完全一致——分片对应用透明。

三大组件

100%
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。

JAVASCRIPT
// === 启用分片 ===
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。

JAVASCRIPT
// === 哈希分片 ===
sh.shardCollection('shopdb.products', { sku: 'hashed' });
// sku 哈希值均匀分布到各分片
维度 Hashed 分片 说明
数据分布 ✅ 均匀 哈希函数天然打散
写入分布 ✅ 均匀 无热点
范围查询 ❌ 需广播 相邻值在不同 Shard
适合场景 等值查询为主 sku, email, userId

(3) Range vs Hashed 对比

维度 Range Hashed
数据分布 可能倾斜 均匀
范围查询 ✅ 目标路由 ❌ 广播所有 Shard
等值查询
排序 ✅ 索引有序
写入热点 ⚠️ 单点热点 ✅ 均匀
复合分片键 ✅ 支持 ❌ 仅单字段

(4) 选择最佳分片键

JAVASCRIPT
// ✅ 好分片键:高基数、低频更新、查询命中
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 自动均衡的核心机制。

工作原理

均衡器原理

100%
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
100%
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
JAVASCRIPT
// === 手动拆分 Chunk ===
sh.splitAt('shopdb.orders', { userId: 5000 });

// === 查看 Chunk 分布 ===
db.orders.getShardDistribution();

// === 平衡器状态 ===
sh.status();
sh.isBalancerRunning();

要点解析

  1. Chunk 拆分是逻辑操作(仅修改元数据),不移动数据;Chunk 迁移是物理操作(实际复制数据)
  2. 均衡器在后台运行,迁移期间不影响读写(使用双写保证一致性)
  3. 维护窗口(如批量导入)建议暂停均衡器,避免迁移影响导入性能

▶ 示例 1: 均衡器管理与 Chunk 手动拆分

JAVASCRIPT
// 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();

输出:

TEXT 📖 仅展示
// 执行成功

5. Docker 分片集群搭建

概念说明:分片集群的部署比副本集复杂——需要 Config Server 副本集、多个 Shard 副本集和 Mongos 路由。Docker Compose 可以一键编排所有组件,适合开发测试环境。

部署架构

100%
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
YAML
# 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() 选择分片键
BASH
# === 初始化分片集群 ===
# 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 是否可达
JAVASCRIPT
// === 查看分片状态 ===
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: 分片监控与问题诊断

JAVASCRIPT
// 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%) → 均衡

输出:

TEXT 📖 仅展示
// mongoose 操作成功执行
// 数据库查询/更新结果

7. 何时使用分片?

概念说明:分片不是越早越好——它带来扩展能力的同时也带来运维复杂度。过早分片会增加不必要的运维成本;过晚分片则面临单机瓶颈和数据迁移困难。需要根据数据量、写入吞吐和增长趋势综合判断。

决策框架

100%
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 部署分片集群 + 数据分片实战

BASH
# === 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 范围)

输出:

TEXT 📖 仅展示
CONTAINER ID   IMAGE     STATUS    
abc123         latest    Up 2 hours
JAVASCRIPT
// === 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 连接无需感知分片细节。

❓ 常见问题

Q 分片后能用 unique 索引吗?
A 可以,但 unique 索引必须包含分片键(compound unique index)。
Q 分片键能修改吗?
A 不能。分片键一旦设定无法修改(需重建集合)。
Q 分片越多越好?
A 不是。分片过多增加运维复杂度。建议 3-12 个分片。
Q 生产用 Atlas 还是自建分片?
A Atlas 推荐(托管 + 自动化)。自建仅大型公司有专门 DBA 团队。

📖 小节


📝 作业

  1. 基础题(⭐):理解分片架构与组件(Mongo / Config Server / Shard)。
  2. 基础题(⭐):分析你的业务场景,判断是否需要分片。
  3. 进阶题(⭐⭐):用 Docker Compose 部署 1 Config + 2 Shard 分片集群。
  4. 进阶题(⭐⭐):测试分片键选择(userId vs sku vs compound)。
  5. 挑战题(⭐⭐⭐):完整分片集群部署(Config 副本集 + 2 Shard 副本集 + Mongos + 监控)。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏