MongoDB复制集:图书馆的“备份馆”机制
上期回顾 vs 本期预告
上期《分片集群》讲的是:
如何把1000万册书分到不同分馆(福田馆放文学、宝安馆放科技...)
本期《复制集》讲的是:
如何为每个分馆建立备份馆(福田馆有备份、南山馆有备份...)
引言:一场真实的"故障演练"
javascript
// 这是深圳图书馆监控中心的一天
主图书馆电力故障:❌
读者借阅服务中断:❌(读者毫无察觉)
图书管理员手忙脚乱:❌(系统自动处理)
读者投诉电话:0
// 秘密就在于...
复制集:✅✅✅一、复制集 = 图书馆的"影分身之术"
现实场景对比
🏢 深圳图书馆总部(福田主馆) ← 你常去的那个
↓
📚 南山备份馆(副本1) ← 读者不知道的存在
📚 宝安备份馆(副本2) ← 另一个秘密基地
📚 龙岗备份馆(副本3) ← 最后的安全网MongoDB 中的对应关系
javascript
// 3个节点的复制集配置
const replicaSet = {
primary: "futian-main:27017", // 主节点 = 福田主馆
secondary1: "nanshan-backup:27017", // 从节点1 = 南山备份馆
secondary2: "baoan-backup:27017", // 从节点2 = 宝安备份馆
arbiter: "longgang-arbiter:27017" // 仲裁节点 = 监督委员会
};二、复制集如何工作?(借书的完整流程)
核心概念:oplog(借阅登记簿)
javascript
// oplog = 图书馆的"借阅登记簿"
// 记录了所有借书还书操作
// 备份馆通过复制这个登记簿来同步数据
// 查看oplog大小和状态
db.oplog.rs.stats()
// 输出:oplog大小决定能回溯多久的数据
// 比喻:借阅登记簿的厚度决定能查到多久前的记录场景:读者借阅《三体》
javascript
// 步骤1:读者在自助机借书(写操作)
读者 → 福田主馆:"借《三体》一本"
福田主馆 → 数据库:db.books.update(
{title: "三体"},
{$inc: {stock: -1}}
)
// 步骤2:主馆同步到备份馆(oplog复制)
福田主馆 → oplog:"记录:读者张三借《三体》"
南山备份馆 ← oplog:"同步:读者张三借《三体》"
宝安备份馆 ← oplog:"同步:读者张三借《三体》"
龙岗备份馆 ← oplog:"同步:读者张三借《三体》"
// 步骤3:所有备份馆更新库存
// 现在4个馆的《三体》库存都-1
// 步骤4:读者收到确认
数据库 → 读者:"借阅成功,当前库存:3本"实际配置代码
javascript
// 初始化一个3节点复制集
rs.initiate({
_id: "sz_library", // 复制集名称
members: [
{_id: 0, host: "futian-main:27017", priority: 3}, // 福田主馆,优先级高
{_id: 1, host: "nanshan-backup:27017", priority: 2}, // 南山备份馆
{_id: 2, host: "baoan-backup:27017", priority: 1}, // 宝安备份馆
{_id: 3, host: "longgang-arbiter:27017", arbiterOnly: true} // 龙岗仲裁节点
],
settings: {
heartbeatIntervalMillis: 2000, // 心跳间隔2秒
electionTimeoutMillis: 10000 // 选举超时10秒
}
});
// 查看复制集状态
rs.status()
// 输出:4个节点,1个主节点,2个从节点,1个仲裁节点三、当主图书馆"停电"时(故障自动转移)
惊心动魄的60秒
javascript
// 更准确的时间线:
T+0s: 福田主馆突然停电!⚡
T+2s: 南山备份馆发送心跳:"主馆,你还好吗?"
T+4s: 宝安备份馆发送心跳:"主馆,收到请回答!"
T+10s: 两次心跳无响应,判定主馆失联
T+12s: 开始选举新主馆:"谁当新馆长?"
T+15s: 南山备份馆获得2票(包括仲裁节点)
T+18s: 南山备份馆当选新主馆!👑
T+20s: 客户端驱动程序检测到主馆变更
T+22s: 所有借书请求自动转到南山备份馆
T+25s: 读者继续借书,只有3秒卡顿感MongoDB 自动故障转移配置
javascript
// 正确的心跳和选举配置
cfg = rs.conf();
cfg.settings = {
// 心跳设置:备份馆之间每2秒"打电话"
heartbeatIntervalMillis: 2000,
// 超时设置:10秒不接电话就判定失联
heartbeatTimeoutSecs: 10,
// 选举超时:最多花10秒选新馆长
electionTimeoutMillis: 10000,
// 允许链式复制:A同步给B,B同步给C
chainingAllowed: true
};
rs.reconfig(cfg);四、不只是备份(复制集的5个高级功能)
功能1:读写分离 - 分流读者压力
javascript
// 配置从节点可读
db.books.find().readPref("secondaryPreferred");
// 实际效果:
读者查询书籍信息 → 南山备份馆或宝安备份馆(从节点)
读者借书还书操作 → 福田主馆(主节点)
管理员统计报表 → 从节点(不干扰主节点)
系统备份任务 → 从节点(夜间执行)功能2:延迟节点 - 防止误操作
javascript
// 创建一个延迟1小时的"时间胶囊"备份馆
rs.add({
host: "sz-lib-delay:27017",
priority: 0, // 不能成为主节点
votes: 0, // 不参与选举投票
hidden: true, // 对客户端隐藏
slaveDelay: 3600, // 延迟1小时同步
tags: { purpose: "delayed_backup" }
});
// 使用场景:
早上9点:管理员误删了所有科幻小说 ❌
早上10点:从延迟节点恢复数据 ✅
// 延迟节点的数据是1小时前的,正好可以恢复功能3:隐藏节点 - 数据分析专用
javascript
// 创建隐藏节点,专门做数据分析
rs.add({
host: "sz-lib-analytics:27017",
priority: 0, // 不参与选举
votes: 0, // 没有投票权
hidden: true, // 对读者隐藏
tags: { role: "analytics" }
});
// 只用于后台数据分析任务
db.getMongo().setReadPref("secondary");
db.borrow_records.aggregate([
{$match: {date: {$gte: "2024-01-01"}}},
{$group: {_id: "$book_type", total: {$sum: 1}}}
]);五、复制集监控(图书馆健康检查)
健康检查面板
javascript
// 查看复制集状态
rs.status()
// 查看复制延迟
rs.printSecondaryReplicationInfo()
// 输出示例:
图书馆集群状态:✅ 健康
┌─────────────────┬────────────┬─────────┬─────────────┐
│ 分馆名称 │ 状态 │ 延迟 │ 最后同步时间 │
├─────────────────┼────────────┼─────────┼─────────────┤
│ 福田主馆 │ PRIMARY │ 0秒 │ 刚刚 │
│ 南山备份馆 │ SECONDARY │ 0.1秒 │ 2秒前 │
│ 宝安备份馆 │ SECONDARY │ 0.3秒 │ 5秒前 │
│ 龙岗仲裁节点 │ ARBITER │ N/A │ N/A │
└─────────────────┴────────────┴─────────┴─────────────┘
// 关键指标解读:
// 延迟 < 1秒:✅ 优秀
// 延迟 1-5秒:⚠️ 警告
// 延迟 > 5秒:❌ 需要检查六、实际部署方案(深圳图书馆的架构)
生产环境架构图
yaml
# docker-compose.yml
version: '3.8'
services:
# 主节点 - 福田主馆
mongodb-futian-primary:
image: mongo:6.0
container_name: futian-primary
command: mongod --replSet sz_library --bind_ip_all
ports: ["27017:27017"]
volumes:
- futian_data:/data/db
- ./keyfile:/keyfile # 认证密钥
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
# 从节点1 - 南山备份馆
mongodb-nanshan-secondary:
image: mongo:6.0
container_name: nanshan-secondary
command: mongod --replSet sz_library --bind_ip_all
ports: ["27018:27017"]
volumes:
- nanshan_data:/data/db
- ./keyfile:/keyfile
depends_on:
- mongodb-futian-primary
# 从节点2 - 宝安备份馆
mongodb-baoan-secondary:
image: mongo:6.0
container_name: baoan-secondary
command: mongod --replSet sz_library --bind_ip_all
ports: ["27019:27017"]
volumes:
- baoan_data:/data/db
- ./keyfile:/keyfile
depends_on:
- mongodb-futian-primary
# 仲裁节点 - 龙岗仲裁节点
mongodb-longgang-arbiter:
image: mongo:6.0
container_name: longgang-arbiter
command: mongod --replSet sz_library --bind_ip_all
ports: ["27020:27017"]
volumes:
- ./keyfile:/keyfile
depends_on:
- mongodb-futian-primary
volumes:
futian_data:
nanshan_data:
baoan_data:初始化脚本
bash
#!/bin/bash
# init-replica-set.sh
# 等待MongoDB启动
sleep 10
# 连接到主节点初始化复制集
mongosh --host futian-primary:27017 -u admin -p ${MONGO_PASSWORD} <<EOF
rs.initiate({
_id: "sz_library",
members: [
{_id: 0, host: "futian-primary:27017", priority: 3},
{_id: 1, host: "nanshan-secondary:27017", priority: 2},
{_id: 2, host: "baoan-secondary:27017", priority: 1},
{_id: 3, host: "longgang-arbiter:27017", arbiterOnly: true}
]
});
EOF七、常见问题与解决方案
问题1:备份馆同步太慢(oplog太小)
javascript
// 原因:oplog(借阅登记簿)太小,记不下太多操作
// 检查当前oplog大小
db.oplog.rs.stats().maxSize // 单位:字节
// 扩大oplog(需要重启)
// 1. 修改配置文件
// storage:
// oplogSizeMB: 20480 # 20GB
// 2. 或者运行时调整(需要主节点有足够空间)
db.adminCommand({replSetResizeOplog: 1, size: 20480})
// 比喻:给借阅登记簿换更厚的本子问题2:选举时间太长(网络问题)
javascript
// 原因:备份馆之间网络延迟高
// 查看当前网络延迟
rs.status().members.forEach(m => {
print(`${m.name}: ${m.lastHeartbeatRecv ? m.lastHeartbeatRecv.getTime() - m.lastHeartbeatSend.getTime() : 'N/A'}ms`);
});
// 调整选举超时时间(网络差的环境可以调大)
cfg = rs.conf();
cfg.settings.electionTimeoutMillis = 15000; // 15秒超时
rs.reconfig(cfg);
// 比喻:给馆长选举更长的讨论时间问题3:脑裂问题(Split Brain)
javascript
// 原因:网络分区导致两个备份馆都认为自己是主馆
// 解决方案:使用奇数节点或仲裁节点
// 如果有4个数据节点(偶数),增加一个仲裁节点
rs.addArb("sz-lib-arbiter:27020");
// 或者调整节点投票权
cfg = rs.conf();
cfg.members[3].votes = 0; // 让一个节点不投票
cfg.members[3].priority = 0; // 也不能成为主节点
rs.reconfig(cfg);
// 比喻:增加一个独立监督员,避免2:2平局八、实践总结与检查清单
图书馆复制集黄金法则
javascript
const replicaSetBestPractices = {
rule1: "至少3个节点(1主2从或1主1从1仲裁)",
rule2: "跨机房部署(福田、南山、宝安各一个)",
rule3: "定期演练故障切换(每季度一次)",
rule4: "监控同步延迟(<5秒为健康)",
rule5: "设置合理的oplog大小(至少容纳24小时操作)",
rule6: "启用认证和加密(保护借阅记录)",
rule7: "定期备份(即使有复制集也要备份)"
};部署检查清单
javascript
// 初始化后的检查
const checklist = {
1: "主节点选举成功": () => rs.isMaster().ismaster,
2: "从节点同步正常": () => rs.status().members.every(m =>
m.stateStr === "PRIMARY" || m.stateStr === "SECONDARY" || m.stateStr === "ARBITER"),
3: "复制延迟 < 5秒": () => {
const info = db.printSecondaryReplicationInfo();
return info && info.lag && info.lag < 5;
},
4: "oplog大小合适": () => {
const oplogSize = db.oplog.rs.stats().maxSize;
return oplogSize >= 10 * 1024 * 1024 * 1024; // 至少10GB
},
5: "有监控告警": () => true, // 假设已配置监控
6: "定期备份配置": () => {
const backups = db.adminCommand({listBackups: 1});
return backups && backups.backups && backups.backups.length > 0;
}
};
// 运行检查
Object.entries(checklist).forEach(([item, check]) => {
try {
const result = check();
console.log(`${item}: ${result ? '✅' : '❌'}`);
} catch (e) {
console.log(`${item}: ❌ (错误: ${e.message})`);
}
});九、分片集群 vs 复制集:一句话总结
分片集群:书太多,一个图书馆装不下,于是分成多个图书馆,每个放不同的书 复制集:图书馆太重要,不能停业,于是建几个一模一样的备份馆,随时能顶上
高级架构 = 分片集群(解决"装不下") + 复制集(解决"停不了")