前言:mongodb 因为高性能、高可用性、支持分片等特性,作为非关系型数据库被大家广泛使用。其高可用性主要是体现在 mongodb 的副本集上面(可以简单理解为一主多从的集群),本篇文章主要从副本集介绍、本地搭建副本集、副本集读写数据这三个方面来带大家认识下 mongodb 副本集。

一、 mongodb 副本集介绍—

mongodb 副本集(Replica Set)包括主节点(primary)跟副本节点(Secondaries)。

主节点只能有一个,所有的写操作请求都在主节点上面处理。副本节点可以有多个,通过同步主节点的操作日志(oplog)来备份主节点数据。

在主节点挂掉后,有选举权限的副本节点会自动发起选举,并从中选举出新的主节点。

副本节点可以通过配置指定其具体的属性,比如选举、隐藏、延迟同步等,最多可以有 50 个副本节点,但只能有 7 个副本节点能参与选举。虽然副本节点不能处理写操作,但可以处理读请求,这个下文会专门讲到。

搭建一个副本集集群最少需要三个节点:一个主节点,两个备份节点,如果三个节点分布合理,基本可以保证线上数据 99.9%安全。三个节点的架构如下图所示:

如果只有一个主节点,一个副本节点,且没有资源拿来当第二个副本节点,那就可以起一个仲裁者节点(arbiter),不存数据,只用来选举用,如下图所示:

当主节点挂掉后,那么两个副本节点会进行选举,从中选举出一个新的主节点,流程如下:

对于副本集成员属性,特别需要说明下这几个:priority、hidden、slaveDelay、tags、votes。

  • priority

对于副本节点,可以通过该属性来增大或者减小该节点被选举成为主节点的可能性,取值范围为 0-1000(如果是 arbiters,则取值只有 0 或者 1),数据越大,成为主节点的可能性越大,如果被配置为 0,那么他就不能被选举成为主节点,而且也不能主动发起选举。

这种特性一般会被用在有多个数据中心的情况下,比如一个主数据中心,一个备份数据中心,主数据中心速度会更快,如果主节点挂掉,我们肯定希望新主节点也在主数据中心产生,那么我们就可以设置在备份数据中心的副本节点优先级为 0,如下图所示:

  • hidden

    隐藏节点会从主节点同步数据,但对客户端不可见,在 mongo shell 执行 db.isMaster() 方法也不会展示该节点,隐藏节点必须 Priority 为 0,即不可以被选举成为主节点。但是如果有配置选举权限的话,可以参与选举。

    因为隐藏节点对客户端不可见,所以跟客户端不会互相影响,可以用来备份数据或者跑一些后端定时任务之类的操作,具体如下图,4 个备份节点都从主节点同步数据,其中 1 个为隐藏节点:

  • slaveDelay

    延迟同步即延迟从主节点同步数据,比如延迟时间配置的 1 小时,现在时间是 09:52,那么延迟节点中只同步到主节点 08:52 之前的数据。另外需要注意延迟节点必须是隐藏节点,且 Priority 为 0。

    那这个延迟节点有什么用呢?有过数据库误操作惨痛经历的开发者肯定知道答案,那就是为了防止数据库误操作,比如更新服务前,一般会先执行数据库更新脚本,如果脚本有问题,且操作前未做备份,那数据可能就找不回了。但如果说配置了延迟节点,那误操作完,还有该节点可以兜底,只能说该功能真是贴心。具体延迟节点如下图所展示:

  • tags

    支持对副本集成员打标签,在查询数据时会用到,比如找到对应标签的副本节点,然后从该节点读取数据,这点也非常有用,可以根据标签对节点分类,查询数据时不同服务的客户端指定其对应的标签的节点,对某个标签的节点数量进行增加或减少,也不怕会影响到使用其他标签的服务。Tags 的具体使用,文章下面章节也会讲到。

  • votes

    表示节点是否有权限参与选举,最大可以配置 7 个副本节点参与选举。

二、副本集的搭建以及测试—

安装 mongodb 教程:https://docs.mongodb.com/manual/installation/

我们来搭建一套 P-S-S 结构的副本集(1 个 Primary 节点,2 个 Secondary 节点),大致过程为:先启动三个不同端口的 mongod 进程,然后在 mongo shell 中执行命令初始化副本集。

启动单个 mongod 实例的命令为:

mongod --replSet rs0 --port 27017 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /data/mongodb/rs0-0 --oplogSize 128

参数说明:

参数 说明 示例
replSet 副本集名称 rs0
port mongod 实例端口 27017
bind_ip 访问该实例的地址列表,只是本机访问可以设置为 localhost 或者 127.0.0.1,生产环境建议使用内部域名 Localhost
dbpath 数据存放位置 /data/mongodb/rs0-0
oplogSize 操作日志大小 128

搭建步骤如下:

  1. 先创建三个目录来分别存放这三个节点的数据

    mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-2

  2. 分别启动三个 mongod 进程,端口分别为:27018,27019,27020

第一个:mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

第二个:mongod --replSet rs0 --port 27019 --bind_ip localhost --dbpath /data/mongodb/rs0-1 --oplogSize 128

第三个:mongod --replSet rs0 --port 27020 --bind_ip localhost --dbpath /data/mongodb/rs0-2 --oplogSize 128

  1. 使用 mongo 进入第一个 mongod 示例,使用  rs.initiate() 进行初始化

登录到 27018: mongo localhost:27018

执行:

rsconf = {_id: "rs0",members: [{_id: 0,host: "localhost:27018"},{_id: 1,host: "localhost:27019"},{_id: 2,host: "localhost:27020"}]
}rs.initiate( rsconf )

以上就已经完成了一个副本集的搭建,在 mongo shell 中执行 rs.conf() 可以看到每个节点中 host、arbiterOnly、hidden、priority、 votes、slaveDelay 等属性,是不是超级简单。。

执行 rs.conf() ,结果展示如下:

rs.conf()
{"_id" : "rs0","version" : 1,"protocolVersion" : NumberLong(1),"writeConcernMajorityJournalDefault" : true,"members" : [{"_id" : 0,"host" : "localhost:27018","arbiterOnly" : false,"buildIndexes" : true,"hidden" : false,"priority" : 1,"tags" : {},"slaveDelay" : NumberLong(0),"votes" : 1},{"_id" : 1,"host" : "localhost:27019","arbiterOnly" : false,"buildIndexes" : true,"hidden" : false,"priority" : 1,"tags" : {},"slaveDelay" : NumberLong(0),"votes" : 1},{"_id" : 2,"host" : "localhost:27020","arbiterOnly" : false,"buildIndexes" : true,"hidden" : false,"priority" : 1,"tags" : {},"slaveDelay" : NumberLong(0),"votes" : 1}],"settings" : {"chainingAllowed" : true,"heartbeatIntervalMillis" : 2000,"heartbeatTimeoutSecs" : 10,"electionTimeoutMillis" : 10000,"catchUpTimeoutMillis" : -1,"catchUpTakeoverDelayMillis" : 30000,"getLastErrorModes" : {},"getLastErrorDefaults" : {"w" : 1,"wtimeout" : 0},"replicaSetId" : ObjectId("5f957f12974186fc616688fb")}
}

特别注意下:在 mongo shell 中,有 rs 跟 db。

  • rs 是指副本集,有 rs.initiate(),rs.conf(), rs.reconfig(), rs.add() 等操作副本集的方法

  • db 是指数据库,其下是对数据库的一些操作,比如下面会用到 db.isMaster(), db.collection.find(), db.collection.insert() 等。

我们再来测试下 Automatic Failover

  1. 可以直接停掉主节点 localhost:27018 来测试下主节点挂掉后,副本节点重新选举出新的主节点,即自动故障转移(Automatic Failover)

杀掉主节点 27018 后,可以看到 27019 的输出日志里面选举部分,27019 发起选举,并成功参选成为主节点:

2020-10-26T21:43:58.156+0800 I  REPL     [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100694 -- target:localhost:27018 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.156+0800 I  REPL     [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100695 -- target:localhost:27020 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.159+0800 I  ELECTION [replexec-301] VoteRequester(term 17) received an invalid response from localhost:27018: ShutdownInProgress: In the process of shutting down; response message: { operationTime: Timestamp(1603719830, 1), ok: 0.0, errmsg: "In the process of shutting down", code: 91, codeName: "ShutdownInProgress", $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } } }
2020-10-26T21:43:58.164+0800 I  ELECTION [replexec-305] VoteRequester(term 17) received a yes vote from localhost:27020; response message: { term: 17, voteGranted: true, reason: "", ok: 1.0, $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, operationTime: Timestamp(1603719830, 1) }
2020-10-26T21:43:58.164+0800 I  ELECTION [replexec-304] election succeeded, assuming primary role in term 17
  1. 然后执行 rs.status() 查看当前副本集情况,可以看到 27019 变为主节点,27018 显示已挂掉 health = 0

rs.status()
{"set" : "rs0","date" : ISODate("2020-10-26T13:44:22.071Z"),"myState" : 1,"heartbeatIntervalMillis" : NumberLong(2000),"majorityVoteCount" : 2,"writeMajorityCount" : 2,"members" : [{"_id" : 0,"name" : "localhost:27018","ip" : "127.0.0.1","health" : 0,"state" : 8,"stateStr" : "(not reachable/healthy)","uptime" : 0,"optime" : {"ts" : Timestamp(0, 0),"t" : NumberLong(-1)},"optimeDurable" : {"ts" : Timestamp(0, 0),"t" : NumberLong(-1)},"optimeDate" : ISODate("1970-01-01T00:00:00Z"),"optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"),"lastHeartbeat" : ISODate("2020-10-26T13:44:20.202Z"),"lastHeartbeatRecv" : ISODate("2020-10-26T13:43:57.861Z"),"pingMs" : NumberLong(0),"lastHeartbeatMessage" : "Error connecting to localhost:27018 (127.0.0.1:27018) :: caused by :: Connection refused","syncingTo" : "","syncSourceHost" : "","syncSourceId" : -1,"infoMessage" : "","configVersion" : -1},{"_id" : 1,"name" : "localhost:27019","ip" : "127.0.0.1","health" : 1,"state" : 1,"stateStr" : "PRIMARY","uptime" : 85318,"optime" : {"ts" : Timestamp(1603719858, 1),"t" : NumberLong(17)},"optimeDate" : ISODate("2020-10-26T13:44:18Z"),"syncingTo" : "","syncSourceHost" : "","syncSourceId" : -1,"infoMessage" : "","electionTime" : Timestamp(1603719838, 1),"electionDate" : ISODate("2020-10-26T13:43:58Z"),"configVersion" : 1,"self" : true,"lastHeartbeatMessage" : ""},{"_id" : 2,"name" : "localhost:27020","ip" : "127.0.0.1","health" : 1,"state" : 2,"stateStr" : "SECONDARY","uptime" : 52468,"optime" : {"ts" : Timestamp(1603719858, 1),"t" : NumberLong(17)},"optimeDurable" : {"ts" : Timestamp(1603719858, 1),"t" : NumberLong(17)},"optimeDate" : ISODate("2020-10-26T13:44:18Z"),"optimeDurableDate" : ISODate("2020-10-26T13:44:18Z"),"lastHeartbeat" : ISODate("2020-10-26T13:44:20.200Z"),"lastHeartbeatRecv" : ISODate("2020-10-26T13:44:21.517Z"),"pingMs" : NumberLong(0),"lastHeartbeatMessage" : "","syncingTo" : "localhost:27019","syncSourceHost" : "localhost:27019","syncSourceId" : 1,"infoMessage" : "","configVersion" : 1}]
}
  1. 再次启动 27018:mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

