复制集简介

Mongodb 复制集由一组 Mongod 实例(进程)组成,包含一个 Primary 节点和多个 Secondary 节点,Mongodb Driver(客户端)的所有数据都写入 Primary,Secondary 从 Primary 同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。

下图(图片源于 Mongodb 官方文档)是一个典型的 Mongdb 复制集,包含一个 Primary 节点和2个 Secondary 节点。

MongoDB 复制集原理深度分析 - 图1

Primary 选举(一)

复制集通过 replSetInitiate 命令(或 mongo shell 的 rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起 Primary 选举操作,获得大多数成员投票支持的节点,会成为 Primary,其余节点成为 Secondary。

初始化复制集

    1. config = {
    2. _id : "my_replica_set",
    3. members : [
    4. {_id : 0, host : "rs1.example.net:27017"},
    5. {_id : 1, host : "rs2.example.net:27017"},
    6. {_id : 2, host : "rs3.example.net:27017"},
    7. ]
    8. }
    9. rs.initiate(config)

“大多数”的定义

假设复制集内投票成员(后续介绍)数量为 N,则大多数为 N/2 + 1,当复制集内存活成员数量不足大多数时,整个复制集将无法选举出 Primary,复制集将无法提供写服务,处于只读状态。

投票成员数大多数容忍失效数
110
220
321
431
532
642
743

通常建议将复制集成员数量设置为奇数,从上表可以看出3个节点和4个节点的复制集都只能容忍1个节点失效,从服务可用性的角度看,其效果是一样的,但无疑4个节点能提供更可靠的数据存储。

特殊的 Secondary 节点

正常情况下,复制集的 Secondary 会参与 Primary 选举(自身也可能会被选为 Primary),并从 Primary 同步最新写入的数据,以保证与 Primary 存储相同的数据。

Secondary 可以提供读服务,增加 Secondary 节点可以提供复制集的读服务能力,同时提升复制集的可用性。另外,Mongodb 支持对复制集的 Secondary 节点进行灵活的配置,以适应多种场景的需求。

Arbiter

Arbiter 节点只参与投票,不能被选为 Primary,并且不从 Primary 同步数据。

比如你部署了一个2个节点的复制集,1个 Primary,1个 Secondary,任意节点宕机,复制集将不能提供服务(无法选出 Primary),这时可以给复制集添加一个 Arbiter 节点,即使有节点宕机,仍能选出 Primary。

Arbiter 本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入一个 Arbiter 节点,以提升复制集可用性。

Priority0

Priority0节点的选举优先级为0,不会被选举为 Primary。

比如你跨机房 A、B 部署了一个复制集,并且想指定 Primary 必须在 A 机房,这时可以将 B 机房的复制集成员 Priority 设置为0,这样 Primary 就一定会是 A 机房的成员。(注意:如果这样部署,最好将大多数节点部署在 A 机房,否则网络分区时可能无法选出 Primary。)

Vote0

Mongodb 3.0里,复制集成员最多50个,参与 Primary 选举投票的成员最多7个,其他成员(Vote0)的 vote 属性必须设置为0,即不参与投票。

Hidden

Hidden 节点不能被选为主(Priority 为0),并且对 Driver 不可见。

因 Hidden 节点不会接受 Driver 的请求,可使用 Hidden 节点做一些数据备份、离线计算的任务,不会影响复制集的服务。

Delayed

Delayed 节点必须是 Hidden 节点,并且其数据落后于 Primary 一段时间(可配置,比如1个小时)。

因 Delayed 节点的数据比 Primary 落后一段时间,当错误或者无效的数据写入 Primary 时,可通过 Delayed 节点的数据来恢复到之前的时间点。

Primary 选举 (二)

Primary 选举除了在复制集初始化时发生,还有如下场景。

复制集被 reconfig

Secondary 节点检测到 Primary 宕机时,会触发新 Primary 的选举,当有 Primary 节点主动 stepDown(主动降级为 Secondary)时,也会触发新的 Primary 选举。Primary 的选举受节点间心跳、优先级、最新的 oplog 时间等多种因素影响。

节点优先级

每个节点都倾向于投票给优先级最高的节点。优先级为0的节点不会主动发起 Primary 选举。当 Primary 发现有优先级更高的 Secondary,并且该 Secondary 的数据落后在10s内,则 Primary 会主动降级,让优先级更高的 Secondary 有成为 Primary 的机会。

Optime

拥有最新 optime(最近一条 oplog 的时间戳)的节点才能被选为 Primary。

网络分区

只有更大多数投票节点间保持网络连通,才有机会被选 Primary;如果 Primary 与大多数的节点断开连接,Primary 会主动降级为 Secondary。当发生网络分区时,可能在短时间内出现多个 Primary,故 Driver 在写入时,最好设置大多数成功的策略,这样即使出现多个 Primary,也只有一个 Primary 能成功写入大多数。

数据同步

Primary 与 Secondary 之间通过 oplog 来同步数据,Primary 上的写操作完成后,会向特殊的 local.oplog.rs 特殊集合写入一条 oplog,Secondary 不断的从 Primary 取新的 oplog 并应用。

因 oplog 的数据会不断增加,local.oplog.rs 被设置成为一个 capped 集合,当容量达到配置上限时,会将最旧的数据删除掉。另外考虑到 oplog 在 Secondary 上可能重复应用,oplog 必须具有幂等性,即重复应用也会得到相同的结果。

如下 oplog 的格式,包含 ts、h、op、ns、o 等字段。

    1. {
    2. "ts" : Timestamp(1446011584, 2),
    3. "h" : NumberLong("1687359108795812092"),
    4. "v" : 2,
    5. "op" : "i",
    6. "ns" : "test.nosql",
    7. "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
    8. }
  • ts:操作时间,当前 timestamp + 计数器,计数器每秒都被重置;
  • h:操作的全局唯一标识;
  • v:oplog 版本信息;
  • op:操作类型;
  • i:插入操作;
  • u:更新操作;
  • d:删除操作;
  • c:执行命令(如 createDatabase,dropDatabase);
  • n:空操作,特殊用途;
  • ns:操作针对的集合;
  • o:操作内容,如果是更新操作;
  • o2:操作查询条件,仅 update 操作包含该字段。

Secondary 初次同步数据时,会先执行 init sync,从 Primary(或其他数据更新的 Secondary)同步全量数据,然后不断通过执行tailable cursor从 Primary 的 local.oplog.rs 集合里查询最新的 oplog 并应用到自身。

init sync 过程

init sync 过程包含如下步骤:

  • T1时间,从 Primary 同步所有数据库的数据(local 除外),通过 listDatabases+ listCollections + cloneCollection 敏命令组合完成,假设 T2时间完成所有操作。

  • 从 Primary 应用[T1-T2]时间段内的所有 oplog,可能部分操作已经包含在步骤1,但由于 oplog 的幂等性,可重复应用。

  • 根据 Primary 各集合的 index 设置,在 Secondary 上为相应集合创建 index。(每个集合_id 的 index 已在步骤1中完成)。

注意:oplog 集合的大小应根据 DB 规模及应用写入需求合理配置,配置得太大,会造成存储空间的浪费;配置得太小,可能造成 Secondary 的 init sync 一直无法成功。比如在步骤1里由于 DB 数据太多、并且 oplog 配置太小,导致 oplog 不足以存储[T1, T2]时间内的所有 oplog,这就 Secondary 无法从 Primary 上同步完整的数据集。

修改复制集配置

当需要修改复制集时,比如增加成员、删除成员、或者修改成员配置(如 priorty、vote、hidden、delayed 等属性),可通过 replSetReconfig 命令(rs.reconfig())对复制集进行重新配置。

比如将复制集的第2个成员 Priority 设置为2,可执行如下命令:

    1. cfg = rs.conf();
    2. cfg.members[1].priority = 2;
    3. rs.reconfig(cfg);

异常处理(rollback)

当 Primary 宕机时,如果有数据未同步到 Secondary,当 Primary 重新加入时,如果新的 Primary 上已经发生了写操作,则旧 Primary 需要回滚部分操作,以保证数据集与新的 Primary 一致。

旧 Primary 将回滚的数据写到单独的 rollback 目录下,数据库管理员可根据需要使用 mongorestore 进行恢复。

复制集的读写设置

Read Preference

默认情况下,复制集的所有读请求都发到 Primary,Driver 可通过设置 Read Preference 来将读请求路由到其他的节点。

  • primary:默认规则,所有读请求发到 Primary;
  • primaryPreferred:Primary 优先,如果 Primary 不可达,请求 Secondary;
  • secondary:所有的读请求都发到 secondary;
  • secondaryPreferred:Secondary 优先,当所有 Secondary 不可达时,请求 Primary;
  • nearest:读请求发送到最近的可达节点上(通过 ping 探测得出最近的节点)。

Write Concern

默认情况下,Primary 完成写操作即返回,Driver 可通过设置Write Concern来设置写成功的规则。

如下的 write concern 规则设置写必须在大多数节点上成功,超时时间为5s。

    1. db.products.insert(
    2. { item: "envelopes", qty : 100, type: "Clasp" },
    3. { writeConcern: { w: majority, wtimeout: 5000 } }
    4. )

上面的设置方式是针对单个请求的,也可以修改副本集默认的 write concern,这样就不用每个请求单独设置。

    1. cfg = rs.conf()
    2. cfg.settings = {}
    3. cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
    4. rs.reconfig(cfg)