在一篇题为“MongoDB和Jepsen”的文章中,MongoDB 官方声称他们的数据库通过了“业界最严格的数据安全性、正确性和一致性测试”。作为回应,Jepsen 官方发表了一篇文章,指出 MongoDB 3.6.4 实际上没有通过他们的测试,而 MongoDB 4.2.6 的问题则更多,包括“回溯关联性事务”(回溯关联性事务是指事务会反转操作顺序,让读操作可以看到未来写操作的结果)。
Jepsen 在官方 Twitter 上对 Maxime Beugnet 做出了回复:
不得不承认,在看到那个网页时,我大吃一惊。在测试报告中,MongoDB 不仅丢失数据,还违背了关联关系,它怎么就成为“当今所有可用数据库中具有最强大的数据一致性、正确性和安全性保证的数据库之一呢”!
上述的报告出自 Kit Patella 的一篇题为“MongoDB 3.6.4”的文章,而 Kyle Kingsbury 的新报告对此进行了扩展:
类似地,MongoDB 的默认读选项允许中止读,即读操作可以观察到未完全提交并可能在未来被丢弃的状态。正如“读隔离级别一致性”文档所指出的:“读未提交是默认的隔离级别”。
我们发现,由于这些默认设置,MongoDB 的关联会话在默认情况下并不会保持关联一致性:用户需要同时指定写操作和读操作的选项为“majority”才能获得关联一致性。MongoDB 关掉了这个问题,说原本就是这样设计的,并更新了隔离级别文档,说即便 MongoDB 提供了“客户端会话的关联一致性”,但并不保证,除非用户小心使用读操作和写操作选项。现在,文档中有一个表格详细描述了较弱的读写关注点属性。
失败的事务隔离
近年来,MongoDB 一直在大力推广它的事务能力。但 Jepsen 发现,在默认情况下 MongoDB 的事务能力并不奏效。在一次测试中,他们通过事务向文档中追加数据。他们发现,即使在数据库/集合级别设置了“majority”写选项,但在事务级别设置了默认的写选项,那么“事务似乎会丢掉已确认的写操作”(这可以通过在事务级别显式指定写选项来解决)。
客户端看到一个元素单调递增的列表[1 2 3 4 5 6 7],此时列表重置为[],并使用[8]重新开始。这可能是 MongoDB 回滚的一个例子,是“数据丢失”的一种奇特说法。
那么一个更微妙的问题出现了:为什么我们能够读到这些值?毕竟,读选项应该只显示多数确认的(即持久化的)写操作。答案有点令人惊讶,文档中写提到了 MongoDB 的设计:
事务中的操作使用的是事务级别的读选项,也就是说,在集合和数据库级别设置的任何读选项在事务内部都会被忽略。
这意味着“没有显式声明读选项的事务会将数据库或集合级别的请求读选项降级为局部默认级别”,从而允许事务读取未提交的数据,而这些数据稍后可能会被回滚。
反过来也是有问题的。根据文档所述,“如果事务没有对提交的数据使用‘majority’写选项,“snapshot”读选项不能保证读操作使用的是通过‘majority’选项提交的数据的快照”。换句话说,在没有设置写选项的情况下,“snapshot”读选项将被忽略。同样,这个是发生在事务级别,因为事务忽略了集合和数据库级别的设置。
回溯关联性事务
即使使用快照隔离,也可能会出现很多意外的结果。它们中的大多数都太复杂了,其中有一个很突出。
在一次测试中,Jepsen 的研究人员使用客户端读取文档,然后向它追加数据。在测试开始时,文档包含序列[2,3,4]。读取数值后,文档被改为[1,2,3,4]。
这通常是有效的,但要在四个事务中从数据库中读取[1、2、3、4],Kingsbury 继续写道:
当然,这是不可能的:我们的测试严格按照顺序提交事务的操作,除非 MongoDB 有时间机器,否则它不可能返回还没有被写入的值。这说明回溯性事务实际上执行了两次,并且在第二次执行时看到了其自身先前执行的结果。这可能是因为不恰当的重试机制导致的结果。
这并不是唯一一次重试机制被人们指责。
我们发现,网络分区可能会导致 MongoDB 重复执行事务。尽管不会将相同的值追加到一个数组两次,但我们发现了数组会包含相同元素的多个副本。
为了更好地理解这些行为,研究人员试图禁用自动重试,结果发现“MongoDB 事务忽略了 retryWrites 设置,仍然会重试”。
除了为开发人员提供如何更安全地使用 MongoDB 的建议之外,Jepsen 还建议“MongoDB 修改营销描述,使用‘快照隔离’而不是‘ACID’”。
编后语:之前的文章暗示在使用事务时总是会发生数据丢失,实际上这个问题只会在使用事务默认写选项时发生。但是,使用“majority”写选项的事务也出现了其他异常。
原文链接:
Jepsen Disputes MongoDB’s Data Consistency Claims
评论 1 条评论