Skip to content

分片集群部署

本文记录一个生产可用的 MongoDB 分片集群部署流程:3 个 Config Server + 2 个 Shard(各为副本集)+ mongos 路由。

🏗️ 架构规划

整体架构

                          ┌──────────────┐
                          │   应用程序     │
                          └──────┬───────┘

                    ┌────────────┼────────────┐
                    │            │            │
              ┌──────────┐ ┌──────────┐ ┌──────────┐
              │  mongos  │ │  mongos  │ │  mongos  │  路由层(无状态)
              │  :27017  │ │  :27017  │ │  :27017  │
              └──────────┘ └──────────┘ └──────────┘
                    │            │            │
                    └────────────┼────────────┘

              ┌──────────────────┼──────────────────┐
              │                  │                  │
        ┌──────────┐      ┌──────────┐      ┌──────────┐
        │ config-0 │      │ config-1 │      │ config-2 │  Config Server RS
        │  :27019  │      │  :27020  │      │  :27021  │  元数据存储
        └──────────┘      └──────────┘      └──────────┘
              │                  │                  │
   ┌──────────┼──────────────────┼──────────────────┼──────────┐
   │          │                  │                  │          │
┌──────┐ ┌──────┐ ┌──────┐                    ┌──────┐ ┌──────┐ ┌──────┐
│s1-n0 │ │s1-n1 │ │s1-n2 │       ...          │s2-n0 │ │s2-n1 │ │s2-n2 │  Shard RS
│:27022│ │:27023│ │:27024│                    │:27025│ │:27026│ │:27027│  数据存储
└──────┘ └──────┘ └──────┘                    └──────┘ └──────┘ └──────┘
   Shard 1 (副本集)                              Shard 2 (副本集)

mongos|查询路由

mongos 是分片集群的唯一入口,应用程序只连接 mongos,不直接连接 Shard 或 Config Server。

职责说明
请求路由解析查询中的分片键,将请求转发到目标 Shard;无分片键时广播到所有 Shard
结果合并从多个 Shard 收集结果,排序、合并后返回客户端
透明代理应用程序感知不到分片存在,接口与单机 mongod 一致
无状态mongos 不持久化任何数据,重启不影响集群

生产环境建议部署 至少 3 个 mongos 实例,前端加负载均衡(Nginx / HAProxy / DNS 轮询)。mongos 可以部署在应用服务器上以减少网络跳数。

mongos 数量与 Shard 数量无关。mongos 是无状态路由,扩缩依据客户端并发连接数查询吞吐量,而非 Shard 数量。3 个 mongos 搭配 10 个 Shard 完全可以。当 mongos CPU 持续 > 80% 或连接数接近上限时再增加 mongos。

Config Server|配置服务器

Config Server 存储集群的元数据,以副本集形式部署(称为 CSRS — Config Server Replica Set)。

存储内容说明
分片信息哪些数据库/集合启用了分片
Chunk 分布每个 Chunk 的键范围、所在 Shard
Shard 拓扑集群中所有 Shard 的地址和状态
均衡锁Balancer 运行在 Primary Config Server 上

mongos 启动时从 Config Server 加载元数据到内存,运行过程中惰性刷新(遇到路由过期时重新加载)。Config Server 宕机会导致元数据不可用,但已缓存了路由的 mongos 仍可继续路由查询。

生产环境强制 3 节点副本集(MongoDB 6.0+ 不再支持单节点 Config Server)。

Shard|数据分片

每个 Shard 存储数据的一个子集,Shard 本身是一个独立的副本集(至少 3 节点),提供自身的高可用。

职责说明
数据存储按 Chunk 存储分片数据的一个子集
本地高可用每个 Shard 是独立副本集,Primary 故障自动切换
独立扩展可单独对 Shard 扩容(磁盘、CPU、内存)

Primary Shard:每个数据库有一个"主分片",该数据库中未分片的集合全部存储在 Primary Shard 上。

Balancer|均衡器

Balancer 是运行在 Primary Config Server 上的后台进程,负责在 Shard 之间迁移 Chunk 以保持数据均匀分布。

特性说明
触发条件分片间数据量差值 ≥ chunkSize × 3(默认 128MB × 3 = 384MB)
迁移过程原子迁移:先复制到目标 Shard → 更新元数据 → 源 Shard 删除
性能影响迁移期间对源和目标 Shard 有少量 I/O/CPU 开销
活动窗口可限制仅在低峰时段运行(如 2:00-6:00)

数据流示例

  应用程序
     │ db.orders.find({ userId: 42 })

  mongos
     │ 分片键 userId,哈希值为 Shard2 的范围
     │ 只向 Shard2 发送查询(Targeted Query)

  Shard 2 (Primary)
     │ 本地查询 → 返回结果

  mongos
     │ 合并结果(此处只有一个 Shard,无需合并)

  返回客户端

组件端口规划

组件进程端口数量
mongosmongos27017≥ 3
Config Servermongod --configsvr27019-270213
Shard 1 RSmongod --shardsvr27022-270243
Shard 2 RSmongod --shardsvr27025-270273

总计最少 10 个进程(3 mongos + 3 Config Server + 2×3 Shard)。

📋 部署前提

网络连通性

分片集群的每个节点都必须能够连接到集群中所有其他节点,包括所有 Shard 节点和 Config Server 节点。确保网络、防火墙、安全组允许这些连接。

主机名(重要)

避免因 IP 变更而需要更新集群配置,必须使用 DNS 主机名而非 IP 地址。MongoDB 5.0 起,仅配置了 IP 地址的节点无法通过启动验证。

localhost 用作主机名时,集群中所有节点都必须统一使用 localhost

环境准备

shell
# 创建数据目录
sudo mkdir -p /data/mongo/{cfg0,cfg1,cfg2,s1n0,s1n1,s1n2,s2n0,s2n1,s2n2}
sudo chown -R $USER:$USER /data/mongo

# 创建日志目录
sudo mkdir -p /var/log/mongodb
sudo chown -R $USER:$USER /var/log/mongodb

⚙️ Config Server 副本集

shell
# 启动 3 个 Config Server
mongod --configsvr --replSet configRS --dbpath /data/mongo/cfg0 \
  --port 27019 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/cfg0.log --fork

mongod --configsvr --replSet configRS --dbpath /data/mongo/cfg1 \
  --port 27020 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/cfg1.log --fork

mongod --configsvr --replSet configRS --dbpath /data/mongo/cfg2 \
  --port 27021 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/cfg2.log --fork

初始化副本集:

js
// mongosh --port 27019
rs.initiate({
  _id: "configRS",
  configsvr: true,
  members: [
    { _id: 0, host: "localhost:27019" },
    { _id: 1, host: "localhost:27020" },
    { _id: 2, host: "localhost:27021" }
  ]
})

rs.status()

生产环境推荐使用配置文件替代命令行参数:

yaml
# /etc/mongod-cfg.conf
sharding:
  clusterRole: configsvr
replication:
  replSetName: configRS
net:
  bindIp: localhost,cfg1.example.net
  port: 27019
storage:
  dbPath: /data/mongo/cfg0
shell
mongod --config /etc/mongod-cfg.conf --fork --logpath /var/log/mongodb/cfg0.log

💾 Shard 副本集

Shard 1

shell
mongod --shardsvr --replSet shard1 --dbpath /data/mongo/s1n0 \
  --port 27022 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s1n0.log --fork

mongod --shardsvr --replSet shard1 --dbpath /data/mongo/s1n1 \
  --port 27023 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s1n1.log --fork

mongod --shardsvr --replSet shard1 --dbpath /data/mongo/s1n2 \
  --port 27024 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s1n2.log --fork
