分片集群部署
本文记录一个生产可用的 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,无需合并)
▼
返回客户端组件端口规划
| 组件 | 进程 | 端口 | 数量 |
|---|---|---|---|
| mongos | mongos | 27017 | ≥ 3 |
| Config Server | mongod --configsvr | 27019-27021 | 3 |
| Shard 1 RS | mongod --shardsvr | 27022-27024 | 3 |
| Shard 2 RS | mongod --shardsvr | 27025-27027 | 3 |
总计最少 10 个进程(3 mongos + 3 Config Server + 2×3 Shard)。
📋 部署前提
网络连通性
分片集群的每个节点都必须能够连接到集群中所有其他节点,包括所有 Shard 节点和 Config Server 节点。确保网络、防火墙、安全组允许这些连接。
主机名(重要)
避免因 IP 变更而需要更新集群配置,必须使用 DNS 主机名而非 IP 地址。MongoDB 5.0 起,仅配置了 IP 地址的节点无法通过启动验证。
将 localhost 用作主机名时,集群中所有节点都必须统一使用 localhost。
环境准备
# 创建数据目录
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 副本集
# 启动 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初始化副本集:
// 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()生产环境推荐使用配置文件替代命令行参数:
# /etc/mongod-cfg.conf
sharding:
clusterRole: configsvr
replication:
replSetName: configRS
net:
bindIp: localhost,cfg1.example.net
port: 27019
storage:
dbPath: /data/mongo/cfg0mongod --config /etc/mongod-cfg.conf --fork --logpath /var/log/mongodb/cfg0.log💾 Shard 副本集
Shard 1
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// 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
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// mongosh --port 27025
rs.initiate({
_id: "shard2",
members: [
{ _id: 0, host: "localhost:27025" },
{ _id: 1, host: "localhost:27026" },
{ _id: 2, host: "localhost:27027" }
]
})Shard 同样推荐配置文件:
# /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 路由
mongos --configdb configRS/localhost:27019,localhost:27020,localhost:27021 \
--port 27017 --bind_ip 0.0.0.0 \
--logpath /var/log/mongodb/mongos.log --forkmongos 配置文件:
# /etc/mongos.conf
sharding:
configDB: configRS/cfg1.example.net:27019,cfg2.example.net:27019,cfg3.example.net:27019
net:
bindIp: localhost,<hostname>
port: 27017mongos --config /etc/mongos.conf --fork --logpath /var/log/mongodb/mongos.log➕ 添加 Shard
// mongosh --port 27017
sh.addShard("shard1/localhost:27022,localhost:27023,localhost:27024")
sh.addShard("shard2/localhost:27025,localhost:27026,localhost:27027")
sh.status()✂️ 启用分片
// 对数据库启用分片
sh.enableSharding("myapp")
// 哈希分片 — 数据均匀分布,适合写多读少
sh.shardCollection("myapp.users", { userId: "hashed" })
// 范围分片 — 按范围路由,适合范围查询
sh.shardCollection("myapp.orders", { region: 1, orderDate: 1 })插入测试数据验证分布:
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) | 按地域/冷热分层 | 数据本地化,延迟低 | 配置复杂 |
// ✅ 推荐:复合分片键
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.4 | refineCollectionShardKey 为片键添加后缀字段 |
| 5.0+ | reshardCollection 完全更换片键 |
⚖️ Chunk 均衡管理
基本概念
- Chunk 默认大小:128 MB(MongoDB 6.0+)
- 当分片间数据量差值 ≥
chunkSize × 3时,Balancer 触发迁移 - Balancer 运行在 Primary Config Server 上
均衡器控制
// 查看状态
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 }
)查看分布
sh.status() // 整体状态
db.users.getShardDistribution() // 单集合分片分布
db.chunks.find({ns: "myapp.users"}).count() // Chunk 数量📈 扩容
添加新 Shard
// 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,避免初始不均衡:
// 哈希分片预分片
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%,选择业务低峰期操作。
📊 监控
关键指标
// 当前运行的操作
db.currentOp({ "secs_running": { $gt: 0.1 } })
// 执行计划分析
db.users.find({ userId: 123 }).explain("executionStats")
// 关注:stage:"COLLSCAN" → 缺索引;totalDocsExamined 远大于 nReturned → 索引不优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 已接入
🔒 安全加固
启用认证
# 1. 生成 keyFile(所有节点共享)
openssl rand -base64 756 > /data/mongo/keyfile
chmod 400 /data/mongo/keyfile
# 2. 所有 mongod / mongos 加上:
# --keyFile /data/mongo/keyfile创建管理员
// 连接 mongos
use admin
db.createUser({
user: "admin",
pwd: "secure_password",
roles: ["root"]
})🐳 Docker Compose 部署
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 不具备这两点,不可用于有状态数据库。
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: 100GiPod 启动后手动初始化副本集:
// 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 |
# 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
EOFK8s 部署要点
| 要点 | 说明 |
|---|---|
| Headless Service | clusterIP: None,提供 Pod 直连 DNS 名称 |
| podAntiAffinity | 确保副本集成员分散到不同物理节点 |
| volumeClaimTemplates | 每个 Pod 独立 PVC,数据不丢失 |
| Parallel podManagementPolicy | 并行启动 Pod,加速初始化 |
| wiredTigerCacheSizeGB | 设为容器内存的 50-60% |
| 探针 | 用 db.adminCommand('ping') 做 liveness/readiness |
| keyFile | 通过 K8s Secret Mount,不写死进 YAML |