MongoDB 复制集


复制集机制及原理

复制集的作用

  • MongoDB 复制集的主要意义在于实现服务高可用,它的现实依赖于两个方面的功能:
    • 数据写入时将数据迅速复制到另一个独立的节点上
    • 在接受写入的节点发生故障时自动选举出一个新的替代节点
  • 在实现高可用的同时,复制集实现了其他几个附加作用:
    • 数据分发:将数据从一个区域复制到另一个区域,减少另一个区域的读延迟
    • 读写分离:不同类型的压力分别在不同的节点上执行
    • 异地容灾:在数据中心故障的时候快速切换到异地

典型复制集结构

一个典型的复制集由 3 个以上具有投票权的节点组成,包括:

  • 一个主节点(PRIMARY):接受写入操作和选举时投票
  • 两个(或多个)从节点(SECONDARY):复制主节点上的新数据和选举时投票
  • 不推荐使用 Arbiter(投票节点)

数据是如何复制的?

  • 当一个修改操作,无论是插入、更新或删除,到达主节点时,它对数据的操作将被记录下来(经过一些必要的转换),这些记录称为 oplog

  • 从节点通过在主节点上打开一个 tailable 游标不断获取新进入主节点的 oplog,并在自己的数据上回放,以此保持跟主节点的数据一致。

通过选举完成故障恢复

  • 具有投票权的节点之前两两互相发送心跳
  • 当 5 次心跳未收到时判断为节点失联
  • 如果失联的是主节点,从节点会发起选举,选出新的主节点
  • 如果失联的是从节点则不会产生新的选举
  • 选举基于RAFT一致性算法实现,选举成功的必要条件是大多数投票节点存活
  • 复制集中最多可以有 50 个节点,但具有投票权的节点最多 7 个

影响选举的因素

  • 整个集群中必须有大多数节点存活
  • 被选举为主节点的节点必须满足:
    • 能够与多数节点建立连接
    • 具有较新的 oplog
    • 具有较高的优先级(如果有配置)

常见选项

复制集在部署的时候有以下常见的选项可以进行配置:

  • 是否具有投票权(v 参数):有则参与投票
  • 优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为0的节点无法成为主节点
  • 隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票权,但优先级必须为0
  • 延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差

复制集注意事项

  • 关于硬件
    • 因为正常的复制集节点都有可能成为主节点,它们的地位是一样的,因此硬件配置上必须一致
    • 为了保证节点不会同时宕机,各节点使用的硬件必须具有独立性
  • 关于软件
    • 复制集各节点软件版本必须一致,以避免出现不可预知的问题
  • 增加节点不会增加系统写性能

搭建 MongoDB 复制集

我们通过在一台机器上运行 3 个实例来搭建一个最简单的复制集,并对复制集运行参数做一些常规调整。

准备工作

  • 安装最新的 MongoDB 版本
  • Windows 系统请事先配置好 MongoDB 可执行文件的环境变量
  • Linux 和 Mac 系统请配置 PATH 变量
  • 确保有 10GB 以上的硬盘空间

创建数据目录

MongoDB 启动时将使用一个数据目录存放所有的数据文件。我们将为3个复制集节点创建各自的数据目录。

Linux/MacOS:

mkdir -p /data/db{1,2,3}

Windows:

md c:\data\db1
md c:\data\db2
md c:\data\db3

准备配置文件

复制集的每个mongod 进程应该位于不同的服务器。我们现在在一台机器上运行 3 个进程,因此要为它们各自配置:

  • 不同的端口。示例中将使用 28017、28018、28019。

  • 不同的数据目录。示例中将使用:

    /data/db1 或 md c:\data\db1
    /data/db2 或 md c:\data\db2
    /data/db3 或 md c:\data\db3
  • 不同日志文件路径。示例中将使用:

    /data/db1/mongod.log 或 c:\data\db1\mongod.log
    /data/db2/mongod.log 或 c:\data\db1\mongod.2og
    /data/db3/mongod.log 或 c:\data\db1\mongod.3og

    这些配置文件标准格式如下,请修改必要的参数完成3个实例各自的配置文件:

    Linux/MacOS:

    # /data/db1/mongod.conf
    systemLog:
      destination: file
      path: /data/db1/mongod.log   # 日志文件路径
      logAppend: true
    storage:
      dbPath: /data/db1    # 数据目录
    net:
      bindIp: 0.0.0.0
      port: 28017   # 端口
    replication:
      replSetName: rs0
    processManagement:
      fork: true

    Windows:

    # c:\data1\mongod.conf
    systemLog:
      destination: file
      path: c:\data\db1\mongod.log   # 日志文件路径
      logAppend: true
    storage:
      dbPath: c:\data\db1    # 数据目录
    net:
      bindIp: 0.0.0.0
      port: 28017   # 端口
    replication:
      replSetName: rs0