js
// mongosh --port 27022
rs.initiate({
  _id: "shard1",
  members: [
    { _id: 0, host: "localhost:27022" },
    { _id: 1, host: "localhost:27023" },
    { _id: 2, host: "localhost:27024" }
  ]
})

Shard 2

shell
mongod --shardsvr --replSet shard2 --dbpath /data/mongo/s2n0 \
  --port 27025 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s2n0.log --fork

mongod --shardsvr --replSet shard2 --dbpath /data/mongo/s2n1 \
  --port 27026 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s2n1.log --fork

mongod --shardsvr --replSet shard2 --dbpath /data/mongo/s2n2 \
  --port 27027 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/s2n2.log --fork
js
// mongosh --port 27025
rs.initiate({
  _id: "shard2",
  members: [
    { _id: 0, host: "localhost:27025" },
    { _id: 1, host: "localhost:27026" },
    { _id: 2, host: "localhost:27027" }
  ]
})

Shard 同样推荐配置文件:

yaml
# /etc/mongod-shard1.conf
sharding:
  clusterRole: shardsvr
replication:
  replSetName: shard1
net:
  bindIp: localhost,s1-mongo1.example.net
  port: 27018
storage:
  dbPath: /data/mongo/s1n0

🔀 mongos 路由

shell
mongos --configdb configRS/localhost:27019,localhost:27020,localhost:27021 \
  --port 27017 --bind_ip 0.0.0.0 \
  --logpath /var/log/mongodb/mongos.log --fork

mongos 配置文件:

yaml
# /etc/mongos.conf
sharding:
  configDB: configRS/cfg1.example.net:27019,cfg2.example.net:27019,cfg3.example.net:27019
net:
  bindIp: localhost,<hostname>
  port: 27017
shell
mongos --config /etc/mongos.conf --fork --logpath /var/log/mongodb/mongos.log

➕ 添加 Shard

js
// mongosh --port 27017
sh.addShard("shard1/localhost:27022,localhost:27023,localhost:27024")
sh.addShard("shard2/localhost:27025,localhost:27026,localhost:27027")

sh.status()

✂️ 启用分片

js
// 对数据库启用分片
sh.enableSharding("myapp")

// 哈希分片 — 数据均匀分布,适合写多读少
sh.shardCollection("myapp.users", { userId: "hashed" })

// 范围分片 — 按范围路由,适合范围查询
sh.shardCollection("myapp.orders", { region: 1, orderDate: 1 })

插入测试数据验证分布:

js
use myapp
for (let i = 0; i < 10000; i++) {
  db.users.insertOne({ userId: i, name: "user-" + i })
}
db.users.getShardDistribution()
// Shard shard1 at shard1/localhost:27022,localhost:27023,localhost:27024
//  data: 24.11MiB docs: 500
// Shard shard2 at shard2/localhost:27025,localhost:27026,localhost:27027
//  data: 24.11MiB docs: 500

🔑 分片键设计

分片键一旦设定不可修改(v5.0+ 支持 reshardCollection),必须提前规划。

选择原则

原则说明
高基数字段值的多样性足够大(如 userId),避免低基数(如 gender 只有 3 个值)
均匀分布写入和读取应均匀分散到各 Shard,避免热点
覆盖查询尽量让查询命中单个 Shard(Targeted Query),减少广播(Scatter-Gather)
避免单调递增自增 ID、纯时间戳会导致所有写入集中在最新 Chunk

策略对比

策略示例优点缺点
哈希分片{ userId: "hashed" }数据极均匀,无热点范围查询需全 Shard 扫描
范围分片{ region: 1, orderDate: 1 }范围查询只命目标 Shard易产生热点
复合分片{ userId: 1, timestamp: 1 }用户数据集中、查询命中单 Shard需理解查询模式
标签分片(Zone)按地域/冷热分层数据本地化,延迟低配置复杂
js
// ✅ 推荐:复合分片键
sh.shardCollection("ecom.orders", { userId: 1, createdAt: 1 })

