暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MongoDB为啥那么渣

数据极客 2015-10-16
377

看到云栖大会MongoDB又出来宣传了,忍不住再来写一篇。


MongoDB自从08年发明以来一直争议不断,然而不幸的是,这样一个没有认真设计的数据库居然一直流行,甚至连同其他组件一起构成了跟LAMP并列的MEAN技术栈,究其原因,可能包含这2方面的因素:

  1. MongoDB实在太好用了,各种语言客户端,涵盖了所有Web开发的需求。此外还有详尽的文档和社区,让使用者感觉逼格很高很安全。

  2. 近十年的互联网架构发展,导致传统数据库没有很快跟上变化。数据库实现一直是门槛很高的行业,许多一知半解的设计师为了应对变化推出各

  3. 种解决方案,在赚取噱头的同时却忽略了质量。

由此,不论是在技术界还是其他领域,赢得成功的总是擅长包装者。抛开这些不谈,做为使用者,还是有必要尽力让自己不要落入坑中。


那么MongoDB设计上都有哪些问题呢?本文只谈论最核心的一些方面。


MongoDB声称可以提供单个文档的ACID。然而,却奇葩得提供了Write Concern作为API的参数,什么意思呢?就是说MongoDB把你的数据弄丢后,你的着急程度,这不是在拿ACID开玩笑么?事实上,在广泛使用的MongoDB 2.0以及之前的版本中,一次写入操作成功的定义为数据写到客户端的Buffer里,如果此时客户端崩溃,那么丢失是必然的。在之后的版本中,虽然默认把Write Concern设置为1(也就是数据弄丢后,你是介意的),但也并不表明数据安全。来看看Write Concern的“介意”级别:

Acknowledged:这是目前MongoDB的默认“介意”级别,意思是MongoDB已经收到了客户端的写入请求,但数据并没有持久化或写入日志。MongoDB官方声称这种级别是安全的,安全么? ACID的D到哪里去了?

Journaled:这个级别要高一些,意思是MongoDB的实例接收到请求,并且数据写入到日志中。听起来基本的D是具备了,但这只是单机写入,如果单机失效进行节点切换时,依然会有数据丢失,下边会介绍。

Fsynced:这个跟Journaled类似,意思是写入日志并且真正持久化到磁盘后返回写入成功。这个问题跟Journaled类似,下边介绍。

Replica Acknowledged:用于高可用环境下的多副本,需要副本确认成功,问题存在。这个级别依然不安全,下边会介绍。

Major Replica:用于高可用环境下的大多数副本确认成功。

在上面5个“介意”级别中,只有最后一个Major Replica是数据安全的,但无疑它会大大影响写入的速度。


在读操作方面,MongoDB的官方定义为“fully-consistent reads”,从文档上可以看到,从主节点可以获得最强的一致性——线性一致性Linearizability,从节点则是最终一致。这意味着在主节点上可以立即读取写入的数据。然而,在aphyr[1]的测试中发现,线性一致也是无法获得的,高并发读写情况下,常常会读到过期数据。这是由于MongoDB的“Read Uncommited”隔离级别导致的——“MongoDB允许客户读取没有持久化到磁盘的数据,即便Write Concern是Journaled以上级别”。假设这种场景:当网络问题导致脑裂时,这时集群中包含2个Primary节点,假设1个Primary连接到多数节点,因此可以进行Write Concern为Major的写入操作,称为Major Primary,另一个称为Minor Primary。假设2个Primary都有一个初始值为0,如果一个用户在Minor Primary上写入该初始值为1,MongoDB会在跟从节点确认之前直接修改本地的状态,因此当脑裂恢复后,这个Primary节点该键值会被回滚到最开始的初始值0,而在此之前,所有Minor Primary上的读请求读到的值都是1。另一方面,如果客户端从Major Primary写入1,而其他客户端从Minor Primary读取时,他们读取到的是0。因此,对数据要求强一致的场景下,这会抓狂的。


因为这些设计,即便MongoDB 3.0把之前很渣的基于mmap的存储引擎替换成了WiredTiger,即便Tokutek(已被Pernoca收购)用最好的存储TokuDB修改了MongoDB成为TokuMX,都不足以解决问题。


2014年TokuMX团队针对MongoDB的如上问题发明了类似Raft的一致性协议Ark[2]用于MongoDB多副本设计,目前已经合并到Pernoca维护的MongoDB版本中,这对于执迷于MongoDB的应用开发人员是个好消息,下面一起来看下设计。


先看看MongoDB的复制协议:在多副本中,有一个Primary,其他成为Secondaries节点,通过oplog进行跨机器数据复制,可类比于MySQL的binlog。Secondaries节点不能处理写入请求,它们从Primary获取oplog,此外也可以从其他Secondaries节点获取。MongoDB采用的是pull模型,意味着Secondaries节点是从Primary拖数据过来,而不是Primary推送数据到Secondaries,数据采用异步复制。Primary节点通过选举做出,节点之间通过心跳检测判断状态,当网络发生错误导致Primary无法响应心跳时,会进行新的选举,当网路恢复后原来的Primary加入集群,需要进行Rollback操作,来把网络中断后的写入操作回滚。如前所述,当网络发生问题后,如果原来的Primary节点没有把自己的角色变为Secondary,那么集群中就会出现2个Primary节点导致脑裂。此外,还有其他一些问题:在发生网络错误进行新Primary的选举时,发起选举的节点会询问所有节点,如果其他节点也无法连接到老的Primary节点,并且它们的oplog不如发起选举的节点新,那么就可以促成一次成功的选举。然而在MongoDB的协议中,如果上述情况不成立,那么就会有30秒的时间没有新的Primary选举出来。在某些情况下,有人测试发现选举耗时达到5-7分钟。另一个问题是选举线程和数据同步的线程相互协调不够,在选举过程中oplog的同步仍然可能同步进行,


Ark协议就是解决上面的问题的:

  1. 当系统中有2个Primary时,它们的oplog不应当有任何交叉,oplog少的Primary应当退出。

  2. 当一个节点对写入操作确认后,不应当发生任何Rollback操作导致数据丢失。

  3. 一个节点对其他节点投票选举后,该节点不应当对写入操作进行确认,因为这可能会引发Rollback。

针对第一点,Ark设计了GTID,跟MySQL 5.6引入的GTID类似,用于标志oplog的位置。Ark的GTID由二元组<term,opid>构成,每个Primary节点的写入操作都会产生递增的opid,每次选举都会产生新的term。在选举时,每个节点只对term大于所有其他节点的成员投票yes,例如一个节点已经对term为13的成员投票yes,那么下次它收到投票请求时,如果请求的term<=13,它将不会投yes。有了term,当集群中有2个Primary时,term小的Primary会自动退出,这可以保证正确性。针对第二点,当发起选举时,oplog的位置也包含在内,如果参与选举的成员的oplog位置超前,那么该成员不会进行投票。针对第三点,当需要对写入进行确认时,成员会检查该写入请求的term是否小于它曾经参与过投票的最大term值,如果是这样,那么写入就不会被确认。

Ark跟Raft非常类似,例如term就是Raft里的概念,那么为什么不直接采用Raft对MongoDB进行修复呢?Raft协议提供了状态复制机实现:如果一个副本在它的状态复制机某位置中添加了一条日志记录,那么其他副本都不会在同样的位置上添加不同的记录。在MongoDB中,这个状态复制机就是数据本身,而数据复制是异步进行的,并且基于拉模型操作,因此复制的间隔会比较大,在复制发生同时需要进行Rollback操作时,由于异步的本质导致不能确保在同一位置的数据总是一样的。Raft因为是同步操作,因此很容易处理,所以TokuMX团队针对Raft协议进行了微调。

尽管Ark修复了大多数MongoDB的问题,但还有一些地方需完善,例如MongoDB的配置是在日志外存放的,这意味着配置的变化无法方便地应用到整个集群。因此,从最早玩具一样的内存版BTree的存储引擎,到开玩笑的ACID,乃至分布式,MongoDB的创作者们都在用玩家的心态来不停地埋雷。对于结构化数据存储这样一个需要严肃对待的领域,他们显然从来没有做好足够的储备。



[1] https://aphyr.com/posts/322-call-me-maybe-mongodb-stale-reads

[2] http://arxiv.org/pdf/1407.4765v1.pdf

文章转载自数据极客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论