可以在节点 27019 日志中看到已检测到 27018,并且已变为副本节点,通过 rs.status 查看结果也是如此。

2020-10-26T21:52:06.871+0800 I  REPL     [replexec-305] Member localhost:27018 is now in state SECONDARY

三、副本集写跟读的一些特性—

写关注(Write concern)

副本集写关注是指写入一条数据,主节点处理完成后,需要其他承载数据的副本节点也确认写成功后,才能给客户端返回写入数据成功。

这个功能主要是解决主节点挂掉后,数据还未来得及同步到副本节点,而导致数据丢失的问题。

可以配置节点个数,默认配置 {“w”:1},这样表示主节点写入数据成功即可给客户端返回成功,“w” 配置为 2,则表示除了主节点,还需要收到其中一个副本节点返回写入成功,“w” 还可以配置为 "majority",表示需要集群中大多数承载数据且有选举权限的节点返回写入成功。

如下图所示,P-S-S 结构(一个 primary 节点,两个 secondary 节点),写请求里面带了 w : “majority" ,那么主节点写入完成后,数据同步到第一个副本节点,且第一个副本节点回复数据写入成功后,才给客户端返回成功。

关于写关注在实际中如何操作,有下面两种方法:

  1. 在写请求中指定 writeConcern 相关参数,如下:

db.products.insert({ item: "envelopes", qty : 100, type: "Clasp" },{ writeConcern: { w: "majority" , wtimeout: 5000 } }
)
  1. 修改副本集 getLastErrorDefaults 配置,如下:

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

读偏好 (Read preference)

读跟写不一样,为了保持一致性,写只能通过主节点,但读可以选择主节点,也可以选择副本节点,区别是主节点数据最新,副本节点因为同步问题可能会有延迟,但从副本节点读取数据可以分散对主节点的压力。

因为承载数据的节点会有多个,那客户端如何选择从那个节点读呢?主要有 3 个条件(Tag Sets、 maxStalenessSeconds、Hedged Read),5 种模式(primary、primaryPreferred、secondary、secondaryPreferred、nearest)

首先说一下 5 种模式,其特点如下表所示:

模式 特点
primary 所有读请求都从主节点读取
primaryPreferred 主节点正常,则所有读请求都从主节点读取,如果主节点挂掉,则从符合条件的副本节点读取
secondary 所有读请求都从副本节点读取
secondaryPreferred 所有读请求都从副本节点读取,但如果副本节点都挂掉了,那就从主节点读取
nearest 主要看网络延迟,选取延迟最小的节点,主节点跟副本节点均可

再说下 3 个条件,条件是在符合模式的基础上,再根据条件删选具体的节点

  1. Tag Sets(标签)

顾名思义,这个可以给节点加上标签,然后查找数据时,可以根据标签选择对应的节点,然后在该节点查找数据。可以通过 mongo shell 使用 rs.conf() 查看当前每个节点下面的 tags, 修改或者添加 tags 过程同上面修改 getLastErrorDefaults 配置 ,如:cfg.members[n].tags = { "region": "South", "datacenter": "A" }

  1. maxStalenessSeconds (可容忍的最大同步延迟)

顾名思义+1,这个值是指副本节点同步主节点写入的时间 跟 主节点实际最近写入时间的对比值,如果主节点挂掉了,那就跟副本集中最新写入的时间做对比。

这个值建议设置,避免因为部分副本节点网络原因导致比较长时间未同步主节点数据,然后读到比较老的数据。特别注意的是该值需要设置 90s 以上,因为客户端是定时去校验副本节点的同步延迟时间,数据不会特别准确,设置比 90s 小,会抛出异常。

  1. Hedged Read (对冲读取)

该选项是在分片集群 MongoDB 4.4 版本后才支持,指 mongos 实例路由读取请求时会同时发给两个符合条件的副本集节点,然后那个先返回结果就返回这个结果给客户端。

那问题来了,如此好用的模式以及条件在查询请求中如何使用呢?

  1. 在代码中连接数据库,使用 connection string uri 时,可以加上下面的这三个参数

参数 说明
readPreference 模式,枚举值有:primary(默认值)、 primaryPreferred、secondary、secondaryPreferred、nearest
maxStalenessSeconds 最大同步延时秒数,取值 0 - 90 会报错, -1 表示没有最大值
readPreferenceTags 标签,如果标签是 { "dc": "ny", "rack": "r1" }, 则在 uri 为 readPreferenceTags=dc:ny,rack:r1

例如下面:

mongodb://db0.example.com,db1.example.com,db2.example.com/?replicaSet=myRepl&readPreference=secondary&maxStalenessSeconds=120&readPreferenceTags=dc:ny,rack:r1

  1. 在 mogo shell 中,可以使用 cursor.readPref()[1]  或者 Mongo.setReadPref()[2]

cursor.readPref() 参数分别为: mode、tag set、hedge options, 具体请求例如下面这样

db.collection.find({ }).readPref("secondary",                      // mode[ { "datacenter": "B" },  { } ],  // tag set{ enabled: true }                 // hedge options
)

Mongo.setReadPref() 类似,只是预先设置请求条件,这样就不用每个请求后面带上 readPref 条件。

可以在搭建好的集群中简单测试下该功能

  1. 登录主节点: mongo localhost:27018

  2. 插入一条数据: db.nums.insert({name: “num0”})

    在当前节点查询: db.nums.find()

    可以看到本条数据: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

  3. 登录副本节点: mongo localhost:27019

查询:db.nums.find()

因为查询模式默认为 primary,所以在副本节点查询会报错,如下:

Error: error: {"operationTime" : Timestamp(1603788383, 1),"ok" : 0,"errmsg" : "not master and slaveOk=false","code" : 13435,"codeName" : "NotMasterNoSlaveOk","$clusterTime" : {"clusterTime" : Timestamp(1603788383, 1),"signature" : {"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),"keyId" : NumberLong(0)}}
}

查询时指定模式为 “secondary”: db.nums.find().readPref(“secondary")

就可以查询到插入的数据:{ "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

结语—

以上内容都是阅读 MongoDB 官方文档后,然后挑简单且重要的一些点做的总结,如果大家对 MongoDB 感兴趣,建议直接啃一啃官方文档[3]

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

参考资料

[1]

cursor.readPref(): https://docs.mongodb.com/manual/reference/method/cursor.readPref/#cursor.readPref

[2]

Mongo.setReadPref(): https://docs.mongodb.com/manual/reference/method/Mongo.setReadPref/#Mongo.setReadPref

[3]

官方文档: https://docs.mongodb.com/manual/replication/

MongoDB 副本集之入门篇相关推荐

  1. MongoDB副本集概述,副本集是什么?有什么用?

    MongoDB 副本集概述 一.为什么要使用副本集? 在了解副本集之前,我们先来聊聊副本集的对立面--单机服务器存在的问题: 在学习MongoDB时,我们一般都只需要启动一个 Mongod服务,也就是 ...

  2. mysql 副本集_再看MongoDB副本集

    因为MongoDB使用内存映射文件,所以必须使用64位版本.MongoDB的副本集不同于以往的主从模式.在集群Master故障的时候,副本集可以 因为MongoDB使用内存映射文件,所以必须使用64位 ...

  3. DockerCompose MongoDB 副本集(集群) (客户端验证密码 - 集群内部使用mongodbKeyfile验证 ) 部署

    本部署MongoDB副本集是没有用其他的配置什么的(例如: mongodb-compass).是最简单的一个 我是为了mongodb的事务才配置的这个副本集,要使用mongodb的事务必须要副本集. ...

  4. 总结—什么妖引起的MongoDB副本集初始化失败?

    前言: 在之前搭建MongoDB集群中,遇到过一些小问题作妖引起的初始化副本集失败,初学至今踩过来的坑,在此做个小结. 1.IP错误引起MongoDB副本集初始化失败 这个错误在另一篇文章已经描述过, ...

  5. Mongodb 副本集+分片集群搭建

    环境需求: 在三台机器上搭建副本集,在副本集上做分片 服务器IP: 192.168.1.232/192.168.1.238/192.168.1.241 安装Mongodb all nodes: tar ...

  6. Mongodb副本集--Out of memory: Kill process 37325 (mongod)

    Mongodb副本集--Out of memory: Kill process 37325 (mongod) 1.Mongodb副本集--Out of memory: Kill process 373 ...

  7. (2)MongoDB副本集自动故障转移原理(含客户端)

    前文我们搭建MongoDB三成员副本集,了解集群基本特性,今天我们围绕下图聊一聊背后的细节. 默认搭建的副本集均在主节点读写,辅助节点冗余部署,形成高可用和备份,具备自动故障转移能力. 集群心跳保活 ...

  8. mongodb副本集_设置MongoDB副本集分为4个步骤

    mongodb副本集 介绍 在详细介绍配置MongoDB副本集之前,让我简要介绍一下它们: 副本集是Mongodb数据库提供的功能,可实现高可用性和自动故障转移. 它是一种传统的主从配置,但具有自动故 ...

  9. 设置MongoDB副本集分为4个步骤

    介绍 在详细介绍配置MongoDB副本集之前,让我简要介绍一下它们: 副本集是Mongodb数据库提供的功能,可实现高可用性和自动故障转移. 它是一种传统的主从配置,但具有自动故障转移功能. 基本上, ...

最新文章

  1. 服务器主机防御系统,主机入侵防御系统
  2. 磨刀不误砍柴功:App开发者必备之8大利器
  3. Qt读取ini配置文件
  4. PHP面试题:请说明 PHP 中传值与传引用的区别。什么时候传值什么时候传引用?
  5. elasticsearch安装bigdest插件
  6. 关于《构建之法》阅读笔记 的致歉博客
  7. 7月最强书单丨博文视点新品畅销TOP10,让技术带你燃爆整个7月
  8. 更改自定义按钮显示值并对单元格赋值
  9. 翻译:PropertyWrapper swift 5 aop特性
  10. PL/SQL工具如何连接oracle数据库
  11. 两个卡巴斯基 6.0 官方简体中文版授权文件
  12. 英特尔® 处理器显卡
  13. 游戏音效只是简单的改原素材吗?
  14. 两表联合查询,求TOP100商品。。。。。。。。。。感激不尽!
  15. Flume之HDFS Sink 的参数解析及异常处理
  16. 面向端到端的情感对话生成研究综述
  17. oracle定时任务实例
  18. mysql数据库常见错误及解决方法
  19. 网站开启https后很慢_网站开启https后地址栏安全锁灰色或黄色叹号
  20. mybait——入门简单项目

热门文章

  1. 双目相机标定流程(使用opencv采集图像、使用matlab标定相机参数)
  2. 免费好用的pdf转换器
  3. python3 tkinter详解_详解python3中tkinter知识点
  4. java eav_使用EAV的客户自定义属性不在Grid中显示值
  5. mysql数据范围什么意思_数据都在mysql里的话,基于经纬度的范围查询有什么高效的方案吗...
  6. 2020一级建造师课件一级建造师教材百度云网盘下载地址
  7. I like this.
  8. 实现机器人自主定位导航必解决的三大问题
  9. 基于Android的计步器(Pedometer)的讲解(四)——后台记步
  10. 蓝桥杯c语言复赛试题,第三届蓝桥杯复赛题解析