本文为阅读BigTable论文后的总结, 因为加入了个人理解. 所以不一定与原文一致. 可能有部分对原文的误解. 欢迎指正.
1 数据模型
1.1 数据
简单理解, Bigtable存储的结构类似 'key-value'.
- 'key' 称为RowKey
- 'value'可以分多列(Column)存储多个Value
- 相关的Column(一般数据类型相同)被聚为列族(ColumnFamily)
- Value可以存储多个版本. 每个版本有Timestamp标识
可以总结为如下关系:
(RowKey, ColumnFamily:Column, Timestamp) → Value
类似如下表格:
1.2 操作
支持通过RowKey获取, 或者Scan RowKey范围操作
2 实现
每一个RowKey范围, 会对应一个数据块. 这个数据块被称作Tablet.
需要注意的是: 这里的Tablet是一个逻辑上的概念, 对应着一块数据. 一般保存在GFS上, 一般会用HDFS替代. 但这个块具体如何保存, 是否是一个文件来保存. 将在后面介绍.
所以BigTable的主要目标就是设计一套. 能够支撑在近实时时间内, 根据RowKey范围, 从大量Tablet中找到指定Tablet, 并读取其中数据的系统.
这其中的两个要点:
- a. 如何根据RowKey. 迅速找到Tablet的位置.
- b. 如何在近实时的时间内, 读取Tablet数据.
此外还需要考虑: - c. 如何调度多个机器. 当机器挂掉时, 如何保证数据仍然可以访问.
2.1 Tablet的存储
这一部分可以看做是对问题a的解答.
Server端的数据组织
用户定义了若干张表. 每张表都有若干数据块Tablet. 从哪去查找这些Tablet的位置?
BigTable会为这些Tablet建立一个B+树的索引(哪些RowKey范围去找哪个Tablet). 这些索引成为MetaData. 用若干数据块来存储, 称为MetaDataTablet.MetaDataTablet也很多. 从哪去查找MetaDataTablet的位置?
所以在MetaDataTablet的上层又建立了一个索引, 用来快速查找MetaDataTablet. 这个索引保存的数据块称为RootTablet.
RootTablet和MetaDataTablet很像, 都可以看做是保存了索引的数据块. 但是不同点在于RootTablet只会有一个, 不会再分割.RootTablet只有一个, 但从哪去查找它的位置?
BigTable会从Chubby上查找. Chubby是Google内部使用的高可用性分布式锁服务, 一般会用zookeeper, etcd来替代.
ok. 这样形成了一个三层结构. 用论文原图表示如下:
Tablet的查找
ClientLibrary(由于后面会讲到, 真正去查找Tablet位置的并不是客户端Client, 而是Tablet Server. 因此这里称为ClientLibrary)第一次查找时, 从Chubby逐层查找. 共查找三次.
ClientLibrary将这三层关系Tablet的位置缓存起来. 查找一个Tablet时, 根据自己的缓存直接找到对应Tablet的位置.
一段时间后, Client缓存失效. Client按照失效的缓存去查找. 发现根据缓存查找Tablet不存在, 或者错误的时候. 会根据缓存找到上一层, 即MetaDataTablet, 根据MetaDataTablet重新查找, 并更新缓存.
如果根据缓存位置查找MetaDataTablet仍然找不到或者找错. 那么会如法炮制, 在从更上一层查找. 直到最终, 通过Chubby找到最新数据.
2.2 Tablet服务
Tablet是一个逻辑上的数据块概念, 你要的数据就在这块, 但具体怎么存储的,
要怎么取走这些数据. Client作为一个小白, 表示有点难办.
这一部分可以看做是对问题b的解答.
因此, BigTable使用若干服务器来帮助Client处理对Tablet的访问(可以理解为代理服务器). 这些服务器称为Tablet Server
每个Tablet Server负责几个Tablet的查找. 客户端只要知道要找的Tablet该找哪个Tablet Server取就行(具体要哪段数据找哪个Server将在后面介绍).
一个Tablet Server采取如下结构:
写操作
首先, Write操作被写进位于GFS的Tablet Log. 写操作就完成了.
然后Tablet Server会将Log里的内容逐一异步处理. 处理的方式是把更新保存到一段内存空间中, 称为MemTable.当Write操作进行多次后, MemTable越来越大, 当达到一定阈值的时候.
原有的MemTable被冻结, 新生成一个新的MemTable. 被冻结的MemTable持久化存储到GFS上, 称为一个SSTable. 这个过程称为Minor Compaction每次Minor Compaction生成一个SSTable. 长此以往, 会有大量SSTable生成. 导致读操作需要检查大量的SSTable. 因此, Tablet Server会将多个SSTableMemTable中的数据Merge起来, 生成一个新的SSTable. 这个过程称为Major Compaction
当进行delete操作的时候, 也同样只是向Log中写入了一条delete记录. 这条记录被加载到MemTable后, 代表操作已经生效. 但并不会覆盖原先的update记录. 当用户读取数据时, 会取最新的操作得知数据已经被delete.
而当进行Major Compaction时, 会对记录进行merge. 删去删除的数据.
读操作
- 读操作会将MemTable和SSTable的存储内容组成一个大的view. 从中进行读取.
重建Tablet
- 一个Tablet指一段RowKey范围对应的逻辑数据块. 而实际存储在一个个SSTable中. 在MetaTablet中存储的就是这些SSTable的列表, 以及一系列的redo points(可以理解为存档点).
- 当Tablet Server恢复一个Tablet数据的时候, 会通过ClientLibrary从MetaTablet中加载SSTable的位置, 然后将SSTable读入内存中, 根据redo points把redo points之后的更新一步步应用过来.
2.3 Tablet分配
一个Tablet Server负责多个Tablet. 那么具体负责哪几个. 如何保证每个Tablet都有Server负责. Server之间不会处理同一个Server.
当有Server挂掉的时候, 如何发现Server挂了. 又如何把它的数据重新分配给其他Server.
这一部分可以看做是对问题c的解答.
BigTable设计了一个Master服务来解决这一问题.
- 通过Chubby的锁机制保证只有一个Master.
- Master通过Chubby锁来检查哪些Server仍然存活.
- Master和所有活着的Server进行通信, 了解他们都负责哪些Tablet.
- Master扫描MetaData Tablet. 看看总共有哪些Tablet.
- 如果有unassigned Tablet. Master会将这些Tablet分给Tablet Server. Tablet Server通过2.2章节的 重建Tablet 过程加载Tablet.
- 当Master通过Chubby发现某个Tablet Server挂掉, 并且断定服务无法继续时. 会将这个Tablet Server负责的Tablet标识为unassigned. 并重新分配给其他Tablet Server
- 此外, 当某个Tablet过大的时候, Tablet会将这个Tablet分为多个. 这时候Tablet Server也会通知Master
Client端通过Master知道哪个RowKey范围应该找哪个Tablet Server. 但是, 数据直接与Tablet Server通信获得. 并且这个关系会缓存在Client端. 因此Master的负载并不重.
3 优化
3.1 Locality groups
client端可以将多个Column Family聚合成一个Locality groups. 不同Locality groups的数据通常不一同读取. 因此在底层存储的时候可以把不同Locality groups的数据分在不同的SSTable上.
另外, 某些Locality groups也可以声明为in-memory, 这样读取的时候可以避免磁盘操作, 提高性能.
3.2 压缩
3.3 缓存
BigTable有两层缓存:
- higher-level cache: SSTablet接口返回的Key-Value结果会缓存.
- lower-level cache: 缓存从GFS取出的SSTable.
3.4 BoomFilter
- 当判断一个SSTable是否有指定RowKey的数据时, 使用BoomFilter. 减少不必要的磁盘操作.
3.5 Commit-log实现
- 为了优化性能, 每个Tablet Server有一个Log. 这个Server负责的Tablet公用这个Log.
- 当恢复数据的时候, Tablet Server负责的Tablet会被分配到不同的Server上. 每个Server都读入这个Log并从中取出属于分配给自己的Tablet相关的log.
- 为了优化上述恢复数据过程. 首先会对Log进行排序, 这样每个其他的Server只需要加载其中属于的一段Log即可.
- 此外, 为了避免GFS的抖动. 一个Server会维护两个写队列. 同时只使用一个, 当抖动的时候切换另一个队列.
3.6 加速Tablet恢复
当Master把一个Tablet从Server迁移到另一个Server的时候.
- 原Server会先进行一次Minor Compaction.
- Minor Compaction进行完后, 停止服务.
- 原Server再进行一次Minor Compaction. 把第一次Compaction到停止服务之间的commit添加进来.
- 原Server unloads Tablet
3.7 利用不变性
- BigTable进行了如下简化约定: 每个SSTablet都是immutable.
- 唯一mutable的数据是MemTable中的数据, 这部分数据会被同时读和写. 为此, BigTable使用Copy-on-Write策略保证读写能够并行.