Hbase

96
kimibob
2016.05.06 17:25* 字数 7543

HBase存储架构图

Hbase Overview.png

HBase Master

  • 为Region server分配region
  • 负责Region server的负载均衡
  • 发现失效的Region server并重新分配其上的region
  • HDFS上的垃圾文件回收(删除表后的遗留文件)
  • 处理schema更新请求(对表的增删改查)

HBase RegionServer

  • 维护master分配给他的region,处理对这些region的io请求
  • 负责切分正在运行过程中变的过大的region

HBase 数据读取过程

Hbase里有一张特殊的元数据表"hbase:meta",它保存着hbase集群中所有region所处的主机位置信息。而zookeeper中保存这这张表所在的主机位置信息。

Hbase Read.png

region定位过程

  1. client首先从zookeeper获取meta表所在的region server
  2. client查询出meta表中包含所需查询key范围的region所在region server。

同时client会缓存下region与region server的映射信息,避免重复查询。如果region由于split或者balancing等原因改变了对应的region server ,则client会重新从zk中查询一遍,并再次缓存。

  1. 连接此region server,查询。

client与region server查询交互

  1. 查询region server的读缓存BlockCache 是否存在rowkey对应数据,如果有就返回,没有的话就行进行第二步查询。
  2. 查询memstore(一个按key排序的树形结构的缓冲区),即写内存是否存储有待查rowkey数据,如果有就返回,没有进行第三步查询;
  3. 用Block Cache indexes和bloom filters加载对应的HFile,根据rowkey遍历查询数据,不管有没有都返回到client。

HBase 数据写入过程

write_path.png

HBase WAL(write ahead log)

WAL.png

一个regionserver上所有的region共享一个HLog,一次数据的提交是先写WAL,再写memstore
HLog类
实现了WAL的类叫做HLog,当hregion被实例化时,HLog实例会被当做一个参数传到HRegion的构造器中,当一个Region接收到一个更新操作时,它可以直接把数据保存到一个共享的WAL实例中去.
HLog.png

HLogKey类
1、当前的WAL使用的是hadoop的sequencefile格式,其key是HLogKey实例。HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括sequence number和timestamp,timestamp是“写入时间“,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。Region打开存储文件,读取每个HFile中的最大的sequence number,如果该值大于HLog 的sequence number, 就将它作为HLog 的sequence number的值。最后当读取了所有hfile的sequence number,hlog也就获得了最近一次数据持久化的位置。
2、HLog sequence File的value是HBase的KeyValue对象,即对应HFile中的KeyValue
WALEdit类
1、客户端发送的每个修改都会封装成WALEdit类,一个WALEdit类包含了多个更新操作,可以说一个WALEdit就是一个原子操作,包含若干个操作的集合

LogSyncer类
1、Table在创建的时候,有一个参数可以设置,是否每次写Log日志都需要往集群的其他机器同步一次,默认是每次都同步,同步的开销是比较大的,但不及时同步又可能因为机器宕而丢日志。同步的操作现在是通过pipeline的方式来实现的,pipeline是指datanode接收数据后,再传给另外一台datanode,是一种串行的方式,n-Way writes是指多datanode同时接收数据,最慢的一台结束就是整个结束,差别在于一个延迟大,一个并发高,hdfs现在正在开发中,以便可以选择是按pipeline还是n-way writes来实现写操作
2、Table如果设置每次不同步,则写操作会被RegionServer缓存,并启动一个LogSyncer线程来定时同步日志。

hbase.regionserver.optionallogflushinterval:将Hlog同步到HDFS的间隔。如果Hlog没有积累到一定的数量,到了时间,也会触发同步。默认是1秒,单位毫秒。

