详细解读 Google Bigtable 论文 | 🏆 技术专题第五期征文 ......

1,332 阅读13分钟

Data Model

A Bigtable is a sparse, distributed, persistent multidimensional sorted map.

  上面是Bigtable的定义,特点是sparse、distributed、multidimensional、sorted map,此外,还要加上一个关键字:structured。      图中,是一个存储网页的例子,Bigtable是一个有序的字典(key value pair),key是 (row:string, column:string, time:int64), value则是任意的string。   在网页存储这个例子中,row是URL(倒过来的URL,为了让同一个网站的网页尽量存放在一起)。column则是由colune family:qualifier组成,上图中,contens、anchor都是colume family,一个colume family下面可以包含一个到多个colume。time则是不同时刻的版本,基于time,bigtable提供了不同的垃圾回收策略:only last n、only new enough。   Bigtable是结构化(Structured)数据,colume family在定义表(table)的时候就需要创建,类似关系型数据库。colume family一般数量较少,但colume family下面的colume是动态添加的,数量可以很多。针对上面的例子,有的文章可能只有一个作者,有的文章可能好几个作者,虽然都有anchor这colume family,但是所包含的colume数量是不一样的,这也是称之为稀疏的原因

以上的图片看起来可能还不太直观,可以看我绘制的这个图 image.png 如上图所示:

  • 第一维:key(屎黄色)
  • 第二维:属性(列和列族)
  • 第三维:time(蓝色)

同一个key,不同属性,不同时间,会存储一个value。 不像以行为单位进行存储的传统关系型数据库,这个三维的大表格BigTable是一个稀疏列存储系统。(能够压缩空间) 它的数据模型的本质是一个map:(key + column + time => value)的一个超级大map。

Rows

一个表中的行键,是任意的字符串(当前在尺寸上有64KB,虽然10-100字节是用户最常用的尺寸)。对每个行键下所包含的数据的读或写都是一个原子操作。  BigTable在行键上根据字典顺序对数据进行维护。对于一个表而言,行区间是动态划分的。每个行区间称为一个Tablet,它是负载均衡和数据分发的基本单位。 因而,读取一个比较短的行区间是非常高效的,通畅只需要和少数几个机器通讯。用户可以利用这种属性,也就是说,用户可以选择分布具有局部性的行区间。

Column Families

列键被分组成称为“列家族”的集合,它成为基本的访问控制单元。 存储在一个列家族当中的所有数据,通常都属于同一个数据类型(对同一个列家族中的数据一起进行压缩) 访问控制以及磁盘和内存帐户输入都是在列族level执行的

Timestamp

在BigTable中的每个单元格当中,都包含相同数据的多个版本,这些版本采用时间戳进行索引。BitTable时间戳是64位整数。BigTable对时间戳进行分配,时间戳代表了真实时间,以微秒来计算。 一个单元格的不同版本是根据时间戳降序的顺序进行存储的,这样,最新的版本可以被最先读取。

API

  • 删除和创建表和列家族的功能
  • 改变簇、表和列家族的元数据,比如访问控制权限。
  • 写和删除BigTable中的值,从单个行中查询值,或者对表中某个数据子集进行遍历

 BigTable支持几种其他的功能,允许用户以更加复杂的方式来操作数据。(注意,BigTable支持单行事务,可以对存储在某个行键下面的数据执行原子的“读-修改-写”操作。但不支持通用的跨行键的事务)

  • 在客户端提供了跨行键批量写入数据的接口
  • 支持在服务器的地址空间内执行客户端提供的脚本

Building Blocks

BigTable是构建在其他几个Google基础设施之上的。

BigTable使用了分布式Google文件系统( GFS ) 来存储日志和数据文件。BigTable的一个簇通常在一个共享机器池内进行操作,这个共享机器池会运行其他一些分布式应用。BigTable的进程通常和其他应用的进程共享同样的机器。BigTable依赖一个簇管理系统来调度作业、在共享机器上调度资源、处理机器失败和监督机器状态。

Google SSTable文件格式作为存储BigTable数据的内部格式。 (可以了解下 LevelDB, LSM, SS Table)一个SSTable提供一个持久化的、排序的、不可变的、从键到值的映射,其中,键和值都是任意的字节字符串。BigTable提供了查询与一个指定键相关的值的操作,以及在一个指定的键区间内遍历所有的“键/值对”的操作。在内部,每个SSTable都包含一个块序列。通常,每个块是64KB,不过块尺寸是可配置的。存储在SSTable结尾的块索引,可以用来快速定位块的位置。当SSTable被打开时,块索引就会被读入内存。一个查询操作只需要进行一次磁盘扫描,我们首先在内存的块索引当中使用二分查找方法找到合适的块,然后从磁盘中读取相应的块。可选地,一个SSTable可以被完全读入内存,这样,我们在进行查找操作时,就不需要读取磁盘。

BigTable依赖一个高可用的、持久性的分布式锁服务Chubby。(可以粗浅的理解为zookeeper,实际上有区别,详细可以见paper: research.google.com/archive/chu…) BigTable使用Chubby来完成许多任务:

  • 保证在每个时间点只有一个主副本是活跃的
  • 来存储BigTable数据的bootstrap的位置(见5.1节)
  • 来发现tablet服务器
  • 宣告tablet服务器死亡
  • 存储BigTable模式信息(即每个表的列家族信息)
  • 存储访问控制列表

如果在一段时间以后,Chubby变得不可用,BigTable就不可用了

事实上还有一个很重要的东西:机群管理软件/服务(borg)

Architechture

Paper中没有绘制整体架构,大概的整体架构图如下 image.png

 BigTable实现包括三个主要的功能组件:

  • 链接到每个客户端的库(a library that is linked into every client)
  • 一个主服务器
  • 许多Tablet服务器。

Tablet服务器可以根据工作负载的变化,从一个簇中动态地增加或删除。 主服务器负责把Tablet分配到Tablet服务器,探测Tablet服务器的增加和过期,进行Table服务器的负载均衡,以及GFS文件系统中的垃圾收集。除此以外,它还处理模式变化,比如表和列家族创建。

每个Tablet服务器管理一个Tablet集合,通常,在每个Tablet服务器上放置10到1000个Tablet。Tablet服务器处理针对那些已经加载的Tablet而提出的读写请求,并且会对过大的Tablet进行划分。

为了减小主服务负载:客户端并不是直接从主服务器读取数据,而是直接从Tablet服务器上读取数据。因为BigTable客户端并不依赖于主服务器来获得Tablet的位置信息,so, 多数客户端从来不和主服务器通信。

Tablet Location

bigtable 使用了一个类似于 B+树的三层架构,来存储Tablet位置信息。

第一个层次是一个文件,存储在Chubby中 它包含了Toot Tablet的位置信息。Root Tablet把Tablet的所有位置信息都保存在一个特定的METADATA表中。每个METADATA表都包含了一个user tablet集合的位置信息。Root Tablet其实就是METADATA表当中的第一个Tablet,但是,它被区别对待,它在任何情况下都不会被拆分,从而保证Tablet位置层次结构不会超过三层。

METADATA表存储了属于某个行键的Tablet的位置信息(行键: 关于Tablet表标识符和它的最后一行这二者的编码。)

细节: 客户端函数库会缓存Tablet位置信息。如果客户端不知道一个Tablet的位置信息,或者它发现,它所缓存的Tablet位置信息部正确,那么,它就会在Tablet位置层次结构中依次向上寻找。如果客户端缓存是空的,那么定位算法就需要进行三次轮询,其中就包括一次从Chubby中读取信息。如果客户端的缓存是过期的,定位算法就要进行六次轮询 虽然,Tablets位置信息是保存在缓存中,从而不需要访问GFS,但是,我们仍然通过让客户端库函数预抓取tablet位置信息,来进一步减少代价,具体方法是:每次读取METADATA表时,都要读取至少两条以上的Tablet位置信息

METADATA表中存储了二级信息,包括一个日志,它记载了和每个tablet有关的所有事件等,用于对于性能分析和程序调试。

Tablet Assignment

每个Tablet可以被分配到一个tablet服务器。主服务器跟踪tablet服务器的情况.

BigTable使用Chubby来跟踪tablet服务器(这个部分主要就是在讲依赖chubby提供的服务去解决一些分布式中的问题)

Tablet Serving

子表的数据最终还是写到GFS里的,子表在GFS里的物理形态就是若干个SSTable文件。下图展示了读写操作基本情况。 当子服务器收到一个写请求,子服务器首先检查请求是否合法。然后它会向 tablet 服务器中的日志追加一条记录,在日志成功追加之后再向 memtable 中插入该条记录;这与现在大多的数据库的实现完全相同,通过顺序写向日志追加记录,然后再向数据库随机写,因为随机写的耗时远远大于追加内容,如果直接进行随机写,由于随机写执行时间较长,在写操作执行期间发生设备故障造成数据丢失的可能性相对比较高。 当 tablet 服务器接收到读操作时,它会在 memtable 和 SSTable 上进行合并查找,因为 memtable 和 SSTable 中对于键值的存储都是字典顺序的,所以整个读操作的执行会非常快。 注意:不要误以为子服务器真的存储了数据(除了内存中memtable的数据),数据的真实位置只有GFS才知道,主服务器将子表分配给子服务器的意思应该是,子服务器获取了子表的所有SSTable文件名,子服务器通过一些索引机制可以知道所需要的数据在哪个SSTable文件,然后从GFS中读取SSTable文件的数据,这个SSTable文件可能分布在好几台chunkserver上。

