
副本集的组成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 sourcesGTE: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 minvalidOct706: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 分钟那么回滚可能会失败。在这些情况下对于回滚失败的节点必须重新进行同步。这种情况最常见的原因是从节点存在同步延迟而主节点停止运行了。如果其中一个从节点变成了主节点那么之前主节点中的许多操作会丢失。为了确保在回滚过程中不会失败最好的方法是让从节点的数据尽可能保持最新。