LogRoller类

  1. 日志写入的大小是有限制的,LogRoller类会作为一个后台线程运行,在特定的时间间隔内滚动日志,通过hbase.regionserver.logroll.period属性控制,默认1小时。所以每60分钟,会打开一个新的log文件。久而久之,会有一大堆的文件需要维护。首先,LogRoller调用HLog.rollWriter(),定时滚动日志,之后,利用HLog.cleanOldLogs()可以清除旧的日志。它首先取得所有存储文件HFile中的最大的sequence number,之后检查是否存在一个log所有的条目的“sequence number”均低于这个值,如果存在,将删除这个log。
  2. log file的数目超过了log files的最大值。这时,会强制调用flush out 以减少log的数目。

“hbase.regionserver.hlog.blocksize”和“hbase.regionserver.logroll.multiplier”两个参数默认将在log大小为SequenceFile(默认为64MB)的95%时回滚。所以,log的大小和log使用的时间都会导致回滚,以先到达哪个限定为准。

HBase Meta Table

HBase Meta Table.png

hbase meta 表保存所有hbase集群中所有的region信息

META Table.png
Meta Table Structure.png
  • regionId = 创建region时的timestamp+"."+encode值(旧版hbase的regionId只有时间戳)+"."
  • name = tablename+","+startKey+","+regionId(等同于rowkey)
  • startKey,region的开始key,第一个region的startKey是空字符串
  • endKey,region的结束key,最后一个region的endKey是空字符串。当startKey,endKey都为空则表示只有一个region
  • encoded(Hash值),该值会作为hdfs文件系统中对应region的目录名
  • serverstartcode 是服务开始的时候的timestamp
  • server 指服务器的地址和端口
  • seqnumDuringOpen:?
hbase sample data.png

hbase原先的设计还含有一张-ROOT-表,用来保存meta表的region信息。HBase 0.96 版本移除了这个特性(HBASE-3171),并且设置meta表再大的数据量也不会像普通表一样进行split,因此meta表总是只有一个region(HBASE-2415)。

HBase的后台合并

小合并:把小HFile合并成一个大HFile,这样可以避免在读一行的时候引用过多文件,提升读性能。在执行合并的时候,HBase读出已有的多个HFile内容,并把记录写入一个新文件。然后把新文件设置为激活状态,删除所有老文件,它会占用大量的磁盘和网络IO,但相比大合并还是轻量级的,可以频繁发生。

HBase Minor Compaction.png

大合并:处理给定region的一个列族的所有HFile,将这个列族所有的HFile合并成一个文件。这个动作相当耗费资源,可以从Shell中手工触发大合并,这也是清理被删除记录的唯一机会。
HBase Major Compaction.png

从合并的操作可以看出,HBase其实不适合存储经常删改的数据,因为删除的记录在大合并前依旧占用空间,而大合并又十分耗费资源。

HBase的Delete命令并不立即删除内容,而是针对那个内容写入一条新的删除标记,这个删除标记叫做“墓碑”(tombstone)。被标记的内容不能在Get和Scan操作中返回结果。作为磁盘文件,HFile在非合并的时候是不能被改变的。且因为“墓碑”记录并不一定和被删除的记录在同一个HFile里面,所以HFile只有在执行一次大合并的时候才会处理墓碑记录,被删除记录占用的空间才会被释放。

Hbase Memstore&Flush

Memstore Usage in HBase ReadWrite Paths.png

用到Memstore最主要的原因是:存储在HDFS上的数据需要按照row key 排序。而HDFS本身被设计为顺序读写(sequential reads/writes),不允许修改。这样的话,HBase就不能够高效的写数据,因为要写入到HBase的数据不会被排序,这也就意味着没有为将来的检索优化。为了解决这个问题,HBase将最近接收到的数据缓存在内存中(in Memstore),在持久化到HDFS之前完成排序,然后再快速的顺序写入HDFS。
除了解决“无序”问题外,Memstore还有一些其他的好处:

  • 作为一个内存级缓存,缓存最近增加数据。一种显而易见的场合是,新插入数据总是比老数据频繁使用。
  • 在持久化写入之前,在内存中对Rows/Cells可以做某些优化。比如,当数据的version被设为1的时候,对于某些CF的一些数据,Memstore缓存了数个对该Cell的更新,在写入HFile的时候,仅需要保存一个最新的版本就好了,其他的都可以直接抛弃。

每一次Memstore的flush,会为每一个ColumnFamily创建一个新的HFile。

MemStore的最小flush单元是Region而不是单个MemStore。可想而知,如果一个Region中Memstore过多,当其中某一个Memstore满足flush条件,则该region对应的所有Memstore都会被flush,这样每次flush的开销必然会很大,因此我们也建议在进行表设计的时候尽量减少ColumnFamily的个数。

Flush操作如果只选择某个Region的Store内的MemStore写入磁盘,而不是统一写入磁盘,那么HLog上key的一致性在Region中各个Store(ColumnFamily)下的MemStore内就会有不一致的key区间。
如下图所示,我们假定该RegionServer上仅有一个Region,由于不同的Row在列簇上有所区别,就会出现有些不同Store内占用的内存不一致的情况,这里会根据整体内存使用的情况,或者RS使用内存的情况来决定是否执行Flush操作。如果仅仅flush使用内存较大的memstore,那么在使用的过程中,一是Scan操作在执行时就不够统一(同一个rowkey的cf有些flush有些还没flush),二是在HLog Replayer还原Region内Memstore故障前的状态,不能简单的根据Hlog的Flush_marker的标记位来执行Replay。

Hbase flush.png

Memstore Flush触发条件

1. Memstore级别限制:当Region中任意一个MemStore的大小达到了上限(hbase.hregion.memstore.flush.size,默认128MB),会触发Memstore刷新。
2. Region级别限制:当Region中所有Memstore的大小总和达到了上限(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默认 2 x 128M = 256M),会触发memstore刷新。
3. Region Server级别限制:当一个Region Server中所有Memstore的大小总和达到了上限(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默认 40%的JVM内存使用量),会触发部分Memstore刷新。Flush顺序是按照Memstore由大到小执行,先Flush Memstore最大的Region,再执行次大的,直至总体Memstore内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认38%的JVM内存使用量)。
4. 当一个Region Server中HLog数量达到上限(可通过参数hbase.regionserver.max.logs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush
5. HBase定期刷新Memstore:默认周期为1小时,确保Memstore不会长时间没有持久化。为避免所有的MemStore在同一时间都进行flush导致的问题,定期的flush操作有20000左右的随机延时。
6. 手动执行flush:用户可以通过shell命令 flush ‘tablename’或者flush ‘region name’分别对一个表或者一个Region进行flush。

Memstore Flush流程
为了减少flush过程对读写的影响,HBase采用了类似于两阶段提交的方式,将整个flush过程分为三个阶段:

  1. prepare阶段:遍历当前Region中的所有Memstore,将Memstore中当前数据集kvset做一个快照snapshot,然后再新建一个新的kvset。后期的所有写入操作都会写入新的kvset中,而整个flush阶段读操作会首先分别遍历kvset和snapshot,如果查找不到再会到HFile中查找。prepare阶段需要加一把updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
  2. flush阶段:遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时。
  3. commit阶段:遍历所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到HStore的storefiles列表中,最后再清空prepare阶段生成的snapshot。

上述flush流程可以通过日志信息查看:

/******* prepare阶段 ********/
2016-02-04 03:32:41,516 INFO  [MemStoreFlusher.1] regionserver.HRegion: Started memstore flush for sentry_sgroup1_data,{\xD4\x00\x00\x01|\x00\x00\x03\x82\x00\x00\x00?\x06\xDA`\x13\xCAE\xD3C\xA3:_1\xD6\x99:\x88\x7F\xAA_\xD6[L\xF0\x92\xA6\xFB^\xC7\xA4\xC7\xD7\x8Fv\xCAT\xD2\xAF,1452217805884.572ddf0e8cf0b11aee2273a95bd07879., current region memstore size 128.9 M

/******* flush阶段 ********/
2016-02-04 03:32:42,423 INFO  [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=1726212642, memsize=128.9 M, hasBloomFilter=true, into tmp file hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/.tmp/021a430940244993a9450dccdfdcb91d

/******* commit阶段 ********/
2016-02-04 03:32:42,464 INFO  [MemStoreFlusher.1] regionserver.HStore: Added hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/d/021a430940244993a9450dccdfdcb91d, entries=643656, sequenceid=1726212642, filesize=7.1 M

其中第二阶段flush又细分为两个阶段

  1. append阶段:memstore中keyvalue首先会写入到HFile中数据块
  2. finalize阶段:修改HFlie中meta元数据块,索引数据块以及Trailer数据块等
    append流程
    具体keyvalue数据的append以及finalize过程在HFileWriterV2文件中,其中append流程可以大体表征为:
    Block Write.png

    a. 预检查:检查key的大小是否大于前一个key,如果大于则不符合HBase顺序排列的原理,抛出异常;检查value是否是null,如果为null也抛出异常
    b. block是否写满:检查当前Data Block是否已经写满,如果没有写满就直接写入keyvalue;否则就需要执行数据块落盘以及索引块修改操作;
    c. 数据落盘并修改索引:如果DataBlock写满,首先将block块写入流;再生成一个leaf index entry,写入leaf Index block;再检查该leaf index block是否已经写满需要落盘,如果已经写满,就将该leaf index block写入到输出流,并且为索引树根节点root index block新增一个索引,指向叶子节点(second-level index)
    d. 生成一个新的block:重新reset输出流,初始化startOffset为-1
    e. 写入keyvalue:将keyvalue以流的方式写入输出流,同时需要写入memstore;除此之外,如果该key是当前block的第一个key,需要赋值给变量firstKeyInBlock
    finalize阶段
    memstore中所有keyvalue都经过append阶段输出到HFile后,会执行一次finalize过程,主要更新HFile中meta元数据块、索引数据块以及Trailer数据块,其中对索引数据块的更新是我们关心的重点,此处详细解析,上述append流程中c步骤’数据落盘并修改索引’会使得root index block不断增多,当增大到一定程度之后就需要分裂,分裂示意图如下图所示:
    Index Split.png

    上图所示,分裂前索引结构为second-level结构,图中没有画出Data Blocks,根节点索引指向叶子节点索引块。finalize阶段系统会对Root Index Block进行大小检查,如果大小大于规定的大小就需要进行分裂,图中分裂过程实际上就是将原来的Root Index Block块分割成4块,每块独立形成中间节点InterMediate Index Block,系统再重新生成一个Root Index Block(图中红色部分),分别指向分割形成的4个interMediate Index Block。此时索引结构就变成了third-level结构。

Hfile文件格式

HFile的核心设计思想是(数据)分块和(索引)分级

HFile Structure.jpg

如上图所示, HFile会被切分为多个大小相等的block块,每个block的大小可以在创建表列簇的时候通过参数blocksize => ‘65535’进行指定,默认为64k,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。而且所有block块都拥有相同的数据结构,如图左侧所示,HBase将block块抽象为一个统一的HFileBlock。HFileBlock支持两种类型,一种类型不支持checksum,一种不支持。

HFile V2.png

HFile V2中,主要包括四个部分:

  • Scanned Block(数据block,表示顺序扫描HFile时所有的数据块将会被读取,包括Leaf Index Block和Bloom Block)
  • Non-Scanned block(元数据block,表示在HFile顺序扫描的时候数据不会被读取,主要包括Meta Block和Intermediate Level Data Index Blocks两部分)
  • Load-on-open(这部分数据在HBase的region server启动时需要加载到内存中,包括FileInfo、Bloom filter block、data block index和meta block index)
  • trailer(文件尾,主要记录了HFile的基本信息、各个部分的偏移值和寻址信息。)。

一个HFile文件包含了多种类型的HFileBlock块,每种类型的HFileBlock主要包括两部分:BlockHeader和BlockData。其中Header主要存储block元数据,Data用来存储具体数据。block元数据中最核心的字段是BlockType字段,用来标示该block块的类型,HBase中定义了8种BlockType,每种BlockType对应的block都存储不同的数据内容,有的存储用户数据,有的存储索引数据,有的存储meta元数据。对于任意一种类型的HFileBlock,都拥有相同结构的BlockHeader,但是BlockData结构却不相同。下面通过一张表简单罗列最核心的几种BlockType:

HFile BlockType.png

  • Trailer Block
    主要记录了HFile的基本信息、各个部分的偏移值和寻址信息,下图为Trailer内存和磁盘中的数据结构,其中只显示了部分核心字段:
    Trailer Block.png

HFile在读取的时候首先会解析Trailer Block并加载到内存,然后再进一步加载LoadOnOpen区的数据,具体步骤如下:

  1. 首先加载version版本信息,HBase中version包含majorVersion和minorVersion两部分,前者决定了HFile的主版本: V1、V2 还是V3;后者在主版本确定的基础上决定是否支持一些微小修正,比如是否支持checksum等。不同的版本决定了使用不同的Reader对象对HFile进行读取解析
  2. 根据Version信息获取trailer的长度(不同version的trailer长度不同),再根据trailer长度加载整个HFileTrailer Block
  3. 最后加载load-on-open部分到内存中,起始偏移地址是trailer中的LoadOnOpenDataOffset字段,load-on-open部分的结束偏移量为HFile长度减去Trailer长度,load-on-open部分主要包括索引树的根节点以及FileInfo两个重要模块,FileInfo是固定长度的块,它纪录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等;
  • Data Block
    DataBlock是HBase中用户数据存储的最小单元。DataBlock中主要存储用户的KeyValue数据(KeyValue后面一般会跟一个timestamp,图中未标出),而KeyValue结构是HBase存储的核心,每个数据都是以KeyValue结构在HBase中进行存储。KeyValue结构在内存和磁盘中可以表示为:
    Data Block.png

每个KeyValue都由4个部分构成,分别为key length,value length,key和value。其中key value和value length是两个固定长度的数值,而key是一个复杂的结构,首先是rowkey的长度,接着是rowkey,然后是ColumnFamily的长度,再是ColumnFamily,最后是时间戳和KeyType(keytype有四种类型,分别是Put、Delete、 DeleteColumn和DeleteFamily),value就没有那么复杂,就是一串纯粹的二进制数据。

  • BloomFilter Metadata Block & Bloom Block
    BloomFilter对于HBase的随机读性能至关重要,对于get操作以及部分scan操作可以剔除掉不会用到的HFile文件,减少实际IO次数,提高随机读性能。在此简单地介绍一下Bloom Filter的工作原理,Bloom Filter使用位数组来实现过滤,初始状态下位数组每一位都为0,如下图所示:

    BloomFilter-1.png

    假如此时有一个集合S = {x1, x2, … xn},Bloom Filter使用k个独立的hash函数,分别将集合中的每一个元素映射到{1,…,m}的范围。对于任何一个元素,被映射到的数字作为对应的位数组的索引,该位会被置为1。比如元素x1被hash函数映射到数字8,那么位数组的第8位就会被置为1。下图中集合S只有两个元素x和y,分别被3个hash函数进行映射,映射到的位置分别为(0,2,6)和(4,7,10),对应的位会被置为1:
    BloomFilter-2.png

    现在假如要判断另一个元素是否是在此集合中,只需要被这3个hash函数进行映射,查看对应的位置是否有0存在,如果有的话,表示此元素肯定不存在于这个集合,否则有可能存在。下图所示就表示z肯定不在集合{x,y}中:
    BloomFilter-3.png

    HBase中每个HFile都有对应的位数组,KeyValue在写入HFile时会先经过几个hash函数的映射,映射后将对应的数组位改为1,get请求进来之后再进行hash映射,如果在对应数组位上存在0,说明该get请求查询的数据肯定不在该HFile中。
    HFile中的位数组就是上述Bloom Block中存储的值,可以想象,一个HFile文件越大,里面存储的KeyValue值越多,位数组就会相应越大。一旦太大就不适合直接加载到内存了,因此HFile V2在设计上将位数组进行了拆分,拆成了多个独立的位数组(根据Key进行拆分,一部分连续的Key使用一个位数组)。这样一个HFile中就会包含多个位数组,根据Key进行查询,首先会定位到具体的某个位数组,只需要加载此位数组到内存进行过滤即可,减少了内存开支。
    在结构上每个位数组对应HFile中一个Bloom Block,为了方便根据Key定位具体需要加载哪个位数组,HFile V2又设计了对应的索引Bloom Index Block,对应的内存和逻辑结构图如下:
    Bloom Index Block.png

    Bloom Index Block结构中totalByteSize表示位数组的bit数,numChunks表示Bloom Block的个数,hashCount表示hash函数的个数,hashType表示hash函数的类型,totalKeyCount表示bloom filter当前已经包含的key的数目,totalMaxKeys表示bloom filter当前最多包含的key的数目, Bloom Index Entry对应每一个bloom filter block的索引条目,作为索引分别指向"scanned block section" 部分的Bloom Block,Bloom Block中就存储了对应的位数组。
    Bloom Index Entry的结构见上图左边所示,BlockOffset表示对应Bloom Block在HFile中的偏移量,FirstKey表示对应BloomBlock的第一个Key。根据上文所说,一次get请求进来,首先会根据key在所有的索引条目中进行二分查找,查找到对应的Bloom Index Entry,就可以定位到该key对应的位数组,加载到内存进行过滤判断。

  • Index Block

HFile V1的时候,在数据块索引很大时,很难全部load到内存。假设每个数据块使用默认大小64KB,每个索引项64Byte,这样如果每台及其上存放了60TB的数据,那索引数据就得有60G,所以内存的占用还是很高的。此外,由于直到加载完所有块索引数据之后,才能认为region启动完成,因此这样的块索引大小会明显地拖慢region的启动速度。所以,将这些索引以树状结构进行组织,只让顶层索引常驻内存,其他索引按需读取并通过LRU cache进行缓存,这样就不用全部加载到内存了。

HFile中索引结构根据索引层级的不同分为两种:single-level和mutil-level,前者表示单层索引,后者表示多级索引,一般为两级或三级。HFile V1版本中只有single-level一种索引结构,V2版本中引入多级索引。
HFile V2版本Index Block则分为两类:Root Index Block和NonRoot Index Block,其中NonRoot Index Block又分为Intermediate Index Block和Leaf Index Block两种。HFile中索引结构类似于一棵树,Root Index Block表示索引数根节点,记录每个块首个key及其索引,Intermediate Index Block表示中间节点,记录每个块最后的key及其索引,Leaf Index block表示叶子节点,叶子节点直接指向实际数据块。随着dateblock数量的不断增多,(root_index-->intermediate_index-->leaf_index-->data_block), 索引的层级会逐渐增多。

HFile index.png

HFile中除了Data Block需要索引之外,上面提到的Bloom Block也需要索引,Bloom Block的索引结构实际上就是采用了single-level结构,是一种Root Index Block。


Root Index Block
Root Index Block表示索引树根节点索引块,可以作为bloom的直接索引,也可以作为data索引的根索引。而且对于single-level和mutil-level两种索引结构对应的Root Index Block略有不同,这里以mutil-level的Root Index Block索引结构为例进行分析,在内存和磁盘中的格式如下图所示:

Root Index Block.png

其中Index Entry表示具体的索引对象,每个索引对象由3个字段组成,Block Offset表示索引指向数据块的偏移量,BlockDataSize表示索引指向数据块在磁盘上的大小,BlockKey表示索引指向数据块中的第一个key。除此之外,还有另外3个字段用来记录MidKey的相关信息,MidKey表示HFile所有Data Block中中间的一个Data Block,用于在对HFile进行split操作时,快速定位HFile的中间位置。需要注意的是single-level索引结构和mutil-level结构相比,就只缺少MidKey这三个字段。
Root Index Block会在HFile解析的时候直接加载到内存中,此处需要注意在Trailer Block中有一个字段为dataIndexCount,就表示此处Index Entry的个数。因为Index Entry并不定长,只有知道Entry的个数才能正确的将所有Index Entry加载到内存。


NonRoot Index Block
当HFile中Data Block越来越多,single-level结构的索引已经不足以支撑所有数据都加载到内存,需要分化为mutil-level结构。mutil-level结构中NonRoot Index Block作为中间层节点或者叶子节点存在,无论是中间节点还是叶子节点,其都拥有相同的结构,如下图所示:

NonRoot Index Block.png

和Root Index Block相同,NonRoot Index Block中最核心的字段也是Index Entry,用于指向叶子节点块或者数据块。不同的是,NonRoot Index Block结构中增加了block块的内部索引entry Offset字段,entry Offset表示index Entry在该block中的相对偏移量(相对于第一个index Entry),用于实现block内的二分查找。所有非根节点索引块,包括Intermediate index block和leaf index block,在其内部定位一个key的具体索引并不是通过遍历实现,而是使用二分查找算法,这样可以更加高效快速地定位到待查找key。


在HFile V2中数据完整索引流程:

  1. 先在内存中对HFile的root索引进行二分查找,如果支持多级索引,则定位到leaf index/intermediate index,如果是单级索引,则定位到数据块data block;
  2. 如果支持多级索引,则会从cache/hdfs中读取leaf/intermediate index chunk,在leaf/intermediate chunk根据key值进行二分查找(leaf/intermediate index chunk支持二分查找),找到对应的data block。
  3. 从cache/hdfs中读取数据块;
  4. 在数据块中遍历查找对应的数据。
data index.png

图中红线表示一次查询的索引过程(HBase中相关类为HFileBlockIndex和HFileReaderV2),基本流程可以表示为:

  1. 用户输入rowkey为fb,在root index block中通过二分查找定位到fb在’a’和’m’之间,因此需要访问索引’a’指向的中间节点。因为root index block常驻内存,所以这个过程很快。
  2. 将索引’a’指向的中间节点索引块加载到内存,然后通过二分查找定位到fb在index ‘d’和’h’之间,接下来访问索引’d’指向的叶子节点。
  3. 同理,将索引’d’指向的中间节点索引块加载到内存,一样通过二分查找定位找到fb在index ‘f’和’g’之间,最后需要访问索引’f’指向的数据块节点。
  4. 将索引’f’指向的数据块加载到内存,通过遍历的方式找到对应的keyvalue。

上述流程中因为中间节点、叶子节点和数据块都需要加载到内存,所以io次数正常为3次。但是实际上HBase为block提供了缓存机制,可以将频繁使用的block缓存在内存中,可以进一步加快实际读取过程。所以,在HBase中,通常一次随机读请求最多会产生3次io,如果数据量小(只有一层索引),数据已经缓存到了内存,就不会产生io。


HBase表误删恢复

hdfs的回收站机制
在hdfs上有一个回收站的设置,可以将删除的数据移动到回收站目录/user/$<username>/.Trash/中,设置回收站的相关参数如下:

  • fs.trash.interval=360
    以分钟为单位的垃圾回收时间,垃圾站中数据超过此时间,会被删除。如果是0,垃圾回收机制关闭。可以配置在服务器端和客户端。如果在服务器端配置trash无效,会检查客户端配置。如果服务器端配置有效,客户端配置会忽略。也就是说,Server端的值优先于Client。如有同名文件被删除,会给文件顺序编号,例如:a.txt,a.txt(1)
  • fs.trash.checkpoint.interval=0
    以分钟为单位的垃圾回收检查间隔。应该小于或等于fs.trash.interval,如果是0,值等同于fs.trash.interval。该值只在服务器端设置。

如果disable+drop误删了hbase表数据,数据不会放到回收站中,hbase有自己的一套删除策略。
HBase的数据主要存储在分布式文件系统HFile和HLog两类文件中。Compaction操作会将合并完的不用的小Hfile移动到<.archive>文件夹,并设置ttl过期时间。HLog文件在数据完全flush到hfile中时便会过期,被移动到.oldlog(oldWALs)文件夹中。

HMaster上的定时线程HFileCleaner/LogCleaner周期性扫描.archive目录和.oldlog目录, 判断目录下的HFile或者HLog是否可以被删除,如果可以,就直接删除文件。

关于hfile文件和hlog文件的过期时间,其中涉及到两个参数,如下:

(1)hbase.master.logcleaner.ttl
HLog在.oldlogdir目录中生存的最长时间,过期则被Master的线程清理,默认是600000(ms);
(2)hbase.master.hfilecleaner.plugins
HFile的清理插件列表,逗号分隔,被HFileService调用,可以自定义,默认org.apache.hadoop.hbase.master.cleaner.TimeToLiveHFileCleaner。

默认hfile的失效时间是5分钟(300000ms)。由于一般的hadoop平台默认都没有对该参数的设置,可以在配置选项中添加对hbase.master.hfilecleaner.ttl的设置。

实际在测试的过程中,删除一个hbase表,在hbase的hdfs目录下的archive文件夹中,会立即发现删除表的所有region数据(不包含regioninfo、tabledesc等元数据文件),超时5分钟所有region(hfile)数据被删除。
恢复步奏:

  1. 抢救数据
    保证在删除表之后的5分钟之内将hdfs目录/apps/hbase/data/archive/文件夹下的相关数据拷贝一份到另外一个安全目录下。
  2. 新建与删除表同名和同列族的表
  3. 将抢救下来的region数据拷贝到hbase表对应的目录下
hadoop fs -cp /apps/hbase/data/archive/data/default/member/0705a8ce0ead4618839b4c9cf9977fa5 /apps/hbase/data/data/default/member/
  1. hbase元数据修复
    因为被删除的数据文件夹中并没有包含.regioninfo文件,需要进行元数据修复
hbase hbck -repair #一次可能不成功,多试几次。

hbase元数据损坏
Hbase的一些启动必要的文件放置在hdfs上,由于人为删除了hdfs的数据块文件,这些文件块恰好包含了hbase的文件数据,所以导致hbase启动失败。

  1. 停止hbase服务
  2. 查看hdfs的文件健康状态: hdfs fsck / | egrep -v '^.+$' | grep -v replica | grep -v Replica
  3. 根据输出列出的所有损坏的文件块,由于已被删除无法恢复,所以清理hdfs中损坏或缺失的数据块。hdfs fsck -delete 或者 hdfs fs -rm /test/xxx.txt ...
  4. 再次复查hdfs的文件健康状态, 结果显示HEALTHY。
  5. 备份hbase,hadoop fs -mv /apps/hbase /apps/hbasebak
  6. 登录 zookeeper ,/usr/lib/zookeeper/zkCli.sh 删除其中的hbase目录 rmr /hbase
  7. 启动hbase服务,确认所有相关服务都正常

hbase的行锁与多版本并发控制(MVCC)
http://my.oschina.net/u/189445/blog/597226

日记本
Web note ad 1