引言
其实对于这篇论文的简单介绍网上已经有很多了,所以这篇文章并不想去陈述其设计,而是想简单说一说我认为的亮点所在,虽说如此,我还是觉得我并没有get到什么其他人没有get到的东西,所以这是一篇比较无聊的纯记录性质的文章,方便过一段时间能够更快的会想起相关内容的细节。
BigTable是什么
这个问题在paper的开头就下了定义,即:Bigtable是一个分布式的结构化数据存储系统,它被设计用来处理海量数据:通常是分布在数千台普通服务器上的PB级的数据。而且由于业务的需求,谷歌对于BigTable的要求是非常苛刻的,高吞吐量,低响应时间,且各个集群的配置大小也不尽相同。
数据模型
BigTable的数据模型是非常简单的,虽然paper中定义中为结构化数据,但是这种简单的模型并不像关系型数据库的表一样有各种各样的约束,它只是一个多维度的有序Map,可以使用之所以说是多维度是因为在每一个Value中我们都可以去定义列,使得这个Map看起来就像是表一样。
如此一来我们就可以根据key和列名唯一的确定一个数据项了,且表中的数据项还可以根据配置来保存历史数据,可以指定为最后N个版本的数据或者是最近一段时间(比如几天)内的数据。
虽然可以把BigTable看作一个结构化的数据库,当然说成nosql我觉得更贴切一点。但是其实BigTable并不支持sql语句,相反的,它提供了一些特定的操作,比如建表,根据key查找行,写入或者删除BigTable中的值,便利表中的一个数据子集等等。从以上接口我们可以看出其实对于用户来说,完全把BigTable看成一个Map而不用考虑其他,当然还需要在建表上下功夫,使得满足paper中所提到的位置相关性以提升效率。
BigTable数据模型的实现让我想起了DDIA上的一个问题,就是在分布式的存储中如何能够构建二级索引来加速查询呢?有两种方法,就是每个分区构建一个索引或者构建一个全局索引,BigTable的做法就是根据Key直接查到想要的数据,巧妙的解决了这个问题,这确实是非常令人惊叹的,这使得BigTable可以满足低响应时间的需求。
基本实现思路
BigTable并不是一个独立的实现,它是基于谷歌一些其他的组件的,比如Chubby和GFS,因为BigTable为了能够支持PB级的数据,它必须要把数据文件存放于GFS中,而且Chubby优秀的存储元数据的能力也可以使得BigTable的实现更为简单。我想第一次了解BigTable的朋友都会有这样一个疑问,就是BigTable如何存储数据以及如何快速的找到数据呢?
和我觉得在这一点上设计和GFS类似,都是数据分成几块(GFS是把文件分成块,BigTable是把表分成块),paper中把每一块数据称为Tablet,而且都是有一个”数据中心“,存储着这些块的位置信息。一个BigTable集群存储了很多表,每个表包含了一个Tablet的集合,而每个Tablet包含了某个范围内的行的所有相关数据。初始状态下,一个表只有一个Tablet。随着表中数据的增长,它被自动分割成多个Tablet,缺省情况下,每个Tablet的尺寸大约是100MB到200MB,整体的数据分布大概是这样:
首先METADATA是一个特殊的tablet,其中存储着所有Tablet的位置信息,也就是一二层的数据,当然第一层又是一个特殊的METDATA,其中存放着其他METDATA的位置,这样BigTable的逻辑架构就是三层。如果METADATA的每一行都存储了大约1KB的内存数据。在一个大小适中的、容量限制为128MB的METADATA Tablet中,BigTable最多可以存储2^61字节的数据,这是一个非常庞大的数字了。
BigTable paper中写道主要有三个组件,即:链接到客户程序中的库、一个Master服务器,多个Tablet服务器。事实上这里我认为Chubby也应该算是主要组件,因为master在这里所做的事情实在是太少,它负责以下几点:
- 为Tablet服务器分配Tablets
- 检测新加入的或者过期失效的Table服务器
- 对Tablet服务器进行负载均衡
- 对保存在GFS上的文件进行垃圾收集
其实我们可以看到位置相关信息其实于master关系不大,都是存放在Chubby中的,这也意味着在正常情况下master几乎于客户端是没有交互的,客户端会在Chubby获取元数据以后到GFS上获取原始数据,这样master看起来更像是一个协调中心,这是于GFS不同的一点。
但是我更关心的是缓存一致性的问题,Chubby使用一种通知客户端缓存失效,从而使得更新缓存的开销分布到多次操作来提高效率。因为Tables可能会分裂或者合并,那么BigTable是如何做到缓存一致的呢?答案其实就是在查看到所缓存的信息无法找到目标值的时候就去重新查找Chubby,因为Tables的改变写入Chubby才算是成功,所以Chubby总能看到最新的位置信息,如果客户端缓存是空的,那么寻址算法需要通过三次网络来回通信寻址,这其中包括了一次Chubby读操作;如果客户端缓存的地址信息过期了,那么寻址算法可能需要最多六次网络来回通信才能更新数据,因为可能Tables在元数据表中的位置都已经改变了。
因为master不需要持久化Tablet相关的位置信息,我想这也是BigTable性能如此令人惊叹的原因之一,至于为什么不需要持久化这些数据,这就和GFS所运用的理念相同了,即只有Tablet服务器自己最清楚它的信息,所以在master启动的时候于所有的Tablet服务器通信,获取最新数据,而这些元数据就存储在Chubby中。Master服务器在启动的时候执行以下步骤:
- Master服务器从Chubby获取一个唯一的Master锁,用来阻止创建其它的Master服务器实例;
- Master服务器扫描Chubby的服务器文件锁存储目录,获取当前正在运行的服务器列表;
- Master服务器和所有的正在运行的Tablet表服务器通信,获取每个Tablet服务器上Tablet的分配信息;
- Master服务器扫描METADATA表获取所有的Tablet的集合。在扫描的过程中,当Master服务器发现了一个还没有分配的Tablet,Master服务器就将这个Tablet加入未分配的Tablet集合等待合适的时机分配。
当然这其中有一些特殊的情况,参考论文即可。
还有一点很重要,就是Tablet并不是数据的最小单位,而是SSTable(Google SSTable)
,SSTable是一个持久化的、排序的、不可更改的Map结构,而Map是一个key-value映射的数据结构,key和value的值都是任意的Byte串。一个Tablet的全局视图由多个SSTable和一个memtable(最近提交的那些存放在一个排序的缓存中,我们称这个缓存为memtable)构成。
优化
这里我并不想把paper中所有的优化策略都搬过来,重点说我比较喜欢的几点:
局部性群组
:客户程序可以将多个列族组合成一个局部性群族。对Tablet中的每个局部性群组都会生成一个单独的SSTable,这样我们就可以在列上加以设计已提高读取效率,因为我们每次操作磁盘的次数就变少了不少(不需要把所有的SSTable都读出磁盘)布隆过滤器
:可以使用Bloom过滤器查询一个SSTable是否包含了特定行和列的数据,对于某些特定应用程序,我们只付出了少量的、用于存储Bloom过滤器的内存的代价,就换来了读操作显著减少的磁盘访问的次数。这里的具体做法paper中并没有详细的解释,我个人认为就是把局部性群组SSTable的列名哈希以后放入布隆过滤器(这样也可以看出这个列名是不可删除的)中,这样不需要读取磁盘就可以以极小的内存代价和O(1)的时间复杂度知道所需的列是否存在于当前SSTable中(我们可以忍受偶尔出现的误判,无非是多读一次磁盘)。
总结
可以看出BigTable的实现至少在论文中看并不是多么的难以理解(当然从paper中也可以看出很多细节并没有展示出来,比如Tablet的数据冗余,如何进行根据key的分片这些重要的东西并没有提起),但是其设计确是巧妙绝伦,这也催生了令人惊叹的高效率于高吞吐量。同时它与GFS,MapReduce并称为谷歌的三驾马车,成为云计算的基石,我觉得不仅仅是云计算,这也是云存储的鼻祖。诚然这已经是十几年前的事情了,但是如今看来却仍让我们这些后辈惊叹不已。当然BigTable是闭源的,但是作为Hadoop的子项目的Hbase却是一个BigTable的开源实现,我还没有玩过Hbase,也并不了解,以后再说啦
参考: