Skip to content

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秒前       │
│ 龙岗仲裁节点    │ ARBITERN/AN/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 复制集:一句话总结

分片集群:书太多,一个图书馆装不下,于是分成多个图书馆,每个放不同的书 复制集:图书馆太重要,不能停业,于是建几个一模一样的备份馆,随时能顶上

高级架构 = 分片集群(解决"装不下") + 复制集(解决"停不了")

上次更新于: