MongoDB 的两种集群形态
MongoDB 生产部署不外乎两种拓扑——副本集(Replica Set)和分片集群(Sharded Cluster),用途完全不同:
| 维度 | 副本集(Replica Set) | 分片集群(Sharded Cluster) |
|---|---|---|
| 目标 | 高可用 + 数据冗余 | 水平扩展 + 写分散 |
| 数据分布 | 每节点全量副本 | 数据按 shard key 切片分布 |
| 节点数 | 3+(1 Primary + N Secondary) | 每个分片是一个副本集 + 配置服务器 + mongos |
| 写性能 | 单点写(Primary) | 多分片并行写 |
| 适用 | 中小规模(<TB) | 大规模(TB+)高写入 |
| 复杂度 | 低 | 高(需要 mongos 路由 + 配置服务器) |
选型原则:先用副本集,写量/数据量到瓶颈再加分片。分片不是银弹——引入复杂度的同时,跨分片事务和 join 性能会下降。
副本集搭建
架构
生产副本集最少 3 节点(避免脑裂):
Primary (读写) ←─── 复制 ───→ Secondary (只读)
↑
Arbiter (仲裁,不存数据)
- Primary:接受所有写操作
- Secondary:异步复制 oplog,可配置为可读
- Arbiter:不存数据,只参与选举投票(2 数据节点 + 1 仲裁 = 3 票,防脑裂)
安装
# CentOS
cat > /etc/yum.repos.d/mongodb.repo << 'EOF'
[mongodb-org-7.0]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/7.0/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-7.0.asc
EOF
yum install -y mongodb-org
systemctl enable --now mongod
MongoDB 7.0 是当前 LTS 版本。安装后默认监听 127.0.0.1:27017,需改配置才能集群通信。
三节点副本集配置
每台机器 /etc/mongod.conf:
# /etc/mongod.conf
storage:
dbPath: /data/mongodb
journal:
enabled: true
wiredTiger:
engineConfig:
cacheSizeGB: 8 # WiredTiger 缓存(建议 50% 物理内存)
systemLog:
destination: file
path: /var/log/mongodb/mongod.log
logAppend: true
net:
port: 27017
bindIp: 0.0.0.0 # 集群通信必须
bindIpAll: true
replication:
replSetName: rs0 # 副本集名称(三节点必须一致)
security:
authorization: enabled
keyFile: /etc/mongodb-keyfile # 集群内部认证密钥
生成 keyFile(三台机器必须一致):
openssl rand -base64 756 > /etc/mongodb-keyfile
chmod 400 /etc/mongodb-keyfile
chown mongod:mongod /etc/mongodb-keyfile
# scp 到另外两台
scp /etc/mongodb-keyfile mongo2:/etc/
scp /etc/mongodb-keyfile mongo3:/etc/
初始化副本集(在 Primary 机器执行):
// 连接任一节点
mongosh
// 初始化副本集
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongo1:27017", priority: 2 }, // 优先当 Primary
{ _id: 1, host: "mongo2:27017", priority: 1 },
{ _id: 2, host: "mongo3:27017", priority: 0, arbiterOnly: true }
]
})
// 查看状态
rs.status()
rs.conf()
// 创建管理员用户
use admin
db.createUser({
user: "admin",
pwd: "StrongPassword123!",
roles: [{ role: "root", db: "admin" }]
})
⚠️ 避坑 #1:keyFile 的权限必须是
400且 owner 是mongod,否则启动报错permissions on key file are too open。
副本集日常操作
// 查看复制延迟
rs.printReplicationInfo()
rs.printSecondaryReplicationInfo()
// 强制切换 Primary(维护场景)
rs.stepDown(120) // 当前 Primary 主动让位 120 秒
// 或指定节点提升
cfg = rs.conf()
cfg.members[1].priority = 3
rs.reconfig(cfg)
// 添加新节点
rs.add({ host: "mongo4:27017", priority: 1 })
// 移除节点
rs.remove("mongo4:27017")
// 设置延迟节点(灾难恢复用)
cfg = rs.conf()
cfg.members[2].priority = 0
cfg.members[2].hidden = true
cfg.members[2].secondaryDelaySecs = 3600 // 延迟 1 小时
rs.reconfig(cfg)
分片集群搭建
架构
┌──────────────────┐
应用 ──写入──→ │ mongos (路由) │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Config Servers │ (3 节点副本集)
│ 元数据/shard映射 │
└────────┬─────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │ (每个分片是一个副本集)
│ (RS) │ │ (RS) │ │ (RS) │
└──────────┘ └──────────┘ └──────────┘
部署步骤
1. 配置服务器(3 节点副本集)
# configsvr 的 mongod.conf
sharding:
clusterRole: configsvr
replication:
replSetName: cfgReplSet
net:
port: 27019
// 在 config1 上初始化
rs.initiate({
_id: "cfgReplSet",
configsvr: true,
members: [
{ _id: 0, host: "config1:27019" },
{ _id: 1, host: "config2:27019" },
{ _id: 2, host: "config3:27019" }
]
})
2. 分片副本集(每个分片一个副本集)
# shard1 的 mongod.conf
sharding:
clusterRole: shardsvr
replication:
replSetName: shard1ReplSet
net:
port: 27018
// 在 shard1-node1 上初始化
rs.initiate({
_id: "shard1ReplSet",
members: [
{ _id: 0, host: "shard1-node1:27018", priority: 2 },
{ _id: 1, host: "shard1-node2:27018", priority: 1 },
{ _id: 2, host: "shard1-node3:27018", priority: 0, arbiterOnly: true }
]
})
3. mongos 路由
# mongos.conf(注意:mongos 不需要 storage.dbPath)
sharding:
configDB: cfgReplSet/config1:27019,config2:27019,config3:27019
net:
port: 27017
4. 添加分片到集群
// 连接 mongos
mongosh --host mongos1:27017
// 添加分片
sh.addShard("shard1ReplSet/shard1-node1:27018,shard1-node2:27018")
sh.addShard("shard2ReplSet/shard2-node1:27018,shard2-node2:27018")
sh.addShard("shard3ReplSet/shard3-node1:27018,shard3-node2:27018")
// 查看集群状态
sh.status()
启用分片
// 1. 对数据库启用分片
sh.enableSharding("mydb")
// 2. 创建 shard key 索引(必须先建索引)
db.orders.createIndex({ orderDate: 1, _id: 1 })
// 3. 对集合分片
sh.shardCollection("mydb.orders", { orderDate: 1, _id: 1 })
// 4. 查看分片分布
db.orders.getShardDistribution()
db.orders.stats().sharded // true
Shard Key 设计
Shard Key 是分片集群最关键的决策——选定后不可更改(6.0+ 支持有限重塑),选错了整个集群性能崩盘。
选择原则
| 原则 | 说明 |
|---|---|
| 高基数 | 值分布广,数据能均匀分散。user_id 好,status(3值)坏 |
| 低频率 | 单值不会过度集中。单调递增的 _id 会热点写 |
| 查询靶向 | 常用查询条件包含 shard key,mongos 能定向路由到单分片 |
| 不可变 | shard key 字段值不能修改 |
常见 Shard Key 模式
// 1. 范围分片(默认)—— 适合范围查询
sh.shardCollection("db.orders", { orderDate: 1, _id: 1 })
// 查询某月订单 → 路由到 1-2 个分片
// 2. 哈希分片 —— 适合均匀分散写入
sh.shardCollection("db.logs", { _id: "hashed" })
// 写入均匀分布,但不支持范围查询
// 3. 复合 key —— 兼顾分散与查询靶向
sh.shardCollection("db.events", { userId: 1, timestamp: 1 })
// 查某用户的事件 → 路由到单分片
// 4. 区域分片(Zone Sharding)—— 地理分布
sh.addShardTag("shard1", "US")
sh.addShardTag("shard2", "EU")
sh.addTagRange("db.users", { region: "US" }, { region: "US~" }, "US")
⚠️ 避坑 #2:单调递增的 shard key(如时间戳、ObjectId)用范围分片 = 所有写入打到最后一个分片(jumbo shard)。要么用哈希分片,要么加随机前缀。
数据均衡(Balancer)
MongoDB 有自动 balancer 在分片间迁移 chunk:
// 查看均衡器状态
sh.getBalancerState() // true
sh.isBalancerRunning() // 是否正在迁移
// 手动控制
sh.startBalancer() // 开启(默认)
sh.stopBalancer() // 暂停(维护窗口用)
// 设置均衡窗口(避免高峰期迁移影响性能)
sh.setBalancerState(true)
db.settings.update(
{ _id: "balancer" },
{ $set: { activeWindow: { start: "02:00", stop: "06:00" } } },
{ upsert: true }
)
// 指定分片大小(默认 64MB,大 chunk 减少迁移但均衡慢)
db.settings.update(
{ _id: "chunksize" },
{ $set: { value: 128 } },
{ upsert: true }
)
⚠️ 避坑 #3:chunk 迁移期间会锁集合(6.0 已优化为非阻塞但仍有影响)。生产高峰期务必设均衡窗口或临时关 balancer。
备份与恢复
mongodump / mongorestore(逻辑备份)
# 全库备份
mongodump --host mongos1:27017 --out /backup/$(date +%Y%m%d) \
--username admin --password 'StrongPassword123!' --authenticationDatabase admin
# 单集合备份
mongodump --host mongos1:27017 --db mydb --collection orders \
--out /backup/$(date +%Y%m%d)
# 压缩备份
mongodump --host mongos1:27017 --gzip --archive=/backup/dump-$(date +%Y%m%d).gz
# 恢复
mongorestore --host mongos1:27017 --gzip --archive=/backup/dump-20260717.gz \
--username admin --password 'StrongPassword123!' --authenticationDatabase admin
⚠️ 避坑 #4:
mongodump在分片集群上连 mongos 会备份所有分片数据,但不保证一致性快照(备份期间有写入则数据可能不一致)。生产环境优先用文件系统快照。
文件系统快照(物理备份)
# 1. 锁定副本集 Secondary(不中断服务)
mongosh --host mongo2:27017
> db.fsyncLock() # 刷盘 + 锁
# 2. LVM/ZFS 快照
lvcreate -s -L 10G -n mongo_snap /dev/vg0/mongodb
# 或 zfs snapshot tank/mongodb@backup-20260717
# 3. 解锁
> db.fsyncUnlock()
# 4. 挂载快照拷贝
mount /dev/vg0/mongo_snap /mnt/snap
rsync -av /mnt/snap/ /backup/mongo-20260717/
umount /mnt/snap
lvremove -f /dev/vg0/mongo_snap
定时备份脚本
#!/bin/bash
# /opt/mongodb/scripts/daily_backup.sh
BACKUP_DIR=/backup/mongodb
DATE=$(date +%Y%m%d)
RETAIN_DAYS=7
mkdir -p $BACKUP_DIR
mongodump --host mongos1:27017 --gzip --archive=$BACKUP_DIR/full-$DATE.gz \
--username admin --password "$MONGO_PWD" --authenticationDatabase admin
# 上传到对象存储(可选)
# ossutil cp $BACKUP_DIR/full-$DATE.gz oss://backup/mongodb/
# 清理旧备份
find $BACKUP_DIR -name "full-*.gz" -mtime +$RETAIN_DAYS -delete
性能调优
WiredTiger 引擎参数
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 16 # 缓存大小(建议 50-60% 物理内存)
journalCompressor: snappy # journal 压缩(snappy/zstd/none)
collectionConfig:
blockCompressor: zstd # 集合数据压缩(zstd 压缩率最高)
indexConfig:
prefixCompression: true # 索引前缀压缩
索引优化
// 查看慢查询(开启 profiler)
db.setProfilingLevel(1, { slowms: 100 }) // >100ms 记录
db.system.profile.find().sort({ ts: -1 }).limit(10)
// 解释执行计划
db.orders.find({ userId: "u123", status: "paid" }).explain("executionStats")
// 关键指标:
// - COLLSCAN(全表扫描)→ 需建索引
// - totalKeysExamined / totalDocsExamined → 越接近返回行数越好
// - executionTimeMillis → 执行时间
// 创建复合索引(ESR 原则:Equality > Sort > Range)
db.orders.createIndex({ userId: 1, status: 1, orderDate: -1 })
// TTL 索引(自动过期删除)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
// 后台建索引(不阻塞,但慢)
db.bigcoll.createIndex({ field: 1 }, { background: true })
⚠️ 避坑 #5:MongoDB 4.2+ 后台建索引已废弃,默认就是非阻塞的。但大集合建索引仍会消耗大量资源,建议低峰执行。
连接池
// 应用端连接串(Node.js driver 示例)
const uri = "mongodb://mongo1:27017,mongo2:27017,mongo3:27017/mydb?" +
"replicaSet=rs0&" +
"maxPoolSize=100&" + // 连接池大小(默认 100)
"minPoolSize=10&" + // 最小连接
"maxIdleTimeMS=30000&" + // 空闲超时
"connectTimeoutMS=5000&" + // 连接超时
"socketTimeoutMS=30000&" + // Socket 超时
"readPreference=secondaryPreferred"; // 读偏好
// 读偏好选项:
// primary - 只读 Primary(强一致)
// primaryPreferred - 优先 Primary,故障时读 Secondary
// secondary - 只读 Secondary
// secondaryPreferred - 优先 Secondary(读分离推荐)
// nearest - 延迟最低的节点
监控与告警
// 关键监控命令
db.serverStatus() // 服务器状态
db.serverStatus().connections // 连接数
db.serverStatus().opcounters // 操作计数
rs.status() // 副本集状态
sh.status() // 分片状态
// 副本集复制延迟
rs.printSecondaryReplicationInfo()
// 分片数据分布
db.getSiblingDB("config").chunks.aggregate([
{ $group: { _id: "$shard", count: { $sum: 1 } } }
])
关键告警阈值:
| 指标 | 警告 | 严重 | 说明 |
|---|---|---|---|
| 复制延迟 | >10s | >60s | Secondary 跟不上 Primary |
| 连接使用率 | >80% | >95% | 连接池不够 |
| 缓存命中率 | <90% | <80% | cacheSizeGB 不够 |
| 磁盘使用 | >80% | >90% | 需扩容 |
| 队列深度 | >10 | >50 | 读写积压 |
| Chunk 迁移失败 | >0 | >5 | balancer 异常 |
常见故障排查
| 故障 | 诊断 | 解决 |
|---|---|---|
| 副本集无 Primary | rs.status() 看选举状态 |
检查网络/多数节点存活;rs.freeze() + rs.stepDown() |
| 复制延迟高 | rs.printSecondaryReplicationInfo() |
Secondary 硬件慢/oplog 窗口太短/网络带宽不足 |
| 分片不均匀 | sh.status() 看 chunk 分布 |
检查 shard key 基数;手动 sh.moveChunk() |
| 慢查询 | db.system.profile.find() |
加索引/优化查询;explain() 看执行计划 |
| 内存溢出 | db.serverStatus().wiredTiger.cache |
增大 cacheSizeGB;优化查询减少扫描 |
| 磁盘满 | df -h + db.stats() |
压缩集合 compact;删旧数据;扩容 |
| 连接拒绝 | db.currentOp() + 网络检查 |
增大连接池;检查 maxIncomingConnections |
| balancer 卡住 | sh.isBalancerRunning() |
检查 chunk 太大(jumbo);手动 split/move |
⚠️ 避坑 #6:副本集只有 2 个数据节点 + 1 仲裁器时,如果 Primary 挂了,Secondary 不能自动选举(只有 1 票 < 多数)。必须确保投票节点数为奇数。
十条避坑清单
- keyFile 权限 400:权限太开放 mongod 启动失败,owner 必须是 mongod
- 投票节点必须奇数:2 数据 + 1 仲裁 = 3 票可以;2 数据 = 脑裂风险
- shard key 不可改:选定后只能重塑(6.0+),选错代价极大
- 单调递增 key 别范围分片:所有写打到最后一个分片 = 热点
- mongodump 不保证一致性:分片集群备份优先用文件系统快照
- balancer 高峰关掉:chunk 迁移消耗资源,生产设均衡窗口
- cacheSizeGB 别超 60%:留内存给 OS 文件缓存和连接
- oplog 要够大:默认 5% 磁盘,大写入场景需手动
--oplogSize调大,否则 Secondary 追不上 - 连接池大小适中:太大压垮 DB,太小排队;100 是合理默认
- 读偏好按场景选:secondaryPreferred 提升读吞吐但有延迟;强一致用 primary
总结
MongoDB 运维核心要点:
- 先用副本集——3 节点起步,写量到瓶颈再加分片
- shard key 决定集群命运——高基数 + 低频率 + 查询靶向,选错全盘皆输
- 文件系统快照 > mongodump——生产备份优先物理快照保证一致性
- balancer 设窗口——迁移消耗资源,生产高峰期必须关
- 监控复制延迟和缓存命中率——这两个指标最能反映集群健康度
掌握副本集搭建、分片集群部署、shard key 设计、备份恢复这四块,MongoDB 生产运维就能覆盖绝大多数场景。
