Big Table 论文笔记

1,171 阅读11分钟

相比于Google File SystemBigtable: A Distributed Storage System for Structured Data 这篇论文对于设计细节没有那么详尽,比较点到为止。另外本篇也是谷歌大脑的负责人 Jeffrey Dean 比较重要的成果之一。

其实这个论文标题是有些奇怪的:Bigtable 明明是一个 NoSQL 数据库,并没有约束 Schema(允许无限扩充列),却是为了结构化数据而设计的。

Data Model

A Bigtable is a sparse, distributed, persistent multidimensional sorted map. The map is indexed by a row key, column key, and a timestamp; each value in the map is an uninterpreted array of bytes.

这段话可以说每个单词都很精要。松散,意味着列式存储;分布式,意味着容错;多维度,意味着形成了 Map 之 Map 的两层结构,也就是列族;排序,实际上是对于性能的优化。最后,Bigtable 是一个 KV 结构(而不是典型的关系数据库),键具有时间戳版本,值是一个字节数组。

Rows

Bigtable 的事务是单行的(而不是跨行的)。这甚至成为了 Jeffrey Dean 最遗憾的一点:

“What is your biggest mistake as an engineer?”
Not putting distributed transactions in BigTable. If you wanted to update more than one row you had to roll your own transaction protocol. It wasn’t put in because it would have complicated the system design. In retrospect lots of teams wanted that capability and built their own with different degrees of success. We should have implemented transactions in the core system. It would have been useful internally as well. Spanner fixed this problem by adding transactions.
— Jeff Dean, March 7th, 2016

不过后来在 Spanner 里算是弥补了这个缺憾了。

Bigtable 的每行都可以动态分区。这里分区的叫法是 Tablet(有点像 Applet),也就是 Hbase 里的 Region。这里 Tablet 是数据划分和负载均衡的最小单位,可以利用数据的局部性(比如同一个域名下的子域名)来划分行。

Column Families

为什么要设计列族?为什么列族不可变而列可变?这里论文语焉不详。列族是一个介于列存储和行存储之间的概念。在列族的世界观中,列族才是真正的『列』,而列族里的列不过是一种自描述的 KV 值。这样做可以保证列是稀疏的,因为当我们插入一行新的数据包括一个新列,其他行甚至不知道这一列的存在。

SSTable 也是按照列族来进行划分的(当然,不是说一个 SSTable 对应一个列族)。以 HBase 为例,即每个 Store 对应一个 Column Family。

TimeStamp

其实 Bigtable 提供这个特性也和它的下层实现有密切关系。LSM 树原本就是存在多版本的,Bigtable 只是顺应的开放了这个功能。

Building Blocks

Bigtable 有这样几个构件:底层的分布式存储 GFS,负责调度的资源管理系统 Borg,和一个分布式锁服务 Chubby。

Bigtable 使用 SSTable 作为它的存储形式。SSTable 意为 Sorted String Table,也就是一个排序的字符串表(按照论文中它是一个字节数组)。注意 SSTable 指的是数据结构,而 Bigtable 整个思想其实是 LSM 树。LSM 树最大的意义就是避免磁盘的随机访问。

SSTable 的核心是排序。为什么要排序?对于一个只添加(append)的文件而言,按照键的排序可以进行更快的合并。此外,一个排序的文件可以进行稀疏索引,这一点对于大数据量是比较关键的。反过来,稀疏索引本身也为 SSTable 的磁盘访问提供了极高的效率:只需要存储每个数据块的起始排序键即可。

Implementation

由于元数据存储在 Chubby 中,Client 甚至可以不经过 Master 直接访问数据。这也是为了避免单点问题。那么 Master 的作用是什么呢?主要是管理 Tablet 服务器和表的元数据修改。

Tablet Location

Bigtable 实现为一个 Tablet 的3层架构:Root Tablet,Metadata Tablet 和 User Tablet。其中 Root Tablet 其实是第一个 Metadata Tablet。不像其他 Tablet,他不会分裂。为什么要采用这个三级索引架构呢?当然是因为即使是 Metadata 层的数据也是很大的,不足以放入到 Chubby 这样的一致性存储中(类比 Zookeeper)。

