一、 Hbase存储框架
图1 Hbase存储架构图
1、 结构
HBase中的每张表都通过行键按照一定的范围被分割成多个子表(HRegion),默认一个HRegion超过256M就要被分割成两个,由HRegionServer管理,管理哪些HRegion由HMaster分配。
HRegionServer存取一个子表时,会创建一个HRegion对象,然后对表的每个列族(Column Family)创建一个Store实例,每个Store都会有0个或多个StoreFile与之对应,每个StoreFile都会对应一个HFile, HFile就是实际的存储文件。因此,一个HRegion有多少个列族就有多少个Store。
另外,每个HRegion还拥有一个MemStore实例。
(发现上图中一个错误,一个HRegionServer只对应一个HLog,也就是说同个HRegionServer中的HRegion共享一个HLog)
2、 流程
a) Client发起了一个HTable.put(Put)请求给HRegionServer
b) HRegionServer会将请求匹配到某个具体的HRegion上面
c) 决定是否写WAL log。WAL log文件是一个标准的Hadoop SequenceFile,文件中存储了HLogKey,这些Keys包含了和实际数据对应的***,主要用于崩溃恢复。
d) Put数据保存到MemStore中,同时检查MemStore状态,如果满了,则触发Flush to Disk请求。
e) HRegionServer处理Flush to Disk的请求,将数据写成HFile文件并存到HDFS上,并且存储最后写入的数据***,这样就可以知道哪些数据已经存入了永久存储的HDFS中。
二、 Hbase存储格式
HBase是基于BigTable的面向列的分布式存储系统,其存储设计是基于Memtable / SSTable设计的,主要分为两部分,一部分为内存中的MemStore (Memtable),另外一部分为磁盘(这里是HDFS)上的HFile (SSTable)。还有就是存储WAL的log,主要实现类为HLog.
MemStore
MemStore源码:private final ConcurrentNavigableMap<KeyValue, KeyValue> delegatee;
本质上MemStore就是一个内存里放着一个保存KEY/VALUE的MAP,当MemStore(默认64MB)写满之后,会开始刷磁盘操作。
HFile结构
图2 HFile结构图
HFile是基于HADOOP TFile的,如图2,文件长度为变长,仅FILE INFO/Trailer定长,Trailer中有指针指向其他数据块的起始点。Index数据块记录了每个Data块和Meta块的起始点。Data块和Meta块都是可有可无的,但对于大多数HFile,你都可以看到Data块。
Data块中,除了头部的MAGIC之外,就是一对对KEY/VALUE对,结构如下:
图3 Data块 KEY/VALUE对结构
关于文件块的大小:
默认块大小64KB,在创建表时可以通过HColumnDescriptor设定每个Family的块大小。
大数据块:适合顺序查找,不适合随机查找。
小数据块,适合随机查找,需要更多内存保存Data Index,创建文件慢,更多的flush操作
图4 HFile总体结构图
当key的value大小超过BLOCK SIZE时,那么查找这些value就无法通过索引去快速查找,而是需要通过遍历进行。
另外,针对目前针对现有HFile的两个主要缺陷:
a) 暂用过多内存
b) 启动加载时间缓慢
提出了HFile Version2设计:
https://issues.apache.org/jira/secure/attachment/12478329/hfile_format_v2_design_draft_0.1.pdf
HLog
HLog是HBase的日志类,在写入时进行write-ahead-log(WAL),主要为数据恢复。每个HRegionServer会对应一个HLog实例,HRegion在初始化的时候HRegionServer会将该HLog的实例作为构造函数传入其中。HLog的核心函数是其append()函数。
HLog File 是一个Sequence File,只能在文件的末尾添加内容。除了文件头以外,HLog File 由一条条HLog.Entry构成。Entry是HLog的基本组成部分,也是Read /Write的基本单位。
Entry由两个部分组成:HLogKey和WALEdit。
HLogKey主要包含以下几个变量:
private byte [] encodedRegionName;
private byte [] tablename;
private long logSeqNum; // Time at which this edit was written.
private long writeTime;
private byte clusterId;
logSeqNum是HLog 类的一个属性,AtomicLong类型。每写一个Entry就自动加1. 由于RS和HLog是一一对应的,所以logSeqNum是在RegionServer范围内的自增量。
WALEdit的主体是一个KeyValue的List。在旧的版本中WALEdit只包含一个KeyValue,然而新的版本可以包含多个KeyValue,而且这些KeyValue可以拥有不同的RowKey。
图5 HLog文件结构图
三、 HBase 预写日志 (WAL)
WAL最重要的作用是灾难恢复,一旦服务器崩溃,通过重放log,我们可以恢复崩溃之前的数据。如果写入WAL失败,整个操作也将认为失败。
图6 WAL
基本流程:首先,客户端初始化一个可能对数据改动的操作,如put(Put),delete(Delete) 和 incrementColumnValue()。这些操作将被封装在一个KeyValue对象实例中,发送给HRegionServer。 一旦达到一定大小,HRegionServer 将其发送给HRegion。这个过程中,数据会首先会被写入WAL,之后将被写到实际存放数据的MemStore中。
HLog是实现WAL的类。一个HRegionServer对应一个HLog实例。当HRegion初始化时,HLog将作为一个参数传给HRegion的构造函数。
WAL中几个重要的类:
1. HLog
HLog最核心的是调用doWrite的append() 方法,任何对数据改动的操作都就将首先调用这个方法。
出于性能考虑,put(), delete() 和incrementColumnValue()可通过开关函数setWriteToWAL (boolean) 禁用WAL。运行MapReduce Job时,可通过关闭WAL获得性能提升。
HLog另一个重要特性是将通过sequence number追踪数据改变。它内部使用AtomicLong保证线程安全。
2. HLogKey
上一部分的存储格式中有提到,HLogKey包含的一些主要变量,主要用于记录Key/Value对的归属信息。
3. LogFlusher
数据以KeyValue形式到达HRegionServer,将写入WAL,之后写入一个SequenceFile。由于数据流在写入文件时经常会缓存以提高性能,因此,有时数据实际保存在内存中。
LogFlusher调用HLog.optionalSync(),后者根据hbase. regionserver. optionallogflushinterval (默认10秒)定期调用Hlog.sync()。HLog.doWrite()也根据hbase.regionserver.flushlogentries (默认100秒)定期调用Hlog.sync()。Sync() 本身调用HLog.Writer.sync(),它由SequenceFileLogWriter实现。
4. LogRoller
Log的大小通过$HBASE_HOME/conf/hbase-site.xml 的hbase.regionserver.logroll.period限制,默认是一个小时。所以每60分钟,会打开一个新的log文件。久而久之,会有一大堆的文件需要维护。LogRoller主要完成日志的清理。
首先,LogRoller调用HLog.rollWriter(),定时滚动日志,之后,利用HLog.cleanOldLogs()可以清除旧的日志。它首先取得存储文件中的最大的sequence number,之后检查是否存在一个log所有的条目的”sequence number”均低于这个值,如果存在,将删除这个log。
5. Replay
旧日志往往由RegionServer 崩溃产生。当HMaster启动或者检测到RegionServer 崩溃,它将日志文件拆分为多份文件,存储在region所属的文件夹。之后,将日志重放。
重放过程:HRegionServer启动,打开所管辖的Region,检查是否存在剩余的log文件,如果存在,调用Store.doReconstructionLog()。重放一个日志只是简单地读入一个日志,将日志中的条目加入到Memstore中。最后,flush操作将Memstore中数据flush到硬盘中。
四、 HBase 索引
目前的HBase新版本没有实现二级索引,以前hbase的版本有过二级索引,实现时也是写写数据再写索引,索引放到后台队列中异步地写。实现最终一致。
HBase移走这块代码的一个原因就是索引到底落后数据多少时间是不确定的,特别是在异常的情况下。这样就导致在清理HLog时无法确定一个HLog是否真正全部写入数据了。
关于HBase二级索引的一篇讨论文章,参考:http://rdc.taobao.com/team/jm/archives/951
问题:为什么一个RegionServer 对应一个HLog,而不是一个region对应于一个log file?
BigTable论文答:如果每一个”tablet”(对应于HBase的region)都提交一个日志文件,会需要并发写入大量的文件到GFS(对应HDFS),这样,根据每个GFS server所依赖的文件系统,写入不同的日志文件会造成大量的磁盘操作。
HBase依照这样的原则。在日志被回滚和安全删除之前,将会有大量的文件。如果改成一个region对应于一个文件,将会不好扩展,会引发问题。