// ✅ 推荐:哈希 + 范围
sh.shardCollection("analytics.logs", { userId: "hashed", timestamp: 1 })

// ❌ 反例:纯时间戳 — 写入热点全集中在最后一个 Chunk
sh.shardCollection("analytics.logs", { timestamp: 1 })

// ❌ 反例:低基数字段 — 只有几个 Chunk,无法均衡
sh.shardCollection("app.users", { status: 1 })

片键版本演进

版本能力
≤ 4.2片键不可修改
4.4refineCollectionShardKey 为片键添加后缀字段
5.0+reshardCollection 完全更换片键

⚖️ Chunk 均衡管理

基本概念

  • Chunk 默认大小:128 MB(MongoDB 6.0+)
  • 当分片间数据量差值 ≥ chunkSize × 3 时,Balancer 触发迁移
  • Balancer 运行在 Primary Config Server 上

均衡器控制

js
// 查看状态
sh.getBalancerState()          // 是否启用
sh.isBalancerRunning()         // 是否正在迁移

// 启停
sh.startBalancer()
sh.stopBalancer()

// 对特定集合禁用均衡(大批量导入时)
sh.disableBalancing("myapp.heavyLogs")
sh.enableBalancing("myapp.heavyLogs")

// 设置活动窗口(凌晨低峰期)
db.getSiblingDB("config").settings.updateOne(
  { _id: "balancer" },
  { $set: { activeWindow: { start: "02:00", stop: "06:00" } } },
  { upsert: true }
)

查看分布

js
sh.status()                          // 整体状态
db.users.getShardDistribution()      // 单集合分片分布
db.chunks.find({ns: "myapp.users"}).count()  // Chunk 数量

📈 扩容

添加新 Shard

js
// 1. 添加新分片(先完成副本集部署和初始化)
sh.addShard("shard3/localhost:27028,localhost:27029,localhost:27030")

// 2. 小集合可临时调小 chunkSize 加速迁移
db.adminCommand({
  configureCollectionBalancing: "myapp.users",
  chunkSize: 16   // MB,迁移完可调回
})

// 3. 监控迁移进度
sh.isBalancerRunning()
sh.status()

预分片(Pre-split)

大批量导入前预先切分 Chunk,避免初始不均衡:

js
// 哈希分片预分片
sh.shardCollection("myapp.users", { userId: "hashed" })
sh.splitAt("myapp.users", { userId: MinKey })
// ... 按需要切分更多 Chunk ...

// 范围分片预分片
sh.shardCollection("myapp.orders", { orderDate: 1 })
for (let i = 1; i < 12; i++) {
  sh.splitAt("myapp.orders", { orderDate: ISODate("2026-" + i + "-01") })
}

每次扩容不超过现有分片数的 30%,选择业务低峰期操作。

📊 监控

关键指标

js
// 当前运行的操作
db.currentOp({ "secs_running": { $gt: 0.1 } })

// 执行计划分析
db.users.find({ userId: 123 }).explain("executionStats")
// 关注:stage:"COLLSCAN" → 缺索引;totalDocsExamined 远大于 nReturned → 索引不优

Shell 实时监控

shell
# 每 1 秒输出集群性能
mongostat --host mongos:27017

# 集合级 I/O 热点
mongotop --host mongos:27017

推荐监控栈

方案适用
MongoDB Atlas托管集群
Ops Manager / Cloud Manager官方自建
Prometheus + MongoDB Exporter + Grafana开源自建

🐛 常见问题

问题原因解决
新加 Shard 数据不迁移Balancer 未启用 / 不在活动窗口sh.startBalancer(),检查 activeWindow
集合数据全在主分片集合未分片sh.shardCollection()
数据量小不触发均衡未达 chunkSize×3 阈值临时调小 chunkSize 触发迁移
ChunkTooBig 错误Jumbo Chunk,过大无法迁移手动 splitAt 或启用 forceJumboChunkMigration
refineCollectionShardKey 后不均衡已知问题手动 moveChunk,监控 getShardDistribution()
写入热点集中在最后一个 Shard单调递增片键改用哈希分片或复合分片键

✅ 生产检查清单

  • [ ] Config Server 为三节点副本集
  • [ ] 每个 Shard 为副本集(至少 3 节点)
  • [ ] mongos 至少 3 实例 + 负载均衡
  • [ ] 分片键高基数、均匀、覆盖常用查询
  • [ ] Balancer 已启用,活动窗口设在低峰期
  • [ ] sh.status() 无数据倾斜
  • [ ] 启用 writeConcern: majority 保证一致性
  • [ ] 启用认证(keyFile / x.509)
  • [ ] 定期 explain("executionStats") 分析慢查询
  • [ ] 监控 Prometheus + Grafana 已接入

🔒 安全加固

启用认证

shell
# 1. 生成 keyFile(所有节点共享)
openssl rand -base64 756 > /data/mongo/keyfile
chmod 400 /data/mongo/keyfile

# 2. 所有 mongod / mongos 加上:
#   --keyFile /data/mongo/keyfile

创建管理员

js
// 连接 mongos
use admin
db.createUser({
  user: "admin",
  pwd: "secure_password",
  roles: ["root"]
})

🐳 Docker Compose 部署

yaml
version: '3.8'
services:
  # Config Server
  cfg0:
    image: mongo:8
    command: mongod --configsvr --replSet configRS --port 27019 --bind_ip_all
    ports: ["27019:27019"]
    volumes: [cfg0-data:/data/db]
  cfg1:
    image: mongo:8
    command: mongod --configsvr --replSet configRS --port 27019 --bind_ip_all
    volumes: [cfg1-data:/data/db]
  cfg2:
    image: mongo:8
    command: mongod --configsvr --replSet configRS --port 27019 --bind_ip_all
    volumes: [cfg2-data:/data/db]

  # Shard 1
  s1n0:
    image: mongo:8
    command: mongod --shardsvr --replSet shard1 --port 27018 --bind_ip_all
    volumes: [s1n0-data:/data/db]
  s1n1:
    image: mongo:8
    command: mongod --shardsvr --replSet shard1 --port 27018 --bind_ip_all
    volumes: [s1n1-data:/data/db]
  s1n2:
    image: mongo:8
    command: mongod --shardsvr --replSet shard1 --port 27018 --bind_ip_all
    volumes: [s1n2-data:/data/db]

  # Shard 2
  s2n0:
    image: mongo:8
    command: mongod --shardsvr --replSet shard2 --port 27018 --bind_ip_all
    volumes: [s2n0-data:/data/db]
  s2n1:
    image: mongo:8
    command: mongod --shardsvr --replSet shard2 --port 27018 --bind_ip_all
    volumes: [s2n1-data:/data/db]
  s2n2:
    image: mongo:8
    command: mongod --shardsvr --replSet shard2 --port 27018 --bind_ip_all
    volumes: [s2n2-data:/data/db]

  # mongos
  mongos:
    image: mongo:8
    command: mongos --configdb configRS/cfg0:27019,cfg1:27019,cfg2:27019 --port 27017 --bind_ip_all
    ports: ["27017:27017"]
    depends_on: [cfg0, cfg1, cfg2, s1n0, s1n1, s1n2, s2n0, s2n1, s2n2]

volumes:
  cfg0-data:  cfg1-data:  cfg2-data:
  s1n0-data:  s1n1-data:  s1n2-data:
  s2n0-data:  s2n1-data:  s2n2-data:

启动后需要手动初始化副本集和添加 Shard(同上步骤 1-4)。

☸️ Kubernetes 部署

MongoDB 在 K8s 上有两种方式:Operator(推荐)和手动 StatefulSet

StatefulSet(手动)

MongoDB 副本集需要稳定网络标识和持久存储,K8s 中只有 StatefulSet 能满足。Deployment 不具备这两点,不可用于有状态数据库。

yaml
apiVersion: v1
kind: Service
metadata:
  name: mongo-shard1
  namespace: mongo
spec:
  clusterIP: None          # Headless Service — 必须,提供 Pod 直连 DNS
  selector:
    app: mongo-shard1
  ports:
    - port: 27018
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo-shard1
  namespace: mongo
spec:
  serviceName: mongo-shard1
  replicas: 3
  podManagementPolicy: Parallel    # 并行启动,避免串行初始化过慢
  selector:
    matchLabels:
      app: mongo-shard1
  template:
    metadata:
      labels:
        app: mongo-shard1
    spec:
      terminationGracePeriodSeconds: 30
      affinity:
        podAntiAffinity:           # 分散到不同节点
          requiredDuringSchedulingIgnoredDuringExecution:
            - topologyKey: kubernetes.io/hostname
              labelSelector:
                matchLabels:
                  app: mongo-shard1
      containers:
        - name: mongod
          image: mongo:8
          args:
            - --shardsvr
            - --replSet=shard1
            - --port=27018
            - --bind_ip_all
          resources:
            requests:
              cpu: "2"
              memory: 4Gi
            limits:
              memory: 8Gi
          volumeMounts:
            - name: data
              mountPath: /data/db
          livenessProbe:
            exec:
              command:
                - mongosh
                - --eval
                - "db.adminCommand('ping')"
            initialDelaySeconds: 30
            periodSeconds: 10
  volumeClaimTemplates:            # 每个 Pod 独立的 PVC
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: ssd-high-iops
        resources:
          requests:
            storage: 100Gi

Pod 启动后手动初始化副本集:

js
// Pod DNS 格式:<pod-name>.<service-name>.<namespace>.svc.cluster.local
rs.initiate({
  _id: "shard1",
  members: [
    { _id: 0, host: "mongo-shard1-0.mongo-shard1.mongo:27018" },
    { _id: 1, host: "mongo-shard1-1.mongo-shard1.mongo:27018" },
    { _id: 2, host: "mongo-shard1-2.mongo-shard1.mongo:27018" }
  ]
})

Operator(推荐)

Operator 自动管理副本集初始化、扩缩容、备份、TLS 轮换等。推荐两个:

Operator特点
MongoDB Controllers for Kubernetes官方新方案,集成 Ops Manager
Percona Operator for MongoDB开源免费,无需企业授权,支持热备份和 PITR
shell
# Percona Operator
kubectl apply -f https://raw.githubusercontent.com/percona/percona-server-mongodb-operator/main/deploy/bundle.yaml

# 通过 CR 定义分片集群
kubectl apply -f - <<EOF
apiVersion: psmdb.percona.com/v1
kind: PerconaServerMongoDB
metadata:
  name: my-sharded-cluster
spec:
  sharding:
    enabled: true
    configsvrReplSet:
      size: 3
      volumeSpec:
        persistentVolumeClaim:
          resources:
            requests:
              storage: 20Gi
    mongos:
      size: 3
  replsets:
    - name: shard1
      size: 3
      volumeSpec:
        persistentVolumeClaim:
          resources:
            requests:
              storage: 100Gi
    - name: shard2
      size: 3
      volumeSpec:
        persistentVolumeClaim:
          resources:
            requests:
              storage: 100Gi
EOF

K8s 部署要点

要点说明
Headless ServiceclusterIP: None,提供 Pod 直连 DNS 名称
podAntiAffinity确保副本集成员分散到不同物理节点
volumeClaimTemplates每个 Pod 独立 PVC,数据不丢失
Parallel podManagementPolicy并行启动 Pod,加速初始化
wiredTigerCacheSizeGB设为容器内存的 50-60%
探针db.adminCommand('ping') 做 liveness/readiness
keyFile通过 K8s Secret Mount,不写死进 YAML

📚 参考