MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MongoDB副本集数据复制原理与实践

2022-06-186.5k 阅读

MongoDB副本集概述

MongoDB副本集是由一组MongoDB实例组成的集群,其中包含一个主节点(Primary)和多个从节点(Secondary)。副本集的主要目的是提供数据冗余、高可用性以及数据复制功能。在正常情况下,主节点负责处理所有的写操作,而从节点则从主节点复制数据,保持与主节点数据的同步。当主节点出现故障时,副本集会自动进行选举,从从节点中选出一个新的主节点,以确保系统的可用性。

数据复制原理

  1. ** oplog(操作日志)**
    • oplog是MongoDB副本集数据复制的核心机制。它是一个特殊的固定集合(capped collection),位于local数据库中。主节点会将所有的写操作记录到oplog中,这些操作以一种紧凑的格式存储,包含了操作的类型(如插入、更新、删除)、操作的目标集合以及操作所涉及的数据。
    • oplog的记录格式示例:
    {
        "ts" : Timestamp(1620000000, 1),
        "h" : NumberLong("12345678901234567890"),
        "v" : 2,
        "op" : "i",
        "ns" : "test.users",
        "o" : {
            "_id" : ObjectId("6096956c0c2c9f4d568b2f74"),
            "name" : "John Doe",
            "age" : 30
        }
    }
    
    • 其中,ts 是时间戳,用于标识操作的顺序;h 是操作的唯一标识符;v 是oplog的版本号;op 表示操作类型,“i” 代表插入,“u” 代表更新,“d” 代表删除;ns 是命名空间,指定操作作用的集合;o 是实际操作的数据。
  2. 复制过程
    • 初始化同步(Initial Sync):当一个新的从节点加入副本集时,它会执行初始化同步。从节点会找到一个同步源(通常是主节点),然后开始全量复制数据。从节点会创建一个临时的复制数据库,将同步源的数据复制过来。在复制数据的同时,从节点也会记录同步源的oplog位置。
    • 持续同步(Continuous Sync):初始化同步完成后,从节点会进入持续同步阶段。从节点会定期轮询主节点(或其他同步源)的oplog,获取自上次同步以来新的操作记录。从节点会按照oplog中的顺序应用这些操作,从而保持与主节点数据的一致性。从节点应用oplog的过程是单线程的,这意味着如果有大量的写操作,应用oplog可能会成为性能瓶颈。
    • 心跳机制(Heartbeat):副本集中的每个节点都会定期(默认每2秒)向其他节点发送心跳消息。这些心跳消息用于检测节点的健康状态,以及确认节点之间的连接。如果主节点在一段时间内(默认10秒)没有收到某个从节点的心跳消息,主节点会认为该从节点已经故障,并将其从副本集中移除。同样,如果从节点在一段时间内没有收到主节点的心跳消息,从节点会发起选举,选出一个新的主节点。

副本集的搭建与配置

  1. 环境准备
    • 为了搭建MongoDB副本集,我们需要至少三个MongoDB实例。这里我们假设使用三个节点,分别命名为node1、node2和node3。每个节点需要有独立的配置文件和数据目录。
    • 首先,下载并安装MongoDB。可以从MongoDB官方网站(https://www.mongodb.com/download-center/community)下载适合你操作系统的安装包。
    • 解压安装包后,在每个节点的工作目录下创建数据目录和日志目录。例如,在node1上创建 /data/mongodb/node1/data 作为数据目录,/data/mongodb/node1/logs 作为日志目录。
  2. 配置文件编写
    • node1的配置文件(mongod1.conf)
    systemLog:
        destination: file
        path: /data/mongodb/node1/logs/mongod.log
        logAppend: true
    storage:
        dbPath: /data/mongodb/node1/data
        journal:
            enabled: true
    net:
        bindIp: 127.0.0.1
        port: 27017
    replication:
        replSetName: myReplSet
    
    • node2的配置文件(mongod2.conf)
    systemLog:
        destination: file
        path: /data/mongodb/node2/logs/mongod.log
        logAppend: true
    storage:
        dbPath: /data/mongodb/node2/data
        journal:
            enabled: true
    net:
        bindIp: 127.0.0.1
        port: 27018
    replication:
        replSetName: myReplSet
    
    • node3的配置文件(mongod3.conf)
    systemLog:
        destination: file
        path: /data/mongodb/node3/logs/mongod.log
        logAppend: true
    storage:
        dbPath: /data/mongodb/node3/data
        journal:
            enabled: true
    net:
        bindIp: 127.0.0.1
        port: 27019
    replication:
        replSetName: myReplSet
    
    • 在上述配置文件中,systemLog 部分配置了日志相关的参数,storage 部分配置了数据存储相关的参数,net 部分配置了网络相关的参数,replication 部分指定了副本集的名称。注意,所有节点的副本集名称必须一致。
  3. 启动MongoDB实例
    • 在每个节点上,使用相应的配置文件启动MongoDB实例。
    • 在node1上执行:
    mongod -f /path/to/mongod1.conf
    
    • 在node2上执行:
    mongod -f /path/to/mongod2.conf
    
    • 在node3上执行:
    mongod -f /path/to/mongod3.conf
    
  4. 初始化副本集
    • 启动一个MongoDB shell,并连接到其中一个节点(例如node1):
    mongo --port 27017
    
    • 在MongoDB shell中,初始化副本集:
    rs.initiate({
        _id: "myReplSet",
        members: [
            { _id: 0, host: "127.0.0.1:27017" },
            { _id: 1, host: "127.0.0.1:27018" },
            { _id: 2, host: "127.0.0.1:27019" }
        ]
    })
    
    • 执行上述命令后,MongoDB会自动进行副本集的初始化,将当前节点(node1)设置为主节点,并将其他节点添加为从节点。可以使用 rs.status() 命令查看副本集的状态。

副本集的读写操作

  1. 写操作
    • 在副本集中,所有的写操作默认都由主节点处理。当客户端发起一个写操作时,主节点会将操作记录到oplog中,并将数据写入自身的数据集。然后,主节点会将oplog中的记录同步给从节点。
    • 例如,使用Node.js的MongoDB驱动进行写操作:
    const { MongoClient } = require('mongodb');
    const uri = "mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/?replicaSet=myReplSet";
    const client = new MongoClient(uri);
    
    async function insertDocument() {
        try {
            await client.connect();
            const database = client.db('test');
            const collection = database.collection('users');
            const document = { name: "Jane Smith", age: 25 };
            const result = await collection.insertOne(document);
            console.log(`Inserted document with _id: ${result.insertedId}`);
        } finally {
            await client.close();
        }
    }
    
    insertDocument();
    
    • 在上述代码中,我们通过连接到副本集的多个节点,并使用 insertOne 方法插入一个文档。主节点会处理这个插入操作,并将其同步给从节点。
  2. 读操作
    • 读操作可以在主节点或从节点上进行。默认情况下,MongoDB驱动会将读操作发送到主节点。但是,如果希望从从节点读取数据,可以在连接字符串中指定 readPreference 参数。
    • 例如,从从节点读取数据(使用Node.js的MongoDB驱动):
    const { MongoClient } = require('mongodb');
    const uri = "mongodb://127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/?replicaSet=myReplSet&readPreference=secondaryPreferred";
    const client = new MongoClient(uri);
    
    async function findDocuments() {
        try {
            await client.connect();
            const database = client.db('test');
            const collection = database.collection('users');
            const cursor = collection.find({});
            const results = await cursor.toArray();
            console.log(results);
        } finally {
            await client.close();
        }
    }
    
    findDocuments();
    
    • 在上述代码中,通过设置 readPreference=secondaryPreferred,表示优先从从节点读取数据。如果从节点不可用,才会从主节点读取。

副本集的选举机制

  1. 选举触发条件
    • 当主节点出现故障时,副本集会触发选举机制,从从节点中选出一个新的主节点。触发选举的条件主要有:
    • 心跳超时:如果从节点在一段时间内(默认10秒)没有收到主节点的心跳消息,从节点会认为主节点已经故障,并发起选举。
    • 网络分区:当副本集中的节点由于网络问题被分成多个部分时,可能会导致部分节点无法与主节点通信,从而触发选举。
  2. 选举过程
    • 优先级与投票权:每个节点在副本集中都有一个优先级(priority),取值范围是0到1000,默认值为1。优先级为0的节点不能成为主节点,只能作为从节点。优先级高的节点在选举中更有可能被选为主节点。每个节点还有一个投票权(votes),默认值为1。只有具有投票权的节点才能参与选举投票。
    • 选举流程:当一个从节点检测到主节点故障时,它会发起选举。首先,该节点会向其他具有投票权的节点发送选举请求。其他节点收到请求后,会根据发起请求节点的优先级、日志的完整性等因素来决定是否投票。如果发起请求的节点获得超过半数(副本集中具有投票权节点总数的一半以上)的投票,它就会成为新的主节点。如果没有节点获得足够的投票,选举会在一段时间后重新发起。
    • 例如,假设副本集有5个节点,其中3个节点具有投票权。当主节点故障时,一个从节点发起选举,如果它能获得2个(3的一半以上)具有投票权节点的投票,它就会成为新的主节点。

副本集的维护与优化

  1. 查看副本集状态
    • 使用 rs.status() 命令可以查看副本集的详细状态信息。该命令会返回一个包含副本集所有节点信息的文档,包括节点的角色(主节点或从节点)、同步状态、oplog的应用进度等。
    rs.status()
    
    • 示例输出:
    {
        "set" : "myReplSet",
        "date" : ISODate("2023 - 05 - 01T12:00:00Z"),
        "myState" : 1,
        "members" : [
            {
                "_id" : 0,
                "name" : "127.0.0.1:27017",
                "health" : 1,
                "state" : 1,
                "stateStr" : "PRIMARY",
                "uptime" : 120,
                "optime" : {
                    "ts" : Timestamp(1683024000, 1),
                    "t" : 1
                },
                "optimeDate" : ISODate("2023 - 05 - 01T12:00:00Z"),
                "syncingTo" : "",
                "syncSourceHost" : "",
                "syncSourceId" : -1,
                "infoMessage" : "",
                "electionTime" : Timestamp(1683023940, 1),
                "electionDate" : ISODate("2023 - 05 - 01T11:59:00Z"),
                "configVersion" : 1
            },
            {
                "_id" : 1,
                "name" : "127.0.0.1:27018",
                "health" : 1,
                "state" : 2,
                "stateStr" : "SECONDARY",
                "uptime" : 110,
                "optime" : {
                    "ts" : Timestamp(1683024000, 1),
                    "t" : 1
                },
                "optimeDate" : ISODate("2023 - 05 - 01T12:00:00Z"),
                "syncingTo" : "127.0.0.1:27017",
                "syncSourceHost" : "127.0.0.1:27017",
                "syncSourceId" : 0,
                "infoMessage" : "",
                "configVersion" : 1
            },
            {
                "_id" : 2,
                "name" : "127.0.0.1:27019",
                "health" : 1,
                "state" : 2,
                "stateStr" : "SECONDARY",
                "uptime" : 100,
                "optime" : {
                    "ts" : Timestamp(1683024000, 1),
                    "t" : 1
                },
                "optimeDate" : ISODate("2023 - 05 - 01T12:00:00Z"),
                "syncingTo" : "127.0.0.1:27017",
                "syncSourceHost" : "127.0.0.1:27017",
                "syncSourceId" : 0,
                "infoMessage" : "",
                "configVersion" : 1
            }
        ],
        "ok" : 1
    }
    
  2. 调整节点优先级
    • 可以使用 rs.reconfig() 命令来调整节点的优先级。例如,将节点2的优先级提高到2:
    var config = rs.conf();
    config.members[1].priority = 2;
    rs.reconfig(config);
    
    • 这样在下次选举时,节点2会比其他优先级为1的节点更有可能成为主节点。
  3. 优化复制性能
    • 增加从节点数量:通过增加从节点的数量,可以提高数据的冗余度和读取性能。但是,过多的从节点可能会增加主节点的同步负担,因为主节点需要将oplog同步给所有的从节点。
    • 优化网络配置:确保副本集中各节点之间的网络连接稳定且带宽充足。网络延迟和丢包会影响数据复制的性能。可以使用工具如 pingiperf 来测试网络连接的质量。
    • 合理设置oplog大小:oplog的大小会影响从节点与主节点之间的数据同步。如果oplog过小,可能会导致从节点在同步过程中丢失一些操作记录,从而出现数据不一致的情况。可以通过修改MongoDB配置文件中的 oplogSizeMB 参数来调整oplog的大小。例如,将oplog大小设置为1024MB:
    storage:
        dbPath: /data/mongodb/node1/data
        journal:
            enabled: true
        oplogSizeMB: 1024
    

故障处理与恢复

  1. 主节点故障
    • 当主节点出现故障时,副本集会自动进行选举,选出一个新的主节点。在选举过程中,副本集可能会出现短暂的不可用状态。一旦新的主节点选举完成,系统会恢复正常的读写操作。
    • 如果主节点故障是由于硬件或软件故障导致的,在修复故障后,可以将该节点重新加入副本集。首先,启动故障节点的MongoDB实例,然后使用 rs.add() 命令将其添加回副本集。例如:
    rs.add("127.0.0.1:27017")
    
    • 新加入的节点会自动进行初始化同步,从当前主节点复制数据,直到与主节点数据一致。
  2. 从节点故障
    • 当从节点出现故障时,主节点会将其从副本集中移除。从节点故障不会影响主节点的正常工作,但会减少数据的冗余度和读取性能。
    • 修复从节点故障后,同样可以使用 rs.add() 命令将其重新加入副本集。从节点重新加入后,会执行初始化同步,恢复与主节点的数据同步。

通过深入理解MongoDB副本集的数据复制原理,并进行合理的配置、操作和维护,可以确保MongoDB集群的高可用性、数据一致性以及良好的性能,满足各种应用场景的需求。在实际应用中,还需要根据具体的业务需求和系统规模,对副本集进行优化和调整,以达到最佳的运行效果。同时,对于可能出现的故障情况,要制定相应的应急预案,确保在最短的时间内恢复系统的正常运行。