咱们之前用Mysql的时候,常常是一台服务器走天下,若是只是用于学习,是没有问题的,可是在生产环境中,这样的风险是很大的,若是服务器由于网络缘由或者崩溃了,就会致使数据库一段时间了不可用,这样的体验很很差。redis
那么应该怎么办呢?既然一台机器不行,我就多上几台机器总能够了吧,好比我上个两台,让他们互为主备,相互同步数据。想到这里我就只想说一个字,稳。sql
其实redis,mongodb,kafka等分布式应用基本上都是这样的思想mongodb
MongoDB也差很少是这样的思想。它经过复制集来解决这个问题,MongoDB复制集由一组Mongod进程组成,包含一个Primary节点和多个Secondary节点,Mongodb Driver(客户端)的全部数据都写入Primary,Secondary从Primary同步写入的数据,以保持复制集内全部成员存储相同的数据集,提供数据的高可用。数据库
要想成为primary节点,你必须保证大多数节点都赞成才行,大多数的节点就是副本中一半以上的成员。安全
成员总数 | 大多数 | 容忍失败数 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
为何要要求大多数呢?实际上是为了不出现两个primary节点。好比一个五个节点的复制集,其中3个成员不可用,剩下的2个仍然正常工做。这两个工做的节点因为不能知足复制集大多数的要求(这个例子中要求要有3个节点才是大多数),因此他们没法选择主节点,即便其中有一个节点是primary节点,当它注意到它没法获取大多数节点的支持时,它就会退位,成为备份节点。bash
若是让这两个节点能够选出primary节点,问题是另外3个节点可能不是真正挂了,而只是网络不可达而已。另外3个节点就必定能够选择出primary节点,这样就存在了两个primary节点了。 因此要求大多数就能够避免产生两个primary节点的问题。服务器
若是MongoDB副本集能够拥有多个primary节点,那么就会面临写入冲突的问题,在支持多线程写入的系统中解决冲突的方式有手动解决和让操做系统任选一个这两种方式,可是这两种方式都不易实现,没法保证写入的数据不被其余节点修改,所以mongodb只支持单一的primary节点,这样使得开发更容易。网络
当一个备份节点没法与主节点连通时,它会联系并请求其余副本集成员将本身选举为主节点,其余成员会作几项理性的检查:自身是否可以与主节点连通?但愿被选举为主节点的备份节点的数据是否最新?有没有其余更高优先级的成员能够被选举为主节点? 若是竞选节点成员可以获得大多数投票,就会成为主节点。可是一旦大多数成员中只有一个否决了本次选举,选举就会取消。多线程
在日志中能够看到得票数为比较大的负数的状况,由于一张否决票至关于10000张同意票。若是有2张同意票,2张否决票,那么选举结果就是-19998,依此类推。app
咱们通常在部署的时候,副本集节点个数至少是3个(由于它容许1个失败),这也就意味着数据要被复制三份。
不少人的应用程序使用量比较小,不想保存三份数据,只想要保存两份就好了,保存第三份纯粹是浪费。对于这种部署MongoDB也是支持的。它有一种特殊的成员叫作仲裁者(arbiter),它惟一的做用就是参与选举,它既不保存数据也不为客户端提供服务,只是为了帮助只有两个成员的副本集知足大多数这个条件而已。
仲裁者其实也是有缺点的。若是真有一个节点挂了(数据没法恢复),另外一个成员称为主节点。为了数据安全,就须要一个新的备份节点,而且将主节点的数据备份到备份节点。复制数据会对服务器形成很大的压力,会拖慢应用程序。相反若是有三个数据成员即便其中一个挂了,仍有一个主节点和一个备份节点,不影响正常运做。这个时候还能够用剩下的那个备份节点来初始化一个新的备份节点服务器,而不依赖于主节点。因此若是可能尽量在副本集中使用奇数个数据成员,而不要使用仲裁者。
若是想让一个节点有更大的机会成为primary的话这须要设置优先级,好比我添加一个优先级为2的成员(默认为1)
rs.add({"_id":4, "host": "10.17.28.190:27017", "priority" : 2});
复制代码
假设其余都是默认优先级,只要10.17.28.190拥有最新数据,那么当前primary节点就会自动退位,10.17.28.190会被选举为新的主节点。若是它的数据不够新,那么当前主节点就会保持不变。
若是设置priority为0,表示不会被选为primary节点。
因为复制集成员最多50个,而参与Primary成员投票的最多7个,因此其余成员的vote必须设置为0(priority也必须为0)。 尽管无投票权的成员不会在选举中投票,但这些成员拥有副本集数据的副本,而且能够接受来自客户端应用程序的读取操做。
客户端不会像隐藏成员发送请求,隐藏成员也不会做为复制源(尽管当其余复制源不可用时隐藏成员)。所以不少人将不够强大的服务器或者备份服务器隐藏起来。经过设置hidden:true能够设置隐藏,只有优先级为0的才能被隐藏。 可以使用Hidden节点作一些数据备份、离线计算的任务,不会影响复制集的服务
数据可能会由于人为错误而遭到毁灭性的破坏,为了防止这类问题,可使用slaveDelay设置一个延迟的备份节点。
延迟备份节点的数据回比主节点延迟指定的时间(单位是秒),slaveDelay要求优先级是0,若是应用会将读请求路由到备份节点,应该将延迟备份节点隐藏掉,以避免读请求被路由到延迟备份节点。
因Delayed节点的数据比Primary落后一段时间,当错误或者无效的数据写入Primary时,可经过Delayed节点的数据来恢复到以前的时间点。
好比我有个副本集叫作rs0,我想修改增长或者删除成员,修改为员的配置(vote,hidden,priority等)能够经过reconfig命令
cfg = rs.conf();
cfg.members[1].priority = 2;
rs.reconfig(cfg);
复制代码
Primary与Secondary之间经过oplog来同步数据,Primary上的写操做完成后,会向特殊的local.oplog.rs特殊集合写入一条oplog,Secondary不断的从Primary取新的oplog并应用。
因oplog的数据会不断增长,local.oplog.rs被设置成为一个capped集合,当容量达到配置上限时,会将最旧的数据删除掉。因为复制操做的过程是先复制数据在写入oplog,oplog必须具备幂等性,即重复应用也会获得相同的结果。
我向test库的coll集合插入了一条数据以后(db.coll.insert({count:1})
),调用db.isMaster()命令能够看到当前节点的最后一次写入时间戳
> db.isMaster()
{
"ismaster" : true,
"secondary" : false,
"lastWrite" : {
"opTime" : {
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1)
},
"lastWriteDate" : ISODate("2019-10-31T08:04:47Z"),
"majorityOpTime" : {
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1)
},
"majorityWriteDate" : ISODate("2019-10-31T08:04:47Z")
}
}
复制代码
命令会返回不少数据,这里我只列出了小部分,能够看到咱们当前所在节点是master节点(primary),若是当前节点不是primary,也会经过primary属性告诉你当前primary节点是哪一个,同时最后一次写入的时间戳是1572509087。
此时咱们登陆另外一台secondary节点,切换到local数据库,执行命令db.oplog.rs.find()
命令,会返回不少条数据,这里咱们查看最后一条便可
{
"ts" : Timestamp(1572509087, 2),
"t" : NumberLong(1),
"h" : NumberLong("6139682004250579847"),
"v" : 2,
"op" : "i",
"ns" : "test.coll",
"ui" : UUID("1be7f8d0-fde2-4d68-89ea-808f14b326da"),
"wall" : ISODate("2019-10-31T08:04:47.925Z"),
"o" : {
"_id" : ObjectId("5dba959fcf287dfd8727a1bf"),
"count" : 1
}
}
复制代码
能够看到oplog的ts和isMater()命令返回的lastTime.opTime.ts的值是一致的,证实咱们的数据是最新的,若是你这个时候访问其余节点查看oplog.rs的数据,会发现数据是如出一辙的。 在来解释下字段含义
副本集中的成员启动以后,就会检查自身状态,肯定是否能够从某个成员那里进行同步。若是不行的话,它会尝试从副本的另外一个成员那里进行完整的数据复制。这个过程就是初始化同步(initial syncing)。
init sync过程包含以下步骤
总结起来就是从其余节点同步全量数据,而后不过从Primary的local.oplog.rs集合里查询最新的oplog并应用到自身。
查询固定集合使用的tailable cursor(docs.mongodb.com/manual/core…)
Primary选举除了在复制集初始化时发生,还有以下场景
Primary的选举受节点间心跳、优先级、最新的oplog时间等多种因素影响。
复制集成员间默认每2s会发送一次心跳信息,若是10s未收到某个节点的心跳,则认为该节点已宕机;若是宕机的节点为Primary,Secondary(前提是可被选为Primary)会发起新的Primary选举。
心跳是为了知道其余成员状态,哪一个是主节点,哪一个能够做为同步源,哪一个挂掉了等等信息
成员状态:
最新optime(最近一条oplog的时间戳)的节点才能被选为主,请看上面对oplog.rs的分析。
只有大多数投票节点间保持网络连通,才有机会被选Primary;若是Primary与大多数的节点断开链接,Primary会主动降级为Secondary。当发生网络分区时,可能在短期内出现多个Primary,故Driver在写入时,最好设置大多数成功的策略,这样即便出现多个Primary,也只有一个Primary能成功写入大多数。
默认状况下,复制集的全部读请求都发到Primary,Driver可经过设置Read Preference来将读请求路由到其余的节点。
默认状况下,Primary完成写操做即返回,Driver可经过设置Write Concern来设置写成功的规则。
以下的write concern规则设置写必须在大多数节点上成功,超时时间为5s。
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: majority, wtimeout: 5000 } }
)
复制代码
上面的设置方式是针对单个请求的,也能够修改副本集默认的write concern,这样就不用每一个请求单独设置。
cfg = rs.conf()
cfg.settings = {}
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)
复制代码
Primary执行了一个写请求以后挂了,可是备份节点尚未来得及复制此次操做。新选举出来的主节点结就会漏掉此次写操做。当旧Primary恢复以后,就要回滚部分操做。
好比一个复制集存在两个数据中心,DC1中存在A(primary),B两个节点,DC2中存在C,D,E这三个节点。 若是DC1出现了故障。其中DC1这个数据中心的最后的操做是126,可是126没有被复制到另外的数据中心。因此DC2中服务器最新的操做是125
DC2的数据中心仍然知足副本集大多数的要求(5台,DC2有3台),所以其中一个会被选举成为新的主节点,这个节点会继续处理后续的写入操做。 当网络恢复以后,DC1中心的服务器就会从其余服务器同步126以后的操做,可是没法找到。这种时候DC1中的A,B就会进入回滚过程。
回滚回将失败以前未复制的操做撤销。拥有126操做的服务器会在DC2的服务器的oplog寻找共同的操做点。这里会定位125,这是两个数据中心相匹配的最后一个操做。
这时,服务器会查看这些没有被复制的操做,将受这些操做影响的文档写入一个.bson文件,保存在数据目录下的rollback目录中。
若是126是一个更新操做,服务器回将126更新的文档写入collectionName.bson文件。若是想要恢复被回滚的操做,可使用mongorestore命令。