Compactions

随着写操作的进行,memtable 会随着事件的推移逐渐增大,当 memtable 的大小超过一定的阈值时,就会将当前的 memtable 冻结,并且创建一个新的 memtable,被冻结的 memtable 会被转换为一个 SSTable 并且写入到 GFS 系统中,这种压缩方式也被称作 Minor Compaction。 每一个 Minor Compaction 都能够创建一个新的 SSTable,它能够有效地降低内存的占用并且降低服务进程异常退出后,过大的日志导致的过长的恢复时间。既然有用于压缩 memtable 中数据的 Minor Compaction,那么就一定有一个对应的 Major Compaction 操作。

Refinements

论文中在这个部分提到了很多相关优化措辞,不细说,只点一下

Locatity groups

为了实现更高效的读,客户端可以把多个列家族一起分组到一个locality group中。我们会为每个tablet中的每个locality group大都创建一个单独的SSTable。 一些有用的参数,可以针对每个locality group来设定。

Compression

客户端可以决定是否对相应于某个locality group的SSTable进行压缩 许多客户端都使用“两段自定义压缩模式”。第一遍使用Bentley and McIlroy[6]模式,它对一个大窗口内的长公共字符串进行压缩。第二遍使用一个快速的压缩算法,这个压缩算法在一个16KB数据量的窗口内寻找重复数据

Cache

改进读性能,tablet服务器使用两个层次的缓存。 Scan缓存是一个高层次的缓存,它缓存了“键–值”对,这些“键–值”对是由tablet服务器代码的SSTable接口返回的。Block缓存是比较低层次的缓存,它缓存了从GFS当中读取的SSTable块。

Bloom filters

加布隆过滤器减少无效查询

Commit-log implementation

Bigtable对tablet提到了一个关于tabletLog的优化,如果每个tablet都拥有一个tabletLog,会导致GFS中存在很多文件并发写入,因此可以对一个chunkServer中所有的tablet创建一个tabletLog,可以大大提高写入的效率。基于tabletLog重建的时候,首先对table按照⟨table, row name, log sequence number⟩进行排序,每个tablet只读取自己相关的部分即可。

Speeding up tablet recovery

如果主服务器把一个tablet从一个tablet服务器转移到另一个tablet服务器。这个源tablet服务器就对这个tablet做一个次压缩(minor compaction)

Exploiting immutability

所产生的所有SSTable都是不变的。例如,当我们从SSTable中读取数据时,不需要进行任何文件系统访问的同步。结果是,针对行级别的并发控制可以高效地执行。唯一发生变化的数据结构是memtable,它同时被读操作和写操作访问。为了减少读取memtable过程中的冲突,我们使得每个memtable行采取”copy-on-write”,并且允许读和写操作并行执行。

Discussion

Bigtable的单行事务

Bigtable提供了单行事务的支持,包括单行的read-update-write及单行多列的事务性。原文中关于单行事务的描述很少,本文基于我的理解描述一下。Bigtable虽然是分布式存储,但是Bigtable的单行事务本质上不是分布式事务。因为Bigtable单行的数据一定在同一个tablet中,所以一定不会跨tabletServer,自然是单机事务。我们先从ACID的维度去分析一下Bigtable的单行事务:

  • A:原子性,体现在两方面:read-update-write过程中数据不能被其他事务更改,多列的更改要么同时成功,要么同时不成功。
  • C:一致性,Bigtable不存在一致性约束。
  • I:隔离性,Bigtable的事务是单行的,隔离性就是RC,没有其他情况。
  • D:持久性,Bigtable基于GFS分布式文件系统,成功写入即认为是可靠的。

所以对这个单行事务而言,关键就是保证原子性。对于单行多列操作的原子性,Bigtable写内存之前会写操作日志,如果写入过程中发生异常,重放操作日志就可以保证多列的原子性。对于read-update-write的原子性,用CAS机制就可以得到保证。 所以,Bigtable的单行事务是很简单的,甚至比Mysql的单机事务都简单很多,这也许就是原文没有讲解单行事务的原因。

🏆 技术专题第五期 | 聊聊分布式的那些事......