做者: 凹凸曼-军军mongodb
前言:mongodb 由于高性能、高可用性、支持分片等特性,做为非关系型数据库被你们普遍使用。其高可用性主要是体如今 mongodb 的副本集上面(能够简单理解为一主多从的集群),本篇文章主要从副本集介绍、本地搭建副本集、副本集读写数据这三个方面来带你们认识下 mongodb 副本集。shell
mongodb 副本集(Replica Set)包括主节点(primary)跟副本节点(Secondaries)。数据库
主节点只能有一个,全部的写操做请求都在主节点上面处理。副本节点能够有多个,经过同步主节点的操做日志(oplog)来备份主节点数据。后端
在主节点挂掉后,有选举权限的副本节点会自动发起选举,并从中选举出新的主节点。安全
副本节点能够经过配置指定其具体的属性,好比选举、隐藏、延迟同步等,最多能够有50个副本节点,但只能有7个副本节点能参与选举。虽然副本节点不能处理写操做,但能够处理读请求,这个下文会专门讲到。网络
搭建一个副本集集群最少须要三个节点:一个主节点,两个备份节点,若是三个节点分布合理,基本能够保证线上数据99.9%安全。三个节点的架构以下图所示:架构
若是只有一个主节点,一个副本节点,且没有资源拿来当第二个副本节点,那就能够起一个仲裁者节点(arbiter),不存数据,只用来选举用,以下图所示:性能
当主节点挂掉后,那么两个副本节点会进行选举,从中选举出一个新的主节点,流程以下:测试
对于副本集成员属性,特别须要说明下这几个:priority、hidden、slaveDelay、tags、votes。ui
对于副本节点,能够经过该属性来增大或者减少该节点被选举成为主节点的可能性,取值范围为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 |
先建立三个目录来分别存放这三个节点的数据
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
登陆到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。
杀掉主节点 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() { "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 } ] }
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
副本集写关注是指写入一条数据,主节点处理完成后,须要其余承载数据的副本节点也确认写成功后,才能给客户端返回写入数据成功。
这个功能主要是解决主节点挂掉后,数据还将来得及同步到副本节点,而致使数据丢失的问题。
能够配置节点个数,默认配置 {“w”:1},这样表示主节点写入数据成功便可给客户端返回成功,“w” 配置为2,则表示除了主节点,还须要收到其中一个副本节点返回写入成功,“w” 还能够配置为 "majority",表示须要集群中大多数承载数据且有选举权限的节点返回写入成功。
以下图所示,P-S-S 结构(一个 primary 节点,两个 secondary 节点),写请求里面带了w : “majority" ,那么主节点写入完成后,数据同步到第一个副本节点,且第一个副本节点回复数据写入成功后,才给客户端返回成功。
关于写关注在实际中如何操做,有下面两种方法:
db.products.insert( { item: "envelopes", qty : 100, type: "Clasp" }, { writeConcern: { w: "majority" , wtimeout: 5000 } } )
cfg = rs.conf() cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 } rs.reconfig(cfg)
读跟写不同,为了保持一致性,写只能经过主节点,但读能够选择主节点,也能够选择副本节点,区别是主节点数据最新,副本节点由于同步问题可能会有延迟,但从副本节点读取数据能够分散对主节点的压力。
由于承载数据的节点会有多个,那客户端如何选择从那个节点读呢?主要有3个条件(Tag Sets、 maxStalenessSeconds、Hedged Read),5种模式(primary、primaryPreferred、secondary、secondaryPreferred、nearest)
模式 | 特色 |
---|---|
primary | 全部读请求都从主节点读取 |
primaryPreferred | 主节点正常,则全部读请求都从主节点读取,若是主节点挂掉,则从符合条件的副本节点读取 |
secondary | 全部读请求都从副本节点读取 |
secondaryPreferred | 全部读请求都从副本节点读取,但若是副本节点都挂掉了,那就从主节点读取 |
nearest | 主要看网络延迟,选取延迟最小的节点,主节点跟副本节点都可 |
顾名思义,这个能够给节点加上标签,而后查找数据时,能够根据标签选择对应的节点,而后在该节点查找数据。能够经过mongo shell 使用 rs.conf() 查看当前每一个节点下面的 tags, 修改或者添加tags 过程同上面修改 getLastErrorDefaults 配置 ,如: cfg.members[n].tags = { "region": "South", "datacenter": "A" }
顾名思义+1,这个值是指副本节点同步主节点写入的时间 跟 主节点实际最近写入时间的对比值,若是主节点挂掉了,那就跟副本集中最新写入的时间作对比。
这个值建议设置,避免由于部分副本节点网络缘由致使比较长时间未同步主节点数据,而后读到比较老的数据。特别注意的是该值须要设置 90s 以上,由于客户端是定时去校验副本节点的同步延迟时间,数据不会特别准确,设置比 90s 小,会抛出异常。
该选项是在分片集群 MongoDB 4.4 版本后才支持,指 mongos 实例路由读取请求时会同时发给两个符合条件的副本集节点,而后那个先返回结果就返回这个结果给客户端。
参数 | 说明 |
---|---|
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
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" }
欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章: