杰普森:MongoDB 4.2.6

2020-05-16 01:43:00

MongoDB是一个分布式文档数据库,它声称提供“当今任何可用的数据库中最强大的数据一致性、正确性和安全性保证”,具有“完整的ACID事务”。Jepsen评估了MongoDB版本4.2.6,发现即使在读写关注度最高的情况下,它也无法保持快照隔离。相反,Jepsen观察到读取偏差、循环信息流、重复写入和内部一致性违规。弱默认值意味着事务可能会丢失写入并允许脏读,甚至会降低数据库和集合级别上请求的安全级别。此外,快照读取关注点不能保证快照,除非与大多数写入关注点配对-即使对于只读事务也是如此。这些设计选择使MongoDB事务的安全使用变得复杂。这项工作是独立进行的,没有报酬,并根据杰普森道德政策进行。本报告中提到的MongoDB、Fauna和YuaByte之前都曾聘请Jepsen进行付费分析。

MongoDB是流行的分布式文档数据库。它通过自主开发的共识协议提供复制,该协议的灵感来自RAFT,并且可以通过mongos跨分片分发数据。我们之前评估了MongoDB的2.4.3、2.6.7、3.4.0-rc3和3.6.4版本。

我们关于MongoDB 3.6.4的最新报告集中于分片集合中的因果一致性和线性化。我们发现,分片集群似乎提供了针对单个文档的可线性化的读取、写入和比较并设置操作,只要用户在运行时使用的是可线性化的读取关注点和多数写入关注点。但是,任何较弱的写入关注度都会导致提交的写入丢失。MongoDB的默认写关注级别是(并保持)由单个节点确认,这意味着MongoDB可能会在默认情况下丢失数据。尽管写入问题文档没有明确说明这一点,但回滚文档声明:

对于默认写入问题,如果主服务器在写入操作复制到任何辅助服务器之前停止,则可能会回滚数据。

类似地,MongoDB的默认读取关注度允许中止读取:读取器可以观察到未完全提交的状态,并且在将来可能会被丢弃。正如读隔离一致性文档所指出的,“未提交的读操作是默认的隔离级别”。

我们发现,由于这些弱缺省值,MongoDB的因果会话在缺省情况下不能保持因果一致性:用户需要同时指定写和读关注点多数(或更高)才能实际获得因果一致性。MongoDB解决了这个问题,说它正在按设计工作,并更新了他们的隔离文档,指出即使MongoDB提供了“客户端会话中的因果一致性”,但除非用户小心地同时使用读和写关注点多数,否则这一保证是站不住脚的。现在有一个详细的表显示了较弱的读写关注点提供的属性。

奇怪的是,MongoDB在他们的MongoDB和Jepsen页面上没有提及这些发现。相反,该页只讨论通过结果,没有提到读取或写入问题,将实际报告隐藏在脚注中,并继续声称:

MongoDB提供当今任何可用的数据库中最强的数据一致性、正确性和安全性保证。

我们鼓励MongoDB在上下文中报告Jepsen的发现:虽然MongoDB确实提供了每个文档的线性化和具有最强设置的因果一致性,但它在大多数配置中也无法提供这些属性。我们认为用户可能想要知道,默认情况下,他们的数据库可能会丢失数据,但是MongoDB对我们工作的总结省略了对此行为的任何提及。

那么,MongoDB是否提供“最强的数据一致性、正确性和安全性保证”呢?过去的工作表明,对于单个文档操作,答案是“是的;MongoDB以最强的设置提供了每个文档的线性化,但不是默认的”。然而,在2018年,MongoDB引入了多文档事务-仅限于一个分片内-并在2019年将这些事务扩展到多个分片。这些交易提供哪些安全属性?

MongoDB主页自豪地宣传“全ACID交易”。Transaction页面声明MongoDB是“唯一完全结合了文档模型的功能和具有ACID保证的分布式系统架构的数据库”,CosmosDB、DynamoDB、FaunaDB、Oracle NoSQL、OrientDB、RavenDB、SAP HANA、YuaByte DB等公司也声称这一组合。

mongodb架构指南承诺ACID事务“保持与您在传统数据库…中习惯的数据完整性保证相同的数据完整性保证”。MongoDB提供了“很强的设计一致性”,但没有提供更具体的声明。ACID白皮书澄清了MongoDB事务提供快照隔离:一个相当强大的模型,它构成了PostgreSQL等系统的一致性基准级别。白皮书称:

…。快照读取隔离可确保在只读事务内执行的查询和聚合将针对跨分片群集的每个主副本的数据库的全局一致快照运行。

MongoDB反复将快照隔离总结为“事务提供一致的数据视图,并强制执行全有或全无执行以维护数据完整性”。这是对快照隔离的简明而直观的总结,但我们应该注意,快照隔离下的“一致视图”可能仍然令人惊讶:正如Fekete、O‘Neil和O’Neil在2004年所写的那样,只读事务在快照隔离下可以观察到不可序列化的行为。快照隔离也不一定维护数据完整性:在他们1995年的论文Defining Snapshot Isolation中,Berenson、Bernstein等人提供了在快照隔离下其完整性约束可能被违反的应用程序示例-例如,由于写入偏差。

我们使用Jepsen分布式系统测试库设计了一个测试套件,并使用它对MongoDB 4.2.6中的事务安全性进行了评估。我们的测试在9个Debian 9节点的集群上安装了MongoDB的官方Debian包。我们在LXC和EC2中都进行了测试;两者都表现出相似的行为。根据部署指导原则,我们构建了双分片集群,将分片和configsvr元数据系统都作为三节点副本集运行。所有9个节点都运行一个mongos实例,该实例充当分片MongoDB集群的前端。

我们的测试工作负载涉及单个MongoDB集合中循环文档池的事务,每个文档包含单个整数数组。每个事务对这些文档执行一到四个操作:要么按primary key_id读取单个文档,要么(使用$PUSH)再次按_id将唯一整数附加到单个文档的值数组。我们按_id对集合进行了分片。

使用与加州大学圣克鲁斯分校的Peter Alvaro合作开发的新交易分析工具Elle,Jepsen自动推断这些交易之间的依赖关系,并在该图中搜索循环以识别隔离异常。检查中止和中间读取以及其他非循环异常的其他技术。

在其中一些测试中,我们引入了网络分区,旨在隔离MongoDB主节点。

我们首先在没有事务的情况下运行测试以获得基线:每个Jepsen“事务”只执行一个读取或追加操作,而不使用会话或事务API。因为我们知道MongoDB会丢失任何设置小于多数的更新,并且在没有读取关注点的情况下表现出陈旧的读取可线性化,所以我们在客户端的数据库句柄上设置了写入关注点多数和读取关注点可线性化。生成的历史记录似乎与快照隔离一致。

然后,我们将这些单个操作包装在事务中,出现了一个令人惊讶的行为:使用网络分区,事务似乎丢失了确认的写入。例如,考虑以下历史记录,其中在30秒内,对8个文档的更新被成功确认,然后消失。以下是阅读文档555的历史记录:

客户端观察到一个单调增长的元素列表,直到[1 2 3 4 5 6 7],此时列表重置为[],并从[8]重新开始。这可能是MongoDB回滚的一个例子,这是“数据丢失”的一种花哨的说法。

这很糟糕,但一个更微妙的问题出现了:我们到底为什么能够读取这些值?毕竟,Read Concerns Linizable应该只显示大多数确认(即不可持续)的写入。答案是一个令人惊讶但有文档记录的MongoDB设计选择:

事务中的操作使用事务级别的读取关注点。也就是说,在集合和数据库级别设置的任何读取关注点在事务内都会被忽略。

数据库社区的普遍看法是,MongoDB历来拒绝提高读写安全的默认级别,因为这样做会影响已经习惯了更快的、偶尔不安全的默认设置的生产用户,而且数据丢失可能不够严重,不足以保证更强的安全设置带来的延迟、吞吐量和资本支出的增加。MongoDB的研究人员在VLDB上报告说:

…。当然,用户更喜欢使用readConcern级别的“多数”和writeConcern w:“多数”,因为每个人都想要安全。但是,当用户发现较强的一致性级别太慢时,他们会切换到使用较弱的一致性级别。这些决策通常基于业务需求和SLA,而不是细粒度的开发人员需求。正如我们在整篇文章中所讨论的那样,决定使用较弱的一致性级别在实践中通常是可行的,因为故障转移很少发生,并且故障转移造成的数据丢失通常很小。

然而,交易是一个全新的功能,用户可能希望牺牲一些速度来换取更好的安全保证。合理的用户可能会期望事务的默认安全级别如承诺的那样提供快照隔离-或者,至少与用户已经从所涉及的数据库或集合请求的读取关注度相同。相反,没有显式读关注点的事务将数据库或集合级别的任何请求的读关注点降级为缺省的本地级别,这“不能保证数据已被写入大多数副本(即可能被回滚)”。

因此:用户应该小心地对每个需要快照隔离的事务使用快照级别的读关注。文档确认:“Read Concerns‘Snapshot’从大多数提交的Data…的快照返回数据”这是有意义的,然后继续:“如果事务以写关注‘多数’提交”,则不是这样。具体而言:

如果事务不使用写关注点“多数”进行提交,则“快照”读关注点不能保证读取操作使用了多数提交数据的快照。

精明的读者可能会问,“拥有不提供快照隔离读取的快照读取关注点有什么意义?”更精明的读者在观察到一种模式后,可能会询问写入关注度的默认级别。

如果事务级别的写入关注点和会话级别的写入关注点未设置,则事务级别的写入关注默认为客户端级的写入关注点。默认情况下,客户端级写入关注点为w:1。

为了获得快照隔离,用户不仅必须小心地将每个事务的读关注点设置为快照,还必须将每个事务的写关注点设置为多数。

令人惊讶的是,这甚至适用于只读事务。在此测试运行中,我们在单操作写事务上设置读关注点快照和写关注点多数,对于单操作读事务,仅设置读关注点快照。发生网络分区时,读取观察到的不同时间线:

这些更新并没有完全丢失-写入1、5和6的事务超时,但它们对读取的影响既可见又不可见,这意味着这些时间线中至少有一个是中止读取的实例。

这种行为可能令人惊讶,但是MongoDB值得称赞的是,这种行为的大部分都在事务文档中清楚地列出了。问题是,用户是否仔细阅读了该文档,而不是依赖于市场宣传,或者像“使用Read Concerns Snapshot意味着阅读提交的快照”这样的假设。我们可能还会问,是否可以期望用户记住将这些设置应用于需要这些设置的每个事务。毕竟,MongoDB提供了精确的数据库和集合级安全设置,因此用户可以假定与这些数据库或集合交互的所有操作都使用这些设置;当用户执行(可能)安全关键型操作时,忽略读写相关设置是令人惊讶的!

在随后的测试中,我们对所有单操作读写使用读关注度可线性化和写关注度多数,对多操作事务使用读关注度快照和写关注度多数。在健康群集(例如,没有故障的群集)中,粗略测试似乎与快照隔离一致。

然而,我们注意到,虽然MongoDB的主页显著地声称提供“全ACID事务”,但人们可能会认为这意味着事务是完全原子的、隔离的、一致的和持久的。事实并非如此:如前所述,快照隔离下的事务并不是完全隔离的。

例如,考虑以下测试运行,它使用读关注点快照、写关注点多数,并且不涉及网络分区或其他外部故障。生成的历史似乎没有违反快照隔离,但仍然表现出循环的事务依赖关系,如下所示:

一个事务将1附加到文档1047,并读取文档1045,发现它是空的。另一个将1附加到文档1045,并读取1047,发现它是空的。标记为RW的行表示这些读写反依赖关系。这些事务不可能是隔离的:如果第一个事务先隔离执行,则其对1047的写入对第二个事务是可见的,反之亦然。由于这些事务没有写入相同的文档,因此允许它们(在快照隔离下)并发执行。

