作者:凹凸曼 - 军军
前言: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 个为暗藏节点:
![](https://storage.360buyimg.com…
)
- slaveDelay
提早同步即提早从主节点同步数据,比方延迟时间配置的 1 小时,当初工夫是 09:52,那么提早节点中只同步到主节点 08:52 之前的数据。另外须要留神提早节点必须是暗藏节点,且 Priority 为 0。
那这个提早节点有什么用呢?有过数据库误操作惨痛经验的开发者必定晓得答案,那就是为了避免数据库误操作,比方更新服务前,个别会先执行数据库更新脚本,如果脚本有问题,且操作前未做备份,那数据可能就找不回了。但如果说配置了提早节点,那误操作完,还有该节点能够兜底,只能说该性能真是贴心。具体提早节点如下图所展现:
- tags
反对对正本集成员打标签,在查问数据时会用到,比方找到对应标签的正本节点,而后从该节点读取数据,这点也十分有用,能够依据标签对节点分类,查问数据时不同服务的客户端指定其对应的标签的节点,对某个标签的节点数量进行减少或缩小,也不怕会影响到应用其余标签的服务。Tags 的具体应用,文章上面章节也会讲到。
- votes
示意节点是否有权限参加选举,最大能够配置 7 个正本节点参加选举。
二、正本集的搭建以及测试
装置 mongodb 教程:[https://docs.mongodb.com/manu…]()
咱们来搭建一套 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 |
搭建步骤如下:
- 先创立三个目录来别离寄存这三个节点的数据
mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-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
-
应用 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
-
能够间接停掉主节点 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
-
而后执行 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 } ] }
-
再次启动 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”,那么主节点写入实现后,数据同步到第一个正本节点,且第一个正本节点回复数据写入胜利后,才给客户端返回胜利。
![](https://storage.360buyimg.com…
)
对于写关注在理论中如何操作,有上面两种办法:
-
在写申请中指定 writeConcern 相干参数,如下:
db.products.insert({ item: "envelopes", qty : 100, type: "Clasp"}, {writeConcern: { w: "majority" , wtimeout: 5000} } )
-
批改正本集 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 个条件,条件是在合乎模式的根底上,再依据条件删选具体的节点
- Tag Sets(标签)
顾名思义,这个能够给节点加上标签,而后查找数据时,能够依据标签抉择对应的节点,而后在该节点查找数据。能够通过 mongo shell 应用 rs.conf() 查看以后每个节点上面的 tags,批改或者增加 tags 过程同下面批改 getLastErrorDefaults 配置,如:
cfg.members[n].tags = {"region": "South", "datacenter": "A"}
- maxStalenessSeconds(可容忍的最大同步提早)
顾名思义 +1,这个值是指正本节点同步主节点写入的工夫 跟 主节点理论最近写入工夫的对比值,如果主节点挂掉了,那就跟正本集中最新写入的工夫做比照。
这个值倡议设置,防止因为局部正本节点网络起因导致比拟长时间未同步主节点数据,而后读到比拟老的数据。特地留神的是该值须要设置 90s 以上,因为客户端是定时去校验正本节点的同步延迟时间,数据不会特地精确,设置比 90s 小,会抛出异样。
- Hedged Read(对冲读取)
该选项是在分片集群 MongoDB 4.4 版本后才反对,指 mongos 实例路由读取申请时会同时发给两个符合条件的正本集节点,而后那个先返回后果就返回这个后果给客户端。
那问题来了,如此好用的模式以及条件在查问申请中如何应用呢?
- 在代码中连贯数据库,应用 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
-
在 mogo shell 中,能够应用 cursor.readPref() 或者 Mongo.setReadPref()
cursor.readPref() 参数别离为:mode、tag set、hedge options, 具体申请例如上面这样
db.collection.find({}).readPref( "secondary", // mode [{ "datacenter": "B"}, {}], // tag set {enabled: true} // hedge options )
Mongo.setReadPref() 相似,只是事后设置申请条件,这样就不必每个申请前面带上 readPref 条件。
能够在搭建好的集群中简略测试下该性能
- 登录主节点:
mongo localhost:27018
- 插入一条数据:
db.nums.insert({name:“num0”})
在以后节点查问:
db.nums.find()
能够看到本条数据:
{"_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }
-
登录正本节点:
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 感兴趣,倡议间接啃一啃官网文档。
欢送关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。