Bigtable和Dynamo的比较

Amazon的dynamo和Google的bigtable是两种非常有代表性的分布式数据库,现在流行的分布式数据库中很多设计思想都来自这两个数据库。同时它们的架构又是非常不同的,将这两个数据库放在一起讨论,有助于理解分布式数据库架构的设计。

要了解一个分布式数据库的实现,可以从这几个方面着手:

  1. 看看数据是怎么分布的。毕竟是分布式数据库,肯定是由很多节点组成的系统。那么数据是依据什么逻辑来分布到这些节点上的。
  2. 考虑如何保证数据不丢失。为了实现数据的高可用,往往会进行多副本存储,那么副本是怎么拷贝的,副本之间怎么保持一致。
  3. 查看集群伸缩时的操作。集群中增加或减少节点时,数据是否需要迁移,怎么进行迁移,是否需要人工接入。

这几个方面可以说是分布式数据库的核心设计,其他的方面比如数据模型,读写接口也很重要,但设计和实现的困难程度相比上面这几个要差一些。把这四个问题搞清楚,基本上就可以从整体上了解一个分布式数据库是如何实现的了。下面来分别看下dynamo和bigtable是如何处理这几个问题的。

数据分布

Dynamo

按照DDIA这本书中描述,将数据分布到多个节点中,常用的只有两个方式:by hash 或者 by range。by hash 就是通过key计算出一个hash值,然后根据hash来决定将数据写到哪个节点;by range 的意思就是按照key其所在的范围来决定写到哪个节点。

Dynamo 是一个去中心化的架构,集群中每个节点都是对等的。它采用了类似一致性 hash的方式来分布数据。一致性hash的基本逻辑很简单,集群所有的节点均匀的分布在hash环中,计算出key的hash值,然后看这个值落在哪个hash环位置,向后寻找第一个节点,然后将数据写在这个节点中。dynamo的做法是,找到值落在hash环的位置后,向后寻找N个节点,将数据分别写入到这N个节点中(这也是dynamo实现数据副本的方式,后面讨论)。

那么接下来要考虑的问题是,key space信息记录在哪里。key space信息指的是hash环是如何划分的。比如说,我现在要写一条数据,已经根据key算出了hash值,那么这个值在哪个区间,后面的节点是哪些,这个信息从哪里获取。前面提到了,dynamo中每个节点都是对等的,这意味着每个节点中都有key space的分布信息。同时,客户端程序也会缓存一份key space信息,这样就可以直接将数据发送到目标节点。这种架构下,如果key space发生了变更,不可能让所有节点马上得到最新的信息。也就是说,客户端可能缓存着过期的key space信息,不过这个问题并不难解决。可以将客户端sdk实现为每次读写时都带上 key space 信息的版本号,让存储节点进行比对,如果客户端信息过期则返回错误提示以及最新的key space,然后客户端这边重新发起读写请求。

Bigtable

Bigtable 采用了by range的方式来分布数据,也就是把数据看作是有序的(按照key的字母序排序),每个存储节点指定一个range,所有key落在在此range中的数据都存储在这个节点(实际比这里描述的复杂一点,不过核心思想是一样的)。此外,bigtable是有中心节点的,它的架构中包括了一个master服务,以及一个chubby存储服务(chubby和zookeeper是非常类似的服务,zk实际上是chubby的开源实现)。它将节点和range的对应关系存储在chubby中(这个说法不是很准确,节点和range的关系被组织为树状结构,这棵树的根在chubby,剩余部分则在存储节点中)。和bigtable交互的客户端程序首先必须要和chubby服务建立一个连接,以便获取到最新的数据分布信息。然后在每次读写的时候则直接和存储节点交互。

数据副本

Dynamo

上面提到,在写数据的时候,dynamo会将数据写入到N个节点中,也就是数据有N个副本。集群中每个节点是对等的,也就是每个节点都有可能会处理写任何key的请求。收到写请求后,节点首先计算出key所在的N个节点是哪些,然后并发向这些节点发起写请求。这里的问题是:如果在写入时某个节点故障,导致数据副本之间不一致,该如何处理?对于短暂的不可用,dynamo使用了一个hinted handoff 的机制,举例来说,当需要将某个key写入到ABC三个节点中时,如果C节点故障了,那么dynamo会将key写入到ABC后面的第一个节点D中,并告诉节点D这条数据的原始位置,等到节点C恢复后,节点D会将数据再发送到节点C。对于持续较长时间的故障,节点恢复后,dynamo 使用merkle 树来进行节点间的数据比对,并将缺失的数据发送到恢复后的故障节点。这里merkle 树的优势有两个:一是加快的数据对比的速度,二是通过精准的匹配减少需要传输的数据量。

Dynamo不能保证数据副本间的强一致(通过舍弃严格的一致性要求,Dynamo做到了高性能和高可用)。具体来说,由于每个节点都可以处理写请求,可能出现多个节点写同一个key的情况,此时key所在的N个节点收到数据的顺序是乱的,导致同一个key在各个节点上的数据不一致。dynamo使用了vector clock来解决这个问题,其核心思想是:将数据的冲突版本记录下来,在客户端读取时全部返回,交由客户端程序来决定哪个版本是正确的。

