副本集的组成
- 1、同步
- 1.1、初始化同步
- 1.2、复制
- 1.3、处理过时数据
- 2、心跳
- 2.1、成员状态
- 3、选举
- 4、回滚
1、同步
复制是指在多台服务器上保持相同的数据副本。MongoDB 实现此功能的方式是保存操作日志(oplog),其中包含了主节点执行的每一次写操作。oplog 是存在于主节点 local 数据库中的一个固定集合。从节点通过查询此集合以获取需要复制的操作。
每个从节点都维护着自己的 oplog,用来记录它从主节点复制的每个操作。这使得每个成员都可以被用作其他成员的同步源,如图所示。从节点从同步源中获取操作,将其应用到自己的数据集上,然后再写入 oplog中。如果应用某个操作失败(只有在基础数据已损坏或数据与主节点不一致时才会发生这种情况),则从节点会停止从当前数据源复制数据。
如果一个从节点由于某种原因而停止运行,那么当它重新启动后,就会从 oplog 中的最后一个操作开始同步。由于这些操作是先应用到数据上然后再写入 oplog,因此从节点可能会重复已经应用到其数据上的操作。MongoDB在设计时就考虑到了这种情况:将 oplog 中的同一个操作执行多次与只执行一次效果是一样的。oplog 中的每个 操作都是幂等的。也就是说,无论对目标数据集应用一次还是多次,oplog 操作都会产生相同的结果。
由于 oplog 的大小是固定的,因此它只能容纳一定数量的操作。通常来说,oplog 使用空间的速度与系统写入的速度差不多:如果在主节点上每分钟写入 1KB 的数据,那么 oplog 就会以每分钟 1KB 的速度被填满。不过,也有一些例外:如果一个操作会影响多个文档,比如删除多个文档或导致多文档更新,那么这个操作将被分解为许多oplog 条目。主节点上的单个操作将为每个受影响的文档分解一个 oplog 操作。因此,如果使用 db.coll.remove() 从集合中删除 1 000 000 个文档,那么 oplog中就会有 1 000 000 条操作日志,每条日志对应一个被删除的文档。如果进行大量的批量操作,那么 oplog 可能会比你预期的更快被填满。
在大多数情况下,默认的 oplog 大小就足够了。如果预 测副本集的工作负载属于以下模式之一,那么你可能会希望创建一个大于默认值的 oplog。相反,如果应用程序主要执行读操作而执行很少的写操作,那么一个较小的oplog 就足够了。以下这些工作负载可能会需要更大的oplog。
- 一次更新多个文档:
为了保持幂等性,oplog 必须将一个多文档更新转换为多个单独的操作。这可能会占用大量的 oplog 空间,但相应的数据大小和数据的磁盘使用量不会增加。 - 删除的数据量与插入的数据量相同
如果删除的数据量与插入的数据量大致相同,那么数据库的磁盘使用量不会显著增加,但是操作日志的大小可能会非常大。 - 大量的就地(in-place)更新
如果很大一部分的工作负载是不增加文档大小的更新,那么数据库会记录大量操作,但磁盘上的数据量不会改变。
在 mongod 进程创建 oplog 之前,可以使用oplogSizeMB 选项指定其大小。然而,在第一次启动副本集成员后,只能使用“更改 oplog 大小”这个流程来更改 oplog 的大小。
MongoDB 中存在两种形式的数据同步:初始化同步用于向新成员中添加完整的数据集,复制用于将正在发生的变更应用到整个数据集。
1.1、初始化同步
MongoDB 在执行初始化同步时,会将所有数据从副本集中的一个成员复制到另一个成员中。当一个副本集成员启动时,它会检查自身的有效状态,以确定是否可以开始从其他成员中同步数据。如果状态有效,它就会尝试从该副本集的另一个成员中复制数据的完整副本。这一过程有几个步骤,可以从 mongod 的日志中看到。
首先,MongoDB 会克隆除 local 数据库之外的所有数据库。mongod 会扫描源数据库中的每个集合,并将所有数据插入目标成员上这些集合的对应副本中。在开始克隆操作之前,目标成员上的任何现有数据都将被删除。
只有当你不再需要数据目录中的数据或者已经将数据移到其他地方时,才对一个成员进行初始化同步,因为在初始化同步时 mongod 首先会将其全部删除。
在 MongoDB 3.4 及之后的版本中,初始化同步在为每个集合复制文档时会创建集合中的所有索引(在早期版本中,只有 “_id” 索引会在此阶段创建)。此过程还会在数据复制期间提取新添加的 oplog 记录,因此为了在这一阶段存储这些记录,应该确保目标成员在 local 数据库中有足够的磁盘空间。
一旦所有的数据库都被克隆,mongod 就会使用这些来自同步源的 oplog 记录来更新它的数据集以反映副本集的当前状态,并将复制过程中发生的所有变更应用到数据集上。这些变更可能包括任何类型的写入(插入、更新和删除),而此过程可能意味着 mongod 必须重新克隆某些被克隆程序移动并因此丢失的文档。
如果某些文档必须被重新克隆,日志中就会有下面这样的内容。根据流量的多少以及同步源上发生的操作类型,有可能存在也有可能不存在丢失的对象:
Mon Jan3015:38:36[rsSync]oplog sync1of3Mon Jan3015:38:36[rsBackgroundSync]replSet syncing to:server-1:27017Mon Jan3015:38:37[rsSyncNotifier]replset setting oplog notifier to server-1:27017Mon Jan3015:38:37[repl writer worker2]replication updateofnon-modfailed:{ts:Timestamp1352215827000|17,h:-5618036261007523082,v:2,op:"u",ns:"db1.someColl",o2:{_id:ObjectId('50992a2a7852201e750012b7')},o:{$set:{count.0:2,count.1:0}}}Mon Jan3015:38:37[repl writer worker2]replication info adding missing object Mon Jan3015:38:37[repl writer worker2]replication missing object not found on source.presumably deleted laterinoplog这时,数据应该与主节点上的数据集完全匹配。成员在完成初始化同步后会过渡到正常同步流程,这使其成了从节点。
从操作者的角度来看,进行初始化同步的过程非常容易:只需用一个干净的数据目录启动 mongod。然而,更推荐从备份中进行恢复。从备份中恢复通常比通过 mongod 复制所有的数据要快。
还有一点,克隆可能会破坏同步源的工作集。在许多情况下,某些数据的子集经常会被访问,因而这部分数据总是存在于内存中(因为操作系统经常对其进行访问)。执行初始化同步会强制此成员将其所有数据分页加载到内存中,从而“驱逐”那些经常使用的数据。当那些通常由RAM 中的数据进行处理的请求突然被迫转到磁盘时,可能此成员的速度会显著降低。不过,对于那些小型数据集和性能较好的服务器,初始化同步是一个简单易用的选择。
进行初始同步时一个最常见的问题就是时间过长。在这种情况下,新成员可能会从同步源的 oplog 末尾“脱离”:由于同步源的 oplog 已经覆盖了成员继续复制所需的数据,因此新成员会远远落后于同步源并且无法再跟上。
除了在不太忙的时候尝试初始化同步或从备份进行恢复之外,没有其他方法可以解决这个问题。如果成员已经脱离了同步源的 oplog,那么初始化同步将无法进行。
1.2、复制
MongoDB 执行的第二种同步是复制。从节点成员在初始化同步之后会持续复制数据。它们从同步源复制 oplog,并在一个异步进程中应用这些操作。从节点可以根据需要自动更改同步源,以应对 ping 时间及其他成员复制状态的变化。有一些规则可以控制给定节点从哪些成员进行同步。例如,拥有投票权的副本集成员不能从没有投票权的成员那里同步数据,从节点不能从延迟成员和隐藏成员那里同步数据。后续章节会讨论副本集成员的选举以及不同的成员种类。
1.3、处理过时数据
如果某个从节点远远落后于同步源当前的操作,那么这个从节点就是过时的。过时的从节点无法赶上同步源,因为同步源上的操作过于领先了:如果继续同步,从节点就需要跳过一些操作。这种情况可能发生在以下场景中:从节点服务器停止运行,写操作超过了自身处理能力,或者忙于处理过多的读请求。
当一个从节点过期时,它将依次尝试从副本集中的每个成员进行复制,看看是否有成员拥有更长的 oplog 以继续进行同步。如果没有一个成员拥有足够长的 oplog,那么该成员上的复制将停止,并且需要重新进行完全同步或从最近的备份中恢复。
为了避免出现不同步的从节点,让主节点拥有一个比较大的 oplog 以保存足够多的操作日志是很重要的。一个更大的 oplog 会占用更多的磁盘空间,但通常这是一个很好的折中,因为磁盘空间一般来说比较便宜,而且实际中使用的 oplog 只有一小部分,所以不会占用太多的RAM。根据经验,oplog 应该可以覆盖两到三天的正常操作(复制窗口)。
2、心跳
每个成员需要知道其他成员的状态:谁是主节点?谁可以作为同步源?谁停止运行了?为了维护副本集的最新视图,所有成员每隔两秒会向副本集的其他成员发送一个心跳请求。
心跳请求用于检查每个成员的状态。
心跳的一个最重要的功能是让主节点知道自己是否满足副本集“大多数”的条件。如果主节点不再得到“大多数”节点的支持,它就会降级,成为一个从节点。
2.1、成员状态
成员还会通过心跳来传达自己的状态。前面已经讨论了两种状态:主节点和从节点。还有以下一些正常状态。
STARTUP:这是成员在第一次启动时的状态,这时 MongoDB正在尝试加载它的副本集配置。一旦配置被加载,它就转换到 STARTUP2 状态。STARTUP2:初始同步过程时会持续处于这个状态,通常只需几秒。成员会创造出几个线程来处理复制和选举,然后转换到下一个状态:RECOVERING。RECOVERING:此状态表明成员运行正常,但不能处理读请求。这可能是因为以下几种情况。- 在启动时,成员必须做一些检查以确保自己处于有效的状态,之后才能接受读请求。因此,在启动过程中,所有的成员在成为从节点之前都需要经历短暂的RECOVERING 状态。在处理一些耗时操作(比如压缩或者相应 replSetMaintenance 命令)时,成员也可能进入RECOVERING 状态。
- 如果一个成员远远落后于其他成员而无法赶上时,也会进入 RECOVERING 状态。通常来说,这是需要进行重新同步的无效状态。这时,该成员不会进入错误状态,因为它希望找到一个拥有足够长 oplog 的成员,从而引导自己回到非过时状态。
ARBITER:这是仲裁者节点独有的一个特殊状态,并且其在正常运行期间应该始终处于这个状态。
以下一些状态表明系统存在问题。
DOWN:如果一个成员被正常启动,但后来变为不可访问,那么就会进入这种状态。注意,被报告为 DOWN 状态的成员实际上可能仍在运行,只是由于网络问题而无法访问。UNKNOWN:如果一个成员从未能访问到另一个成员,那么就不知道它处于什么状态,因此会将其报告为 UNKNOWN。这通常表示这个未知成员已停止运行或两个成员之间存在网络问题。REMOVED:这个状态表示此成员已被从副本集中移除。如果移除的成员被添加回副本集,它就会转换回“正常”的状态。ROLLBACK:当成员正在回滚数据时会处于此状态。在回滚过程结束时,服务器会转换回 RECOVERING 状态,然后成为从节点。
3、选举
当一个成员无法访问到主节点(而且本身有资格成为主节点)时,便会申请选举。申请选举的成员会向其所能访问到的所有成员发出通知。如果这个成员不适合作为主节点,那么其他成员会知道原因:可能这个成员的数据落后于副本集,或者已经有一个主节点在申请选举,而那个失败的成员无法访问到此节点。在这些情况下,其他成员将投票反对该成员的申请。
假如没有理由反对,其他成员就会为申请当选的成员投赞成票。如果申请选举的成员从副本集中获得了大多数选票,选举就成功了,该成员将过渡到 PRIMARY 状态。如果没有获得大多数选票,那么它会继续处于从节点状态,以后可能会试图再次成为主节点。主节点会一直处于主节点状态,直到不能满足“大多数”的要求、停止运行、降级,或者副本集被重新配置为止。
如果网络状态正常并且大多数服务器正常运行,那么选举过程应该是很快的。如果主节点不可用,那么两秒(由于前面提到过心跳的间隔是两秒)之内就会有成员发现这个问题,它会立即开始一个选举,而这个过程应该只需要几毫秒。然而,实际情况不会这么理想:选举可能由网络问题或服务器过载导致的响应过慢而触发。在这些情况下,选举过程可能需要更多的时间,甚至会花几分钟。
4、回滚
上一节所描述的选举过程意味着,如果主节点执行一个写操作之后停止了运行,而从节点还没来得及复制此操作,那么新选举出的主节点可能会丢失这个写操作。假设有两个数据中心,其中一个数据中心拥有一个主节点和一个从节点,另一个数据中心拥有 3 个从节点,如图所示。
如图下图所示,假设在两个数据中心之间发生了网络分区故障,其中第一个数据中心最后的操作是 126,但是该操作还没有复制到另一个数据中心的服务器。
另一个数据中心的服务器仍然可以满足副本集的“大多数”要求(一共 5 台服务器,3 台即可满足要求)。因此,其中一台可能会被选为主节点。这个新的主节点会开始进行自己的写入请求处理,如下图所示。
当网络恢复后,第一个数据中心的服务器会寻找 126 号操作以开始与其他服务器的同步,但无法找到这个操作。当这种情况发生时,A 和 B 将开始一个名为回滚(rollback)的过程。回滚用于撤销在故障转移前未复制的操作。具有 126 号操作的服务器会在另一个数据中心服务器的 oplog 中寻找公共的操作点。它们会发现 125号操作是相互匹配的最后一个操作。下图展示了oplog 的情况。A 显然是在复制 126 至 128 号操作之前崩溃的,所以这些操作不在 B 上,而 B 有更新的操作。A 在恢复同步之前必须回滚这 3 个操作。
这时,服务器会遍历这些操作,并将受这些操作影响的每个文档写入一个 .bson 文件,保存在数据目录下的rollback 目录中。因此,如果 126 是一个更新操作,那么被 126 号操作所更新的文档会被写入<collectionName>.bson 文件中。然后会从当前主节点的版本中复制这个文档。
下面是一个典型的回滚过程所产生的日志:
Fri Oct706:30:35[rsSync]replSet syncing to:server-1Fri Oct706:30:35[rsSync]replSet our last op time written:Oct706:30:05:3Fri Oct706:30:35[rsSync]replset source'sGTE:Oct706:30:31:1Fri Oct706:30:35[rsSync]replSet rollback0Fri Oct706:30:35[rsSync]replSetROLLBACKFri Oct706:30:35[rsSync]replSet rollback1Fri Oct706:30:35[rsSync]replSet rollback2FindCommonPoint Fri Oct706:30:35[rsSync]replSet info rollback our last optime:Oct706:30:05:3Fri Oct706:30:35[rsSync]replSet info rollback their last optime:Oct706:30:31:2Fri Oct706:30:35[rsSync]replSet info rollback diffinendoflog times:-26seconds Fri Oct706:30:35[rsSync]replSet rollback found matching events at Oct706:30:03:4118Fri Oct706:30:35[rsSync]replSet rollback findcommonpoint scanned:6Fri Oct706:30:35[rsSync]replSet replSet rollback3fixup Fri Oct706:30:35[rsSync]replSet rollback3.5Fri Oct706:30:35[rsSync]replSet rollback4n:3Fri Oct706:30:35[rsSync]replSet minvalid=Oct706:30:314e8ed4c7:2Fri Oct706:30:35[rsSync]replSet rollback4.6Fri Oct706:30:35[rsSync]replSet rollback4.7Fri Oct706:30:35[rsSync]replSet rollback5d:6u:0Fri Oct706:30:35[rsSync]replSet rollback6Fri Oct706:30:35[rsSync]replSet rollback7Fri Oct706:30:35[rsSync]replSet rollback done Fri Oct706:30:35[rsSync]replSetRECOVERINGFri Oct706:30:36[rsSync]replSet syncing to:server-1Fri Oct706:30:36[rsSync]replSetSECONDARY服务器一开始是从另一个成员(本例中为 server-1)进行同步的,但发现在同步源上找不到它的最新操作。此时,它会进入到 ROLLBACK 状态(replSetROLLBACK)从而启动回滚过程。
接下来,服务器会找到两个 oplog 之间的共同点,也就是 26 秒之前的一个操作,然后从 oplog 中撤销最近 26秒内的操作。回滚完成后,它会转换为 RECOVERING状态,并进行正常的同步。
要想将被回滚的操作应用到当前的主节点,首先需要使用mongorestore 将它们加载到一个临时集合中:
$ mongorestore--db stage--collection stuff \/data/db/rollback/important.stuff.2018-12-19T18-27-14.0.bson然后检查文档(使用 shell),并将它们与相应集合的当前内容进行比较。如果有人在被回滚的成员上创建了一个“普通”索引,而在当前主节点上创建了一个唯一索引,那么就要确保回滚数据中没有任何重复文档,如果有的话则需要进行处理。
如果希望保留 staging 集合中某个版本的文档,那么可以将它加载到主集合中:
>staging.stuff.find().forEach(function(doc){prod.stuff.insert(doc);})对于那些只允许插入操作的集合,可以直接将回滚文档加载到集合中。然而,如果在集合中执行更新操作,则需要更加小心地对回滚数据进行合并处理。
一个经常被误用的成员配置选项是每个成员的投票数量设置。改变成员的投票数量通常不会得到想要的结果,并且会导致大量的回滚。(这就是为什么它没有包含在第 10章的成员配置选项列表中。)除非做好了定期处理回滚的准备,否则不要更改成员的投票数量。
当回滚失败时
在旧版本的 MongoDB 中,如果要回滚的内容太多,则可能导致回滚无法执行。从 MongoDB 4.0 开始,回滚的数据量就没有限制了。在 MongoDB 4.0 之前,如果要回滚的数据量超过 300MB 或者要回滚的操作超过 30 分钟,那么回滚可能会失败。在这些情况下,对于回滚失败的节点,必须重新进行同步。
这种情况最常见的原因是从节点存在同步延迟,而主节点停止运行了。如果其中一个从节点变成了主节点,那么之前主节点中的许多操作会丢失。为了确保在回滚过程中不会失败,最好的方法是让从节点的数据尽可能保持最新。