第十章:高可用与故障转移

最后更新: 2024-01-01 作者: MongoDB Team
页面目录

第十章:高可用与故障转移

MongoDB 高可用架构设计与故障处理

10.1 高可用概述

┌─────────────────────────────────────────────────────────────────┐
│                      高可用架构层级                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    应用层高可用                              ││
│  │  - 多个应用实例                                              ││
│  │  - 负载均衡                                                  ││
│  └─────────────────────────────────────────────────────────────┘│
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    路由层高可用 (mongos)                     ││
│  │  - 多个 mongos 实例                                          ││
│  │  - 客户端连接池                                               ││
│  └─────────────────────────────────────────────────────────────┘│
│                              ↓                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    数据层高可用                              ││
│  │  - Config Server 副本集                                     ││
│  │  - Shard 副本集                                              ││
│  │  - 自动故障转移                                              ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

10.2 多副本集架构

跨数据中心部署

// 跨地域副本集配置
rs.initiate({
  _id: "geo-replica-set",
  members: [
    // 区域 A (主数据中心)
    {
      _id: 0,
      host: "node-a1:27017",
      priority: 3,
      tags: { region: "region-a", dc: "dc1" }
    },
    {
      _id: 1,
      host: "node-a2:27017",
      priority: 2,
      tags: { region: "region-a", dc: "dc1" }
    },
    // 区域 B (灾备中心)
    {
      _id: 2,
      host: "node-b1:27017",
      priority: 1,
      tags: { region: "region-b", dc: "dc2" }
    },
    {
      _id: 3,
      host: "node-b2:27017",
      priority: 1,
      tags: { region: "region-b", dc: "dc2" }
    },
    // 仲裁节点
    {
      _id: 4,
      host: "node-arbiter:27017",
      arbiterOnly: true,
      tags: { dc: "dc1" }
    }
  ]
})

标签感知写入

// 确保写入到主数据中心
db.orders.insertOne(
  { data: "critical" },
  { writeConcern: { w: "region-a" } }
)

// 确保数据写入多个区域
db.orders.insertOne(
  { data: "important" },
  { writeConcern: { w: { region: { $in: ["region-a", "region-b"] } } } }
)

10.3 故障检测与恢复

心跳配置

// 配置心跳超时
cfg = rs.conf()
cfg.settings = {
  heartbeatTimeoutSecs: 10,  // 默认 10 秒
  heartbeatIntervalMillis: 2000  // 默认 2 秒
}
rs.reconfig(cfg)

// 查看心跳状态
rs.status().members.forEach(m => {
  print(`Member: ${m.name}`)
  print(`  State: ${m.stateStr}`)
  print(`  Health: ${m.health}`)
  print(`  Last Heartbeat: ${m.lastHeartbeat}`)
})

故障转移流程

┌─────────────────────────────────────────────────────────────────┐
│                      主节点故障转移流程                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 心跳超时                                                     │
│     Secondary 检测到 Primary 心跳超时                            │
│                                                                 │
│  2. 发起选举                                                     │
│    符合条件的 Secondary 发起选举请求                              │
│                                                                 │
│  3. 投票过程                                                     │
│     - 候选节点请求投票                                           │
│     - 其他节点投票                                               │
│     - 获得多数票的节点成为新 Primary                              │
│                                                                 │
│  4. 故障转移                                                     │
│     - 新 Primary 处理写入                                        │
│     - 客户端自动重连                                             │
│                                                                 │
│  选举条件:                                                       │
│  - 候选节点数据最新                                              │
│  - 优先级足够高                                                  │
│  - 能够获得多数投票                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

选举配置

// 配置选举参数
cfg = rs.conf()
cfg.settings = {
  electionTimeoutMillis: 15000,  // 选举超时
  heartbeatTimeoutSecs: 10,
  catchUpTimeoutMillis: -1,     // -1 表示无限等待
  catchUpTimeoutMillis: 30000   // 或设置具体时间
}
rs.reconfig(cfg)

// 强制选举
rs.freeze(0)  // 解除冻结
rs.stepDown(60)  // 主节点降级,等待 60 秒后可能重新成为主节点

10.4 变更流(Change Streams)

基本使用

// 监视集合变更
const cursor = db.orders.watch()

// 监视副本集变更
const cursor = db.orders.watch([], { fullDocument: "updateLookup" })

// 监视数据库变更
const cursor = db.watch({ $db: "mydb" })

// 监视集群变更(需要 MongoDB 5.0+)
const cursor = db.adminCommand({ watch: 1 })

变更流配置

// 只监视插入操作
db.orders.watch([], {
  fullDocument: "updateLookup",
  fullDocumentBeforeChange: "whenAvailable"
})

// 监视特定类型的操作
db.orders.watch([
  {
    $match: {
      operationType: {
        $in: ["insert", "update", "replace", "delete"]
      }
    }
  }
])

// 使用管道过滤
db.orders.watch([
  {
    $match: {
      "fullDocument.status": "completed"
    }
  }
])

// 恢复令牌
const resumeToken = cursor.getResumeToken()
const newCursor = db.orders.watch([], { resumeAfter: resumeToken })

变更流应用

// Node.js 应用示例
const { MongoClient } = require('mongodb');

