最近看了Bigtable的论文,所以结合着看了HBase的实现
HBase是一个开源的,分布式,版本化的非关系型数据库,模仿Google Bigtable实现
架构
Data Model
Table
HBase以表的形式存储数据,一个table包含多个row
可以将HBase的table当做一个多维映射,存储的是键值对,类型如下:
(row:string, column:string, time:int64) -> string
Row
每个row由一个row key和一个或多个列值组成,行按照row key的字母顺序排序,所以row key的设计非常重要,要尽量将相关行相邻存储,这样有利于查询,压缩等
例如将网站域名反转作为key (org.apache.www, org.apache.mail, org.apache.jira),这样所有Apache域都在表中彼此靠近,而不会基于子域名的第一个字母分散开
Column Family
通常出于性能原因而将一组列及其值物理地排布在一起,叫做列族
每个列族都有一组存储属性,例如是否应将其值缓存在内存中,如何压缩其数据或对其行键进行编码等
表中的row都具有相同的列族,但可能不会在列族中存储任何内容
Column Qualifier
列修饰符将添加到列族中,用来给数据提供索引
虽然列族在创建表时是固定的,但列修饰符是可变的,不同行之间可能有很大差异
Column
由列族和列修饰符组成 family:qualifier
Cell
由{row, column, version}
指定,cell中的数据是没有类型的,全部是字节码形式存储
Timestamp
cell可以保存着同一份数据的多个版本,版本通过时间戳来索引,时间戳的类型是 64位整型
时间戳与value一起写入,是给定值的版本标识符,不同版本降序排列,最新版本首先被读到
默认情况下,timestamp表示写入数据时RegionServer上的时间,但是也可以显示指定
Region
Region是表的可用性和分布的基本单元,对应表的一个行范围,最初表只有一个Region。随着数据不断插入表,Region不断增大,当Region的某个列族达到一个阀值(默认256M)时就会分成两个新的Region
Region内部由每个列族对应的的Store组成,其层次结构如下:
Table (HBase table)
Region (Regions for the table)
Store (Store per ColumnFamily for each Region for the table)
MemStore (MemStore for each Store for each Region for the table)
StoreFile (StoreFiles for each Store for each Region for the table)
Block (Blocks within a StoreFile within a Store for each Region for the table)
Region 定位
HBase最初模仿论文中,使用三层类似B+树的结构来保存region位置:
所有Region元数据被存储在.META.表中,随着Region的增多,.META.表中的数据也会增大,并分裂成多个新的Region。为了定位.META.表中各个Region的位置,把.META.表中所有Region的元数据保存在-ROOT-表中,最后由Zookeeper记录-ROOT-表的位置信息。所有客户端访问用户数据前,需要首先访问Zookeeper获得-ROOT-的位置,然后访问-ROOT-表获得.META.表的位置,最后根据.META.表中的信息确定用户数据存放的位置,如下图所示
5.1 Tablet Location 论文段
Root Tablet包含了一个特殊的METADATA表里所有的Tablet的位置信息
METADATA表的每个Tablet包含了一个用户Tablet的集合
Root Tablet实际上是METADATA表的第一个Tablet,只不过对它的处理比较特殊 — Root Tablet永远不会被分割 — 保证Tablet的位置信息存储结构不会超过三层
在METADATA表里面,每个Tablet的位置信息都存放在一个行关键字下面,而这个行关键字由Tablet所在的table的标识符和Tablet的最后一行编码而成
METADATA的每一行都存储了大约1KB的内存数据。在一个大小适中的、容量限制为128MB的METADATA Tablet中,采用这种三层结构的存储模式,可以标识 2^34 个Tablet的地址(如果每个Tablet存储128MB数据,那么一共可以存储2^61字节数据)。
客户程序使用的库会缓存Tablet的位置信息。如果客户程序没有缓存某个Tablet的地址信息,或者发现它缓存的地址信息不正确,客户程序就在树状的存储结构中递归的查询Tablet位置信息;如果客户端缓存是空的,那么寻址算法需要通过三次网络来回通信寻址,这其中包括了一次Chubby读操作;如果客户端缓存的地址信息过期了,那么寻址算法可能需要最多6次网络来回通信才能更新数据,因为只有在缓存中没有查到数据的时候才能发现数据过期(假设METADATA的Tablet没有被频繁的移动)。
尽管Tablet的地址信息是存放在内存里的,对它的操作不必访问GFS文件系统,但是,通常我们会通过预取Tablet地址来进一步的减少访问的开销:每次需要从METADATA表中读取一个Tablet的元数据的时候,它都会多读取几个Tablet的元数据。
在METADATA表中还存储了辅助信息(secondary information),包括每个Tablet的事件日志(例如,什么时候一个服务器开始为该Tablet提供服务)。这些信息有助于排查错误和性能分析
当前版本具体实现有所改变,内部使用的是hbase:meta
表,存储一系列的Region信息
表的键值对格式如下:
Key
* Region key of the format (`[table],[region start key],[region id]`)
Values
* `info:regioninfo` (serialized HRegionInfo instance for this region)
* `info:server` (server:port of the RegionServer containing this region)
* `info:serverstartcode` (start-time of the RegionServer process containing this region)
其中HRegionInfo中含有Region的endKey
RegionServer
论文中的Tablet Server
,启动时会向Zookeeper注册,Master将一定数目的region分配给RegionServer,负责对这些region的读写管理,会切分过大的region
对写入region的数据,采用的Log-Structured Merge-Tree (LSM-Tree)
方式,所有的写操作都会记录到HLog中,同时在内存中缓存,内存达到阈值写出文件StoreFile,定时进行文件的合并,合并时对旧数据进行相应的修改
Store
每一个region由一个或多个Store组成,每个 ColumnFamily对应一个Store。一个Store包含一个memStore和多个StoreFile
MemStore
MemStore大小到达阀值(hbase.hregion.memstore.flush.size
,默认是128MB)后会Flush到StoreFile
,写出数据时会进行Compaction,压缩是一种通过合并来减少Store中StoreFiles数量的操作,以提高读取操作的性能
Compaction分类两类Minor compaction
和Major compactions
- Minor Compaction
只合并小文件,不会对需要删除的文件内容进行清理 - Major Compaction
对Region
下同一个Column family
的StoreFile
合并为一个大文件,并且清除删除、过期、多余版本的数据
StoreFile HFile
StoreFile是HFile的包装,HFile格式模仿了BigTable论文中的SSTable,SSTable是一个持久化的、排序的、不可更改的Map结构,不可变方便进行并发读取,但是对数据的更新删除操作,都会记录到日志中,最后在合并时进行实际处理
HFile的文件存储格式如下:
Data Block
包含实际数据的键/值,之后是两个额外的元数据块:Meta
和FileInfo
,这两个类型的数据一直保存在内存中,直到文件关闭时再进行写出,Meta块用于保存大规模数据,其key为String,而FileInfo是一个简单的Map,首选小信息,其键和值均为字节数组。Regionserver的StoreFile使用Meta块来存储Bloom过滤器,FileInfo用于存储HFile相关的信息,例如K/V的平均长度
Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。
为了加速查找,为Data-Blocks 和 Meta-Blocks块创建了索引,这些索引包含n条块信息记录(其中n是块的数量,块信息包括块偏移,大小和第一个key)
最后是一个固定大小的文件Trailer,该块包含所有HFile文件中索引的偏移和数目,HFile版本,解压缩等信息
读取文件时会通过Trailer,首先加载索引
读取操作必须获取组成region状态的所有HFile,如果这些HFile不在内存中,那么就需要多次访问硬盘。可以使用Bloom过滤器查询一个HFile是否包含了特定行和列的数据,来减少硬盘访问的次数。
Bloom filter的数据存在StoreFile的Meta Block中,一旦写入无法更新,因为StoreFile是不可变的。Bloomfilter是一个列族级别的配置属性,如果在表中设置了Bloomfilter,那么HBase会在生成StoreFile时包含一份bloomfilter结构的数据,称其为MetaBlock;MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以,开启bloomfilter会有一定的存储及内存cache开销,但是可以使读操作显著减少磁盘访问的次数
改进版本的原因是大Bloom过滤器和块索引导致高内存占用和启动时间缓慢(第一个需要Bloom过滤器查找的get请求将导致加载整个Bloom过滤器位阵列,产生高延迟),具体解决就是切分成多个块
HLog File
HLog( write ahead log,WAL)日志文件,位于HBase根目录下的.log
目录内
每个RegionServer
对应一个HLog(如果每个Region创建一个单独的文件的话,同时会打开很多文件,导致大量的磁盘操作,同时不利于批量提交),客户端往提交数据的时候,会先写WAL日志
当一个RegionServer宕机时,会导致内存中的数据丢失,它加载的Region将会被移到其它的服务器上。为了恢复Region的状态保证数据的完整性,新的RegionServer要从原RegionServer写的日志中提取修改操作的信息,并重新执行
读写流程
HBase为了保证数据的随机读取性能,在HFile中存储RowKey时,按照顺序存储,即有序性。在客户端的请求到达RegionServer后,HBase为了保证RowKey的有序性,不会将数据立即写入到HFile中,而是将每个执行动作的数据保存在内存中,即MemStore中。MemStore能够很方便的兼容操作的随机写入,并且保证所有存储在内存中的数据是有序的。当MemStore到达阀值时,HBase会触发Flush机制,将MemStore中的数据Flush到HFile中,这样便能充分利用HDFS写入大文件的性能优势,提供数据的写入性能,所以顺序写入和随机写入的性能是相近的
读取流程相对简单,Client访问Zookeeper,通过mete表定位Region,到对应的RegionServer查找数据即可,RegionServer的内存分为MemStore和BlockCache两部分,MemStore主要用于写数据,BlockCache主要用于读数据。读请求先到MemStore中查数据,查不到就到BlockCache中查,再查不到就会到StoreFile上读,并把读的结果放入BlockCache
参考:
Apache HBase ™ Reference Guide
HBase MemStore和Compaction剖析
Apache HBase I/O – HFile
Appendix F. HFile format
Hbase原理分享