mongodb副本集
使用复制可以将数据副本保存到多台服务器上,即使一台或多台服务器出错,也可以保证应用程序正常运行和数据安全。
在mongodb中,创建一个副本集之后就可以使用复制功能了。副本集是一组服务器,其中有一个主服务器,用于处理客户端请求;还有多个备份服务器,用于保存主服务器的数据副本。如果主服务器崩溃了,备份服务器会自动将其中一个成员升级为新的主服务器。
客户端不能在备份节点执行写操作
默认情况下,客户端不能从备份节点读取数据。在备份节点上显式地执行setSlaveOK之后,客户端就可以从备份节点读取数据了。
副本集中很重要的一个概念是大多数:选择主节点时需要由大多数决定,主节点只有在得到大多数支持时才能继续作为主节点。写操作被复制到大多数成员时这个写操作就是安全的。这里的大多数被定义为副本集中一半以上的成员。
假设一个包含5个成员的副本集,其中3个成员不可用,仍然有2个可以正常工作,剩余的2个成员已经无法达到副本集大多数的要求(大多是为3个),所以它们无法选举主节点。如果这2个成员中有一个是主节点,当它注意到它无法得到大多数成员支持时,就会从主节点上退位。几秒钟之后,这个副本集中会包含2个备份节点和3个不可达节点。
选举机制
当一个备份节点无法与主节点连通时,它就会联系其他的副本集成员将自己选举为主节点。其他成员会做几项理性检查:
- 其他成员自身是否能和主节点连通
- 希望被选举为主节点的备份节点数据是否最新
- 有没有其他更高优先级的成员被选举为主节点
如果要求被选举为主节点的成员能够得到副本集中大多数成员的投票,它就会成为主节点。如果大多数成员中只有一个否决了本次选举,选举就会被取消。如果成员发现任何原因,表明当前希望成为主节点的成员不应该成为主节点,那么它就会否决此次的选举。
一张否决票相当于10000张赞成票,即值为-10000
复制操作是严格按时间排序的,所以候选人的最后一条操作要比它能连通的其他所有成员更晚(或者与其他成员相等)。
成员配置选项
仲裁者
很多人的应用程序使用量比较小,并不想保存三份数据副本。两份副本已经足够了,保存三份副本的话纯粹是浪费人力,物力,财力。对于这种部署,mongodb支持一种特殊类型的成员,成为仲裁者。仲裁者的唯一作用就是参与选举。仲裁者并不保存数据,也不会为客户端提供服务,它只是为了帮助具有两个成员的副本集能够满足大多数这个条件。
由于仲裁者并不需要履行传统mongod服务器的责任,所以可以将仲裁者作为轻量级进程,运行在配置比较差的服务器上。如果可以,可以将仲裁者放在单独的故障域中,与其他成员分开。这样他就可以以外部视角来看待副本集中的成员了。
可以使用rs.addArb("$ip:$port")
或在成员配置中指定arbiterOnlt选项rs.add({"_id":$id, "host":"$ip:$port", "arbiterOnly":true})
。
成员一旦以仲裁者的身份添加到副本集中,它就永远只能是仲裁者,无法将仲裁者重新配置为非仲裁者,反之亦然。
使用仲裁者的另一个好处是,如果节点拥有的节点数是偶数,仲裁者可以投出决定胜负的关键一票。
优先级
优先级用于表示一个成员渴望成为主节点的程度,取值范围是1-100,默认是1。将优先级设为0有特殊含义:优先级为0的成员永远不能够成为主节点,这样的成员被称为被动成员。拥有最高优先级的成员会优先选举为主节点(只要它能够得到集合中大多数成员的赞成票,并且数据是最新的)
隐藏成员
客户端不会向隐藏成员发送请求,隐藏成员也不会作为复制源。因此,很多人会将不够强大的服务器或备份服务器隐藏起来。只有优先级为0的成员才能被隐藏。即设置priority=0
和hidden=true
延迟备份节点
数据可能会因为人为错误而遭受毁灭性的破坏,为了防止这类问题,可以使用slaveDelay设置一个延迟的备份节点。
延迟备份节点会比主节点延迟指定的时间(秒),这是有意为之的。这样,如果有人不小心摧毁了你的主集合,还可以将数据从先前的备份中恢复过来。slaveDelay要求成员的优先级是0,如果应用会将读请求路由到备份节点,应该将延迟备份节点隐藏掉,以免读请求被路由到延迟备份节点。
不创建索引的成员
有时备份节点并不需要与主节点拥有相同的索引,甚至可以没有索引。如果某个备份节点的用途仅仅是处理数据备份或者离线的批量任务,那么你可能希望在它的成员配置中指定"buildIndexes":false
。这个选项可以阻止备份节点创建索引。这是一个永久选项,这个成员将永远无法恢复为可以创建索引的成员。要求成员的优先级为0。
同步的过程
mongodb的复制功能是使用操作日志oplog实现的,操作日志包含了主节点的每一次写操作。oplog是主节点的local数据库中的一个固定集合。备份节点通过查询这个集合就可以知道需要进行复制的操作。
每个备份节点都维护着自己的oplog,记录着每一次从主节点复制数据的操作。这样,每个成员都可以作为同步源提供给其他成员使用。
备份节点从当前使用的同步源中获取需要执行的操作,然后在自己的数据集上执行这些操作,最后再将这些操作写入自己的oplog。如果某个备份节点挂掉了,当它重新启动后,就会自动从oplog中最后一个操作开始进行同步。由于复制操作的过程是先复制数据再写入oplog,所以备份节点可能会在已经同步过的数据上再次执行复制操作。mongodb在设计之初就考虑到了这种情况:将oplog的同一个操作执行多次与只执行一次的效果是一样的。
由于oplog的大小是固定的,它只能保存特定数量的操作日志。如果单个操作会影响多个文档,例如db.coll.remove()
,删除了10000个文档,那么oplog中就会有10000条操作日志,每条日志对应一个被删除的文档。如果执行大量的批量操作,oplog很快就会被填满。
选择同步源
MongoDB 默认是采取级联复制的架构,就是默认不一定选择主库作为自己的同步源,如果不想让其进行级联复制,可以通过 chainingAllowed 参数来进行控制。在级联复制的情况下,你也可以通过 replSetSyncFrom 命令来指定你想复制的同步源。所以这里说的同步源其实相对于从库来说就是它的主库。那么同步源的选取流程是怎样的呢?
MongoDB 从库会在副本集其他节点通过以下条件筛选符合自己的同步源。
- 如果设置了 chainingAllowed 为 false,那么只能选取主库为同步源
- 找到与自己 ping 时间最小的并且数据比自己新的节点(在副本集初始化的时候,或者新节点加入副本集的时候,新节点对副本集的其他节点至少 ping 两次)
- 该同步源与主库最新 optime 做对比,如果延迟主库超过 30s,则不选择该同步源。
在第一次的过滤中,首先会淘汰比自己数据还旧的节点。如果第一次没有,那么第二次需要算上这些节点,防止最后没有节点可以做为同步源了。最后确认该节点是否被禁止参与选举,如果是则跳过该节点。通过上述筛选最后过滤出来的节点作为新的同步源。
其实 MongoDB 同步源在除了在 Initial Sync 和增量复制 的时候选定之后呢,并不是一直是稳定的,它可能在以下情况下进行变更同步源:
- ping 不通自己的同步源
- 自己的同步源角色发生变化
- 自己的同步源与副本集任意一个节点延迟超过 30s
删除除local以外的所有数据库
全量同步
- 在创建的集合的时候同时创建了索引(与主库一样),在 MongoDB 3.4 版本之前只创建 _id 索引,其他索引等待数据 copy 完成之后进行创建。
- 在创建集合和拷贝数据的同时,也将 oplog 拷贝到本地 local 数据库中,等到数据拷贝完成之后,开始应用本地 oplog 数据。
克隆数据是耗时操作,如果克隆完成后,新成员数据同步速度赶不上同步源的变化速度,同步源可能会将新成员需要复制的某些数据oplog覆盖掉。
备份节点远远落后于同步源当前的操作,那么这个备份节点就是陈旧的。
当一个备份节点陈旧之后,它会查看副本集中的其他成员,如果某个成员的oplog足够详尽,可以用于处理那些落下的操作,就从这个成员处进行同步。如果任何一个成员的oplog都没有参考价值,那么这个成员上的复制操作就会中止,这个成员需要重新进行全量同步。
增量同步
- Sencondary 初始化同步完成之后,开始增量复制,通过 produce 线程在 Primary oplog.rs 集合上建立 cursor,并且实时请求获取数据。
- Primary 返回 oplog 数据给 Secondary。
- Sencondary 读取到 Primary 发送过来的 oplog,将其写入到队列中。
Sencondary 的同步线程会通过 tryPopAndWaitForMore 方法一直消费队列,当每次达到一定的条件之后,条件如下:
- 总数据大于 100MB
- 已经取到部分数据但没到 100MB,但是目前队列没数据了,这个时候会阻塞等待一秒,如果还没有数据则本次取数据完成。
上述两个条件满足一个之后,就会将数据给 prefetchOps 方法处理,prefetchOps 方法主要将数据以 database 级别切分,便于后面多线程写入到数据库中。如果采用的 WiredTiger 引擎,那这里是以 Docment ID 进行切分。
- 最终将划分好的数据以多线程的方式批量写入到数据库中(在从库批量写入数据的时候 MongoDB 会阻塞所有的读)。
- 然后再将 Queue 中的 Oplog 数据写入到 Sencondary 中的 oplog.rs 集合中。
心跳
每个成员需要知道其他成员的状态:哪个是主节点?哪个可以作为同步源?哪个挂掉了?为了维护集合的最新视图,每个成员每隔2秒就会向其他成员发送一个心跳请求。心跳请求的信息量很小,用于检查每个成员的状态。
心跳最重要的功能之一就是让主节点知道自己是否满足集合大多数的条件。如果主节点不再得到大多数服务器的支持,它就会退位,变成备份节点。
成员状态
各个成员会通过心跳将自己当前的状态告诉其他成员。
- STARTUP
成员刚启动时处于这个状态,在这个状态下,mongodb会尝试加载成员的副本集配置。配置加载成功后,就进入STARTUP2状态。
- STARTUP2
整个初始化同步过程都处于这个状态。当初始化同步完成后,进入RECOVERING状态。
- RECOVERING
这个状态表明成员运转正常,但是暂时还不能处理读取请求。在处理非常耗时的操作时,成员也可能进入RECOVERING状态,比如压缩或replSetMaintenance
命令。当一个成员和其他成员脱节时,也会进入这个状态,通常来说,这时这个成员处于无效状态,需要重新同步。但是,成员这时并没有进入错误状态,因为它期望一个拥有足够详尽oplog的成员,然后继续同步oplog,然后回到正常状态。
- ARBITER
仲裁者状态。
- DOWN
不可达的成员会被报告为DOWN状态。它有可能仍然是运行状态,只是网络不可达。
- UNKNOWN
别的成员暂时不知道该成员的状态,会被报告为该状态
- REMOVED
成员被移除副本集时的状态
- ROLLBACK
正在进行数据回滚的状态,回滚完成后会转换为RECOVERING状态
- FATAL
一个成员发生了不可挽回的错误,也不再尝试恢复正常。应该查看详细日志查明为何该成员处于FATAL状态。通常应该重启服务器,进行重新同步或者从备份中恢复
- PRIMARY
主节点状态
- SECONDARY
备份节点状态
选举
当一个节点无法到达主节点时,它就会申请被选举为主节点。希望被选举为主节点的成员会向它能到达的所有成员发通知。如果这个成员不符合候选人的要求:
- 这个成员的数据落后于副本集
- 有一个运行中的主节点(那个力求被选举为主节点的成员无法到达这个主节点)
在这些情况下,其他成员不会允许进行选举。
加入没有反对的理由,其他成员就会对这个成员进行选举投票。如果这个成员得到副本集中大多数成员的赞成票,它就会选举成功,会转换到主节点状态。如果达不到大多数成员的要求,就会选举失败,仍然处于备份节点的状态。之后还可以再次申请被选举为主节点。主节点若不再满足大多数节点的要求,会退位。
心跳超时时间为20秒,若选举达成平局,每个成员需要等待30秒才能开始下一次选举。
回滚
我们知道在发生切换的时候是有可能造成数据丢失的,主要是因为主库宕机,但是新写入的数据还没有来得及同步到从库中,这个时候就会发生数据丢失的情况。
那针对这种情况,MongoDB 增加了回滚的机制。在主库恢复后重新加入到复制集中,这个时候老主库会与同步源对比 oplog 信息,这时候分为以下两种情况:
- 在同步源中没有找到比老主库新的 oplog 信息。
- 同步源最新一条 oplog 信息跟老主库的 optime 和 oplog 的 hash 内容不同。
针对上述两种情况 MongoDB 会进行回滚,回滚的过程就是逆向对比 oplog 的信息,直到在老主库和同步源中找到对应的 oplog,然后将这期间老主库的 oplog 全部记录到 rollback 目录里的文件中,并将这期间的oplog撤销。
之后可以将这些被回滚的操作应用到当前的主节点。
以下情况会终止回滚:
- 对比老主库的 optime 和同步源的 optime,如果超过了 30 分钟,那么放弃回滚。
- 在回滚的过程中,如果发现单条 oplog 超过 512M,则放弃回滚。
- 如果有 dropDatabase 操作,则放弃回滚。
- 最终生成的回滚记录超过 300M,也会放弃回滚。
这种情况最常见的原因是备份节点远远落后于主节点,而这时主节点挂了。如果其中一个备份节点成为主节点,这个主节点与旧的主节点相比,缺少很多操作。为了保证成员不会在备份中失败,最好的方式是保持备份节点的数据尽可能的最新。
参考文章
https://www.infoq.cn/article/...
《mongodb权威指南》