async function watchChanges() {
  const client = new MongoClient('mongodb://localhost:27017');
  await client.connect();
  
  const collection = client.db('mydb').collection('orders');
  
  const changeStream = collection.watch();
  
  changeStream.on('change', (change) => {
    console.log('变更类型:', change.operationType);
    console.log('文档键:', change.documentKey);
    console.log('完整文档:', change.fullDocument);
    
    // 根据变更类型处理
    switch (change.operationType) {
      case 'insert':
        handleInsert(change.fullDocument);
        break;
      case 'update':
        handleUpdate(change.fullDocument);
        break;
      case 'delete':
        handleDelete(change.documentKey);
        break;
    }
  });
  
  changeStream.on('error', (err) => {
    console.error('Change stream error:', err);
  });
}

// 关闭连接
// changeStream.close();

10.5 自动重连

MongoDB Driver 配置

// Node.js Driver 配置
const client = new MongoClient('mongodb://mongos1:27017,mongos2:27017', {
  replicaSet: 'rs0',
  retryWrites: true,
  retryReads: true,
  maxPoolSize: 100,
  minPoolSize: 10,
  serverSelectionTimeoutMS: 30000,
  connectTimeoutMS: 10000,
  socketTimeoutMS: 45000
})

// Python Driver 配置
from pymongo import MongoClient
client = MongoClient(
  'mongodb://mongos1:27017,mongos2:27017',
  replicaSet='rs0',
  retryWrites=True,
  retryReads=True,
  serverSelectionTimeoutMS=30000,
  connectTimeoutMS=10000,
  socketTimeoutMS=45000
)

// Java Driver 配置
MongoClientURI uri = new MongoClientURI(
  "mongodb://mongos1:27017,mongos2:27017/?replicaSet=rs0"
);
MongoClient client = new MongoClient(uri);

重试配置

// Retryable Writes (MongoDB 3.6+)
// 自动重试的单文档写入操作
// - insertOne, updateOne, deleteOne, replaceOne

// 启用/禁用重试
db.adminCommand({ setParameter: 1, supportsRetryableWrites: true })

// 自定义重试参数
db.adminCommand({
  setParameter: 1,
  transactionLifetimeLimitSeconds: 300
})

// Retryable Reads (MongoDB 4.2+)
// 自动重试的读取操作
// - find, aggregate, findOne, distinct

10.6 故障恢复

数据恢复流程

// 1. 检查副本集状态
rs.status()

// 2. 恢复故障节点
// Docker 场景
docker restart mongo1

// 物理机场景
sudo systemctl start mongod

// 3. 等待同步完成
rs.status()  // 查看 stateStr

// 4. 如果同步失败,手动修复
// 查看同步状态
db.adminCommand({ replSetGetStatus: 1 })

// 强制重新同步
db.adminCommand({ resync: "mongo2" })

oplog 恢复

// 1. 查看 oplog 状态
use local
db.oplog.rs.stats()

// 2. 查找最近的操作
db.oplog.rs.find().sort({ ts: -1 }).limit(1)

// 3. 应用 oplog 到新节点
// 在新节点上
db.adminCommand({
  applyOps: db.getSiblingDB("local").oplog.rs.find().toArray()
})

// 4. 检查同步延迟
db.adminCommand({ replSetGetStatus: 1 }).members.forEach(m => {
  print(m.name + ": " + m.stateStr)
  if (m.optime) {
    print("  optime: " + m.optimeDate)
  }
})

灾备切换

// 演练灾备切换
// 1. 停止主节点
docker stop mongo1

// 2. 观察副本集状态
rs.status()

// 3. 验证应用连接
// 检查应用是否连接到新主节点

// 4. 恢复原主节点
docker start mongo1

// 5. 观察同步
rs.status()

10.7 监控告警

关键监控指标

指标 说明 阈值建议
主从延迟 复制延迟时间 > 10s 告警
节点健康 心跳状态 非 healthy 告警
磁盘使用 磁盘空间使用率 > 80% 告警
连接数 当前连接数 > 80% 最大值
操作延迟 平均操作时间 > 100ms 告警
队列长度 等待操作数 > 100 告警

监控脚本

// health-check.js
function checkReplicaSetHealth() {
  const status = rs.status();
  const health = {
    ok: status.ok === 1,
    primary: null,
    secondaries: [],
    issues: []
  };
  
  status.members.forEach(m => {
    if (m.stateStr === 'PRIMARY') {
      health.primary = m.name;
    } else if (m.stateStr === 'SECONDARY') {
      health.secondaries.push(m.name);
    }
    
    if (m.health !== 1) {
      health.issues.push(`${m.name} is unhealthy`);
    }
    
    if (m.stateStr !== 'ARBITER' && m.optimeDate) {
      const lag = new Date() - m.optimeDate;
      if (lag > 10000) {
        health.issues.push(`${m.name} lag: ${lag}ms`);
      }
    }
  });
  
  if (!health.primary) {
    health.issues.push('No primary detected');
  }
  
  return health;
}

printjson(checkReplicaSetHealth());

💡 实践提示

  1. 副本集成员数:使用奇数成员(3、5、7)避免脑裂
  2. 监控复制延迟:关注主从数据同步状态
  3. 合理设置优先级:确保高配置节点优先成为主节点
  4. 测试故障转移:定期进行故障转移演练
  5. 使用变更流:实现实时数据同步和处理

📚 继续学习