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

MongoDB副本集初始化实战

2022-12-132.4k 阅读

环境准备

在开始初始化 MongoDB 副本集之前,我们需要准备好相应的环境。这包括安装 MongoDB 软件以及配置合适的服务器节点。

安装 MongoDB

  1. Linux 系统:以 Ubuntu 为例,首先需要导入 MongoDB 的官方 GPG 密钥,这是为了确保下载的软件包来源可靠。
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -

接着,根据你的 Ubuntu 版本,将 MongoDB 的软件源添加到系统中。比如对于 Ubuntu 20.04:

echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list

完成软件源添加后,更新 apt 软件包索引并安装 MongoDB:

sudo apt-get update
sudo apt-get install -y mongodb-org
  1. Windows 系统:从 MongoDB 官方网站(https://www.mongodb.com/try/download/community)下载 Windows 版本的安装包。运行安装程序,在安装过程中,可以选择自定义安装路径等选项。安装完成后,需要将 MongoDB 的 bin 目录添加到系统的环境变量中,这样才能在命令行中方便地执行 MongoDB 相关命令。

配置服务器节点

假设我们要搭建一个由三个节点组成的 MongoDB 副本集,分别命名为 node1node2node3。每个节点都需要有独立的配置文件和数据存储目录。

  1. 创建数据存储目录:在每个节点上,为 MongoDB 创建数据存储目录。例如,在 /var/lib/mongodb 下创建不同的子目录用于各个节点的数据存储。
sudo mkdir -p /var/lib/mongodb/node1
sudo mkdir -p /var/lib/mongodb/node2
sudo mkdir -p /var/lib/mongodb/node3

同时,设置正确的权限,确保 MongoDB 服务可以读写这些目录。

sudo chown -R mongodb:mongodb /var/lib/mongodb
  1. 配置文件编写:为每个节点创建独立的配置文件。以 node1 为例,在 /etc/mongod.conf 中添加如下内容:
systemLog:
  destination: file
  path: /var/log/mongodb/node1/mongod.log
  logAppend: true
storage:
  dbPath: /var/lib/mongodb/node1
  journal:
    enabled: true
processManagement:
  fork: true
  pidFilePath: /var/run/mongodb/node1/mongod.pid
net:
  port: 27017
  bindIp: 192.168.1.101  # 替换为实际的节点 IP
replication:
  oplogSizeMB: 1024
  replSetName: rs0

上述配置文件中,systemLog 部分定义了日志的输出方式和路径;storage 部分指定了数据存储路径和日志功能;processManagement 用于设置 MongoDB 以守护进程方式运行并指定 PID 文件路径;net 部分设置了监听端口和绑定的 IP 地址;replication 部分定义了副本集相关配置,包括操作日志大小和副本集名称。

同样地,为 node2node3 创建类似的配置文件,只需修改 net.bindIp 为对应节点的 IP 地址以及日志和数据存储路径。

启动 MongoDB 服务

在完成环境准备和配置文件编写后,就可以启动各个节点的 MongoDB 服务了。

  1. 启动节点服务:在每个节点上,使用以下命令启动 MongoDB 服务:
sudo systemctl start mongod

可以通过以下命令检查服务状态,确保服务正常启动:

sudo systemctl status mongod

如果服务启动失败,可以查看相应节点的日志文件(在配置文件中 systemLog.path 指定的路径)来排查问题。例如,如果是权限问题导致无法读写数据目录,日志中会有相关提示。

初始化副本集

当所有节点的 MongoDB 服务都正常启动后,就可以进行副本集的初始化操作了。

  1. 连接到其中一个节点:打开一个 MongoDB 客户端,连接到副本集中的任意一个节点。例如,连接到 node1
mongo --host 192.168.1.101 --port 27017
  1. 初始化副本集配置:在 MongoDB 客户端中,使用以下 JavaScript 代码来初始化副本集:
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "192.168.1.101:27017" },
    { _id: 1, host: "192.168.1.102:27017" },
    { _id: 2, host: "192.168.1.103:27017" }
  ]
})

上述代码中,rs.initiate() 函数用于初始化副本集。_id 字段指定了副本集的名称,必须与配置文件中 replication.replSetName 的值一致。members 数组定义了副本集中的各个成员节点,每个成员包含 _idhost 两个字段,_id 是成员的唯一标识符,host 是节点的 IP 地址和端口号。

执行上述代码后,如果初始化成功,会看到类似以下的输出:

{
  "info2" : "no configuration specified. Using a default configuration for the set",
  "me" : "192.168.1.101:27017",
  "ok" : 1,
  "operationTime" : Timestamp(1661082713, 1),
  "$clusterTime" : {
    "clusterTime" : Timestamp(1661082713, 1),
    "signature" : {
      "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
      "keyId" : NumberLong(0)
    }
  }
}
  1. 查看副本集状态:初始化完成后,可以使用 rs.status() 命令查看副本集的状态:
rs.status()

正常情况下,会看到类似以下的输出:

{
  "set" : "rs0",
  "date" : ISODate("2022-08-20T12:34:56Z"),
  "myState" : 1,
  "term" : NumberLong(1),
  "syncingTo" : "",
  "syncSourceHost" : "",
  "syncSourceId" : -1,
  "heartbeatIntervalMillis" : NumberLong(2000),
  "majorityVoteCount" : 2,
  "writeMajorityCount" : 2,
  "votingMembersCount" : 3,
  "writableVotingMembersCount" : 3,
  "optimes" : {
    "lastCommittedOpTime" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "lastCommittedWallTime" : ISODate("2022-08-20T12:34:53Z"),
    "readConcernMajorityOpTime" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "readConcernMajorityWallTime" : ISODate("2022-08-20T12:34:53Z"),
    "appliedOpTime" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "durableOpTime" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "lastAppliedWallTime" : ISODate("2022-08-20T12:34:53Z"),
    "lastDurableWallTime" : ISODate("2022-08-20T12:34:53Z")
  },
  "lastStableRecoveryTimestamp" : Timestamp(1661082713, 1),
  "electionCandidateMetrics" : {
    "lastElectionReason" : "electionTimeout",
    "lastElectionDate" : ISODate("2022-08-20T12:34:53Z"),
    "electionTerm" : NumberLong(1),
    "lastCommittedOpTimeAtElection" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "lastSeenOpTimeAtElection" : {
      "ts" : Timestamp(1661082713, 1),
      "t" : NumberLong(1)
    },
    "numVotesNeeded" : 2,
    "priorityAtElection" : 1,
    "electionTimeoutMillis" : NumberLong(10000),
    "numCatchUpOps" : NumberLong(0),
    "newTermStartDate" : ISODate("2022-08-20T12:34:53Z"),
    "wmajorityWriteAvailabilityDate" : ISODate("2022-08-20T12:34:53Z")
  },
  "members" : [
    {
      "_id" : 0,
      "name" : "192.168.1.101:27017",
      "health" : 1,
      "state" : 1,
      "stateStr" : "PRIMARY",
      "uptime" : 30,
      "optime" : {
        "ts" : Timestamp(1661082713, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2022-08-20T12:34:53Z"),
      "syncingTo" : "",
      "syncSourceHost" : "",
      "syncSourceId" : -1,
      "infoMessage" : "",
      "electionTime" : Timestamp(1661082713, 1),
      "electionDate" : ISODate("2022-08-20T12:34:53Z"),
      "configVersion" : 1,
      "self" : true,
      "lastHeartbeatMessage" : ""
    },
    {
      "_id" : 1,
      "name" : "192.168.1.102:27017",
      "health" : 1,
      "state" : 2,
      "stateStr" : "SECONDARY",
      "uptime" : 28,
      "optime" : {
        "ts" : Timestamp(1661082713, 1),
        "t" : NumberLong(1)
      },
      "optimeDurable" : {
        "ts" : Timestamp(1661082713, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2022-08-20T12:34:53Z"),
      "optimeDurableDate" : ISODate("2022-08-20T12:34:53Z"),
      "lastHeartbeat" : ISODate("2022-08-20T12:34:55Z"),
      "lastHeartbeatRecv" : ISODate("2022-08-20T12:34:55Z"),
      "pingMs" : NumberLong(0),
      "syncingTo" : "192.168.1.101:27017",
      "syncSourceHost" : "192.168.1.101:27017",
      "syncSourceId" : 0,
      "infoMessage" : "",
      "configVersion" : 1,
      "lastHeartbeatMessage" : ""
    },
    {
      "_id" : 2,
      "name" : "192.168.1.103:27017",
      "health" : 1,
      "state" : 2,
      "stateStr" : "SECONDARY",
      "uptime" : 27,
      "optime" : {
        "ts" : Timestamp(1661082713, 1),
        "t" : NumberLong(1)
      },
      "optimeDurable" : {
        "ts" : Timestamp(1661082713, 1),
        "t" : NumberLong(1)
      },
      "optimeDate" : ISODate("2022-08-20T12:34:53Z"),
      "optimeDurableDate" : ISODate("2022-08-20T12:34:53Z"),
      "lastHeartbeat" : ISODate("2022-08-20T12:34:55Z"),
      "lastHeartbeatRecv" : ISODate("2022-08-20T12:34:55Z"),
      "pingMs" : NumberLong(0),
      "syncingTo" : "192.168.1.101:27017",
      "syncSourceHost" : "192.168.1.101:27017",
      "syncSourceId" : 0,
      "infoMessage" : "",
      "configVersion" : 1,
      "lastHeartbeatMessage" : ""
    }
  ],
  "ok" : 1,
  "operationTime" : Timestamp(1661082713, 1),
  "$clusterTime" : {
    "clusterTime" : Timestamp(1661082713, 1),
    "signature" : {
      "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
      "keyId" : NumberLong(0)
    }
  }
}

从上述输出中可以看到副本集的名称(set)、各个节点的状态(stateStr)、操作时间(optime)等重要信息。myState 字段表示当前连接节点的状态,1 表示 PRIMARY,2 表示 SECONDARY。

副本集原理深入

理解 MongoDB 副本集的工作原理对于更好地使用和维护副本集至关重要。

选举机制

MongoDB 副本集采用的是基于心跳检测和投票的选举机制来确定 PRIMARY 节点。每个节点会定期向其他节点发送心跳信息,以表明自己的存活状态。当 PRIMARY 节点出现故障时,SECONDARY 节点会发起选举。

  1. 选举条件:要成为 PRIMARY 节点,节点需要满足一定的条件。首先,节点的 priority(优先级)必须大于 0,默认情况下所有节点的优先级为 1。优先级越高,在选举中获胜的概率越大。其次,节点必须拥有最新的操作日志(oplog),这是通过比较节点的 optime 来确定的。

  2. 选举过程:当一个节点发现 PRIMARY 节点不可达(通过心跳检测),它会向其他节点发送选举请求。其他节点会根据请求节点的优先级、oplog 等信息来决定是否投票。如果一个节点获得了超过半数(包括自身投票)的选票,它就会成为新的 PRIMARY 节点。

数据同步

SECONDARY 节点会从 PRIMARY 节点同步数据,以保持数据的一致性。数据同步是通过操作日志(oplog)来实现的。

  1. 操作日志(oplog):PRIMARY 节点会将所有的写操作记录到操作日志中。操作日志是一个特殊的集合,位于 local 数据库中。SECONDARY 节点会定期从 PRIMARY 节点获取操作日志,并应用这些操作来更新自己的数据。

  2. 同步过程:SECONDARY 节点启动后,会向 PRIMARY 节点请求最新的操作日志。PRIMARY 节点会将操作日志发送给 SECONDARY 节点,SECONDARY 节点会按照操作日志中的记录顺序依次应用这些操作。在同步过程中,SECONDARY 节点会维护一个 syncSource 字段,记录从哪个节点同步数据。

读操作

在 MongoDB 副本集中,读操作可以分发到 PRIMARY 节点或 SECONDARY 节点。

  1. 从 PRIMARY 节点读:默认情况下,读操作会发送到 PRIMARY 节点,以确保读取到最新的数据。这种方式适用于对数据一致性要求较高的场景,例如银行转账等操作,必须读取到最新的账户余额。

  2. 从 SECONDARY 节点读:可以通过设置 readPreference 来指定从 SECONDARY 节点读取数据。这种方式适用于对数据一致性要求不高,但对读性能要求较高的场景,例如统计报表等操作。从 SECONDARY 节点读可以减轻 PRIMARY 节点的负载,提高系统的整体性能。

常见问题及解决方法

在初始化和使用 MongoDB 副本集的过程中,可能会遇到一些常见问题。

节点无法加入副本集

  1. 问题描述:在执行 rs.add() 命令添加节点时,出现节点无法加入副本集的错误。
  2. 可能原因
    • 网络问题:节点之间的网络连接不通,可能是防火墙设置、IP 地址配置错误等原因导致。可以使用 ping 命令检查节点之间的网络连通性,使用 telnet 命令检查端口是否开放。例如,在 node1 上执行 telnet 192.168.1.102 27017 检查与 node2 的连接。
    • 配置错误:配置文件中的副本集名称不一致,或者节点的 IP 地址和端口号配置错误。仔细检查每个节点的配置文件,确保 replication.replSetName 一致,并且 net.bindIpnet.port 配置正确。
  3. 解决方法:根据具体原因进行相应的调整。如果是网络问题,修改防火墙规则或者纠正 IP 地址配置;如果是配置错误,修改配置文件并重启 MongoDB 服务。

选举失败

  1. 问题描述:当 PRIMARY 节点故障后,SECONDARY 节点无法成功选举出新的 PRIMARY 节点。
  2. 可能原因
    • 节点数量不足:副本集中存活的节点数量不足半数,无法形成有效的选举。例如,一个三节点的副本集,当两个节点故障时,剩余的一个节点无法选举出 PRIMARY 节点。
    • 优先级设置不合理:所有节点的优先级都设置为 0,导致没有节点能够成为 PRIMARY 节点。或者优先级设置过高,导致多个节点竞争 PRIMARY 节点,出现选举冲突。
  3. 解决方法:确保副本集中存活的节点数量超过半数。合理设置节点的优先级,避免优先级设置不合理的情况。可以通过 rs.conf() 命令查看副本集配置,通过 rs.reconfig() 命令修改配置。

数据同步问题

  1. 问题描述:SECONDARY 节点的数据同步出现异常,数据不一致。
  2. 可能原因
    • 网络不稳定:节点之间的网络连接不稳定,导致操作日志传输中断。可以通过监控网络带宽、延迟等指标来排查网络问题。
    • 操作日志损坏:由于磁盘故障等原因,导致 PRIMARY 节点的操作日志损坏,SECONDARY 节点无法正确应用。
  3. 解决方法:如果是网络问题,优化网络环境,确保节点之间网络稳定。如果怀疑操作日志损坏,可以尝试修复操作日志或者重新初始化副本集。在重新初始化副本集之前,务必备份重要数据。

副本集扩展与收缩

在实际应用中,可能需要根据业务需求对 MongoDB 副本集进行扩展或收缩。

扩展副本集

  1. 添加节点:假设要在现有的 rs0 副本集中添加一个新节点 node4,首先在新节点上安装 MongoDB 并配置好环境,创建数据存储目录和配置文件。配置文件内容与其他节点类似,只需修改 net.bindIpnode4 的 IP 地址。 然后,连接到副本集的 PRIMARY 节点,使用 rs.add() 命令添加新节点:
rs.add("192.168.1.104:27017")

添加成功后,可以使用 rs.status() 命令查看副本集状态,确认新节点已成功加入。

  1. 调整优先级:添加新节点后,可能需要根据业务需求调整节点的优先级。例如,希望新节点 node4 具有较高的优先级,可以使用以下命令修改配置:
var cfg = rs.conf();
cfg.members[3].priority = 2;
rs.reconfig(cfg);

上述代码中,首先获取副本集的当前配置(rs.conf()),然后修改新节点(cfg.members[3],索引从 0 开始)的 priority 为 2,最后使用 rs.reconfig() 命令重新配置副本集。

收缩副本集

  1. 移除节点:如果要从副本集中移除 node2,连接到 PRIMARY 节点,使用 rs.remove() 命令移除节点:
rs.remove("192.168.1.102:27017")

移除节点后,同样使用 rs.status() 命令查看副本集状态,确保节点已成功移除。

  1. 调整配置:移除节点后,可能需要根据剩余节点的情况调整副本集的配置,例如重新计算多数节点的数量等。可以通过 rs.conf()rs.reconfig() 命令来查看和修改配置。

副本集与高可用性

MongoDB 副本集的主要目标之一是提供高可用性。通过多个节点的冗余,当某个节点出现故障时,副本集能够自动选举出新的 PRIMARY 节点,确保服务的连续性。

故障恢复

当 PRIMARY 节点出现故障时,副本集的选举机制会迅速启动,从 SECONDARY 节点中选举出新的 PRIMARY 节点。整个过程对应用程序是透明的,应用程序只需保持与副本集的连接,无需手动干预。例如,在一个电商系统中,当处理订单的 PRIMARY 节点故障时,SECONDARY 节点会快速被选举为 PRIMARY 节点,继续处理订单,保证业务的正常进行。

负载均衡

除了提供高可用性,副本集还可以通过在 SECONDARY 节点上分担读操作来实现负载均衡。通过设置合适的 readPreference,可以将部分读请求分发到 SECONDARY 节点,减轻 PRIMARY 节点的负载。这在一些读多写少的应用场景中非常有效,例如新闻网站,大量的用户读取新闻文章,而写操作主要是管理员发布新文章,通过将读操作分发到 SECONDARY 节点,可以提高系统的整体性能。

总结

通过本文的介绍,我们详细了解了 MongoDB 副本集初始化的实战过程,包括环境准备、服务启动、副本集初始化以及相关原理、常见问题解决和副本集的扩展与收缩等内容。掌握这些知识后,我们能够更加熟练地搭建和管理 MongoDB 副本集,为应用程序提供高可用性和高性能的数据存储服务。在实际应用中,还需要根据业务需求不断优化副本集的配置和使用,以充分发挥 MongoDB 的优势。