第九章:分片集群

最后更新: 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

💡 实践提示

  1. 分片键选择:基于查询模式选择,考虑基数、写入分布和查询局部性
  2. 避免热点:单调递增的分片键会导致写入热点
  3. 预分片:在数据量小的时候进行分片,避免后期迁移
  4. 监控块分布:确保数据均匀分布在各个分片
  5. 使用副本集作为分片:提高单个分片的高可用性

📚 继续学习