mongodb 聚合性能(mongodb数据模型)

作者:夏德俊
本文来自于看了MongoDB在VLDB 19上发表的论文《MongoDB中可调一致性》后的内部分享。现在,我把分享的内容整理到这篇文章中,补充了之前分享中跳过的一些细节,以及分享中没有提到的MongoDB Causal的一致性(也出现在另一篇SIGMOD’19论文中),希望能帮助你对MongoDB的一致性模型设计有一个清晰的认识。
需要注意的是,文章后续涉及的具体实现分析都是基于MongoDB 4.2 (WiredTiger Engine)的,但是大部分关于原理的描述仍然适用于4.2之前的版本。
众所周知,MongoDB可调一致性的概念和理论支持,早期的数据库系统往往部署在单台机器上。随着业务的发展,对可用性和性能的要求越来越高,数据库系统已经演变为分布式架构。这种架构通常表现为由若干单个数据库节点通过一定的复制协议组成的整体,称为“Shared-nothing”,如MySQL、PG、MongoDB等。另外值得一提的是,随着“云”的流行,为了充分发挥云环境下资源池化的优势,出现了Aurora、PolarDB等“云原生”架构。这种架构被称为“共享存储”,因为它通常采用存储和计算分离以及存储资源共享。无论是哪种架构,在分布式环境下,按照大家都很熟悉的CAP理论,要解决所谓的一致性问题,即如何保证每次读写发生在不同的节点时都能获得最新的写入数据。这个一致性就是我们今天要讨论的MongoDB的可调一致性模型中的一致性,不同于单机数据库系统中经常提到的ACID理论中的一致性。
CAP理论中的一致性直观地强调了读取数据的新近性,但个人认为也隐含了持久性的要求,即如果当前已经读取了最新的数据,则读取更新不能因为节点失效或网络分区而丢失。关于这一点,在后面讨论具体设计的时候,也可以看到MongoDB的一致性模型注重持久性。既然标题提到了可调一致性,那么这种可调性具体指的是什么呢?这里不得不提分布式系统中的另一个理论,PACELC。PACELC是在2012年的一篇论文中正式提出的,也就是CAP提出的10年后。它的核心思想是,根据CAP,在一个有网络分区(P)的分布式系统中,我们面临可用性(A)和一致性(C)的选择,但除此之外(E),即使暂时没有网络分区,在实际系统中,我们也要面临这样的问题,因此,PACELC理论是CAP理论结合实际的一个扩展。
我们今天要讨论的MongoDB一致性模型的可调整性,是指调整MongoDB读写操作对L和C的选择,或者更具体的说,是调整性能(Performance——Latency,Throughput)和正确性(Correctness——Recency,Durability)的选择(权衡)。
在讨论MongoDB一致性模型设计的具体实现之前,我们先试着从功能设计的角度了解一下MongoDB的可调一致性模型。这样的好处是我们可以对它有一个全局的了解,后续也可以帮助我们更好的了解它的实现机制。在学术界,一致性模型的划分和定义有一些标准,如线性一致性、因果一致性等。我们已经听到了。MongoDB的一致性模型设计自然不能偏离这个标准。然而,与许多其他数据库系统一样,设计需要考虑与其他子系统的关系,如复制和存储引擎,并且具体实现往往不完全符合标准。在下面的第一节中,我们将详细讨论标准一致性模型和MongoDB一致性模型之间的关系,从而对它们有一个基本的了解。在此基础上,我们再来看看MongoDB的一致性模型在具体的功能设计中是如何做出来的,在实际的业务场景中是如何使用的。
标准一致性模型与MongoDB一致性模型的关系在基于复制构建的分布式系统中,一致性模型通常可以分为“以数据为中心”和“以客户端为中心”。下图中的线性化、顺序化、因果化和最终化属于以数据为中心的。
以数据为中心的一致性模型要求我们放眼整个系统,所有访问进程(客户端)的读写顺序都满足相同的特定约束。比如对于线性一致性,要求读写顺序与操作的实时完全一致,这是最强的一致性模型,在实际系统中很难实现。对于因果一致性,它只限制有因果关系的操作。虽然以数据为中心的一致性模型提供了访问过程的全局一致视图,但在现实系统中,不同的读写过程(客户端)经常访问不同的数据,维护这样的全局视图会产生不必要的成本。例如,在因果一致性模型下,P1执行Write1(X=1),P2执行read1 (x=1)和write2 (x=3),那么P1和P2之间存在因果关系,从而导致P13360Write1 (x=1)和P23360Write2 (x=3)的可见顺序。其他需要访问的进程看到这两个写操作的顺序相同,Write1排在第一位。但是,如果其他进程不读取X,显然没有必要提供这种全局一致的视图。因此,为了简化全局一致性约束,有了以客户端为中心的一致性模型。与以数据为中心的一致性模型相比,它只需要单客户端维度的一致性视图,并为单客户端读写操作提供了这些一致性承诺:RYW(Read Your Write)、MR(Monotonic Read)、MW(Monotonic Write)和WFR。至于这些一致性模型的概念和划分,本文就不详细介绍了。如果你有兴趣,你可以看看CMU的这两个讲座(Lec1,Lec2),非常清楚。MongoDB的因果一致性会议提供了这些承诺:RYW,MW先生,WFR。然而,这就是MongoDB和标准的区别。MongoDB的因果一致性提供了以客户端为中心的一致性模型下的承诺,而不是以数据为中心的。这主要是从系统开销的角度来做的。在以数据为中心的情况下,实现因果一致性所需的全局一致性视图过于昂贵。在实际场景中,以客户端为中心的一致性模型通常就足够了。关于这一点的详细讨论,请参考MongoDB官方论文SIGMOD’19的2.3节。因果一致性是MongoDB中相对独立的实现。只有当客户端读写时,才会打开因果一致性会话来提供相应的承诺。当因果一致性会话未打开时,MongoDB通过writeConcern和readConcern接口提供可调整的一致性,包括线性一致性和最终一致性。最后,标准中对一致性的定义非常松散,是最弱的一致性模型。但是,在这个一致性级别上,MongoDB还通过writeConcern和readConcern接口的配合,提供了丰富的性能和正确性选择,从而接近真实的业务场景。
MongoDB可调一致性模型功能接口—— writeConcern和readConcern在MongoDB中,writeConcern是针对写操作的配置,readConcern是针对读操作的配置,两者都支持在单个操作级别调整这些配置,所以使用起来非常灵活。WriteConcern和readConcern相互配合,形成MongoDB可调一致性模型的外部功能接口。
writeConcern ——唯一关心的是写入数据的持久性。首先,让我们看看写操作的writeConcern。写操作改变了数据库的状态,所以存在读操作的一致性问题。同时,正如我们将在后面的章节中看到的,MongoDB的一些readConcern级别的实现也强烈依赖于writeConcern的实现。MongoDB writeConcern包含以下选项,
{w:j:wtimeout:}注:*左右滑动阅读。
w,它指定这个写操作需要复制和应用多少副本集成员才能返回成功。它可以是一个数字或“major”(为了避免引入太多的复杂性,这里忽略了基于tag的自定义writeConcern)。W:0比较特殊,就是客户端不需要接收任何关于写操作是否成功的确认,性能最高。W:多数需要接收多数节点(包括主节点)关于操作成功执行的确认,具体数目由MongoDB根据副本集配置自动获取。j,当另外要求节点回复确认时,对应于写操作的修改已经被持久化到存储引擎日志中。当等待足够数量的确认时,主节点的超时。超时返回错误,但并不意味着写操作失败。从上面的定义我们可以看出,writeConcern唯一关心的是写操作的持久性。这种持久化不仅包括传统单机数据库级由J决定的持久化,还包括整个副本集(集群)级由W决定的持久化。w确定当重新选择副本集时,返回成功写入的更改是否会“丢失”。在MongoDB中,我们称之为回滚。w的值越大,对客户端的数据持久性保证就越强,写操作的延迟就越大。
这里还提到了“本地提交”和“多数提交”两个概念,分别对应于writeConcern w:1和w: majority,在后续的实现分析中会多次涉及。每个MongoDB写操作都会在底层WiredTiger引擎上启动一个事务,如下图所示。w:1要求事务在本地提交,而w:多数要求事务在副本集的多数节点提交。
ReadConcern ——关注读取数据的新近性和持久性。在MongoDB 4.2中,有五个级别的读取问题。我们先来看前四个:“局部”、“可用”、“主要”、“线性”。他们对一致性的承诺由弱变强。其中“可线性化”对应的是前面提到的标准一致性模型中的线性一致性,其他三个级别的readConcern代表MongoDB在最终一致性模型下对延迟和一致性(Recency耐久性)的选择。让我们结合一个三节点副本集复制架构图来简要解释这些readConcern级别的含义。在这个图中,oplog代表MongoDB的复制日志,类似于MySQL中的binlog。复制日志上最新的x=表示节点的复制进度。
local/available:local和available的语义基本相同,都是直接读取最新本地数据的读取操作。但是available是用在MongoDB碎片集群的场景中,包含了特殊的语义(孤儿文档可以返回以保证性能)。这种特殊的语义与本文的主题关系不大,所以我们稍后将只讨论本地readConcern。在这个级别,当重新选择主设备时,可以回滚读取的数据。多数:读取“多数提交”的数据可以保证读取的数据不会回滚,但不能保证读取最新的本地数据。比如上图中的主节点read,虽然x=5是最新提交的值,但并不是“多数提交”,所以当read操作使用多数readConcern时,只返回x=4。可线性化:它承诺线性一致性,即既保证可以读取最新的数据(新近性保证),又保证读取的数据不会回滚(持久性保证)。正如我们前面所说,线性一致性在实际系统中很难实现,MongoDB在这里采用了一种相当简化的设计。当读操作指定线性读关注级别时,读操作只能读取主节点,而考虑到写操作只能发生在主节点,相当于MongoDB对线性一致性的承诺仅限于单机环境,而不是分布式环境,所以实现自然简单很多。考虑到会有重选,MongoDB在这个readConcern级别下需要解决的唯一问题就是保证每次读取都发生在真正的主节点上。在分析了具体实现之后,我们可以看到,解决这个问题是以增加读取延迟为代价的。上述readConcern级别在延迟、持久性和新近性方面的权衡如下:
还有我们没有提到的最后一种readConcern级别,即“快照readConcern”。之所以放在这里单独讨论,是因为“snapshot readConcern”是用4.0新的多文档事务设计的,只能用在显式打开的多文档事务中。在4.0之前的版本中,对于一个读写操作,MongoDB默认只支持单个文档上的事务性语义(单行事务)。上面提到的四种readConcern级别是为这些常见的读写操作设计的(多个文档事务没有显式启动)。从定义上看,“snapshot readConcern”类似于majority readConcern,即读取“majority committed”的数据不一定能读取最新提交的数据,但其特殊性在于,在多文档事务中使用时,它承诺了真正一致的快照语义,而其他readConcern级别不提供。我们将在后面的实现部分详细讨论这一点。
writeConcern和readConcern之间的关系是在分布式系统中。当我们讨论一致性时,通常指的是对数据的读操作的关注,即“读关注什么”。那么为什么我们要在MongoDB中单独讨论writeConcern呢?从一致性承诺的角度来看,writeConcern将通过以下两种方式影响readConcern,
“可线性化的readConcern”读取的数据需要用“多数writeConcern”写入并保存在日志中,以便提供真正的“线性一致性”语义。考虑下面的情况:数据写入主节点后,它没有保存在日志中。当主节点重启恢复后,之前使用“线性化readConcern”读取的数据可能会丢失,这显然不符合“线性一致性”的语义。在MongoDB中,参数write concern Majority journal default控制在指定“majority writeConcern”时是否保证写操作在日志中持久化。默认情况下,该参数为真。在另一种情况下,写操作保留在日志中,但不会复制到多数节点。重新选择主节点后,也可能会发生数据丢失,违反了一致性承诺。“多数readConcern”要求读取多数提交的数据,因此由于不同节点的复制进度,它可能会读取较旧的值。但是,如果数据是用更高的writeConcern w值写入的,也就是说,写入操作直到扩散到更多的副本集节点才会返回写入成功,显然会在以后读取,所以“多数readConcern”将有更大的概率读取最新写入的值(更近的保证)。因此,虽然writeConcern只关注写入数据的持久性,但作为读取操作的数据源,也间接影响了MongoDB对读取操作一致性的承诺。
writeConcern和readConcern在实际业务中的应用是对writeConcern和readConcern的函数定义的介绍。可以看出,读写使用不同的配置,每个配置下包含不同的级别。这个界面设计对于用户来说略显复杂(社区里有很多类似的反馈)。我们先来看看真实业务中writeConcern和readConcern的统计数据以及几个典型的应用场景,加深对它们的理解。
以上统计数据来自于MongoDB自带的Atlas云服务中用户驱动上报的数据。统计样本在百亿量级,所以精度是可以保证的。从数据中,我们可以分析出以下结论。
事实上,大多数用户只是简单地使用默认值。在读取数据时,99%以上的用户只关心是否能以最快的速度读取数据,也就是在使用本地readConcern写入数据时,虽然大多数用户只要求写操作在本地成功,但仍有很大比例使用多数writeConcern(16%,远高于使用多数readConcern的比例),因为回滚写操作通常更影响用户的体验。另外,MongoDB的默认配置({W:1}写关注,本地读关注)更倾向于保护延迟,这主要是基于主备切换事件发生的概率比较低,即使发生数据丢失。
统计数据让我们直观地了解了MongoDB读关注/写关注在真实业务场景中的使用情况,即大多数用户更关注延迟而非一致性。但统计数据也显示,readConcern/writeConcern的组合非常丰富,用户可以通过使用不同的配置值来满足不同业务场景的一致性和性能需求,如以下实际业务场景中的应用案例(均来自Atlas cloud services中的用户使用场景)。
多数读取和写入:这种组合意味着对数据安全性的关注是第一优先。考虑一个学生贷款网站。网站流量不高,一分钟写(提交)两次左右。对于一个申请贷款的学生来说,在后台MongoDB数据库重新选择时,他成功提交申请的数据会“丢失”,这显然是无法接受的。同样,在应用程序通过时再次查询也是不可接受的,可能是因为读取的数据被回滚,结果发生了变化。因此,服务选择使用多数读关注写关注的组合,牺牲读写延迟来换取数据安全性。本地读取和多数写入:考虑一个用于食品和饮料评价的应用程序,例如大众点评。用户可能要花很多精力才能编辑出一篇精彩的评论。如果因为后端MongoDB实例在主备间切换导致审核丢失,显然是用户无法接受的。所以用户评价的提交(撰写)需要使用多数writeConcern。然而,用户阅读可能随后由于回滚而“消失”的评估通常是可以接受的。考虑到性能,使用本地readConcern显然是更好的选择。多个Write Concern值:在相同的业务场景中,您不必局限于一个writeConcern/readConcern值。您可以在不同的条件下使用不同的值,以兼顾性能和一致性。例如,考虑一个文档系统。通常,这样的系统会在用户编辑文档时提供自动保存功能。对于非用户主动触发的发布或保存,如果自动保存的结果丢失,用户往往不会感知。而自动保存功能触发更频繁(写压力更大),所以对于这种写动作使用local writeConcern显然更合理,写延迟更低。但是,低频率的主动保存或发布应该使用majority writeConcern,因为在这种情况下,用户对要保存的数据有明确的感知,很难接受数据的丢失。
MongoDB因果一致性模型的功能接口——因果一致性会话前面已经提到了。与writeConcern/readConcern构建的可调一致性模型相比,MongoDB因果一致性模型是另一个相对独立的实现,有自己特殊的函数接口。MongoDB的因果一致性是通过客户端的因果一致会话来实现的,可以理解为执行载体来维护一系列具有因果关系的读写操作之间的因果一致性。因果一致会话通过维护服务端返回的元信息(主要是关于操作排序的信息),结合服务端的实现,提供MongoDB一致性定义的一致性承诺(RYW,MR,MW,WFR)。具体原理将在后面的实现部分详细描述。对于因果一致的会话,我们可以看一个简单的例子。例如,现在有一个订单集orders,它用来存储用户的订单信息。为了扩大阅读流量,客户端采用主库写,从库读的方式。用户希望在提交订单后,能一直读到最新的订单信息(读你写的)。为了满足这个条件,客户端可以通过因果一致的会话来实现这个目标。
”’新订单’ ‘ ‘ with client . start _ session(causal _ consistency=True)as s 1: orders=client . get _ database(‘ test ‘,read _ concern=read concern(‘ majority ‘),write _ concern=write concern(‘ majority ‘,wtimeout=1000))。orders orders . insert _ one({ ‘ order _ id ‘ : ‘ 123 ‘,’ user’: ‘tony ‘,’ order_info’: {},session=s1)”’另一个会话获取用户订单’ ‘ ‘与client . start _ session(causal _ consistency=True)as s 2: S2 . advance _ cluster _ time(S1 . cluster _ time)# hybrid逻辑时钟S2 . advance _ operation _ time(S1 . operation _ time)orders=client . get _ database(‘ test),read _ preference=SECONDARY,read _ concern=read concern(‘ majority ‘),write _ concern=write concern(‘ majority ‘,wtimeout=1000)。orders . find({ ‘ user ‘ : ‘ Tony ‘ },Session=s2): print(order)注:*左右滑动。从上面的例子中,我们可以看到,要使用因果一致的会话,仍然需要指定适当的readConcern/writeConcern值。原因是,只有指定了多数写关注读关注,MongoDB才能提供因果一致性的完整语义,即满足前面定义的四个承诺(RYW、MR、MW、WFR)。
为简单起见,我们只拿其中一种情况作为例子:为什么在{w: 1} writeConcern和majority readConcern下RYW(读你的写)不能得到满足?
上图是一个5节点的副本集。当网络分区发生时(P~old~,S~1~和P~new~,S~2~,S~3~),W~1~ write on P~old~会向客户端返回成功,因为使用了{w:1} writeConcern,但最终会在网络恢复后回滚。虽然R~1~出现在W~1~之后,但是W~1~的结果不能从S~2~中读出,不符合RYW语义。在其他情况下,为什么不能满足因果一致性的语义?你可以参考官方文件,里面有非常详细的解释。
MongoDB一致性模型的实现机制及优化。在过去,MongoDB的可调一致性和因果一致性模型是在理论和具体的功能设计中描述的。下面我们深入内核层面,看看MongoDB一致性模型的具体实现机制,以及在其中做了哪些优化。
WriteConcern在MongoDB中,writeConcern的实现相对简单,因为不同的writeConcern值实际上只决定了写操作的返回速度。当w=1时,写操作的执行和返回过程只发生在本地,不涉及等待副本集其他成员的确认。比较简单,所以我们只讨论w 1中writeConcern的实现。
当实现w1 writeConcern时,每个用户的写操作将在WiredTiger引擎层启动一个事务。提交该事务时,会附带记录该写操作对应的Oplog条目的时间戳(Oplog可以理解为MongoDB的复制日志,具体请参考文档)。这个时间戳在代码中被称为lastOpTime。
//mongo : recoveryunit :3360 oncommitchange : commit-mongo : repl client info : setlastopvoid repl client INF o :3: setlastop(operation context * opCtx,const OpTime ot){ invariant(ot=_ lastOp);_ lastOp=otlastOpInfo(opCtx)。lastOpSetExplicitly=true}注:*左右滑动阅读引擎层的事务提交后,相当于本地完成了这个写操作。对于w:1的writeConcern,已经可以直接向客户端返回成功,但是当w 1时,需要等待足够多的次节点来确认写操作执行成功。此时MongoDB会通过执行replication coordinatorimp :3360 _ await replication _ in lock在一个条件变量上阻塞,等待被唤醒,被阻塞的用户线程会被添加到_replicationWaiterList中。Secondary会在主上拉取本次写操作对应的Oplog并申请完成后更新自己的站点信息,并通知另一个后台线程向上游上报自己的appliedOpTime和durableOpTime信息(主要方式是有一些其他特殊的上报机会)。
void replication coordinatorimp : setmylastappliedoptimeandwalltimeforward(.if(opTime myLastAppliedOpTime){ _ setMyLastAppliedOpTimeAndWallTime(lock,OpTimeAndWallTime,false,consistency);_ report upstream _ in lock(STD : move(lock));//以下是有关您自己的操作日志应用到同步源的进度的信息}.}注:*左右滑动阅读appliedOpTime和durableOpTime的含义和区别如下,
Applied Optime:在辅助服务器上应用一批操作日志后,最新操作日志条目的时间戳。Durable Optime:在辅助服务器上完成并保存在磁盘上的操作日志条目的最新时间戳。Oplog也实现为WiredTiger引擎的表,但是WT引擎默认的WAL同步策略是100ms一次,所以这个时间戳通常滞后于Apply Optime。通过向上游发送replSetUpdatePosition命令来报告上述信息。收到该命令后,如果通过比较发现副本集成员报告的时间戳信息比上次更新,upstream将触发并唤醒等待writeConcern的用户线程的逻辑。唤醒逻辑将比较用户线程等待的lastOptime是否小于或等于由次级报告的时间戳TS。如果是,这意味着辅助节点已经满足了这个writeConcern的要求。那么,TS希望使用辅助服务器报告的什么时间戳呢?如果writeConcern中指定的j参数为false,这意味着此写操作与磁盘上的持久性无关,则TS使用appliedOpTime,否则使用durableOpTime。当指定W节点(包括主节点本身)上报的TS大于或等于lastOptime时,可以唤醒用户线程,并向客户端返回成功。
//topologycordinator : havenumnodesreachedoptime for(auto member data : _ member data){ const OpTime member OpTime=durablyWritten member data . getlastdurableoptime(): member data . getlasappliedoptime();if(member optime=target optime){-num nodes;} if(numNodes=0){ return true;}}注:*左右滑动阅读此处,用户线程因writeConcern被阻塞的基本过程完成。但是,我们还是需要思考一个问题。MongoDB支持链式复制,即P-S1-S2复制拓扑。如果对P执行写操作,并且使用writeConcern w:3,即需要三个节点的确认,但是S2并不直接向P上报自己的Oplog Apply信息,这种场景下如何满足writeConcern?MongoDB采用信息转发的方式来解决这个问题。当S1接收到S2上报的ReplSetUpdatePosition命令并对其进行处理(processReplSetUpdatePosition())时,如果发现不是主节点角色,会立即触发一个forwardSlaveProgress任务,即利用自己的Oplog Apply信息构造一个replSetUpdatePosition命令,并发送给上游,从而保证在任何一个次节点的Oplog Apply进度提前时,主节点都能及时收到消息,从而最大限度地减少w1中writeConcern造成的写操作延迟。
与writeConcern相比,ReadConcern的实现要复杂得多,因为它与存储引擎的关系更密切。有些情况下依赖于writeConcern的实现,有些readConcern级别的实现依赖于MongoDB的复制机制和存储引擎提供支持。此外,为了在满足readConcern级别指定要求的前提下,最大限度地降低读取操作的延迟和事务执行效率,MongoDB做了一些优化。下面,我们将结合不同的readConcern级别来描述它们的实现原理和优化方法。
“major”读关注“major”读关注的语义在前面几章已经介绍过了,这里不再赘述。为了确保客户端可以读取多数提交的数据,MongoDB根据存储引擎的不同功能实现了两种机制来提供这种提交。
WiredTiger依赖于存储引擎快照的实现。WiredTiger还采用了MVCC的并发控制策略,保证并发事务执行过程中不同事务的读写不会互相阻塞,提高事务执行的性能,即当提交不同的写事务时,会生成多个版本的数据,每个版本的数据用一个时间戳(commit_ts)来标识。所谓的存储引擎快照,其实就是由版本历史数据组成的一致的数据视图,是在某个时间点看到的。因此,在引擎中,快照也由时间戳标识。前面提到过,由于MongoDB采用异步复制机制,不同节点的复制进度会有所不同。如果我们在副本集节点直接读取最新提交的数据,如果它没有被复制到大多数节点,显然不满足“多数”readConcern的语义。此时你仍然可以读取最新的数据,但是要等待其他节点确认这次读取的数据已经被apply后再返回给客户端,但是这样显然会大大增加读取的延迟(虽然在这种情况下,一致性体验更好,因为你可以读取更新的数据,但是正如我们前面分析的,大部分用户在读取时都希望更快的返回数据,而不是追求一致性)。因此,MongoDB在存储引擎级别维护一个多数提交数据视图(快照),这个快照对应的时间戳在MongoDB中称为多数提交点(mcp)。当客户端指定要读取的多数时,它可以通过直接读取该快照来快速返回数据,而无需等待。需要注意的是,由于复制进度的差异,mcp无法反映最新提交的数据,也就是说这种方法是牺牲延迟来换取更低的延迟。
//以getMore命令为例:void applycurreadconcern(操作上下文* opctx,repl :3360 readconcernargs rcArgs){.switch(rcargs . getmajorityreadmechanism()){ case repl : readconcernargs : majorityreadmechanism :3360 majority snapshot : {//确保我们从多数快照中读取。opCtx-recovery unit()-settimestamprepradsource(recovery unit :3360 read source : kmajoritycommitted);//获取多数提交快照UserstateOK(OPC TX-Recovery Unit()-Attainmentality提交快照());打破;}注意:*左右滑动,但是基于mcp快照的实现需要解决一个问题,就是如何保证这个快照的有效性?再者,如何保证mcp视图所依赖的版本历史数据不会被WiredTiger引擎清理掉?一般情况下,WiredTiger会根据事务提交自动清理多版本数据。只要当前的活动事务不依赖于某个版本历史的数据,就可以将其从内存中的MVCC列表中删除(不考虑LAS机制,WT多版本数据只是被设计存储在内存中)。但是,所谓的多数提交点,其实是服务器层的一个概念,引擎层是感知不到的。如果仅根据事务的依赖性来清理版本历史数据,则mcp所依赖的版本历史版本数据可能会被提前清理。例如,在下面的三节点副本集中,如果客户端从主节点读取并指定了majority readConcern,由于mcp=4,MongoDB只能向客户端返回commit_ts=4的历史值。但是对于WiredTiger引擎,当前活动事务列表中只有T1,可以清理commit_ts=4的版本历史。但是如果清理了这个版本,mcp所依赖的快照显然无法保证。所以WiredTiger引擎层需要提供一种新的机制,根据服务器层通知的复制进度,即mcp站点,来清理版本历史数据。
在wired tiger版本中,提供了“特定于应用的事务时间戳”功能。解决服务器层对事务提交顺序的需求(基于应用时间戳)与WiredTiger引擎层的事务提交顺序(基于内部事务ID)不一致的问题(根源来自基于Oplog的复制机制,此处不展开)。再者,在此功能的基础上,WT还提供了所谓的“read ‘ as ‘ a Timestamp”功能(在某些文章中也称为“Time Travel Query”),即支持从指定时间戳开始的快照读取,这一特性是前面提到的基于mcp站点实现‘majority’read concern的功能基础。WiredTiger为服务器层提供set_timestamp()的API来更新相关的应用时间戳。WT当前包含以下语义应用时间戳,
要回答前面提到的关于mcp快照有效性保证的问题,我们需要重点关注红框中的几个时间戳。首先,稳定时间戳在MongoDB中的意义是,在这个时间戳之前提交的写操作不会回滚,所以符合多数提交点(mcp)的语义。稳定时间戳对应的快照被存储引擎持久化后,称为“稳定检查点”。这个检查点在MongoDB中也具有重要意义。我们将在下一章“‘本地’read concern”中详细描述。当MongoDB处于崩溃恢复时,它总是从稳定检查点初始化,然后重新应用增量操作日志来完成恢复。因此,为了提高崩溃恢复的效率和回收日志空间,引擎层需要定期生成新的稳定的检查点,这意味着稳定的时间戳也需要不断地被服务器层提升(更新)。MongoDB在更新稳定时间戳的同时,也会根据时间戳更新最旧的时间戳。因此,在基于快照的实现机制下,最旧时间戳和稳定时间戳的语义也是一致的。
.-replication coordinatorimp : _ updateLastCommittedOpTimeAndWallTime()-replication coordinatorimp : _ setStableTimestampForStorage()-wiredtigerkvingen : SetStableTimestamp()-wiredtigerkvingen 33603360 SetOldestTimeStamp from stable()-wiredtigerkvingen 3360: SetOldestTimeStamp()注:*左右滑动
当当前WiredTiger接收到新的最旧的时间戳时,它将通过组合当前活动事务(oldest_reader)和最旧的时间戳来计算新的全局固定时间戳。清理版本历史数据时,不会清理固定时间戳之后的版本,从而保证了mcp快照的有效性。
//计算新的全局固定时间戳_ _ conn _ set _ timestamp-_ _ wt _ txn _ global _ set _ timestamp-_ _ wt _ txn _ update _ pinned _ timestamp-_ _ wt _ txn _ get _ pinned _ timestamp {.tmp _ ts=include _ oldest txn _ global-oldest _ timestamp : 0;如果(!include_oldest tmp_ts==0)返回(WT _ not found);* tsp=tmp _ ts.}//判断版本历史是否可以清理静态内联bool _ _ wt _ txn _ visible _ all(wt _ session _ impl * session,uint64 _ t ID,wt _ timestamp _ t timestamp) {._ wt _ txn _ pinned _ timestamp(session,pink return(timestamp=pinned _ ts);}注:*在分析完mcp快照有效性保证的机制后,我们需要回答以下两个关键问题,才能完整的了解整个细节。
辅助节点的复制进度和根据复制进度进一步计算的mcp由操作日志中的ts字段标识,而数据的版本号由commit_ts标识。它们之间是什么关系,为什么有可比性?如前所述,引擎的崩溃恢复需要不断提升稳定时间戳(mcp)以产生新的稳定检查点。mcp是如何推广的?为了回答第一个问题,我们需要首先看看插入操作的相应操作日志条目的ts字段值是如何来的,以及该操作日志和插入操作之间的关系。首先,当服务器层接收到一个插入操作时,会提前调用localologinfo :3360 getnexttimes()为其即将写入的操作日志条目生成ts值。有必要锁定此ts,以避免并发写入生成相同的ts。然后服务器层会调用WiredTigerRecovery单元:3360 set timestamp启动WiredTiger引擎层的事务,并将该事务中后续写操作的commit_ts设置为oplog条目的ts。在引擎层完成插入操作后,也会通过同一个事务将其对应的oplog条目写入WiredTiger表,然后提交该事务。
也就是说,MongoDB通过将写操作日志和写操作放在同一个事务中,保证了复制的日志和实际数据的一致性,同时也保证了操作日志条目的版本号和写操作本身产生的变化是一致的。至于第二个问题,如何推进mcp,我们在上一章writeConcern的实现中提到过。在应用一批操作日志之后,下游将向上游报告其自身应用进度信息,且上游也将该信息转发给其自身的上游。基于这种机制,对于Primary来说,很明显它可以不断获取整个副本集所有成员的oplog apply进度信息,然后推进自己的多数提交点(计算方法比较简单,详见拓扑协调器3360: UpdateLastCOMMITTED OptimandTime)。但以上是单向传播机制,副本集的次节点也可以提供读取。还需要获得其他节点的操作日志应用信息来更新mcp视图。因此,MongoDB还提供了以下两种机制来保证二级节点的mcp能够持续提升:1。基于副本集高可用性的心跳机制:I .默认情况下,每个副本集节点每2秒钟向其他成员发送一次心跳(replSetHeartBeat命令);二。其他成员返回的信息会包含$replData元信息,次节点会根据lastOpCommitted直接提升自己的mcp。
$ repldata: {$ TERM : 147,LastopCommittee3360 { TS3360时间戳(1598455722,1),T: 147}.注意:*左右滑动。2.基于副本集的增量同步机制:一、基于心跳机制的mcp提升方法,显然,实时性不够。在主节点计算出一个新的mcp后,下游节点最多需要2秒钟就可以更新它的mcpii。所以在MongoDB对oplog进行增量同步的过程中,上游也会在返回给下游的oplog批次中携带$replData元信息。下游节点收到这个信息后,也会根据lastOpCommitted信息直接推送自己的mcpiii。由于次节点的oplog fetcher线程不断从上游拉取oplog,只要有新的写入,导致主mcp的推送,下游就会立即拉取新的oplog,这样可以保证自己的mcp在ms级别同步推送。
还需要一点。于 MySQL,MongoDB 也是支持插件式的存储引擎体系的,但是并非每个支持的存储引擎都实现了 MVCC,即具备快照能力,比如在 MongoDb 3.2 之前默认的 MMAPv1 引擎就不具备。此外,即使对于具备 MVCC 的 WiredTiger 引擎,维护 majority commit point 对应的 snapshot 是会带来存储引擎 cache 压力上涨的,所以 MongoDB 提供了 replication.enableMajorityReadConcern 参数用于关闭这个机制。所以,结合以上两方面的原因,MongoDB 需要提供一种不依赖快照的机制来实现 majority readConcern,MongoDB 把这个机制称之为 Speculative Read ,中文上我觉得可以称为“未决读”。Speculative Read 的实现方式非常简单,上一小节实际上也基本描述了,就是直接读当前最新的数据,但是在实际返回 Client 前,会等待读到的数据在多数节点 apply 完成,故可以满足 majority readConcern 语义。本质上,这是一种后验的机制,在其他的数据库系统中,比如 Hekaton,VoltDB ,事务的并发控制中也有类似的做法。在具体的实现上,首先在命令实际执行前会通过 WiredTigerRecoveryUnit::setTimestampReadSource() 设置自己的读时间戳,即 readTs,读事务在执行的过程中只会读到 readTs 或之前的版本。在命令执行完成后,会调用 waitForSpeculativeMajorityReadConcern() 确保 readTs 对应的时间点及之前的 oplog 在 majority 节点应用完成。这里实际上最终也是通过调用 ReplicationCoordinatorImpl::_awaitReplication_inlock 阻塞在一个条件变量上,等待足够多的 Secondary 节点汇报自己的复制进度信息后才被唤醒,完全复用了 majority writeConcern 的实现。所以,writeConcern,readConcern 除了在功能设计上有强关联,在内部实现上也有互相依赖。需要注意的是,Speculative Read 机制 MongoDB 并不打算提供给普通用户使用,如果把 replication.enableMajorityReadConcern 设置为 false 之后,继续使用 majority readConcern,MongoDB 会返回 ReadConcernMajorityNotEnabled 错误。目前在一些内部命令的场景下才会使用该机制,测试目的的话,可以在 find 命令中加一个特殊参数: allowSpeculativeMajorityRead: true,强制开启 Speculative Read 的支持。
针对 readConcern 的优化 —— Query Yielding考虑到后文逻辑上的依赖,在分析其他 readConcern level 之前,需要先看一个 MongoDB 针对 readConcern 的优化措施。默认情况下,MongoDB Server 层面所有的读操作在 WiredTiger 上都会开启一个事务,并且采用 snapshot 隔离级别。在 snapshot isolation 下,事务需要读到一个一致性的快照,且读取的数据是事务开始时最新提交的数据。而 WiredTiger 目前的多版本数据只能存放在内存中,所以在这个规则下,执行时间太久的事务会导致 WiredTiger 的内存压力升高,进一步会影响事务的执行性能。
比如,在上图中,事务 T1 开始后,根据 majority commit point 读取自己可见的版本,x=1,其他的事务继续对 x 产生修改并且提交,会产生的新的版本 x=2,x=3……,T1 只要不提交,那么 x=2 及之后的版本都不能从内存中清理,否则就会违反 snapshot isolation 的语义。面对上述情况,MongoDB 采用了一种称之为「Query Yielding」的手段来“优化” 这个问题。
「Query Yielding」的思路其实非常简单,就是在事务执行的过程中,定期的进行 yield,即释放锁,abort 当前的 WiredTiger 事务,释放 hold 的 snapshot,然后重新打开事务,获取新的 snapshot。显然,通过这种方式,对于一个执行时间很长的 MongoDB 读操作,它在引擎层事务的 read_ts 是不断推进的,进而保证 read_ts 之后的版本能够被及时从内存中清理。之所以在优化前面加一个引号的原因是,这种方式虽然解决了长事务场景下,WT 内存压力上涨的问题,但是是以牺牲快照隔离级别的语义为代价的(降级为 read committed 隔离级别),又是一个典型的牺牲一致性来换取更好的访问性能的应用案例。”local” 和 “majority” readConcern 都应用了「Query Yielding」机制,他们的主要区别是,”majority” readConcern 在 reopen 事务时采用新推进的 mcp 对应的 snapshot,而 “local” readConcern 采用最新的时间点对应的 snapshot。Server 层在一个 Query 正常执行的过程中(getNext()),会不断的调用 _yieldPolicy->shouldYieldOrInterrupt() 来判定是否需要 yield,目前主要由如下两个因素共同决定是否 yield:
internalQueryExecYieldIterations:shouldYieldOrInterrupt() 调用累积次数超过该配置值会主动 yield,默认为 128,本质上反映的是从索引或者表上获取了多少条数据后主动 yield。yield 之后该累积次数清零。internalQueryExecYieldPeriodMS:从上次 yield 到现在的时间间隔超过该配置值,主动 yield,默认为 10ms,本质上反映的是当前线程获取数据的行为持续了多久需要 yield。最后,除了根据上述配置主动的 yield 行为,存储引擎层面也会因为一些原因,比如需要从 disk load page,事务冲突等,告知计划执行器(PlanExecutor)需要 yield。MongoDB 的慢查询日志中会输出一些有关执行计划的信息,其中一项就是 Query 执行期间 yield 的次数,如果数据集不变的情况下,执行时长差别比较大,那么就可能和要访问的 page 在 WiredTiger Cache 中的命中率相关,可以通过 yield 次数来进行一定的判断。
“snapshot” readConcern前面我们已经提到了 “snapshot” readConcern 是专门用于 MongoDB 的多文档事务的,MongoDB 多文档事务提供类似于传统关系型数据库的事务模型(Conversational Transaction),即通过 begin transaction 语句显示开启事务, 根据业务逻辑执行不同的操作序列,然后通过 commit transaction 语句提交事务。”snapshot” readConcern 除了包含 “majority” readConcern 提供的语义,同时它还提供真正的一致性快照语义,因为多文档事务中的多个操作只会对应到一个 WiredTiger 引擎事务,并不会应用「Query Yielding」。
这里这么设计的主要考虑是,和默认情况下为了保证性能而采用单文档事务不同,当应用显示启用多文档事务时,往往意味着它希望 MongoDB 提供类似关系型数据库的,更强的一致性保证,「Query Yielding」导致的 snapshot “漂移”显然是无法接受的。而且在目前的实现中,如果应用使用了多文档事务,即使指定 “majority” 或 “local” readConcern,也会被强制提升为 “snapshot” readConcern。
// If “startTransaction” is present, it must be true due to the parsing above.const bool upconvertToSnapshot(sessionOptions.getStartTransaction());auto newReadConcernArgs = uassertStatusOK( _extractReadConcern(invocation.get(), request.body, upconvertToSnapshot)); // 这里强制提升为 “snapshot” readConcern注:*左右滑动阅览不采用 「Query Yielding」也就意味着存在上节所说的“WiredTiger Cache 压力过大”的问题,在 “snapshot” readConcern 下,当前版本没有太好的解法(在 4.4 中会通过 durable history,即支持把多版本数据写到磁盘,而不是只保存在内存中来解决这个问题)。MongoDB 目前采用了另外一个比较简单粗暴的方式来缓解这个问题,即限制事务执行的时长,transactionLifetimeLimitSeconds 配置的值决定了多文档事务的最大执行时长,默认为 60 秒。超出最大执行时长的事务由后台线程负责清理,默认每 30 秒进行一次清理动作。每个多文档事务都会和一个 Logical Session 关联,清理线程会遍历内存中的 SessionCatalog 缓存找到所有过期事务,清理和事务关联的 Session,然后 abortTransaction(具体可参考killAllExpiredTransactions())。”snapshot” readConcern 为了同时维持分布式环境下的 “majority” read 语义和事务本地执行的一致性快照语义,还会带来另外一个问题:事务因为写冲突而 abort 的概率提升。在单机环境下,事务的写冲突往往是因为并发事务的执行修改了同一份数据,进而导致后提交的事务需要 abort(first-writer-win)。但是通过后面的解释我们会看到,”snapshot” readConcern 为了同时维持两种语义,即使在单机环境下看起来是非并发的事务,也会因为写冲突而 abort。要说明这个问题,先来简单看下事务在 snapshot isolation 下的读写规则。
对于读:
对任意事务 $T_i$ ,如果它读到了数据 $X$ 的版本 $X_j$,而 $X_j$ 是由事务 $T_j$ 修改产生,则 $T_j$ 一定已经提交,且 $T_j$ 的提交时间戳一定小于事务 $T_i$ 的快照读时间戳,即只有这样, $T_j$ 的修改对 $T_i$ 才是可见的。这个规则保证了事务只能读取到自己可见范围内的数据。另外,对任意事务 $T_k$,如果它修改了 $X$ 并且产生了新的版本 $X_k$,且 $T_k$ 已提交,那么 $T_k$ 要么在事务 $T_j$ 之前提交($commit(T_k) < commit(T_j)$),要么在事务 $T_i$ 的快照读时间戳之后提交。这个规则保证了事务在可见范围内读取最新的数据。对于写:
对于任意事务 $T_i$ 和 $T_j$,他们都成功提交的前提是没有产生冲突。冲突的定义:如果 $T_j$ 的提交时间戳在事务 $T_i$ 的观测时间段([$snapshot(T_i)$, $commit(T_i)$])内,且二者的修改数据集存在交集,则二者存在冲突。这种情况下 $T_i$ 需要 abort。对这个规则可以有一个通俗的理解,即事务的并发控制存在一个基本原则:「过去不能修改将来」,$snapshot(T_i) < commit(T_j)$ 表明 $T_i$ 相对于 $T_j$ 发生在过去(此时 $T_i$ 看不到 $T_j$ 产生的修改), $T_i$ 如果正常提交,因为 $commit(T_i) > commit(T_j)$,也就意味着发生在过去的 $T_i$ 的写会覆盖将来的 $T_j$。然后再回到前面的问题:为什么在 “snapshot” readConcern 下事务冲突 abort 的概率会提升?这里我们结合一个例子来进行说明,
上图中,C1 发起的事务 T1 在主节点(P)上提交后,需要复制到一个从节点(S) 并且 apply 完成才算是 majority committed。在事务从 local committed 变为 majority committed 这个延迟内(上图中的红圈),如果 C2 也发起了一个事务 T2,虽然 T2 是在 T1 提交之后才开始的,但根据 “majority” read 语义的要求,T2 不能够读取 T1 刚提交的修改,而是基于 mcp 读取 T1 修改前的版本,这个是符合前面的 snapshot read rule 的( D1 规则)。但是,如果 T2 读取了这个更早的版本并且做了修改,因为 T2 的 commit_ts(有递增要求) 大于 T1 的,根据前面的 snapshot commit rule(D2 规则),T2 需要 abort。需要说明的是,应用对数据的访问在时间和空间上往往呈现一定的局部性,所以上述这种 back-to-back transaction workload(T1 本地修改完成后,T2 接着修改同一份数据)在实际场景中是比较常见的,所以很有必要对这个问题作出优化。MongoDB 对这个问题的优化也比较简单,采用了和 “majority” readConcern 一样的实现思路,即「speculative read」。MongoDB 把这种基于「speculative read」机制实现的 snapshot isolation 称之为「speculative snapshot isolation」。
仍然使用上面的例子,在「speculative snapshot isolation」机制下,事务 T2 在开始时不再基于 mcp 读取 T1 提交前的版本,而是直接读取最新的已提交值(T1 提交),这样 $snapshot(T_2) >= commit(T_1)$ ,即使 T2 修改了同一条数据,也不会违反 D2 规则。但是此时 T1 还没有被复制到 majority 节点,T2 如果直接返回客户端成功,显然违反了 “majority” read 的语义。MongoDB 的做法是,在事务 T2 提交时,如果要维持 “majority” read 的语义,其必须也以 “majority” writeConcern 提交。这样,如果 T2 产生了修改,在其等待自身的修改成为 majority committed 时,发生它之前的事务 T1 的修改显然也已经是 majority committed(这个是由 MongoDB 复制协议的顺序性和 batch 并发 apply 的原子性保证的),所以自然可保证 T2 读取到的最新值满足 “majority” 语义。这个方式本质上是一种牺牲 Latency 换取 Consistency 的做法,和基于 snapshot 的 “majority” readConcern 做法正好相反。这里这么设计的原因,并不是有目的的去提供更好的一致性,主要还是为了降低事务冲突 abort 的概率,这个对 MongoDB 自身性能和业务的影响非常大,在这个基础上,也可以说,保证业务读取到最新的数据总是更有用的。关于牺牲 Latency,实际上上述实现机制,对于写事务来说并没有导致额外的延迟,因为事务自身以 “majority” writeConcern 提交进行等待以满足自身写的 majority committed 要求时,也顺便满足了 「speculative read」对等待的需求,缺点就是事务的提交必须要和 “majority” readConcern 强绑定,但是从多文档事务隐含了对一致性有更高的要求来看,这种绑定也是合理的,避免了已提交事务的修改在重新选主后被回滚。真正产生额外延迟的是只读事务,因为事务本身没有做任何修改,仍然需要等待。实际上这个延迟也可以被优化掉,因为事务如果只是只读,不管读取了哪个时间点的快照,都不会和其他写事务形成冲突,但是 MongoDB 目前并没有提供标记多文档事务为只读事务的接口,期待后续的优化。
“local” readConcern”local” readConcern 在 MongoDB 里面的语义最为简单,即直接读取本地最新的已提交数据,但是它在 MongoDB 里面的实现却相对复杂。首先我们需要了解的是 MongoDB 的复制协议是一种类似于 Raft 的复制状态机(Replicated State Machine)协议,但它和 Raft 最大区别是,Raft 先把日志复制到多数派节点,然后再 Apply RSM,而 MongoDB 是先 Apply RSM,然后再异步的把日志复制到 Follower(Secondary) 去 Apply。
这种实现方式除了可以降低写操作(在 default writeConcern下)的延迟,也为实现 “local” readConcern 提供了机会,而 Recency,前面的统计数据已经分析了,正是大部分的业务所更加关注的。MongoDB 的这种设计虽然更贴近于用户需求,但也为它的 RSM 协议引入了额外的复杂性,这点主要体现在重新选举时。重新选主时可能会发生,已经在之前的 Primary 上追加的部分 log entry 没有来及复制到新的 Primary 节点,那么在前任 Primary重新加入集群时,需要把这部分多余的 log entry 回滚掉(注:这种情况,除了旧主可能发生,其他节点也可能发生)。对于 Raft 来说这个回滚动作特别简单,只需对 replicated log 执行 truncate,移除尾部多余的 log entry,然后重新从现任 Primary 追日志即可。但是,对于 MongoDB 来说,由于在追加日志前就已经对状态机进行了 apply,所以除了 Log Truncation,还需要一个状态机回滚(Data Rollback)流程。Data Rollback 是一个代价比较大的过程,而 MongoDB 本身的日志复制是通常是很快的,真正在发生重新选举时,未及时同步到新主的 log entry 是比较少的,所以如果能够让新主在接受写操作之前,把旧主上“多余”的日志重新拉取过来并应用,显然可以避免旧主的 Data Rollback。
重选举时的 Catchup PhaseMongoDB 从 3.4 版本开始实现了上述机制(catchup phase),流程如下,
候选节点在成功收到多数派节点的投票后,会通过心跳(replSetHeartBeat 命令)向其他节点广播自己当选的消息;其他节点的的 heartbeat response 中会包含自己最新的 applied opTime,当选节点会把其中最大的 opTIme 作为自己 catchup 的 targetOpTime;从 applied opTime 最大的节点或其下游节点同步数据,这个过程和正常的基于 oplog 的增量复制没有太大区别;如果在超时时间(由 settings.catchUpTimeoutMillis 决定,3.4 默认 60 秒)内追上了 targetOpTime,catchup 完成;如果超时,当选节点并不会 stepDown,而是继续作为新的 Primary 节点。
void ReplicationCoordinatorImpl::CatchupState::signalHeartbeatUpdate_inlock() { auto targetOpTime = _repl->_topCoord->latestKnownOpTimeSinceHeartbeatRestart(); … ReplicationMetrics::get(getGlobalServiceContext()).setTargetCatchupOpTime(targetOpTime.get()); log() << “Heartbeats updated catchup target optime to ” << *targetOpTime; …}注:*左右滑动阅览上述第 5 步意味着,catchup 过程中如果有超时发生,其他节点仍然需要回滚,所以在 3.6 版本中,MongoDB 对这个机制进行了强化。3.6 把 settings.catchUpTimeoutMillis 的默认值调整为 -1,即不超时。但为了避免 catchup phase 无限进行,影响可用性(集群不可写),增加了 catchup takeover 机制,即集群当前正在被当选节点作为同步源 catchup 的节点,在等待一定的时间后,会主动发起选举投票,来使“不合格”的当选节点下台,从而减少 Data Rollback 的几率和保证集群尽快可用。这个等待时间由副本集的 settings.catchUpTakeoverDelayMillis 配置决定,默认为 30 秒。
stdx::unique_lock ReplicationCoordinatorImpl::_handleHeartbeatResponseAction_inlock( … case HeartbeatResponseAction::CatchupTakeover: { // Don’t schedule a catchup takeover if any takeover is already scheduled. if (!_catchupTakeoverCbh.isValid() && !_priorityTakeoverCbh.isValid()) { Milliseconds catchupTakeoverDelay = _rsConfig.getCatchUpTakeoverDelay(); _catchupTakeoverWhen = _replExecutor->now() + catchupTakeoverDelay; LOG_FOR_ELECTION(0) << “Scheduling catchup takeover at ” << _catchupTakeoverWhen; _catchupTakeoverCbh = _scheduleWorkAt( _catchupTakeoverWhen, [=](const mongo::executor::TaskExecutor::CallbackArgs&) { _startElectSelfIfEligibleV1(StartElectionReasonEnum::kCatchupTakeover); // 主动发起选举 }); } …注:*左右滑动阅览Data Rollback 是无法彻底避免的,因为 catchup phase 也只能发生在拥有最新 log entry 的节点在线的情况下,即能够向当选节点恢复心跳包,如果在选举完成后,节点才重新加入集群,仍然需要回滚。MongoDB 目前存在两种 Data Rollback 机制:「Refeched Based Rollback」 和 「Recover To Timestamp Rollback」,其中后一种是在 4.0 及之后的版本,伴随着 WiredTiger 存储引擎能力的提升而演进出来的,下面就简要描述一下它们的实现方式及关联。
Refeched Based Rollback「Refeched Based Rollback」 可以称之为逻辑回滚,下面这个图是逻辑回滚的流程图,
首先待回滚的旧主,需要确认重新选主后,自己的 oplog 历史和新主的 oplog 历史发生“分叉”的时间点,在这个时间点之前,新主和旧主的 oplog 是一致的,所以这个点也被称之为「common point」。旧主上从「common point」开始到自己最新的时间点之间的 oplog 就是未来及复制到新主的“多余”部分,需要回滚掉。common point 的查找逻辑在 syncRollBackLocalOperations() 中实现,大致流程为,由新到老(反向)从同步源节点获取每条 oplog,然后和自己本地的 oplog 进行比对。本地 oplog 的扫描同样为反向,由于 oplog 的时间戳可以保证递增,扫描时可以通过保存中间位点的方式来减少重复扫描。如果最终在本地找到一条 oplog 的时间戳和 term 和同步源的完全一样,那么这条 oplog 即为 common point。由于在分布式环境下,不同节点的时钟不能做到完全实时同步,而 term 可以唯一标识一个主节点在任期间的修改(oplog)历史,所以需要把 oplog ts 和 term 结合起来进行 common point 的查找。在找到 common point 之后,待回滚节点需要把当前最新的时间戳到 common point 之间的 oplog 都回滚掉,由于回滚采用逻辑的方式,整个流程还是比较复杂的。首先,MongoDB 的 oplog 本质上是一种 redo log,可以通过重新 apply 来进行数据恢复,而且 oplog 记录时对部分操作进行了重写,比如 {$inc : {quantity : 1}} 重写为 {$set : {quantity : val}} 等,来保证 oplog 的幂等性,按序重复应用 oplog,并不会导致数据不一致。但是 oplog 并不包含 undo 信息,所以对于部分操作来说,无法实现基于本地信息直接回滚,比如对于 delete,dropCollection 等操作,删除掉的文档在 oplog 并无记录,显然无法直接回滚。对于上述情况,MongoDB 采用了所谓「refetch」的方式进行回滚,即重新从同步源获取无法在本地直接回滚的文档,但是这个方式的问题在于 oplog 回滚到 tcommon 时,节点可能处于一个不一致的状态。举个例子,在 tcommon 时旧主上存在两条文档 {x : 10} 和 {y : 20},在重新选主之后,旧主上对 x 的 delete 操作并未同步到新主,在新主新的历史中,客户端先后对 x 和 y 做了更新:{$set : {y : 200}} ; {$set : {x : 100}}。在旧主通过「refetch」的方式完成回滚后,它在 tcommon 的状态为: {x : 100} 和 {y : 20},显然这个状态对于客户端来说是不一致的。这个问题的根本原因在于,「refetch」时只能获取到被删除文档当前最新的状态,而不是被删除前的状态,这个方式破坏了在客户端看来可能存在因果关系的不同文档间的一致性状态。我们具体上面的例子来说,回滚节点在「refetch」时相当于直接获取了 {$set : {x : 100}} 的状态变更操作,而跳过了 {$set : {y : 200}},如果要达到一致性状态,看起来只要重新应用 {$set : {y : 200}} 即可。但是回滚节点基于现有信息是无法分析出来跳过了哪些状态的,对于这个问题,直接但是有效的做法是,把同步源从 tcommon 之后的 oplog 都重新拉取并「reapply」一遍,显然可以把跳过的状态补齐。而这中间也可能存在对部分状态变更操作的重复应用,比如 {$set : {x : 100}},这个时候 oplog 的幂等性就发挥作用了,可以保证数据在最终「reapply」完后的一致性不受影响。剩下的问题就是,拉取到同步源 oplog 的什么位置为止?对于回滚节点来说,导致状态被跳过的原因是进行了「refetch」,所以只需要记录每次「refetch」时同步源最新的 oplog 时间戳,「reapply」时拉取到最后一次「refetch」对应的这个同步源时间戳就可以保证状态的正确补齐,MongoDB 在实现中把这个时间戳称之为 minValid。MongoDB 在逻辑回滚的过程中也进行了一些优化,比如在「refetch」之前,会扫描一遍需要回滚的操作(这个不需要专门来做,在查找 common point 的过程即可实现),对于一些存在“互斥”关系的操作,比如 {insert : {_id:1} 和 {delete : {_id:1}},就没必要先 refetch 再 delete 了,直接忽略回滚处理即可。但是从上面整体流程看,「Refeched Based Rollback」仍然复杂且代价高:
「refetch」阶段需要和同步源通信,并进行数据拉取,如果回滚的是删表操作,代价很大「reapply」阶段也需要和同步源通信,如果「refetch」阶段比较慢,需要拉取和重新应用的 oplog 也比较多实现上复杂,每种可能出现在 oplog 中的操作都需要有对应的回滚逻辑,新增类型时同样需要考虑,代码维护代价高所以在 4.0 版本中,随着 WiredTiger 引擎提供了回滚到指定的 Timestamp 的功能后,MongoDB 也用物理回滚的机制取代了上述逻辑回滚的机制,但在某些特殊情况下,逻辑回滚仍然有用武之地,下面就对这些做简要分析。
Recover To Timestamp Rollback「Recover To Timestamp Rollback」是借助于存储引擎把物理数据直接回滚到某个指定的时间点,所以这里把它称之为物理回滚,下面是 MongoDB 物理回滚的一个简化的流程图,
前面已经提到了 stable timestamp 的语义,这里不再赘述,MongoDB 有一个后台线程(WTCheckpointThread)会定期(默认情况下每 60 秒,由 storage.syncPeriodSecs 配置决定)根据 stable timestamp 触发新的 checkpoint 创建,这个 checkpoint 在实现中被称为 「stable checkpoint」。
class WiredTigerKVEngine::WiredTigerCheckpointThread : public BackgroundJob {public:… virtual void run() { … { stdx::unique_lock lock(_mutex); MONGO_IDLE_THREAD_BLOCK; _condvar.wait_for(lock, stdx::chrono::seconds(static_cast( wiredTigerGlobalOptions.checkpointDelaySecs))); } … UniqueWiredTigerSession session = _sessionCache->getSession(); WT_SESSION* s = session->getSession(); invariantWTOK(s->checkpoint(s, “use_timestamp=true”)); … }… }注:*左右滑动阅览stable checkpoint 本质上是一个持久化的历史快照,它所包含的数据修改已经复制到多数派节点,所以不会发生重新选主后修改被回滚。其实 WiredTiger 本身也可以配置根据生成的 WAL 大小或时间来自动触发创建新的 checkpoint,但是 Server 层并没有使用,原因就在于 MongoDB 需要保证在回滚到上一个 checkpoint 时,状态机肯定是 “stable” 的,不需要回滚。WiredTiger 在创建 stable checkpoint 时也是开启一个带时间戳的事务来保证 checkpoint 的一致性,checkpoint 线程会把事务可见范围内的脏页刷盘,最后对应到磁盘上就是一个由多个变长数据块(WT 中称之为extent)构成的 BTree。回滚时,同样要先确定 common point,这个流程和逻辑回滚没有区别,之后, Server 层会首先 abort 掉所有活跃事务,接着调用 WT 提供的 rollback_to_stable() 接口把数据库回滚到 stable checkpoint 对应的状态,这个动作主要是重新打开 checkpoint 对应的 BTree,并重新初始化 catalog 信息,rollback_to_stable() 执行完后会向 Server 层返回对应的 stable timestamp。考虑到 stable checkpoint 触发的间隔较大,通常 common point 总是大于 stable checkpoint 对应的时间戳,所以 Server 层在拿到引擎返回的时间戳之后会还需要从其开始重新 apply 本地的 oplog 到 common point 为止,然后把 common point 之后的 oplog truncate 掉,从而达到和新的同步源一致的状态。这个流程主要在 RollbackImpl::_runRollbackCriticalSection() 中实现,
Status RollbackImpl::_runRollbackCriticalSection( OperationContext* opCtx, RollBackLocalOperations::RollbackCommonPoint commonPoint) noexcept try { … killSessionsAbortAllPreparedTransactions(opCtx); // abort 活跃事务 … auto stableTimestampSW = _recoverToStableTimestamp(opCtx); // 引擎层回滚 … Timestamp truncatePoint = _findTruncateTimestamp(opCtx, commonPoint); // 查找并设置 truncate 位点 _replicationProcess->getConsistencyMarkers()->setOplogTruncateAfterPoint(opCtx, truncatePoint); … // Run the recovery process. // 这里会进行 reapply oplog 和 truncate oplog _replicationProcess->getReplicationRecovery()->recoverFromOplog(opCtx, stableTimestampSW.getValue()); … }注:*左右滑动阅览此外,为了确保回滚可以正常进行,Server 层在 oplog 的自动回收时还需要考虑 stable checkpoint 对部分 oplog 的依赖。通常来说,stable timestamp 之前的 oplog 可以安全的回收,但是在 4.2 中 MongoDB 增加了对大事务(对应的 oplog 大小超过 16MB)和分布式事务的支持,在 stable timestamp 之前的 oplog 在回滚 reapply oplog 的过程中也可能是需要的,所以在 4.2 中 oplog 的回收需要综合考虑当前最老的活跃事务和 stable timestamp。
StatusWith WiredTigerKVEngine::getOplogNeededForRollback() const { … if (oldestActiveTransactionTimestamp) { return std::min(oldestActiveTransactionTimestamp.value(), Timestamp(stableTimestamp)); } else { return Timestamp(stableTimestamp); }}注:*左右滑动阅览整体上来说,基于引擎 stable checkpoint 的物理回滚方式在回滚效率和回滚逻辑复杂性上都要优于逻辑回滚。但是 stable checkpoint 的推进要依赖 Server 层 majority commit point 的推进,而 majority commit point 的推进受限于各个节点的复制进度,所以复制慢时可能会导致 Primary 节点 cache 压力过大,所以 MongoDB 提供了 replication.enableMajorityReadConcern 参数用于控制是否维护 mcp,关闭后存储引擎也不再维护 stable checkpoint,此时回滚就仍然需要进行逻辑回滚,这也是在 4.2 中仍然保留「Refeched Based Rollback」的原因。
“linearizable” readConcern在一个分布式系统中,如果总是把可用性摆在第一位,那么因果一致性是其能够实现的最高一致性级别。前面我们也通过统计数据分析了在大部分情况下用户总是更关注延迟(可用性)而不是一致性,而 MongoDB 副本集,正是从用户需求角度出发,被设计成了一个在默认情况下总是优先保证可用性的分布式系统,下图是一个简单的例证。
既然如此,那 MongoDB 是如何实现 “linearizable” readConcern,即更高级别的线性一致性呢?MongoDB 的策略很简单,就是把它退化到几乎是单机环境下的问题,即只允许客户端在 Primary 节点上进行 “linearizable” 读。说是“几乎”,因为这个策略仍然需要解决如下两个在副本集这个分布式环境下存在的问题,
Primary 角色可能会发生变化,“linearizable” readConcern 需要保证每次读取总是能够从当前的 Primary 读取,而不是被取代的旧主。需要保证读取到读操作开始前最新的写,而且读到的结果不会在重新选主后发生回滚。MongoDB 采用同一个手段解决了上述两个问题,当客户端采用 “linearizable” readConcern 时,在读取完 Primary 上最新的数据后,在返回前会向 Oplog 中显示的写一条 noop 的操作,然后等待这条操作在多数派节点复制成功。显然,如果当前读取的节点并不是真正的主,那么这条 noop 操作就不可能在 majority 节点复制成功,同时,如果 noop 操作在 majority 节点复制成功,也就意味着之前读取的在 noop 之前写入的数据也已经复制到多数派节点,确保了读到的数据不会被回滚。
// src/mongo/db/read_concern_mongod.cpp:waitForLinearizableReadConcern()… writeConflictRetry( opCtx, “waitForLinearizableReadConcern”, NamespaceString::kRsOplogNamespace.ns(), [&opCtx] { WriteUnitOfWork uow(opCtx); opCtx->getClient()->getServiceContext()->getOpObserver()->onOpMessage( opCtx, BSON(“msg” << “linearizable read”)); // 写 noop 操作 uow.commit(); });… auto awaitReplResult = replCoord->awaitReplication(opCtx, lastOpApplied, wc); // 等待 noop 操作 majority committed注:*左右滑动阅览这个方案的缺点比较明显,单纯的读操作既产生了额外的写开销,也增加了延迟,但是这个是选择最高的一致性级别所需要付出的代价。
Causal Consistency前面几个章节描述的由 writeConcern 和 readConcern 所构成的 MongoDB 可调一致性模型,仍然是属于最终一致性的范畴(特殊实现的 “linearizable” readConcern 除外)。虽然最终一致性对于大部分业务场景来说已经足够了,但是在某些情况下仍然需要更高的一致性级别,比如在下图这个经典的银行存款业务中,如果只有最终一致性,那么就可能导致客户看到的账户余额异常。
这个问题虽然可以在业务端通过记录一些额外的状态和重试来解决,但是显然会导致业务逻辑过于复杂,所以 MongoDB 实现了「Causal Consistency Session」功能来帮助降低业务复杂度。Causal Consistency 定义了分布式系统上的读写操作需要满足一个偏序(Partial Order)关系,即只部分操作发生的先后顺序可比。这个部分操作,进一步来说,指的是存在因果关系的操作,在 MongoDB 的「Causal Consistency Session」实现中,什么样的操作间算是存在因果关系,可参考前文提到的 Client-centric Consistency Model 下的 4 个一致性承诺分别对应的读写场景,此处不再赘述。所以,要实现因果一致性,MongoDB 首要解决的问题是如何给分布式环境下存在因果关系的操作定序,这里 MongoDB 借鉴了 Hybrid Logical Clock 的设计,实现了自己的 ClusterTime 机制,下面就对其实现进行分析。
分布式系统中存在因果关系的操作定序关于分布式系统中的事件如何定序的论述,最有影响力的当属 Leslie Lamport 的这篇 《Time, Clocks, and the Ordering of Events in a Distributed System》,其中提到了一种 Logical Clock 用来确定不同事件的全序,后人也把它称为 Lamport Clock。
Lamport Clock 只用一个单纯的变量(scalar value)来表示,所以它的缺点之一是无法识别存在并发的事件(independent event),而这个会在实际的系统带来一些问题,比如在支持多点写入的系统中,无法基于 Lamport Clock 对存在写冲突的事件进行识别和处理。所以,后面又衍生出了 vector clock 来解决这一问题,但 vector clock 会存储数据的多个版本,数据量和系统中的节点数成正比,所以实际使用会带来一些扩展性的问题。Lamport Clock 存在的另外一个缺点是,它完全是一个逻辑意义上的值,和具体的物理时钟没有关联,而在现实的应用场景中,存在一些需要基于真实的物理时间进行访问的场景,比如数据的备份和恢复。Google 在其 Spanner 分布式数据库中提到了一种称之为 TrueTime 的分布式时钟设计,为事务执行提供时间戳。TrueTime 和真实物理时钟关联,但是需要特殊的硬件(原子钟/GPS)支持,MongoDB 作为一款开源软件,需要做到通用的部署,显然无法采用该方案。考虑到 MongoDB 本身不支持 「Multi-Master」 架构,而上述的分布式时钟方案均存在一些 MongoDB 在设计上需要规避的问题,所以 MongoDB 采用了一种所谓的混合逻辑时钟(Hybrid Logical Clock)的方案。HLC 设计上基于 Lamport Clock,只使用单个时钟变量,在具备给因果操作定序的能力同时,也能够(尽可能)接近真实的物理时钟。
Hybrid Logical Clock 基本原理
先来了解一下 HLC 中几个基本的概念,
pt:节点本地的物理时钟,通常是基于 ntp 进行时钟同步,HLC 只会读取该值,不会对该值做修改。l:HLC 的高位部分,是 HLC 的物理部分,和pt关联,HLC 保证 l 和 pt间的差值不会无限增长(bounded)。c:HLC 的低位部分,是 HLC 的逻辑部分。从上面的 HLC 时钟推进图中,可以看到,如果不考虑 l 部分(假设 l 总是不变),则 c 等同于 Lamport Clock,如果考虑 l 的变化,因为 l 是高位部分,只需要保证 if e hb f, l.e <= l.f,仍然可以确定存在因果关系的事件的先后顺序,具体的更新规则可以参考上面的算法。但是 l 的更新机制也决定了其他节点的时钟出现跳变或不同步,会导致 HLC 被推进,进而导致和 pt 产生误差,但 HLC 的机制决定了这个误差是有限的。上面的图就是一个很好的案例,假设当前的真实物理时钟是 0,而 0 号节点的时钟出现了跳变,变为 10,则在后续的时钟推进中,l 部分不会再增长,只会增加 c 部分,直到真实的物理时钟推进到 10,l 才会关联新的 pt 。MongoDB 在实现 Causal Consistency 之前就已经在副本集同步的 oplog 时间戳中使用了类似的设计,选择 HLC,也是为了方便和现有设计集成。Causal Consistency 不仅是在单一的副本集层面使用,在基于副本集构建的分片集群中同样有需求,所以这个新的分布式时钟,在 v3.6 中被称为 ClusterTime。
MongoDB ClusterTime 实现MongoDB ClusterTime 基本上是严格按照 HLC 的思路来实现的,但它和 HLC 最大的一点不同是,在 HLC 或 Lamport Clock 中,消息的发送和接受都被认为是一个事件,会导致时钟值增加,但在 MongoDB ClusterTime 实现中,只有会改变数据库状态的操作发生才会导致 ClusterTime 增加,比如通常的写操作,这么做的目的还是为了和现有的 oplog 中的混合时间戳机制集成,避免更大的重构开销和由此带来的兼容性问题,同时这么做也并不会影响 ClusterTime 在逻辑上的正确性。因为有了上述区别,ClusterTime 的实现就可以被分为两部分,一个是 ClusterTime 的增加(Tick),一个是 ClusterTime 的推进(Advance)。ClusterTime 的 Tick 发生在 MongoDB 接收到写操作时,ClusterTime 由 来表示,是一个 64bit 的整数,其中高 32 位对应到 HLC 中的物理部分,低 32 位对应到 HLC 中的逻辑部分。而每一个写操作在执行前都会为即将要写的 oplog 提前申请对应的 OpTime(调用 getNextOpTimes() 来完成),OpTime 由 来表示,ElectionTerm 和 MongoDB 的复制协议相关,是一个本地的状态值,不需要被包含到 ClusterTime 中,所以原有的 OpTime 在新版本中实际上是可以由 ClusterTime 直接转化得来,而 ClusterTime 也会随着 Oplog 写到磁盘而被持久化。
std::vector LocalOplogInfo::getNextOpTimes(OperationContext* opCtx, std::size_t count) {… // 申请 OpTime 时会 Tick ClusterTime 并获取 Tick 后的值 ts = LogicalClock::get(opCtx)->reserveTicks(count).asTimestamp(); const bool orderedCommit = false;… std::vector oplogSlots(count); for (std::size_t i = 0; i < count; i++) { oplogSlots[i] = {Timestamp(ts.asULL() + i), term}; // 把 ClusterTime 转化为 OpTime… return oplogSlots;}// src/mongo/db/logical_clock.cpp:LogicalClock::reserveTicks() 包含了 Tick 的逻辑,和 HLC paper 一致,主要逻辑如下{ newCounter = 0; wallClockSecs = now(); // _clusterTime is a current local value of node’s ClusterTime currentSecs = _clusterTime.getSecs(); if (currentSecs > wallClockSecs) { newSecs = currentSecs; newCounter = _clusterTime.getCounter() + 1; } else { newSecs = wallClockSecs; } _clusterTime = ClusterTime(newSecs, newCounter); return _clusterTime;}注:*左右滑动阅览ClusterTime 的 Advance 逻辑比较简单,MongoDB 会在每个请求的回复中带上当前节点最新的 ClusterTime,如下,
“$clusterTime” : { “clusterTime” : Timestamp(1495470881, 5), “signature” : { “hash” : BinData(0, “7olYjQCLtnfORsI9IAhdsftESR4=”), “keyId” : “6422998367101517844” }}注:*左右滑动阅览
接收到该 ClusterTime 的角色(mongos,client)如果发现更新的 ClusterTime,就会更新本地的值,同时在和别的节点通信的时候,带上这个新 ClusterTime,从而推进其他节点上的 ClusterTime,这个流程实际上是一种类似于 Gossip 的消息传播机制。因为 Client 会参与到 ClusterTime 的推进(Advance),如果有恶意的 Client 篡改了自己收到的 ClusterTime,比如把高位和低位部分都改成了 UINT32_MAX,则收到该 ClusterTime 的节点后续就无法再进行 Tick,这个会导致整个服务不可用,所以 MongoDB 的 ClusterTime 实现增加了签名机制(这个安全方面的增强 HLC 没有提及),上面的signature 字段即对应该功能,mongos 或 mongod 在收到 Client 发送过来的 $ClusterTime 时,会根据 config server 上存储的 key 来进行签名校验,如果 ClusterTime 被篡改,则签名不匹配,就不会推进本地时钟。除了恶意的 Client,操作失误也可能导致 mongod 节点的 wall clock 被更新为一个极大的值,同样会导致 ClusterTime 不能 Tick,针对这个问题,MongoDB 做了一个限制,新的 ClusterTime 和当前 ClusterTime 的差值如果超出 maxAcceptableLogicalClockDriftSecs,默认为 1 年,则当前的 ClusterTime 不会被推进。
MongoDB Causal Consistency 实现在 ClusterTime 机制的基础上,我们就可以给不同的读写操作定序,但是操作对应的 ClusterTime 是在其被发送到数据节点(mongod)上之后才被赋予的,如果要实现 Causal Consistency 的承诺,比如前面提到的「Read Your Own Write」,显然我们需要 Client 也知道写操作在主节点执行完后对应的 ClusterTime。
… “operationTime” : Timestamp(1612418230, 1), # Stable ClusterTime “ok” : 1, “$clusterTime” : { … }注:*左右滑动阅览所以 MongoDB 在请求的回复中除了带上 $clusterTIme 用于帮助推进混合逻辑时钟,还会带上另外一个字段 operationTime 用来表明这个请求包含的操作对应的 ClusterTime,operationTime 在 MongoDB 中也被称之为 「Stable ClusterTime」,它的准确含义是操作执行完成时,当前最新的 Oplog 时间戳(OpTime)。所以对于写操作来说,operationTime 就是这个写操作本身对应的 Oplog 的 OpTime,而对于读操作,取决于并发的写操作的执行情况。
Client 在收到这个 operationTime 后,如果要实现因果一致,就会在发送给其他节点的请求的 afterClusterTime 字段中带上这个 operationTime,其他节点在处理这个请求时,只会读取 afterClusterTime 之后的数据状态,这个过程是通过显式的等待同步位点推进来实现的,等待的逻辑和前面提到的 speculative “majority” readConcern 实现类似。上图是 MongoDB 副本集实现「Read Your Own Write」的基本流程。如果是在分片集群形态下,由于混合逻辑时钟的推进依赖于各个参与方(client/mongos/mongd)的交互,所以会暂时出现不同分片间的逻辑时钟不一致的情况,所以在这个架构下,我们需要解决某个分片的逻辑时钟滞后于 afterClusterTime 而且一直没有新的写入,导致请求持续被阻塞的问题,MongoDB 的做法是,在这种情况下显式的写一条 noop 操作到 oplog 中,相当于强制把这个分片的数据状态推进到 afterClusterTime 之后,从而确保操作能够尽快返回,同时也符合因果一致性的要求。
总结本文对 MongoDB 一致性模型在设计上的一些考虑和主要的实现机制进行了分析,这其中包括由 writeConcern 和 readConcern 机制构建的可调一致性模型,对应到标准模型中就是最终一致性和线性一致性,但是 MongoDB 借助read/write concern 这两者的配合,为用户提供更丰富的一致性和性能间的选择。此外,我们也分析了 MongoDB 如何基于 ClusterTime 混合逻辑时钟机制来给分布式环境下的读写操作定序,进而实现因果一致性。从功能和设计思路来看,MongoDB 无疑是丰富和先进的,但是在接口层面,读写采用不同的配置和级别,事务和非事务的概念区分,Causal Consistency Session 对 read/writeConcern的依赖等,都为用户的实际使用增加了门槛,当然这些也是 MongoDB 在易用性、功能性和性能多方取舍的结果,相信 MongoDB 后续会持续的做出改进。最后,伴随着 NewSQL 概念的兴起,「分布式+横向扩展+事务能力」逐渐成为新数据库系统的标配,MongoDB 也不例外。当我们在传统单机数据库环境下谈论一致性,更多指的是事务间的隔离性(Isolation),如果把隔离性这个概念映射到分布式架构下,可以容易看出,MongoDB 的 “local” readConcern 即对应 read uncommitted,”majority” readConcern 即对应 read committed,而 “snapshot” readConcern 对应的就是分布式的全局快照隔离,即这些新的概念部分也是来自于经典的 ACID 理论在分布式环境下的延伸,带上这样的视角可以让我们更容易理解 MongoDB 的一致性模型设计。
参考文档
Tunable Consistency In MongoDB: http://www.vldb.org/pvldb/vol12/p2071-schultz.pdfImplementation of Cluster-wide Logical Clock and Causal Consistency in MongoDB: https://dl.acm.org/doi/pdf/10.1145/3299869.3314049Logical Physical Clocks: https://cse.buffalo.edu/~demirbas/publications/hlc.pdfPACELC: http://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdfConsistency and Replication 1: https://web2.qatar.cmu.edu/~msakr/15440-f11/lectures/Lecture11_15440_VKO_10Oct_2011.pptxConsistency and Replication 2: https://web2.qatar.cmu.edu/~msakr/15440-f11/lectures/Lecture11_15440_VKO_10Oct_2011.pptxMongoDB writeConcern: https://docs.mongodb.com/manual/reference/write-concern/MongoDB readConcern: https://docs.mongodb.com/manual/reference/read-concern/WiredTiger Application-specified Transaction Timestamps: https://source.wiredtiger.com/develop/transactions.html#transaction_timestampsDatabase Replication Using Generalized Snapshot Isolation: https://infoscience.epfl.ch/record/53561/files/srds2005-gsi.pdfMongoDB Logical Session: https://www.mongodb.com/blog/post/transactions-background-part-2-logical-sessions-in-mongodb4 modifications for Raft consensus: https://www.openlife.cc/blogs/2015/september/4-modifications-raft-consensusTime, Clocks, and the Ordering of Events in a Distributed System: https://lamport.azurewebsites.net/pubs/time-clocks.pdfMongoDB Sharding Internals: https://github.com/mongodb/mongo/blob/master/src/mongo/db/s/README.mdMongoDB Replication Internals: https://github.com/mongodb/mongo/blob/master/src/mongo/db/repl/README.md#阿里云# #数据库#

其他教程

简单配音软件(实用的配音软件)

2022-8-21 8:57:04

其他教程

pr 2020字幕(pr 2020 横屏转竖屏)

2022-8-21 8:59:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索