至于客户端会缓存 Tablet 的位置这一点就不说了,和 GFS 类似。

Tablet Assignment

BigTable 使用 Chubby 来记录 Tablet 服务器的状态,同时分配 Tablet。假设某个 Tablet 服务器丢失了在 Chubby 上的独占锁,意味着 Tablet 会被重新分配(Master 会在将其归为未分配前删除 Chubby 上的服务器信息)。

什么时候 Tablet 集合会发生变化呢?

The set of existing tablets only changes when a table is created or deleted, two existing tablets are merged to form one larger tablet, or an existing tablet is split into two smaller tablets.

除了分裂之外,其他都是借由 Master 完成的。分裂相当于是一个 Tablet 服务器对 Master 的通知。这里论文写的也很简单,然而其实细想却有很多现实问题:分裂期间如何提供服务?分裂如何保证原子性(在 HBase 里叫做 RIT,运维大坑)?还有这一段关于 Tablet 服务器通知丢失的恢复,其实我没有看懂:

In case the split notification is lost (either because the tablet server or the master died), the master detects the new tablet when it asks a tablet server to load the tablet that has now split. The tablet server will notify the master of the split, because the tablet entry it finds in the METADATA table will specify only a portion of the tablet that the master asked it to load.

Tablet Serving

如果一个 Tablet 宕机了,如何进行容错?

To recover a tablet, a tablet server reads its metadata from the METADATA table. This metadata contains the list of SSTables that comprise a tablet and a set of a redo points, which are pointers into any commit logs that may contain data for the tablet. The server reads the indices of the SSTables into memory and reconstructs the memtable by applying all of the updates that have committed since the redo points.

论文有点一笔带过的意味(所以感觉 Bigtable 相对来说干货没那么多,藏私了):首先必须要有一个持久化的 WAL,即 redo log,这里叫做 commit log。同时,也要有数据的状态持久化,也就是 SSTable。 重建 Tablet 需要对 commit log 进行重放,这和关系数据库是一致的。对于那些已经持久化的数据,只需要访问下层的分布式文件系统即可恢复。不过,仍然有一些细节值得注意。比如,由于每个服务器都有多个 Tablet,如何进行 commit log 的存储和写入?这个在后面性能优化会提到。

读取和写入也是泛泛而谈,除了提到数据库领域常用的组提交之外。并没有提到如何利用后面的 BlockCache 之类的,也没有提到为什么合并和分割可以不阻塞读写。

Incoming read and write operations can continue while tablets are split and merged.

Compactions

压实部分是 SSTable 的精华(注意,这里的压实虽然的确是减少文件的数量和大小,但却不是通过压缩算法来的,而是状态合并)。对于 SSTable 而言,压实相当于一种变相的日志重放,这样就可以删除旧的日志。压实的触发条件是 Memtable 达到某个阈值(在 HBase 里默认是 128M),然后就会被冻结并拷贝到 SSTable 中。这个过程被叫做 Minor Compaction。而如果生成了多个 SSTable,就需要进行 Merging Compaction,它会被定期执行,以合并部分 SSTable 和 memtable 的内容。最后,合并所有 SSTable 的 Merge Compaction 被叫做 Major Compaction。显然,Google 团队把这个名字叫得如此像分代垃圾回收,因为这非常相似。

这里依然留了许多问题给我们:如何确定各个 Compaction 的时机和顺序?如何选择内存中(也就是 memtable)的数据结构?如何进行垃圾回收(尽管使用的是 Java 这样的语言,垃圾回收算法也有可能发生退化)?不过后面的性能优化中还是提到了一个关键设计:布隆过滤器。它对于 LSM 树必不可少。

Refinements

Locality groups

这里的局部群组本质上也是一个族。按照 Bigtable 的设计,应该是列族放数据类型接近于相同的列,这样可以方便压缩;然后群组放具有相关性的数据,这样可以避免访问多个群组。

注意不要把它和 HBase 的 RS Group 混为一谈。

Compression

压缩可以进行分块,这样就不必在读取某一部分数据时解压整个SSTable。

压缩算法通常使用两趟压缩,第一遍采用 Bentley and McIlroy’s算法,在一个很大的扫描窗口里对公共的长字符串进行压缩;第二遍是采用快速压缩算法,在一个 16KB 的小扫描窗口中寻找重复数据。这个在 HBase 里没有,所以我也不知道这个在 Google 究竟是如何实现的。

Caching for read performance

Bigtable 设计了两级缓存:

  • Scan Cache
  • Block Cache

前一个论文中说是缓存的 KV 结构。感觉没有 get 到这个设计的点——在我的印象里 HBase 好像是没有的。

其中 BlockCache 和 memtable,最容易让人混淆。虽然有时候在 HBase 里我们会把前者当成读缓存,后者(即 MemStore)当成写缓存,但实际上二者都可以作为读缓存。你可以这样看他们的用途:memtable 是为了维护 SSTable 有序而存在的,因此它必须是一个排序的数据结构,且是 KV 结构,它不受磁盘块大小之类的影响。而 Block Cache 从名字就可以看出,它可以优化数据局部性的读取。

Bloom filters

由于 SSTable 存在多个,每次读必须读取全部的 SSTable(假设它不在 memtable 中)。这里使用稀疏索引是不行的,因为稀疏索引只能找到数据块的位置,而不能确定是否存在(不过,使用哈希索引也许可以,但这相当于把所有的 SSTable 读到内存里)。解决方法就是布隆过滤器,但它是概率型数据结构,因此不能保证100%准确。

Commit-log implementation

Bigtable 给每个服务器设置了一个 commit log(类似于 HLog)。这是因为,如果给每个 Tablet 一个文件,那么文件数过多,且磁盘 seek 过多。同时,像组提交这样的技术带来的好处也会减少。但是,共享一个 commit log 这就需要在恢复时切分日志。

Bigtable 解决这个问题的方法是对日志进行排序,其中 Tablet 是排序键的首位。同时为了提高效率,这个排序将是一个分布式的排序过程,Master 负责协调。这个过程确实比较复杂,因为它实现了一个新的任务调度功能,所以在旧版本的 HBase 里没有实现(后来增加了分布式的日志切分)。

为了避免一些网络延迟和 GFS 服务器宕机引起的日志写入问题,Bigtable 使用两个日志写入线程(不过,这样 commit log 中就会有重复序列号)。

Speeding up tablet recovery

如何加快 Tablet 的恢复呢?这里只提到了在转移 Tablet 时,利用 Minor Compaction 来减少 commit log 的大小。另外在源 Tablet 服务器(这里其实有点奇怪,这个恢复并不是源服务器崩溃,可能只是备份)卸装 Tablet 时,还会进行一次 Minor Compaction 来避免读取这个时间段的日志。

Exploiting immutability

因为 SSTable 只通过 memtable 生成,可以认为是不变的。Immutable 最大的好处就是并发编程里,可以不需要进行数据同步。memtable 则是可变的,因此使用了 COW 技术。

另外,Tablet 在分裂时可以共享之前的 SSTable。

Lessons

  • One lesson we learned is that large distributed systems are vulnerable to many types of failures, not just the standard network partitions and fail-stop failures assumed in many distributed protocols.
  • Another lesson we learned is that it is important to delay adding new features until it is clear how the new features will be used.
  • A practical lesson that we learned from supporting Bigtable is the importance of proper system-level monitoring.
  • The most important lesson we learned is the value of simple designs.

这个看看就好了,毕竟很快就反悔了……这让我想到了那个成功人士总结自己成功是因为自己做俯卧撑上来的例子……

不过这里的经验单独拿出来,确实挺有道理……


这篇相比 GFS,我应该花的时间非常少——虽然也有许多理论已经是数据库的老生常谈的原因。不得不说 Google 这篇文章虽然意义很大,但雷声大雨点小啊。