执行进程

Linux/Mac:

mongod -f /data/db1/mongod.conf
mongod -f /data/db2/mongod.conf
mongod -f /data/db3/mongod.conf

注意:如果启用了SELinux,可能阻止上述进程启动。简单起见请关闭SELinux。

Windows:

mongod -f c:\data\db1\mongod.conf
mongod -f c:\data\db2\mongod.conf
mongod -f c:\data\db3\mongod.conf

因为Windows不支持fork,以上命令需要在3个不同的窗口执行,执行后不可关闭窗口否则进程将直接结束。

确认 mongod 进程已经在运行:

$ ps -elf | grep mongod
  501 36106     1        4   0  31  0  6625980  37540 -      S                   0 ??         0:00.78 mongod -f /data/  5:30下午
  501 36140     1        4   0  31  0  6896316  36560 -      S                   0 ??         0:00.77 mongod -f /data/  5:30下午
  501 36153     1        4   0  31  0  6608572  33468 -      S                   0 ??         0:00.79 mongod -f /data/  5:30下午

配置复制集

进入mongo shell,习惯把第一个节点做为主节点:

mongo --port 28017

创建复制集,默认是rs0:SECONDARY按回车变成 rs0:PRIMARY

rs0:PRIMARY> rs.initiate({
    _id: "rs0",
    members: [{
        _id: 0,
        host: "localhost:28017"
    },{
        _id: 1,
        host: "localhost:28018"
    },{
        _id: 2,
        host: "localhost:28019"
    }]
})

查看复制集状态:

rs0:PRIMARY> rs.status()
{
        "set" : "rs0",
        "date" : ISODate("2022-11-18T09:40:36.308Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "syncingTo" : "",
        "syncSourceHost" : "",
        "syncSourceId" : -1,
        "heartbeatIntervalMillis" : NumberLong(2000),
        "majorityVoteCount" : 2,
        "writeMajorityCount" : 2,
        "optimes" : {
                "lastCommittedOpTime" : {
                        "ts" : Timestamp(1668764430, 1),
                        "t" : NumberLong(1)
                },
                "lastCommittedWallTime" : ISODate("2022-11-18T09:40:30.507Z"),
                "readConcernMajorityOpTime" : {
                        "ts" : Timestamp(1668764430, 1),
                        "t" : NumberLong(1)
                },
                "readConcernMajorityWallTime" : ISODate("2022-11-18T09:40:30.507Z"),
                "appliedOpTime" : {
                        "ts" : Timestamp(1668764430, 1),
                        "t" : NumberLong(1)
                },
                "durableOpTime" : {
                        "ts" : Timestamp(1668764430, 1),
                        "t" : NumberLong(1)
                },
                "lastAppliedWallTime" : ISODate("2022-11-18T09:40:30.507Z"),
                "lastDurableWallTime" : ISODate("2022-11-18T09:40:30.507Z")
        },
        "lastStableRecoveryTimestamp" : Timestamp(1668764390, 1),
        "lastStableCheckpointTimestamp" : Timestamp(1668764390, 1),
        "electionCandidateMetrics" : {
                "lastElectionReason" : "electionTimeout",
                "lastElectionDate" : ISODate("2022-11-18T09:36:50.122Z"),
                "electionTerm" : NumberLong(1),
                "lastCommittedOpTimeAtElection" : {
                        "ts" : Timestamp(0, 0),
                        "t" : NumberLong(-1)
                },
                "lastSeenOpTimeAtElection" : {
                        "ts" : Timestamp(1668764199, 1),
                        "t" : NumberLong(-1)
                },
                "numVotesNeeded" : 2,
                "priorityAtElection" : 1,
                "electionTimeoutMillis" : NumberLong(10000),
                "numCatchUpOps" : NumberLong(0),
                "newTermStartDate" : ISODate("2022-11-18T09:36:50.414Z"),
                "wMajorityWriteAvailabilityDate" : ISODate("2022-11-18T09:36:51.806Z")
        },
        "members" : [
                {
                        "_id" : 0,
                        "name" : "localhost:28017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 635,
                        "optime" : {
                                "ts" : Timestamp(1668764430, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2022-11-18T09:40:30Z"),
                        "syncingTo" : "",
                        "syncSourceHost" : "",
                        "syncSourceId" : -1,
                        "infoMessage" : "",
                        "electionTime" : Timestamp(1668764210, 1),
                        "electionDate" : ISODate("2022-11-18T09:36:50Z"),
                        "configVersion" : 1,
                        "self" : true,
                        "lastHeartbeatMessage" : ""
                },
                {
                        "_id" : 1,
                        "name" : "localhost:28018",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 236,
                        "optime" : {
                                "ts" : Timestamp(1668764430, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1668764430, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2022-11-18T09:40:30Z"),
                        "optimeDurableDate" : ISODate("2022-11-18T09:40:30Z"),
                        "lastHeartbeat" : ISODate("2022-11-18T09:40:34.599Z"),
                        "lastHeartbeatRecv" : ISODate("2022-11-18T09:40:35.637Z"),
                        "pingMs" : NumberLong(0),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "localhost:28017",
                        "syncSourceHost" : "localhost:28017",
                        "syncSourceId" : 0,
                        "infoMessage" : "",
                        "configVersion" : 1
                },
                {
                        "_id" : 2,
                        "name" : "localhost:28019",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 236,
                        "optime" : {
                                "ts" : Timestamp(1668764430, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1668764430, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2022-11-18T09:40:30Z"),
                        "optimeDurableDate" : ISODate("2022-11-18T09:40:30Z"),
                        "lastHeartbeat" : ISODate("2022-11-18T09:40:34.599Z"),
                        "lastHeartbeatRecv" : ISODate("2022-11-18T09:40:35.674Z"),
                        "pingMs" : NumberLong(0),
                        "lastHeartbeatMessage" : "",
                        "syncingTo" : "localhost:28017",
                        "syncSourceHost" : "localhost:28017",
                        "syncSourceId" : 0,
                        "infoMessage" : "",
                        "configVersion" : 1
                }
        ],
        "ok" : 1,
        "$clusterTime" : {
                "clusterTime" : Timestamp(1668764430, 1),
                "signature" : {
                        "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                        "keyId" : NumberLong(0)
                }
        },
        "operationTime" : Timestamp(1668764430, 1)
}

验证:

  • 重新开一个 shell 窗口,我们在 MongoDB 从节点进行读取:

    $ mongo localhost:28018
    MongoDB shell version v4.2.23
    connecting to: mongodb://localhost:28018/test?compressors=disabled&gssapiServiceName=mongodb
    ......
    ---
    
    rs0:SECONDARY> db.test.find()
    Error: error: {
            "operationTime" : Timestamp(1668765000, 1),
            "ok" : 0,
            "errmsg" : "not master and slaveOk=false", //告诉我们这不是主节点,并且不要在从节点是读取,执行 rs.slaveOk()
            "code" : 13435,
            "codeName" : "NotPrimaryNoSecondaryOk",
            "$clusterTime" : {
                    "clusterTime" : Timestamp(1668765000, 1),
                    "signature" : {
                            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
                            "keyId" : NumberLong(0)
                    }
            }
    }
    rs0:SECONDARY> rs.slaveOk()
    rs0:SECONDARY> db.test.find() // 这时就不会报错了
  • MongoDB 主节点进入写入:

    rs0:PRIMARY> db.test.insert({a:1})
    WriteResult({ "nInserted" : 1 })
  • 回到从节点读取:

    rs0:SECONDARY> db.test.find()
    { "_id" : ObjectId("637756750e386c972af7e689"), "a" : 1 }  // 可以看到已经能免在从节点上读取主节点上写入的数据

简单使用:

rs0:PRIMARY> 
rs0:PRIMARY> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
test    0.000GB
rs0:PRIMARY> use local
switched to db local
rs0:PRIMARY> show tables
oplog.rs
replset.election
replset.minvalid
replset.oplogTruncateAfterPoint
startup_log
system.replset
system.rollback.id

调整复制集配置

var conf = rs.conf()
// 将0号节点的优先级调整为10
conf.members[0].priority = 10;
// 将1号节点调整为hidden节点
conf.members[1].hidden = true;
// hidden节点必须配置{priority: 0}
conf.members[1].priority = 0;
// 应用以上调整
rs.reconfig(conf);

参考


文章作者: 张权
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 张权 !
评论
  目录