数据库运维15 min read

MongoDB 副本集与分片集群运维实战

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

⚠️ 避坑 #4mongodump 在分片集群上连 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 票 < 多数)。必须确保投票节点数为奇数。

十条避坑清单

  1. keyFile 权限 400:权限太开放 mongod 启动失败,owner 必须是 mongod
  2. 投票节点必须奇数:2 数据 + 1 仲裁 = 3 票可以;2 数据 = 脑裂风险
  3. shard key 不可改:选定后只能重塑(6.0+),选错代价极大
  4. 单调递增 key 别范围分片:所有写打到最后一个分片 = 热点
  5. mongodump 不保证一致性:分片集群备份优先用文件系统快照
  6. balancer 高峰关掉:chunk 迁移消耗资源,生产设均衡窗口
  7. cacheSizeGB 别超 60%:留内存给 OS 文件缓存和连接
  8. oplog 要够大:默认 5% 磁盘,大写入场景需手动 --oplogSize 调大,否则 Secondary 追不上
  9. 连接池大小适中:太大压垮 DB,太小排队;100 是合理默认
  10. 读偏好按场景选:secondaryPreferred 提升读吞吐但有延迟;强一致用 primary

总结

MongoDB 运维核心要点:

  • 先用副本集——3 节点起步,写量到瓶颈再加分片
  • shard key 决定集群命运——高基数 + 低频率 + 查询靶向,选错全盘皆输
  • 文件系统快照 > mongodump——生产备份优先物理快照保证一致性
  • balancer 设窗口——迁移消耗资源,生产高峰期必须关
  • 监控复制延迟和缓存命中率——这两个指标最能反映集群健康度

掌握副本集搭建、分片集群部署、shard key 设计、备份恢复这四块,MongoDB 生产运维就能覆盖绝大多数场景。

分享:

相关文章

评论区