除此之外,dynamo的副本机制提供了灵活的配置选项。数据的副本数N是可以配置的,针对读写一致性还可以配置R和W,R的含义是:在读取数据时,必须要从R个节点中读取到数据才能返回;W的含义是:在写入数据时,必须要写入W个节点成功后才能返回写入成功。对于一致性要求高的用户,可以使配置满足 N + W > R,这其实就是要求读写要在多个节点上成功。对于性能要求高的应用,可以将W 和 R配置的较小,使读写响应更快。

Bigtable

Bigtable 本身并没有做数据副本,它依赖底层的GFS来保证数据的冗余存储。Bigtale的数据都存储在一个个sst文件中,而这些文件又存储在GFS中。GFS把文件切片为一个个chunk,对每个chunk都存储了多个副本。同时使用了一种租约机制来保证数据的一致性,其写入过程和二阶段提交很相似:首先在多个副本中挑选一个为primary,客户端直接将数据复制到各个节点中,然后发送一个write命令给primary,由primary节点来通知各个节点如何更新数据。

此外,在bigtable之后的spanner数据库中,google使用了paxos来维持数据副本之间的一致性,这个做法现在被认为是主流的副本一致性方案,tikv的数据副本采用了相似的机制,不过使用的一致性算法是raft。

水平伸缩

Dynamo

由于每个节点都存储着一份全局的数据分布以及和成员节点信息,因此每个节点都可以承担添加新节点的工作,也就是说,管理员可以通过命令工具和任意一个节点联系,告诉它现在有一个新的节点添加到集群中。然后在集群内部,这个新的全局节点信息会通过类似gossip协议的方式传播到其他所有节点。这个传播的方式大概是:每个节点都会周期的和任意一个节点通信,同步自己的全局信息。这种方式在多数时候都能够使得所有节点达到最终信息一致。但是它的缺点是在新节点刚刚加入的一段时间内,整个集群中可能处于一个信息分裂的状态,也就是可能有的节点已经知道了新节点,有的节点还不知道。为了处理整个问题,dynamo在集群中指定了一些seed节点,所有的节点在一定的时间范围内都会和seed节点通信一次,这样就保证了新的节点在一个可以预期的时间内会传播到整个集群。

在新的节点加入后,它所负责的key还在其他的节点上,此时这些其他节点会逐渐的将数据迁移到新的节点上,这个过程以一种可控的方式进行,以避免影响到正常的读写请求。具体来说,dynamo使用了一个后台任务入场控制机制,每个后台任务有会一个入场控制器,这个控制器会周期的获取节点的资源使用情况,请求的延迟情况,用这些信息来决定后台任务应该何时运行,运行多久。

Bigtable

Bigtable通过master服务来控制添加节点的过程。具体来说,master中记录着每个节点负责的数据范围内,到新的节点添加进来后,master将现有节点负责的数据分配一些给这个新节点来负责,保证整个集群的负载是均衡的。由于数据的分布信息是只记录在master中的,客户端程序可以立即和master交互来获取新的分布信息。

新的节点加入后,bigtable并不会做数据的迁移,只是按照上面说的将一部分数据分配给新的节点负责,这个过程不会导致数据的迁移,原因是bigtable底层使用的是GFS。前面提到过,bigtable的数据是以sst文件的形式存储在GFS中,无论是哪个存储节点去GFS读取这个文件,都只需要使用文件路径就足够了。新节点和旧节点之间不需要迁数据,只是说负责读写的节点发生了变化。如果说底层GFS增加了新的节点,那么就需要迁移数据了(GFS同样是一个中心化的架构,它内部的master服务会负责这个数据迁移的过程)。

其他

除了上面这几个点之外,我们还可以查看在单个节点上使用了哪一种存储引擎,来了解底层数据的组织形式,以及读写的性能。Bigtable的数据虽然是写在GFS中的,但其实相比于写本地文件系统,用法上并没有大的区别,都是写文件。它以一种在当时看来非常独特的方式组织数据文件的格式,也就是SSTable,同时使用了LSM结构的思想来提升写入的性能,这其中包括:先将数据写入到内存,在后台将内存数据落盘,并和磁盘中的数据进行合并(这里的合并在后台进行,合并只包括顺序读、顺序写,删除这些操作,这也正是GFS所擅长的场景)。除此之外,bigtable还使用了bloom filter来提升读取的性能。(由于Bigtale的原因,LSM这种结构受到了很多关注,并逐渐有了很多开源的实现。现在,如果一个数据引擎如果使用sst文件存数据,写入数据时先写内存,后台进行落盘以及compaction,都被称作是基于LSM结构实现)。

Dynamo论文中没有提及在单个节点上存储引擎的实现,不过依照dynamo架构实现的开源数据库cassandra和scylla在单机存储引擎上都使用了类似LSM的数据结构,数据在磁盘上以SST的格式存储,通过后台compation来降低存储放大。

你可能感兴趣的:(Bigtable和Dynamo的比较)