其他周期则更为复杂。例如,这里是一个由12个事务组成的集群,同样在MongoDB最强的安全设置下执行。这些事务中的每一个都(以各种方式)相互依赖。标记为WW的箭头显示写-写依赖关系,其中一个事务覆盖另一个事务的写入。具有WR的事务表现出写-读依赖关系:一个事务读取另一个事务的写。

或者考虑一下这个由123个事务组成的集群,所有这些事务似乎都是在每个事务之前执行的,但也是在每个事务之后执行的。这个星团是“全酸”吗?也许吧,但如果是这样的话,我们必须承认,酸性中的“我”只意味着部分隔离,或者“完全”意味着略低于完全。

这些异常现象并不少见。例如,这个历史记录每秒大约包含100个事务,我们识别出1,461个事务(总共13914个)具有循环依赖关系。大约10%的事务在正常运行期间出现异常,没有故障。

重要的是要记住,仅仅因为我们可以在这个简单的工作负载中检测到异常,并不意味着这些异常对用户很重要。并发性可能足够低,因此在很大程度上不需要进行并发控制。可以证明某些事务集在快照隔离下可串行执行-例如,当它们的写入集相交时。其他的则表现出不可序列化的异常,但不违反应用程序级一致性约束。还有一些确实违反了约束,但还不够频繁,用户不会注意到或在意。对于这些目的,快照隔离就足够好了!

当我们在测试中引入网络故障时,我们遇到(正如人们所预期的)各种客户端错误。MongoDB事务文档说:

当事务中止时,事务中所做的所有数据更改都将被丢弃,而不会变得可见。例如,如果事务中的任何操作失败,事务将中止,事务中所做的所有数据更改都将被丢弃,而不会变得可见。1个。

然而,反之亦然:一些事务错误消息似乎表明事务已中止,但事实并非如此。例如,TransactionConsulatorSteppingDown异常实际上可能意味着事务已提交。同样,命令失败,出现错误6(HostUnreacable):';无法初始化集合jepsendb.jepsencoll::Reduced By::Connection Rejected';的写操作的目标器。我们在测试中反复遇到这些错误,结果发现它们的写入对于以后的读取是可见的。

这不一定是错误-在任何分布式系统中,总会有一类错误指示成功或失败。但是,如果清楚地标记这些错误,或者提供文本指导,则会很有帮助。错误6,就像我们遇到的大多数错误一样,没有文档记录;官方文档中只剩下几个代码,谷歌也没什么可说的。完整的错误文档(可能带有代码指示确定故障与不确定故障的表)可以为错误消息提供一种实用的替代方案。

在正确解释错误的情况下,我们发现网络分区可能会导致MongoDB复制事务的影响。尽管从未两次将相同的值附加到一个数组中,但我们反复观察到具有同一元素的多个副本的数组。例如,以此测试运行为例,它包含以下事务:

这里,元素6在阅读文档436时出现了两次。这种重复提高了将6写入密钥436的事务发生在其他事务之前和之后的可能性;根据人们选择的解释(如果有的话),所得到的历史也表现出G-Single和G2反依赖循环。即使在读关注点快照和写关注点占多数的情况下也会出现这种异常,这表明即使在最强的设置下,MongoDB事务也不提供快照隔离。

此行为可能表明事务重试机制不正确-MongoDB将自动重试作为一项功能进行通告。为了确定重试机制是否有问题,我们尝试禁用它-结果发现MongoDB事务忽略了retryWrites设置,并且不管怎样都重试。我们不确定用户是否可以解决此问题。

在这种情况下,使用读关注点快照和写关注点多数运行的测试执行了三个事务,具有以下依赖关系图:

最上面的事务将2附加到文档79,紧随其后的是中间事务的附加5。我们可以推断这些写入按此顺序发生,因为观察到另一个事务(不是此周期的一部分):

在这些写入之后,底部事务将5附加到文档77,中间事务的读取77没有观察到这一点。但是,追加5对于最上面的事务是可见的!

因为这个循环正好包含一个反依赖(RW)边缘,所以它很可能是Adya的G-Single异常(也称为读偏差)的一个例子。本质上,中间事务观察到逻辑上优先事务的部分(但不是全部)影响。快照禁止读取偏差。

..