欢迎投稿

今日深度:

Hbase,

Hbase,


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定位过程

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

  1. 连接此region server,查询。

client与region server查询交互

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类

“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过程分为三个阶段:

上述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又细分为两个阶段

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区的数据,具体步骤如下:

  • 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中数据完整索引流程:

data index.png

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

上述流程中因为中间节点、叶子节点和数据块都需要加载到内存,所以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)数据被删除。
恢复步奏:

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启动失败。


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

www.htsjk.Com true http://www.htsjk.com/hbase/36859.html NewsArticle Hbase, HBase存储架构图 Hbase Overview.png HBase Master 为Region server分配region 负责Region server的负载均衡 发现失效的Region server并重新分配其上的region HDFS上的垃圾文件回收(删除表后的遗留文件...
相关文章
    暂无相关文章
评论暂时关闭