复制(Replication) 是MongoDB内部的一个重要模块。复制模块将数据持续的从主节点(Primary)同步到从节点(Secondary),当主节点宕机后,从节点可以被选举成为新的主节点。这个功能对于用户是透明的,因为客户端会重试节点状态变更导致的错误。MongoDB的多个节点通过复制模块组成复制集(replica sets)。
复制集是一组MongoDB节点,其中包含一个主节点和多个从节点。主节点负责处理所有的写请求,用户可以通过设置slaveOK参数读从节点,slaveOK并不是默认开启的参数。
稳态复制(Steady State Replication)
复制集在正常运行中的状态被称为稳态复制。这个过程中,有一个主节点和多个从节点,每个从节点从主节点同步数据,或者从另一个从节点链式同步数据。
主节点的职责(Life as a Primary)
写操作(Doing a Write)
当用户执行写操作时,主节点就像独立节点(Standalone Mode)一样对数据库执行写操作。副本集节点与独立节点写入的一个不同之处在于,副本集节点有一个OpObserver
,只要对数据库执行写操作,副本集节点 就会在oplog中插入一条文档,以描述该写操作。oplog是local
数据库中一个CappedCollection,名为oplog.rs,WiredTiger有专门的针对Oplog进行优化,oplog集合也是MongoDB数据库中唯一一个不包含_id字段的集合。
如果一个写操作包含多个子操作,则每个子操作都有自己的oplogEntry。例如,使用隐式集合创建的插入操作(对一个不存在的表执行插入操作)会创建两个oplog,一个用于建表,一个用于插入。
任何一条Oplog都是幂等的,因此update操作中的$inc算符会被MongoDB改写为$set算符。从节点通过拉取其他节点的Oplog并回放来进行稳态复制。
写操作可以指定write concern。当一个命令包含write concern时,该操作所在的线程会一直阻塞,直到操作产生的oplogEntries被write concern指定的节点复制。主节点需要跟踪从节点的最新状态(Oplog复制延时情况)来明确该命令何时返回。write concern可以指定要等待的节点数或多数节点(majority)。如果指定了majority,则写操作还将等待该操作已经包含在已提交的快照(committed snapshot)中,以便可以通过readConcern: { level: majority } 读取。
从节点的职责 (Life as a Secondary)
一般来说,从节点仅需要选择一个要从其同步数据的节点,即它们的同步源(sync source),从同步源的Oplog中拉取数据,然后将这些Oplog应用到本地的数据库中。
从节点在同步数据过程中还不断将自己的复制进度更新给自己的同步源,主节点利用从节点的同步进度判断当前写操作的write concern是否能被满足。
Oplog拉取(Oplog Fetching)
Oplog的拉取通过OplogFetcher模块完成。OplogFetcher模块不直接应用(应用指的是将Oplog写入本地数据库中)拉取到的Oplog,该模块将OplogEntries写入一个缓冲区中(OplogBuffer),有另一个线程从OplogBuffer中取出Oplog并应用。OplogBuffer是一个纯内存的阻塞式队列。
OplogFetcher的生命周期(Oplog Fetcher Lifecycle)
OplogFetcher是由BackgroundSync线程创建的,当节点处于SECONDARY状态时,BackgroundSync线程会持续运行。BackgroundSync处于一个while循环中,在每次循环中,它首先通过SyncSourceResolver选择一个同步源,然后启动OplogFetcher。在稳态复制时,OplogFetcher会持续地接收和处理来自同步源的Oplog。
OplogFetcher在运行过程中有很多情况会导致终止,比如它从同步源收到了一个错误码,再比如进程终止前BackgroundSync模块会退出,从而终止OplogFetcher。OplogFetcher每接收到一批Oplog,会校验Oplog的合法性。根据合法性校验的返回值,OplogFetcher可能会切换同步源,或者进入ROLLBACK状态,甚至退出进程。
Oplog Fetcher实现细节(Oplog Fetcher Implementation Details)
这里我们举个例子,以节点A作为同步源,节点B为要获取数据的节点。
OplogFetcher启动后,会首先与同步源节点A建立一个连接,通过这个连接,OplogFetcher会建立一个exhaust cursor来从节点A获取日志。这意味着,当节点B开始发出find和getMore命令后,节点A就将持续向节点B发送数据,而不需要节点B再发出getMore命令了。节点B的OplogFetcher发给源节点A的find命令满足下面的条件。
db.oplog.rs.find({ts:{$gte:X}}) // X 是上一批拉取到的Oplog的最后一条Oplog的时间戳
因此,find命令发出后,节点B每次应该都要收到返回数据。如果没有,那就说明节点A的日志是落后于节点B的,而不应该再作为B的源节点。如果返回了数据,但是返回的数据与B的最新Oplog不匹配,那就有两种可能性,一种是A相对于B来说太新了,A中最老的Oplog都比B中最新的Oplog新,另一种可能是B的Oplog已经与A分叉了,B需要ROLLBACK。
在收到find命令的响应时,从节点会检查响应中的元数据来判断同步源节点是否仍然符合资格,从节点主要检查两方面
-
该节点被选为同步源后有没有出现过回滚 -
同步源数据是否依然领先于本节点(同步源不一定是主节点,因此同步源可能比本节点落后)
awaitData: true, tailable: true
参数,以便之后的每轮请求不立即返回数据,而是在maxTimeMS到期前等待更多数据一起返回。如果在maxTimeMS到期后没有返回数据,说明OplogFetcher在本轮请求没有获得数据,将会等待下一轮数据请求批次到来。同步源选择过程(Sync Source Selection)
选择一个同步源候选者(Choosing a sync source candidate)
-
首先,从节点会检查进程内的TopologyCoordinator模块的副本集视图,以了解各节点的最新OpTime时间。如果某个节点的最新OpTime比主节点落后 maxSyncSourceLagSecs
秒,就不会被列为候选人。 -
然后,从节点会循环遍历每个节点,选择满足一系列琐碎的条件的最近节点。这里的“最近”取决于ping值。 -
如果没有节点满足这个上述条件,BackgroundSync会等待1s然后重新发起同步源选择过程。
同步源探测(Sync Source Probing)
-
如果同步源候选者没有Oplog或者发生错误,从节点就会将该同步源放入黑名单一段时间,重新寻找新的同步源候选者。 -
如果同步源候选者最老的OplogTime比本节点最新的OplogTime还要新,该节点仍然将该同步源候选者放入黑名单,由于该候选者太超前了。 -
在节点初始化、回滚或从异常关闭中恢复期间,节点会初始化一个特殊的OpTime,叫做 minValid
,节点在可以安全的提供读操作或者角色变为SECONDARY之前,Oplog必须应用到超过minValid。因此同步源的minValid也会被检查是否满足条件。 -
同步源的RollbackID也会被拉取并检查。
Oplog应用(Oplog Entry Application)
ReplBatcher
运行OplogBatcher
模块,OplogBatcher模块从oplog buffer中拉取oplog记录并创建下一个要应用(前面解释过,应用指的是将oplog回放到本地数据库中的过程)的batches。这些batches称为oplog applier batches,与oplog fetcher batches不同的是,oplog fetcher batches是生产者消费者模型中的生产者,oplog applier batches 是生产者消费者模型中的消费者。oplog fetcher batches是在从节点的oplog fetching过程中,由节点的同步源发送到该节点上的。一个batch中的操作会尽可能的被并行应用,但是并不是所有的操作都可以被并行应用的,比如说,一个dropDatabase操作必须和其他操作互斥。OplogApplier
负责应用oplog的具体工作。它运行在一个while循环中,每次循环做以下事情:-
获取下一批Oplog -
加PBWM(Parallel Batch Writer Mode)锁。 -
将 oplogTruncateAfterPoint
设置为节点上一次应用的optime(在此批次之前),以帮助节点在oplog写入的过程中异常关闭时恢复启动。 -
将这批oplog写入local.oplog.rs。 -
清除 oplogTruncateAfterPoint
并将minValid
设置为这批oplog中最后一条oplog的optime。在应用到minValid optime之前,数据库的状态相对于oplog是不一致的。 -
多个线程并行应用oplog, 这意味着同一批oplog不会被顺序执行。 -
每个批次中的操作会由不同的写线程执行,同一个线程中的操作串行执行。 -
同一个collection的oplog一定会被同一个线程处理。 -
不同collection的oplog可能会被分配到不同的线程处理。 -
每个线程会将相邻的insert操作合并为一个bulk执行以提高性能,其他操作单独执行。
-
-
存储引擎(wiredTiger/rocksDB) Flush WAL。 -
持久化”applied through” optime(一批oplog中的最后一条oplog的optime)。由于这批oplog已经应用完了,oplog和数据库数据保持一致了,因此此时可以更新 minValid
记录了。 -
通知存储引擎(mongo-wiredTiger/mongoRocks)更新oplog可见性。由于oplog是并行应用的,因此只有在完整应用完一批oplog之后,更新oplog可见性才是安全的,否则会有oplog 空洞出现。 -
析构这批oplog,并推进全局时间戳(和节点的lastAppliedOpTime)。
ReplicationCoordinatorExternalState
与存储层及其他节点通信。ExternalState模块拥有和管理所有复制模块的所有线程。TopologyCoordinator
负责维护集群的拓扑状态。在集群拓扑状态发生变化(任何导致isMaster命令结果变化)时,TopologyCoordinator更新其TopologyVersion。isMaster
命令在返回之前会等待TopologyVersion的更改。在进程退出时,如果是从节点,将进入静默模式:TopologyVersion自增1,并对isMaster
请求响应一个ShutdownInProgress错误,以便客户端停止下发新操作给该节点。TopologyCoordinator
是非阻塞的,处理大部分复制命令的请求和响应。-
检测其他节点是否存活(心跳) -
保持和主节点的同步(Oplog拉取) -
向自己的同步源更新同步进度(replSetUpdatePosition 命令)
OpTime
用于描述操作何时发生,其他节点通过对比自己和别人的最新的oplog的OpTime得知各自的同步进度。OpTime
包括一个64位逻辑时间戳和一个term字段。Term字段表示自副本集启动以来发生了多少次选举。Oplog Fetcher返回值处理(Oplog Fetcher Responses)
ReplSetMetadata
和OplogQueryMetadata
。(OplogQueryMetadata是4.0之后新加的数据结构,为了向后兼容,结构中存在一些临时冗余变量)ReplSetMetadata
last committed OpTime
(这里的committed说的是被raft majority提交,不是本地事务提交)OplogQueryMetadata
OplogFetcher
的响应。它包含:last committed OpTime
。该值的推进,会更新readConcern: majority
读到的快照。last committed OpTime
更新自己的,同时通知存储引擎层(mongo-wiredTiger/mongoRocks)推进oldestTimestamp以释放旧的快照。心跳(Heartbeats)
replica set name
与自己的replica set name
相同,否则,它将发送一个错误。接收方节点的TopologyCoordinator更新MemberHeartbeatData ,重置最后一次收到心跳的时间。-
无行为 -
优先级变更导致再选主 -
reconfig
提交点传播(Commit Point Propagation)
lastAppliedOptime
和lastDurableOpTime
的影响。lastAppliedOptime
或lastDurableOpTime
的最大值来推进提交点。这个OpTime必须大于当前commit point,以便主节点可以推进它。阻塞在writeConcern上的任何线程都会被唤醒,以检查它们现在是否满足了它们请求的writeConcern。_lastCommittedOpTime由
lastDurableOpTime`推进,意味着只有大多数已经被WAL持久化的Oplog才被认为是安全的。否则就使用lastAppliedOpTime推进_lastCommittedOpTime,表示只要写入大多数节点内存的Oplog就认为是安全的。lastAppliedOpTime
有相同的term,这是为了保证Oplog历史不会分叉。更新位置命令(Update Position Commands)
SyncSourceFeedback
对象,负责发送replSetUpdatePosition命令。-
包含每个副本集成员对象的optimes数组。该信息是来自ReplicationCoordinator的SlaveInfo信息。处于down状态的节点是不被包含在内的。每个节点包括: -
Last durable OpTime -
Last applied OpTime -
memberId -
ReplicaSetConfig版本
-
-
ReplSetMetadata。通常,它只出现在响应中,但是这里它也出现在请求中。
enableMajorityReadConcern Flag
readConcern: majority
在WT引擎中默认打开,如果副本集中部署“arbiters”节点,这会带来一个问题。比如在Secondary 节点故障的场景下,此时系统可以处理写操作,但是却无法推进“majority commit point多数提交点”,这会增加WT缓存压力,进而导出性能下降或者写Stalling。enableMajorityReadConcern
设置为false(eMRC=false),此时存储引擎将不再需要维护“majority commit point多数提交点”之前历史版本。副本集会将stableTimestamp
设置为最新的all_durable
时间戳 ,这意味着可以对稳定版本进行checkpoint,而不必等待“majority committed”后再做checkpoint。rollbackViaRefetch
算法进行回滚,而不是采用RTT(Recover to A Timestamp)算法回滚。如前面提到的,eMRC设置为false时,副本集会将stableTimestamp
设置为最新的all_durable
时间戳,这让我们可以不用等待多数提交,就可以创建checkpoint。因此,stableTimestamp 会超过majority 提交时间点,这不满足恢复到Recover to A Timestamp
算法的要求。rollbackViaRefetch
minValid
并继续从同步源节点拉取oplog直到达到minValid
,然后,将节点状态从RECOVERING状态切换到SECONDARY 状态。Recover To A Timestam
方法进行回滚 ,但是rollbackViaRefetch
,仍需要用于支持eMRC=false
场景保留了下来。readConcern: majority
为false的情况下仍然可以正常工作。eMRC=false
在跨分片的事务中是不被允许的。如果存在Arbitor节点则会导致持续写入但是无法被raft提交的情况,这会影响分布式事务。比如我们尝试提交一个跨分片事务,但是由于Arbitor节点的存在,shard上可能不存在大多数可写入数据的节点,因此事务就无法进入prepare状态。rollbackViaRefetch
算法不支持prepare的oplog,所以如果某个shard的eMRC=false
,无法进行跨分片事务操作。这种场景下prepareTransaction
会自动失败,从而阻止在eMRC=false的shard上进程prepare操作,由于事务还未prepare,因此此时终止是安全的。prepareTransaction
命令显示失败,在eMRC=false
的节点上,启动恢复过程中回放prepare的oplog时,节点会崩溃。直接崩溃是最直接的处理方式,避免了我们基于一个不稳定的checkpoint回放prepare oplogEntry需要面对的复杂度。这种场景下唯一的方法是修改eMRC=true
,然后重新执行恢复流程。事务(Transactions)
TransactionParticipant
类进行管理,这个类是Session的一个装饰器。任何线程尝试修改事务状态前,都必须使用正确的会话ID check out对应的Session。修改事务状态的操作包含“提交事务”,”中止事务”,和在事务中“增加新的更新操作”。在同一时间点只能有一个操作进行check out,使用这个Session的其他操作,必须等待前面的操作将Session check in后,才能重新check out。开启一个事务(Starting a Transaction)
startTransaction: true
参数,则会自动启动一个事务。同一个事务中的所有操作包含lsid
(标识一个session的唯一ID),txnNumber
和autocommit:false
参数。同一个Session上的txnNumber必须比之前事务的txnNumber要大,否则会抛出一个TransactionTooOld
的错误。txnState
更新为kInProgress
,通过维护txnState可以让我们对事务状态的合法性进行判断。最后,我们将前一个事务内存中的事务状态和其他的信息都进行重置。RecoveryUnit
上打开一个WriteUnitOfWork
启动一个存储引擎层的事务。RecoveryUnit
类负责保证数据的持久化,所有对存储引擎的数据更新必须走RecoveryUnit对象。同一个事务内,未提交的数据可以被本事务自身读到,但是无法被其他事务读到。在事务中添加操作(Adding Operations to a Transaction)
config.transactions
表是在支持retryable write特性时引入的,用来跟踪一个session上的retryable write和事务状态。当一个session被check out后,通过该表恢复事务的状态。阅读Recovering Prepared Transactions章节的内容,了解该表在事务恢复过程中如何工作的。单分片事务的提交流程(Committing a Single Replica Set Transaction)
applyOps
命令写入oplog中。由于一个applyOps
大小上限为16M,当它大于16MB时,事务则需要将oplog拆分成多个applyOps批次。OperationContext
在引擎层提交事务。这是通过在WUOW中调用commit实现的。一旦在WUOW中调用了commit,事务生命周期中所有的写操作都会被提交到存储引擎中。txnState
设置为kCommitted
,记录事务相关的其他指标,然后释放事务相关资源。单分片事务的终止流程(Aborting a Single Replica Set Transaction)
seesionTxnRecord
并且在Oplog中插入一条中止事务的oplogEntry。最后将txnState
状态设置为kAbortedWithoutPrepare
. 同时,将更新事务相关的统计指标并且重置TransactionParticipant
内存中保存的状态。abortTransaction
命令以外还有一些情况会导致事务被中止,比如,non-prepared事务遇到写冲突或者节点状态变更(主备倒换)。TransactionCoorinator
,也就是启动事务候收到第一个操作的shard节点。TransactionCoorinator
会协调所有参与此事务的shard最终提交或者终止这个事务。TransactionCoordinator
在被要求提交一个事务时,它必须保证所有参与事务的shard节点在提交前都处于prepared状态。协调者会给每个参与事务的shard发送一个内部命令prepareTransaction
来确保这点。TransactionCoordinator
发出commitTransaction
命令前,每个参与者shard执行prepareTransaction
命令的oplogEntry必须是多数提交的(这样来确保prepare 操作不会被回滚)。如果有一个shard在prepare时失败了,则协调者TransactionCoordinator
会通过发送abortTransaction
给所有参与的shard,通知这些shard事务中止,不用考虑这些shard是否已经prepared。prepareTransaction
命令执行之前,事务执行的过程与副本集事务是一样的。但是一旦事务成为Prepared状态,则不允许在事务中增加新的操作。中止一个事务的Prepared状态只能通过commitTransaction
或者abortTransaction
命令。这意味着Prepared状态的事务必须 不会被宕机和主从切换影响。在主节点上Prepare一个事务(Preparing a Transaction on the Primary)
prepareTransaction
命令,它会将对应的txnState
更新为kPrepared
。然后,构造一条prepareTransaction
的oplog(拥有唯一的opTime),prepareTransaction
的oplog会包含事务中的所有操作,这意味着当大于16M时,需要更多条oplog。这里的opTime会作为prepareTimestamp
使用。RecoveryUnit
设置prepareTimestamp
,并且设置对应的存储引擎的事务为prepared状态。prepared状态的事务会阻塞其他相关(读/写相同的记录)事务的读写,直到事务提交或者回滚。prepareTransaction
的oplog记录并写入到oplog表中。这条记录包含 事务中所有的操作,并以一个oplog记录的形式存储起来(大事务需要多条oplog)。节点还需要把事务的Oplog的optime更新到config.transaction表中,如果存在多个oplog条目,会使用第一条oplog的optime。还会将事务状态kPrepared
更新到事务表中。这些信息在节点需要恢复或者倒换时会起到关键的作用。TransactionCoordinator
返回失败的响应消息。这会触发TransactionCoordinator
通知其他参与者的shard节点中止事务来保证事务的原子性。所以,这种情况发生时,可以重试整个事务。prepareTimestamp
一并在响应消息中返回给TransactionCoordinator
。Prepare冲突(Prepare Conflicts)
提交一个Prepared状态的事务(Committing a Prepared Transaction)
prepareTransactions
后,TransactionCoordinator
会向所有参与事务的shard发送commitTransaction
命令。commitTransaction
必须包含提交时间戳commitTimestamp
,这样才能让所有参与事务的shard在同一时间提交。这个值就是影响事务是否可见的时间戳。commitTransaction
命令,节点会再次获取RSTL锁来阻止事务执行过程中,发生主备倒换。然后,它会分配一条oplog,以commitTimestamp时间戳提交事务,在oplog表中写入commitTransaction的oplogEntry,更新config.transactions表,将记录状态改为kCommitted,记录统计信息,清理事务资源。终止一个Prepared状态的事务(Aborting a Prepared Transaction)
commitTransaction
命令没有被大多数节点提交,在宕机或主从切换后就有可能丢失。降备与Prepared状态事务的关系(Stepdown with a Prepared Transaction)
prepareTransaction
的多数提交。在事务prepare后,节点会释放RSTL锁,所以不会与降备冲突。在拿到RSTL锁之前降备只会中止未prepare的事务。一旦降备完成,节点会释放所有prepared事务的锁,从节点上的事务不会持有锁。升主与Prepared状态事务的关系(Step Up with a Prepared Transaction)
Prepared状态事务的恢复(Recovering Prepared Transactions)
prepareTransaction
的oplog,本节点就会更新config.transactions表中事务的状态。在遇到commitTransaction
的oplog记录之前,事务不会被恢复到内存中。在遇到commitTransaction
的oplog记录时,本节点会通过TransactionHistoryIterator
类,反向遍历oplog找到事务对应的prepareTransaction
的oplog条目,恢复出内存状态,一次性提交。prepareTransaction
oplog条目。然后checkout与事务关联的session,并应用保存在oplog条目中的所有操作,最后prepare这个事务。投机行为
,在多文档事务中与不在多文档事务中的读表现是不同的。这种投机行为是什么呢?Local and Majority Read Concerns
all_durable
时间戳对应的快照,是因为all_durable时间戳往往比较滞后,事务执行中会遇到更多的写冲突,从而导致事务被终止。投机性
的Majority Read/Write行为(比如findAndModify)。除非哪一天MongoDB事务的默认ReadConcern不再是Majority了,在这之前Local和Majority ReadConcern的表现都应该一致,这很重要。Snapshot Read Concern
atClusterTime
参数,则该参数会被用作读时间戳,如果该参数未被指定,则使用事务启动时的 all_durable
时间戳作为读时间戳,这个时间戳对应的数据不会有oplog空洞。OplogApplier
来应用oplog记录。参考oplog entry application章节的内容获取更多从点应用oplog的细节。Unprepared Transactions Oplog Application
prevOpTime
字段记录的前一个oplog记录的opTime,通过这样的方式把事务的多条oplog串联起来。使用partialTxn: true
来标识事务还不完整,不能立即应用。因为partialTxn
字段 不是所有的oplog都需要的,所以,把它作为‘o’字段的一个子字段。从节点在应用oplog之前必须等待收到收到最后一条applyOps的oplog,即prevOpTime
非空,并且无partialTxn
字段。这保证在应用oplog之前,节点上有这个事务的所有oplog。lastApplied
来保证。Prepared Transactions Oplog Application
prepare: true
字段来标识oplog记录是对应prepared 事务的。对于大事务,这个字段会出现在事务的oplog链中的最后一条,表示从节点必须prepare这个事务。这个oplog中的时间戳将作为prepareTimestamp
。prepareTransaction
的所有oplog记录会在同一个WUOW中执行。当应用处于prepared状态的事务中的oplog时,写线程必须先将事务关联的Session checkout并unstash(把txnResources
从Session中拿出来的流程)事务相关的资源txnResources
。这里的资源txnResources
指的是事务的意向锁状态和存储状态。当我们unstash这些资源后,会把txnResources的管理责任交给OperationContext
。应用线程会把prepare的操作添加到存储引擎的事务中,最后释放事务用到的锁。这意味着prepared事务在stash(把txnResources
从OperationContext中解绑,归还给Session的流程)时只需要stash RecoveryUnit而不需要stash意向锁。如果这里stash意向锁的话,会导致oplog的复制流程和prepared状态的事务有冲突。这些意向锁资源在下一次unstash txnResources操作(只有可能是commitTransaction或者abortTransaction)时被恢复。commitTransaction
或者abortTransaction
的oplog。commitTransaction
的oplog不需要包含事务所涉及的操作,因为我们已经把他们保存在prepare操作的oplog记录中了。这些记录同样会以类似的顺序被执行:checkout相关的session,释放事务资源 并提交或者中止存储引擎上的事务。PreparedTransactionInProgress Errors
TransactionCoordinator
的commitTransaction
或者abortTransaction
命令。因此在一个有Prepared状态的事务的Session上建新事务会报PreparedTransactionInProgress
。prepareTransaction
,abortTransaction
, 和commitTransaction
命令。除此之外的其他命令都会失败并返回PreparedTransactionInProgress
错误。PreparedTransactionInProgress
,则会在响应消息中有一个TransientTransactionError
标签。NoSuchTransaction Errors
NoSuchTransaction
发生在尝试 Continue,Prepare,Commit或者Abort一个不在执行中的事务时。这个错误常见的两个原因有:启动了一个新的事务或者事务已经被中止。commitTransaction
以外,其他任何事务中的命令在返回NoSuchTransaction
失败时,在响应消息中都会有一个TransientTransactionError
标签。如果commitTransaction
命令失败返回NoSuchTransaction
时没有一个write concern的错误,那么,它会在响应消息中附带一个TransientTransactionError
标签。如果返回write concern错误,那么把整个事务重试一遍是不安全的,因为这个事务可能在某个节点已经提交了。TransactionTooOld Errors
TransactionTooOld
错误。TransientTransactionError Label
TransientTransactionError
标签(响应消息中还是会有原始的错误码的),这样调用者就知道,他们可以安全的重试整个事务。并发控制(Concurrency Control)
pbwm(Parallel Batch Writer Mode)锁是一个全局资源,用来协调从节点在回放一批oplog操作的并发操作,因为从节点的oplog回放是并发操作的,所以节点需要持有PBWM锁来等待整个批量回放的完成。对于从节点,为了能够实现可以读取一个一致状态的数据, 如果不想持有PBWM锁, 那么就需要读取这个节点的lastapplied时间戳,这个时间戳是当一批oplog回放完成后设置的,是一批回放操作的边界值,不存在空洞。但是对于初始化同步来说,backgroundIndex可能会导致数据在lastapplied 时间戳后还有变化。如果一个节点观察到在lastapplied时间戳之后数据还有变化,那就需要持有PBWM锁来保证读取数据的时候没有批量回放的数据,并且不带时间戳读(不带时间戳读默认读到最新的数据,包括那些晚于lastApplied的操作)。
选举(Elections)
-
如果节点在选举超时时间(默认为10s)内没有看到主节点。 -
如果节点发现自己的优先级高于主节点的优先级时,节点将会等待然后发起选举(称之为优先级接管)。节点在发起选举前所等待的时间与节点优先级跟其他节点优先级的大小排序有直接的关系(也就是说,更高优先级的节点将会比低优先级的节点更早的发起选举动作)。优先级接管功能允许用户指定一个节点更容易成为主节点。 -
在副本集中, 新选举出来的主节点会尝试追赶到 latest applied Optime,在主节点追赶的过程中,新主节点不接受任何写入操作。如果某个从节点发现的数据比主节点更新, 并且主节点追赶操作花费的时间超过了 catchUpTakeoverDelayMillis
(默认30s), 那么这个从节点将会发起选举. 这种行为被称之为追赶接管。当主节点追赶操作花费过长时间的时候, 追赶接管操作可以让副本集更快的接受写入,因为拥有最新数据的节点不会在追赶上花费太长时间(或者不花时间)。参考”Transitioning toPRIMARY
“章节,以获得主节点追赶操作的更多细节。 -
replSetStepUp
命令,被用来运行在某个合适的节点上,可以使该节点立即发起选举。我们不期望用户使用该命令,该命令被用于副本集内部选举切换和测试。 -
当一个节点被 replSetStepDown
命令降级时,如果enableElectionHandoff
参数是true(默认值),我们会尽可能的选择一个符合条件的从节点来执行replSetStepUp命令。这种行为被称作选举交接.这样做可以缩短副本集故障时间,因为跳过了等待选举超时的时间。如果replSetStepDown 带参数force: true
,或者当节点降备的时候enableElectionHandoff参数为false,那么副本集的节点将会在选举超时时间过后才会发起选举操作。
候选者角度(Candidate Perspective)
VoteRequester
,使用ScatterGatherRunner
向每个节点发送replSetRequestVotes 命令,去询问是否投票选自己。候选节点在dry-run阶段不会增加term,因为如果primary看到term比自己持有的term高的时候,就会降级。通过首先运行dry-run选举,我们让不可能选举成功的节点不会增加term, 防止不必要的主节点降级。如果节点在dry-run选举中失败,它会继续自己的复制工作(还当从节点的意思)。如果节点在dry-run选举中获胜,则开始真正的选举。如果选举节点被选举交接选中, 它将跳过dry-run,并立即调用真正选举。在真正的选举时, 节点首先增加自己的term和并给自己投票。然后节点遵循和dry-run过程一样,也就是开启一个VoteRequester来向每个节点发送replSetRequestVotes 命令。每个节点接下来决定投赞成票或反对票,并将结果返回给投票发起者。候选节点的Oplog必须至少与大多数投票成员一样新,才能够被选举。如果候选节点收到了大多数节点包括自己的投票,则该候选者获胜。投票者角度(Voter Perspective)
replSetRequestVotes
命令时,它首先检查term是否是最新的, 更新自己的term。然后,ReplicationCoordinator
将询问TopologyCoordinator
是否应该投票。否决投票的条件是:-
来自一个更旧的term。 -
配置不匹配(细节请参阅配置排序和选举)。 -
副本集name不匹配。 -
投票请求中的 last applied OpTime 比 投票者的 last applied OpTime 更早。 -
如果不是dry-run选举并且投票者已经在本次term投过票了。 -
如果投票者是仲裁者,并且它能看到一个更大或者相等的优先级的健康的主节点。这是为了防止primary flapping:两个节点互相不能通讯,而仲裁者可以和二者通讯。
local.replset.election
集合中。此信息会在启动时被读入内存,并被用于后续的选举。这就确保了即使是节点重启,节点也不会在相同的term任期投票给2个节点。过渡到PRIMARY状态(Transitioning toPRIMARY
)
replSetGetStatus
会将节点显示为PRIMARY
状态. Oplog 应用仍将继续运行,当应用完oplog buffer数据后,会发信号给ReplicationCoordinator
去完成升级过程。有条件的降级(Conditional)
replSetStepDown
命令是一种让节点放弃primary地位的方式。这是一个有条件的降级,因为如果以下条件不满足,就可能降级失败:-
force
是 true 并且 已经超过了waitUntil
的deadline,也就是降级之前等的时间(如果 force = true,则只有这个条件需要满足) -
primary的 lastApplied
必须已经被复制了大多数节点。 -
至少有一个最新数据的从节点是可选举的。
replSetStepDown
命令运行时,节点就开始检查自己是否可以降级。首先,节点企图获得RSTL。为此,它必须停止所有冲突的用户/系统操作,并终止未prepared的事务。lastApplied
optime,也就是它们都可以追赶上来。该节点也会检查至少一个节点是可选举的。如果froce=true,在到达waitUntil
deadline后,它将不会等待这些条件,并且立即降级.SECONDARY
。无条件降级(Unconditional)
-
当 primary 看到了更高的 term。 -
超时: 如果主节点无法与大多数节点通讯,也会导致降级。主节点不需要能够和大多数节点直接通讯。如果主节点A不能和B节点通讯,但是A可以和C通讯,C可以和B通讯,这也是可以的。如果你考虑一个cluster中最小生成树(minimum spanning tree),边是从节点到它同步源的连接,那么只要主节点能和大多数节点在树上连通,它就依然可以作为副本集的主节点。 -
使用 replSetReconfig
命令,且force = true
。 -
通过心跳重新配置 :如果节点通过心跳发现一个新版本的副本集设置,也会调度一个副本集配置变更。
并发降级尝试(Concurrent Stepdown Attempts)
_leaderMode
到 kSteppingDown, 来防止并发有条件降级
的发生。通过跟踪当前降级状态,我们可以防止另一个有条件降级操作在降级时启动,但是仍允许无条件降级来取代有条件降级。回滚(Rollback)
stable_timestamp
,该时间戳是存储引擎检查可以获取检查点的最大时间戳。这可以被认为是一致的、大多数节点已经提交的时间点。oplogfetch
就会返回空数据并且报OplogStartMissing
的错误。stableTimestamp
之前,我们必须中止所有的prepare事务,通过这种方式,来释放那些事务持有的资源,并使内存中记录的状态失效。createRollbackDataFiles = true
(默认设置),则开始为已回滚的文档生成回滚文件。重要的是,写回滚文件安排在 abort prepared transaction之后,这样做,当我们尝试读取那些被prepare transaction修改过的文档时, 可以避免不必要的 prepare conflicts,不管怎样,这些事务必须中止才可以进行回滚。最后,如果我们回滚了任何操作,我们将该节点上的所有会话设置为无效。stable_timestamp
。一旦成功后,存储引擎会重建数据库数据到stable_timestamp
时的数据。但这个操作不不会回退oplog。为了回退oplog, 回滚需要删除所有common point 以后的Oplog记录。这个节点被称为截断点,并会被写入到oplogTruncateAfterpoint
记录,到这一步,回滚流程就知道从哪里去截断oplog。DropPendingCollectionReaper
的状态,该类管理了那些因为两阶段drop算法而处于drop-pending 状态的集合,上述操作完成后,则可以执行oplog恢复过程,此过程会截断common point后的oplog, 然后应用该点 之后的所有同步源上的oplog。参考启动恢复来获取截取oplog和应用oplog的细节信息。OpObserver
, 然后通知外部系统,回滚已经发生了. 例如,config server必须要更新他的shard registry,以便确保不再持有那些被回滚的数据。最后,我们向日志记录整个回滚摘要信息,然后变更到SECONDARY
状态,如果我们已经进入到rollback状态, 那么这个状态变更一定要成功,否则节点会被关闭。初始化同步(Initial Sync)
ReplicationCoordinator
发起,在InitialSyncer
结束。当节点开始执行初始化同步时,它的状态为STARTUP2。STARTUP是新增节点加载副本集配置时的状态。-
设置初始同步的flag以初始化同步进行中,并将此flag持久化。如果节点启动时发现此flag已经被设置了,节点将会重启初始化同步无论当前节点是否已经有了数据,这是因为这个标记表明上次的初始化同步操作没有执行完毕。同时读取oplog的时候也会检查此标记位,防止在初始化同步过程中读取oplog。 -
寻找一个同步源。 -
删掉所有除local库外的数据,重新创建新的oplog。 -
拿到同步源的Rollback ID (RBID),用来确保数据同步过程中,同步源没有发生过rollback。 -
获取同步源节点的 latest OpTime 作为 defaultBeginFetchingOpTime
,如果同步源节点没有正在进行的事务时,这个时间戳即为beginFetchingTimestamp
,也就是后面 fetching oplog 的开始时间戳。 -
获取同步源节点所有活跃事务中最早的开始的OpTime。如果这个时间戳存在(意味着同步源上有一个活跃的事务), 这个时间戳会作为 beginFetchingTimestamp
. 如果这个时间戳不存在, 节点会用defaultBeginFetchingOpTime
来代替。这样可以确保即使在同步源节点上确定了最老的活跃事务时间戳之后,同步源节点又开始了新事务,新加节点也能确保会拥有与同步过来的活跃事务相关联的完整的oplog。 -
查询其同步源节点oplog获取最新的OpTime作为 beginApplyingTimestamp
,即数据克隆阶段结束后立即开始apply oplog的时间戳。如果同步源上没有活跃事务,则beginFetchingTimestamp
与beginApplyingTimestamp
相同。 -
创建一个 OplogFetcher
,并开始从同步源提取和缓存oplog条目,以便后面apply。这些操作被缓存到一个集合中,这样可以不受可用内存不足的约束。
InitialSyncer
会构造一个AllDatabaseCloner
用于克隆同步源节点上的所有数据库。AllDatabaseCloner
会从同步源节点获取到所有的数据库list,然后为每个数据库创一个DatabaseCloner
来克隆该数据库。每个DatabaseCloner
又会从同步源节点获取其所有集合的list,并为每个集合创一个CollectionCloner
来克隆该集合。 CollectionCloner
通过调用listIndexes
枚举Index并创建一个CollectionBulkLoader
,以并行方式创建所有索引。CollectionCloner
接着对每个collection使用exhaust cursor通过在同步源运行find
请求,插入查到的文档,直到获取所有文档。与显式地需要在一个打开的游标上运行getMore
来获取数据不同, exhaust cursor 在find
没有耗尽游标的情况下,同步源会持续发送数据,直到没有剩余数据。error_codes.yml
中标记为RetriableError
的错误时,cloners会重试它正在进行的网络操作。重试持续时长由参数initialSyncTransientErrorRetryPeriodSeconds
决定,此参数为可配参数。重试结束时候如果还是失败,则认为是永久失败。永久失败后, cloners将选择一个新的同步源,并重试所有初始同步,最多重试参数numInitialSyncAttempts
设置的次数。一个值得注意的例外是,对于真实地查询集合数据的操作,我们并不重试整个操作。对于查询操作,有一个称为resume tokens的特性。在查询中设置一个标记:$_requestResumeToken
,这样我们从同步源接收的每个批处理都包含一个不透明的令牌,用来指示我们在集合中的当前位置。在存储完一批数据之后,我们将最新的resume tokens存储在CollectionCloner
的成员变量中。然后在重试时,在查询操作中提供resume token,这样可以避免重新获取已经存储的那部分集合。initialSyncTransientErrorRetryPeriodSeconds
参数也用于控制初始化同步时,数据克隆开始之后, oplog fetcher和所有网络操作的重试。stopTimestamp
,在成为副本集从节点并和其他成员保持一致前,必须进行apply oplog。如果beginFetchingTimestamp
与stopTimestamp
相同,说明没有oplog记录需要被落盘且没有操作需要被apply。在这种情况下,新节点将使用同步源的最后一条oplog作为后续起始同步点,并完成初始同步。beginFetchingTimestamp
与stopTimestamp
不同,则新节点会持续拉取主节点的Oplog,将它们写入自己的oplog,如果它们的时间戳在beginApplyingTimestamp
之后,则apply这些oplog使得数据落盘。这种情况下会继续不断的获取Oplog数据,并添加到OplogBuffer中。prepareTransaction
涉及的oplog数据。与startup和rollback recovery中重构 prepared transactions类似,每次看到prepareTransaction
oplog条目时,都会更新事务表。因为新加节点会将从源节点beginFetchingTimestamp
开始的所有oplog条目写入本地oplog,所以新加节点在oplog应用阶段完成后,最终会有一套完整的用于重建prepared transaction事务的oplog。-
开始缓存oplog数据 -
在表 foo
中插入{a: 1, b: 1}
-
在表 foo
中插入{a: 1, b: 2}
-
删除表 foo
-
重新创建集合 foo
-
在集合 foo
中a
字段上创建唯一索引 -
克隆 foo
-
开始apply oplog,apply oplog会尝试插入 {a: 1, b: 1}
和{a: 1, b: 2}
DuplicateKey
errors ),并假定这些错误最终会自洽。stopTimestamp
时,apply oplog阶段就结束了。新加节点会检查其同步源节点的RollbackID,看看初始化同步期间是否发生了回滚,如果是,则重新启动初始同步,如果没有回滚, 就开始析构InitialSyncer
。lastApplied
OpTime,以确保查询oplog时所有之前的oplog都是可见的。在此之后,会重构所有的prepared transaction。都做完后,新加节点会清除initial sync 标志,并告诉存储引擎initialDataTimestamp
是该节点的last applied OpTime。最后,InitialSyncer
关闭,ReplicationCoordinator
开始工作,负责后续的正常运行期间的同步。更改副本集配置(Reconfiguration)
replSetReconfig
命令用来更新当前的配置。Reconfigurations可以运行在safemode 或者forcemode. 为简便起见,我们将重新配置称为reconfigs. Safe Reconfig只能在主节点上运行,并保证对于大多数提交的写操作都不会回滚。Force Reconfig 可以在主节点或者从节点上运行, 而且该操作可能会导致rollback大多数已经提交的写操作. 尽管Force Reconfig是不安全的,但这种使用方式可以使用户能够挽救或修复那些不可被操作或无法访问的副本集节点。安全的重配置协议(Safe Reconfig Protocol)
-
ConfigReplication: 当前的config内容c,必须已经在大多数节点投票节点被初始化成功了。 -
OplogCommitment:上一个配置c0的所有的oplog操作记录,必须已经被复制到了当前config c1的大多数投票节点.
force: true
参数绕过了条件1和条件2的所有检查,并且它们不强制满足单节点变更条件
。副本集配置的顺序与选举(Config Ordering and Elections)
(version,term)
来排序,其中首先用term来比较排序,然后用version来比较,类似于optime比较的规则。配置的term是当前主节点的term,版本
是个单调递增的数字。执行reconfig时,new config的版本必须大于current config的版本。如果节点A的(version,term)大于节点B的,则认为A比配置B“更新”。如果一个节点通过来自另一个节点的心跳获取到一个更新的配置,它将发送一个心跳来获取该配置并应用。选举
章节。正式的规范(Formal Specification)
启动恢复流程(Startup Recovery)
STARTUP
的时候,将数据和oplog两者恢复一致的过程。启动时,如果当前节点没有数据或者不存在oplog,或者节点有初始化同步的flag, 则跳过该启动恢复阶段,直接开始初始化同步流程。如果节点有数据,则会进入启动恢复流程。initialDataTimestamp
时间戳),时间戳recovery timestamp 后续会作为stable_timestamp
,表示该节点恢复到了一个stable checkpoint,stable checkpoint对应了一个特定时间点的持久化数据视图。需要注意的是,因为journaling特性,oplog和local库下的很多集合是个例外,它们在启动的时候,数据始终是最新的,而非对应到recovery timestamp 点的数据状态(备注,对于大部分表而言,数据都是checkpoint + 增量oplog恢复的,然而oplog表本身是不走checkpoint恢复的,wiredTiger会为oplog记录journal,对于普通的业务表,wiredTIger不记录journal。这是因为,如果业务表既有oplog又有journal,就重复记录了,是对IO的浪费)。oplogTruncateAfterPoint
来控制的。oplogTruncateAfterPoint信息会持久化下来并且和时间戳无关,能反应比latest stable checkpoint更多信息,即便是在节点异常的情况下。oplogTruncateAfterPoint
。如果在写的过程中,节点异常崩溃,那么在重启恢复的时候,就恢复到写入前保存的时间点。即:oplogTruncateAfterPoint
(lastApplied), 如果成功写完了,oplogTruncateAfterPoint
会重设为null。即表示没有oplog空洞,或者说没有需要truncate的oplog。oplogTruncateAfterPoint
. 主节点发现一旦某个oplog前面没有空洞了,就立刻允许从节点去复制它。从节点的复制动作不会等待oplog落到主节点的磁盘,也不会等待主节点磁盘上没有oplog空洞之后. 因此,有些数据已经同步到从节点, 但因为主节点异常了,没有持久化存储到磁盘上,这些数据在主上就不存在了。主节点会持续地更新oplogTruncateAfterPoint
以便跟踪和推进一个没有空洞的点(磁盘数据层面),以防类似这种节点异常挂掉的场景。所以,启动恢复流程需要考虑该异常场景下,依据oplogTruncateAfterPoint
信息,与其他副本集节点的数据进行一致性协调。prepareTransaction
对应的oplog entries。对prepareTransaction
对应的oplog entries的处理,和 初始化流程或者回滚流程中的如何重建 prepared transaction 处理相似,每次遇到 prepareTransaction oplog entry,都会更新事务表。一旦节点完成所有oplog的应用,节点将会重建所有prepared 状态的事务。lastApplied
和lastDurable
两个时间戳为最后一条oplog的时间戳。最后开启同步过程。删除库和表(Dropping Collections and Databases)
enableMajorityReadConcern=false
的节点均使用此逻辑。DropPendingCollectionReaper
(会带着OpTime一起)的列表中,但是此时存储引擎还不会删除集合数据。每次ReplicationCoordinator
向前推进commit point,节点都会检查任何涉及到drop操作的OpTime是否已被majority commit point覆盖。如果已经覆盖,那么这些相关的drop操作就可以推进到二阶段了,此时DropPendingCollectionReaper
会通知存储引擎删除集合。通过等待“dropCollection”对应的oplog被 majority committed point覆盖,可以保证阶段1中的删除可以被回滚。因为存储引擎没有删集合的数据,在发生回滚时,可以轻松地还原集合。dropDatabase
命令时,它将对相关数据库中的每个集合进行如上所述的两阶段删除。一旦所有集合的删除操作都复制到大多数节点,节点将删除已经为空的数据库,并写一条dropDatabase
的oplog。复制相关时间戳词汇表(Replication Timestamp Glossary)
all_durable
: 在该时间戳之前的事务,都是已经被提交了的事务。这个点的oplog不存在oplog gap。该时间戳,是在执行相关写操作之前,就已经获取了的。该时间戳,用于维护oplog的可见性点。所有小于或者等于该时间戳的操作,已经提交并且持久化。正因为如此,才可以无gap地进行oplog的同步。commit oplog entry timestamp
:指的是 prepared transaction 中,commitTransaction这一步的oplog entry的时间戳,或者 non-prepared transaction 中 applyOps 这一步的oplog entry的时间戳。在一个跨shard的分布式事务中,每个shard都有各自的commit oplog entry timestamp,并且彼此可能不同。这个时间戳,一定是大于preparedTimestamp
的。commitTimestamp
:这个是提交多文档事务的时间点。是 prepared transaction 中 commitTransaction 的oplog entry中的commitTimestamp
或者 non-prepared transaction中 applyOps 这一步的oplog entry的时间戳在一个跨shard的分布式事务中,每个shard上的这个时间戳,是一样的。事务的可见性依赖于这个时间戳。对于 non-preapred transaction而言,commitTimestamp
和commit oplog entry timestamp
是一样的。因为,事务提交前,都不会写oplog。对于 preapred transaction而言,遵循这样的规则:prepareTimestamp
<=commitTimestamp
<=commit oplog entry timestamp
。currentCommittedSnapshot
:维护在ReplicationCoordinator中的optime,用于适配 majority reads,并且始终小于等于 lastCommittedOpTime。如果eMRC=true
,该时间戳被临时设为stable opTime,stable optime可以确保oplog已经在从上同步。此后,每次计算stableTime,都会更新该时间戳。如果eMRC=false
,这时间戳被设为lastCommittedOpTime
,lastCommittedOpTime
不保证oplog已经在从上,可能在从节点的oplog中不存在。stable_timestamp
不允许大于all_durable
。所以,除非eMRC=false
,否则该时间戳不应该小于all_durable
。initialDataTimestamp
:用于标记时间戳的最初开始点,当节点完成了初始化流程,会记录该时间戳。此时,该时间戳就是对应的节点的lastApplied时间戳。通过将该时间戳设为0,即通知存储引擎获取一个不稳定的checkpoint。稳定的checkpoint可以看作是一个基于时间戳的读 并且 读的数据是已经持久化了的。不稳定的checkpoint就只是简单地打开DB,读所有的数据包括当前未结束的事务提交的数据。此时的读快照,不关联任何特定的时间戳。lastApplied
内存中,最大的已经applied的oplog的时间戳。可能会晚于存储引擎可见的最大oplog时间戳。因为事务提交后,才会更新后者的值。lastCommittedOpTime
:节点本地的最大的majority commit的时间戳。每次更新该时间戳的时候,会重新计算stable_timestamp
。需要注意的是,如果 还没有同步最新的majority commit的oplog entry 的话,lastCommittedOpTime
可能会大于lastApplied
。要了解lastCommittedOpTime
是如何更新的,请参考章节(Commit Point Propagation)lastDurable
:从节点上的最新的oplog 或者 主上的写入journal的没有oplog空洞的最大oplog。后续写入操作持久化时候,存储引擎会异步地更新该时间戳。journal日志刷新的频率默认是100ms.minValid
:节点至少必须应用到该时间戳,才能达到数据一致性的点。在ReplicationConsistencyMarkers
模块中,将该时间戳记录在minValid文档中。该时间戳会持久化,不受重启影响。oldest_timestamp
:存储引擎保留的最早的时间戳。新事物的开启,不能早于该时间戳。推进stable_timestamp
时间戳时,也会推进oldest_timestamp。并且,oldest_timestamp 始终 小于等于stable_timestamp
。oplogTruncateAfterPoint
:记录最大的没有oplog空洞的点。主上,在存储引擎刷盘journal日志之前,会更新该时间戳。在oplog批量应用之前设置该时间戳,在应用之后清除。启动恢复流程,会用oplogTruncateAfterPoint时间戳把oplog截取到一个和其他副本集成员一致的数据点。对应的异常场景是:源节点crash之前的一些内存数据,可能已经同步到从节点上,但源节点因为crash而丢掉或者找不到这些数据了。prepareTimestamp
:对应prepared 事务中,prepare这一步的oplog entry的时间戳。这个是事务提交最早的时间戳。这个时间戳被存储引擎用来阻塞对prepared 数据的读操作,直到prepare的事务被提交或者放弃。readConcernMajorityOpTime
:指的是replSetGetStatus命令中的readConcernMajorityOpTime字段,来源于ReplicationCoordinator
模块中 的currentCommittedSnapshot
时间戳。stable_timestamp
:存储引擎允许打快照(table checkpoint)的最大的时间戳,该时间戳对应数据的一致性快照点。用于在同步过程中告知存储引擎,在哪(指时间戳的点)可以安全打读快照。这个时间戳,是有majority写保证的。RTT回滚过程中,会用到。如果是eMRC=false的场景,该时间戳,可能不具备majority写保证。这也是为什么会用到Refetch rollback algorithm进行回滚的原因。除了eMRC=false的场景,时间戳要求是单调递增的。eMRC=false
时,在回滚的某些特殊场景下,stableTimestamp
可能会被回退。eMRC=false
时,在回滚的某些特殊场景下,stableTimestamp
可能会被回退。
这篇真心长,得分天看
图解下就更好了,这样看有点不太好理解
oplogTruncateAfterPoint应该是设置成这批 oplog 的开始时间点!