Hbase要掌握的内容

HBase与传统关系型数据库(如MySQL)的区别

数据类型:没有数据类型,都是字节数组(有一个工具类Bytes,将java对象序列化为字节数组)。
数据操作:HBase只有很简单的插入、查询、删除、清空等操作,表和表之间是分离的,没有复杂的表和表之间的关系,而传统数据库通常有各式各样的函数和连接操作。
存储模式:Hbase适合于非结构化数据存储,基于列存储而不是行。
数据维护:HBase的更新操作不应该叫更新,它实际上是插入了新的数据,而传统数据库是替换修改
时间版本:Hbase数据写入cell时,还会附带时间戳,默认为数据写入时RegionServer的时间,但是也可以指定一个不同的时间。数据可以有多个版本。
可伸缩性,Hbase这类分布式数据库就是为了这个目的而开发出来的,所以它能够轻松增加或减少硬件的数量,并且对错误的兼容性比较高。而传统数据库通常需要增加中间层才能实现类似的功能

HBase的架构和基本原理

 HMaster

主要负责表和Region的管理工作:

  • 管理用户对表的增删改查操作
  • 实现不同region服务器之间的负载均衡
  • 在region分裂或合并后,负责重新调整region的分布
  • 对发生故障失效的region服务器上的region进行迁移

客户端访问hbase上的数据过程不需要master的参与,客户端可以访问zk获取meta表的地址并最终到达相应的region服务器进行数据读写。

RegionServer

RegionServer相当于一个HBase的服务器,其上面有一个或多个Region。

HregionServer直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:

管理master为其分配的Region

处理来自客户端的读写请求

负责和底层HDFS的交互,存储数据到HDFS

负责Region变大以后的拆分

负责Storefile的合并工作

Region

表的一部分数据。Table在行的方向上分割为多个HRegion,HRegion是HBase中分布式存储和负载均衡的最小单元,即不同的HRegion可以分别在不同的HRegionServer上,但同一个HRegion是不会拆分到多个HRegionServer上的。HRegion按大小分割,每个表一般只有一个HRegion,随着数据不断插入表,HRegion不断增大,当HRegion的某个列簇达到一个阀值(默认256M)时就会分成两个新的HRegion。

每个region对象由多个Store组成,每个Store代表的是一个列族的存储,每个store里面又包含一个Memstore和若干个StoreFile,其中,MemStore是内存中的焕春,保存最近更新的数据;StoreFile是磁盘中的文件,它的底层实现方式是HDFS的HFile。

ZooKeeper

Hbase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:

  • 通过Zoopkeeper来保证集群中只有1个master在运行,如果master异常,会通过竞争机制产生新的master提供服务;
  • 通过Zoopkeeper来监控RegionServer的状态,当RegionSevrer有异常的时候,通过回调的形式通知Master RegionServer上下限的信息;
  • 通过Zoopkeeper存储元数据的统一入口地址。

HLog(WAL Log)

WAL意为write ahead log,HBase中的预写日志,用来做灾难恢复使用,底层实现是HLog,HLog记录数据的所有变更。使用WAL的原因:因为MemStore存储的数据是驻留在内存中的,是不稳定的(比如宕机时),所以采用了WAL预写日志来解决这个问题。(运行MApReduce作业时,可以通过关闭WAL功能来获得性能的提升——setWriteToWAL(boolean))

其实HLog文件就是一个普通的Hadoop Sequence File, Sequence File的value是key时HLogKey对象,其中记录了写入数据的归属信息,除了table和region名字外,还同时包括sequence number和timestamp,timestamp是写入时间,sequence number的起始值为0,或者是最近一次存入文件系统中的sequence number。 Sequence File的value是HBase的KeyValue对象,即对应HFile中的KeyValue。


HBase读写流程

读流程

1.首先,客户端需要获知其想要读取的信息的Region的位置,这个时候,Client访问hbase上数据时并不需要Hmaster参与(HMaster仅仅维护着table和Region的元数据信息,负载很低),只需要访问zookeeper,从meta表获取相应region信息(地址和端口等)。【Client请求ZK获取.META.所在的RegionServer的地址。】

2.客户端会将该保存着RegionServer的位置信息的元数据表.META.进行缓存。然后在表中确定待检索rowkey所在的RegionServer信息(得到持有对应行键的.META表的服务器名)。【获取访问数据所在的RegionServer地址】

3.根据数据所在RegionServer的访问信息,客户端会向该RegionServer发送真正的数据读取请求。服务器端接收到该请求之后需要进行复杂的处理。

4.先从MemStore找数据,如果没有,再到StoreFile上读(为了读取的效率)。

 

注:
1.客户端只需要配置zookeeper的访问地址以及根目录,就可以进行正常的读写请求。不需要配置集群的RegionServer地址列表。
2.在Hbase 0.96版本以前,Hbase有两个特殊的表,分别是-ROOT-表和.META.表,其中-ROOT-的位置存储在ZooKeeper中,-ROOT-本身存储了 .META. Table的RegionInfo信息,并且-ROOT-不会分裂,只有一个region。而.META.表可以被切分成多个region。0.96版本以后将-ROOT-表去掉了。

写流程

前三点和读类似:
1.Client先访问zookeeper,从.META.表获取相应region信息,然后从meta表获取相应region信息
2.根据namespace、表名和rowkey根据meta表的数据找到写入数据对应的region信息
3.找到对应的regionserver
把数据先写到WAL中,即HLog,然后写到MemStore上
4.MemStore达到设置的阈值后则把数据刷成一个磁盘上的StoreFile文件。
5.当多个StoreFile文件达到一定的大小后(这个可以称之为小合并,合并数据可以进行设置,必须大于等于2,小于10——hbase.hstore.compaction.max和hbase.hstore.compactionThreshold,默认为10和3),会触发Compact合并操作,合并为一个StoreFile,(这里同时进行版本的合并和数据删除。)
6.当Storefile大小超过一定阈值后,会把当前的Region分割为两个(Split)【可称之为大合并,该阈值通过hbase.hregion.max.filesize设置,默认为10G】,并由Hmaster分配到相应的HRegionServer,实现负载均衡


 

问题:

WAL是存储在HDFS上的,Memstore是存储在内存中的,HFile又是存储在HDFS上的;

数据先写入WAL,再被放入Memstore,最后被持久化到HFile。

那么,数据在进入HFile之前已经被存储到HDFS一次了,为什么还需要被放入Memstore?

因为HDFS上的文件只能创建、追加、删除,但不能修改。对于一个数据库来说,按顺序地存放数据是非常重要的,所以我们不能按照数据到来的顺序来写入硬盘。在数据写入Memstore之前,先要被写入WAL,所以增加Memstore的大小并不能加速写入速度。Memstore存在的意义是维持数据按照rowkey顺序排列,它会将数据整理成顺序存放,再一起写入硬盘,而不是做一个缓存。

MemStore刷盘

 为了提高Hbase的写入性能,当写请求写入MemStore后,不会立即刷盘。而是会等到一定的时候进行刷盘的操作。具体是哪些场景会触发刷盘的操作呢?总结成如下的几个场景:

  •  全局内存控制

这个全局的参数是控制内存整体的使用情况,当所有memstore占整个heap的最大比例的时候,会触发刷盘的操作。这个参数是hbase.regionserver.global.memstore.upperLimit,默认为整个heap内存的40%。但这并不意味着全局内存触发的刷盘操作会将所有的MemStore都进行输盘,而是通过另外一个参数hbase.regionserver.global.memstore.lowerLimit来控制,默认是整个heap内存的35%。当flush到所有memstore占整个heap内存的比率为35%的时候,就停止刷盘。这么做主要是为了减少刷盘对业务带来的影响,实现平滑系统负载的目的。

  •  MemStore达到上限

  当MemStore的大小达到hbase.hregion.memstore.flush.size大小的时候会触发刷盘,默认128M大小

  • RegionServer的Hlog数量达到上限

前面说到Hlog为了保证Hbase数据的一致性,那么如果Hlog太多的话,会导致故障恢复的时间太长,因此Hbase会对Hlog的最大个数做限制。当达到Hlog的最大个数的时候,会强制刷盘。这个参数是hase.regionserver.max.logs,默认是32个。

  • 手工触发

可以通过hbase shell或者java api手工触发flush的操作。

  • 关闭RegionServer触发

在正常关闭RegionServer会触发刷盘的操作,全部数据刷盘后就不需要再使用Hlog恢复数据。

  • Region使用HLOG恢复完数据后触发

当RegionServer出现故障的时候,其上面的Region会迁移到其他正常的RegionServer上,在恢复完Region的数据后,会触发刷盘,当刷盘完成后才会提供给业务访问。

 

HLog

Hlog是Hbase实现WAL(Write ahead log)方式产生的日志信息,内部是一个简单的顺序日志。每个RegionServer对应1个Hlog(备注:1.x版本的可以开启MultiWAL功能,允许多个Hlog),所有对于该RegionServer的写入都被记录到Hlog中。Hlog实现的功能就是我们前面讲到的保证数据安全。当RegionServer出现问题的时候,能跟进Hlog来做数据恢复。此外为了保证恢复的效率,Hbase会限制最大保存的Hlog数量,如果达到Hlog的最大个数(hase.regionserver.max.logs参数控制)的时候,就会触发强制刷盘操作。对于已经刷盘的数据,其对应的Hlog会有一个过期的概念,Hlog过期后,会被监控线程移动到.oldlogs,然后会被自动删除掉。
 

WAL滚动

WAL的检查间隔由hbase.regionserver.logroll.period定义,默认为一小时,检查的内容是把当前WAL中的操作跟持久化到HDFS的操作比较,看看哪些操作被持久化了,被持久化的操作会被移动到 .oldlogs文件内。
其他触发滚动的条件:
  • 当WAL文件所在的block块满了
  • 当WAL所占空间大于或者等于某个阈值
WAL文件何时从/hbase/.oldlogs文件删除?
Master会负责定期地去清理这个文件夹,条件是:没有任何引用指向这个WAL文件。目前有两种服务可能会引用WAL文件:
  • TTL进程:该进程会保证WAL文件一直存活到hbase.master.logcleaner.ttl定义的超时时间(默认为10mins)。
  • 备份机制:如果开启了hbase的备份机制,那么hbase要保证备份集群已经完全不需要这个wal文件才会删除它。

Hlog结构

从上图我们可以看出都个Region共享一个Hlog文件,单个Region在Hlog中是按照时间顺序存储的,但是多个Region可能并不是完全按照时间顺序。

每个Hlog最小单元由Hlogkey和WALEdit两部分组成。

Hlogky 由sequenceid、timestamp、cluster ids、regionname以及tablename等组成,
WALEdit 是由一系列的KeyValue组成,对一行上所有列(即所有KeyValue)的更新操作,都包含在同一个WALEdit对象中,这主要是为了实现写入一行多个列时的原子性。

注意,图中有个 sequenceid的东东。sequenceid是一个store级别的自增序列号,它非常重要,region的数据恢复和Hlog过期清除都要依赖这个东东。下面就来简单描述一下sequenceid的相关逻辑。
Memstore在达到一定的条件会触发刷盘的操作,刷盘的时候会获取刷新到最新的一个sequenceid的下一个sequenceid,并将新的sequenceid赋给oldestUnflushedSequenceId,并刷到Ffile中。
有点绕,举个例子来说明:比如对于某一个store,开始的时候oldestUnflushedSequenceId为NULL,此时,如果触发flush的操作,假设初始刷盘到sequenceid为10,那么hbase会在10的基础上append一个空的Entry到HLog,最新的sequenceid为11,然后将sequenceid为11的号赋给oldestUnflushedSequenceId,并将oldestUnflushedSequenceId的值刷到Hfile文件中进行持久化。
Hlog文件对应所有Region的store中最大的sequenceid如果已经刷盘,就认为Hlog文件已经过期,就会移动到.oldlogs,等待被移除。当RegionServer出现故障的时候,需要对Hlog进行回放来恢复数据。回放的时候会读取Hfile的oldestUnflushedSequenceId中的sequenceid和Hlog中的sequenceid进行比较,小于sequenceid的就直接忽略,但与或者等于的就进行重做。回放完成后,就完成了数据的恢复工作。

 

 HFile

HFile是数据存储的实际载体,我们创建所有表、列等数据都存在里面。
 

HFile由一个个块组成,默认一个块的大小为64KB(即图中的一个框格)。其中各个块的角色如下

  • Data:数据块,可以有多个,数据存放的地方。
  • Meta:元数据块,可选,保存用户自定义的kv段。在HFile 2.0版本之前布隆过滤器的信息存放在这里。
  • FileInfo:文件信息,必选,它只有在文件关闭时写入,存储的是这个文件的信息,比如最后一个key,平均的key长度。
  • dataIndex:存储Data块索引信息的块文件。索引的信息其实就是Data块的偏移值。
  • MetaIndex:存储Meta块索引信息的块文件。
  • Trailer:必须的,存储了FileInfo,dataIndex,MetaIndex的偏移值。

只有Trailer和FileInfo的长度是固定的,它们也是HFile中必须存在的。

读取一个HFile时,会首先读取Trailer,Trailer保存了每个段的起始位置,然后DataIndex会读取到内存中,这样,当检索Key时,不需要扫描整个HFile,只需要从内存中找到key所在的block,通过一次磁盘IO就将整个block读取到内存中

HBase的拆分问题

拆分策略

(1)ConstantSizeRegionSplitPolicy(0.94版本前的默认拆分策略)

它是按照固定大小拆分Region,它唯一的参数是:

hbase.hregion.max.filesize

默认大小为10G。

当单个Region超过了10G,就会被分成2个Region。

(2)IncreasingToUpperBoundRegionSplitPolicy(0.94之后的默认)

这种情况下,文件尺寸限制是动态的。公式如下

Math.min(tableRegionsCount^3 * initialSize, defaultRegionMaxFileSize)
tableRegionsCount: 表在所有region服务器上region的数目
initialSize: hbase.increasing.policy.initial.size,如果没有定义,就用hbase.hregion.memstore.flush.size * 2
defaultRegionMaxFileSize: hbase.hregion.max.filesize
这种切分策略很好的弥补了ConstantSizeRegionSplitPolicy的短板,能够自适应大表和小表。而且在大集群条件下对于很多大表来说表现很优秀,但并不完美,这种策略下很多小表会在大集群中产生大量小region,分散在整个集群中。而且在发生region迁移时也可能会触发region分裂。

(3)KeyPrefixRegionSplitPolicy

它是IncreasingToUpperBoundRegionSplitPolicy的子类,增加了对拆分点的定义,保证相同前缀的rowkey不会被拆分到两个不同的region里面。它只能根据固定长度的前缀判断,使用prefix_length所定义的长度来截取rowkey作为分组的依据。

(4)DelimitedKeyPrefixRegionSplitPolicy

它也是IncreasingToUpperBoundRegionSplitPolicy的子类,也是根据前缀来进行切分的,但是它是根据分隔符来判断的,因为有些rowkey的前缀不一定是定长的。

(5)SteppingSplitPolicy

2.0版本默认切分策略。这种切分策略的切分阈值又发生了变化,相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂region所属表在当前regionserver上的region个数有关系,如果region个数等于1,切分阈值为flush size * 2,否则为MaxRegionFileSize。这种切分策略对于大集群中的大表、小表会比 IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不会再产生大量的小region,而是适可而止。

Region切分准备工作-寻找SplitPoint

 region切分策略会触发region切分,切分开始之后的第一件事是寻找切分点-splitpoint。所有默认切分策略,无论是ConstantSizeRegionSplitPolicy、IncreasingToUpperBoundRegionSplitPolicy抑或是SteppingSplitPolicy,对于切分点的定义都是一致的。当然,用户手动执行切分时是可以指定切分点进行切分的,这里并不讨论这种情况。

那切分点是如何定位的呢?整个region中最大store中的最大文件中最中心的一个block的首个rowkey。这是一句比较消耗脑力的语句,需要细细品味。另外,HBase还规定,如果定位到的rowkey是整个文件的首个rowkey或者最后一个rowkey的话,就认为没有切分点。

什么情况下会出现没有切分点的场景呢?最常见的就是一个文件只有一个block,执行split的时候就会发现无法切分。很多新同学在测试split的时候往往都是新建一张新表,然后往新表中插入几条数据并执行一下flush,再执行split,奇迹般地发现数据表并没有真正执行切分。原因就在这里,这个时候仔细的话你翻看debug日志是可以看到这样的日志:

Region核心切分流程

 HBase将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare – execute – (rollback) ,操作模版如下:

  • prepare阶段:在内存中初始化两个子region,具体是生成两个HRegionInfo对象,包含tableName、regionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展,具体见rollback阶段。
  • execute阶段:切分的核心操作。见下图

1.regionserver 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。

2.master通过watch节点/region-in-transition检测到region状态改变,并修改内存中region的状态,在master页面RIT模块就可以看到region执行split的状态信息。

3.在父存储目录下新建临时文件夹.split保存split后的daughter region信息。

4.关闭parent region:parent region关闭数据写入并触发flush操作,将写入region的数据全部持久化到磁盘。此后短时间内客户端落在父region上的请求都会抛出异常NotServingRegionException。

5.核心分裂步骤:在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成reference文件,分别指向父region中对应文件。这个步骤是所有步骤中最核心的一个环节,生成reference文件日志如下所示:

2017-08-12 11:53:38,158 DEBUG [StoreOpener-0155388346c3c919d3f05d7188e885e0-1] regionserver.StoreFileInfo: reference 'hdfs://hdfscluster/hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66' to region=00bb6239169411e4d0ecb6ddfdbacf66 hfile=d24415c4fb44427b8f698143e5c4d9dc

 其中reference文件名为d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66,格式看起来比较特殊,那这种文件名具体什么含义呢?那来看看该reference文件指向的父region文件,根据日志可以看到,切分的父region是00bb6239169411e4d0ecb6ddfdbacf66,对应的切分文件是d24415c4fb44427b8f698143e5c4d9dc,可见reference文件名是个信息量很大的命名方式,如下所示:

 6. 父region分裂为两个子region后,将daughter A、daughter B拷贝到HBase根目录下,形成两个新的region。

 7. parent region通知修改 hbase.meta 表后下线,不再提供服务。下线后parent region在meta表中的信息并不会马上删除,而是标注split列、offline列为true,并记录两个子region。为什么不立马删除?且听下文分解。

 8. 开启daughter A、daughter B两个子region。通知修改 hbase.meta 表,正式对外提供服务。

 

  • rollback阶段:如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。代码中使用 JournalEntryType 来表征各个子阶段,具体见下图:

 

Region切分事务性保证

整个region切分是一个比较复杂的过程,涉及到父region中HFile文件的切分、两个子region的生成、系统meta元数据的更改等很多子步骤,因此必须保证整个切分过程的事务性,即要么切分完全成功,要么切分完全未开始,在任何情况下也不能出现切分只完成一半的情况。

为了实现事务性,hbase设计了使用状态机(见SplitTransaction类)的方式保存切分过程中的每个子步骤状态,这样一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。遗憾的是,目前实现中这些中间状态都只存储在内存中,因此一旦在切分过程中出现regionserver宕机的情况,有可能会出现切分处于中间状态的情况,也就是RIT状态。这种情况下需要使用hbck工具进行具体查看并分析解决方案。在2.0版本之后,HBase实现了新的分布式事务框架Procedure V2(HBASE-12439),新框架将会使用HLog存储这种单机事务(DDL操作、Split操作、Move操作等)的中间状态,因此可以保证即使在事务执行过程中参与者发生了宕机,依然可以使用HLog作为协调者对事务进行回滚操作或者重试提交,大大减少甚至杜绝RIT现象。这也是是2.0在可用性方面最值得期待的一个亮点!!!

 Regon切分对其他模块的影响

通过region切分流程的了解,我们知道整个region切分过程并没有涉及数据的移动,所以切分成本本身并不是很高,可以很快完成。切分后子region的文件实际没有任何用户数据,文件中存储的仅是一些元数据信息-切分点rowkey等,那通过引用文件如何查找数据呢?子region的数据实际在什么时候完成真正迁移?数据迁移完成之后父region什么时候会被删掉?

1. 通过reference文件如何查找数据?

这里就会看到reference文件名、文件内容的实际意义啦。整个流程如下图所示:

 

(1)根据reference文件名(region名+真实文件名)定位到真实数据所在文件路径

(2)定位到真实数据文件就可以在整个文件中扫描待查KV了么?非也。因为reference文件通常都只引用了数据文件的一半数据,以切分点为界,要么上半部分文件数据,要么下半部分数据。那到底哪部分数据?切分点又是哪个点?还记得上文又提到reference文件的文件内容吧,没错,就记录在文件中。

2. 父region的数据什么时候会迁移到子region目录?

答案是子region发生major_compaction时。我们知道compaction的执行实际上是将store中所有小文件一个KV一个KV从小到大读出来之后再顺序写入一个大文件,完成之后再将小文件删掉,因此compaction本身就需要读取并写入大量数据。子region执行major_compaction后会将父目录中属于该子region的所有数据读出来并写入子region目录数据文件中。可见将数据迁移放到compaction这个阶段来做,是一件顺便的事。

3. 父region什么时候会被删除?

实际上HMaster会启动一个线程定期遍历检查所有处于splitting状态的父region,确定检查父region是否可以被清理。检测线程首先会在meta表中揪出所有split列为true的region,并加载出其分裂后生成的两个子region(meta表中splitA列和splitB列),只需要检查此两个子region是否还存在引用文件,如果都不存在引用文件就可以认为该父region对应的文件可以被删除。现在再来看看上文中父目录在meta表中的信息,就大概可以理解为什么会存储这些信息了:


Region的合并

1.小合并(MinorCompaction)

我们知道当MemStore达到hbase.hregion.memstore.flush.size大小的时候会将数据刷到磁盘,生产StoreFile,因此势必产生很多的小文件,对于Hbase的读取,如果要扫描大量的小文件,会导致性能很差,因此需要将这些小文件合并成大一点的文件。

因此所谓的小合并,就是把多个小的StoreFile组合在一起,形成一个较大的StoreFile,通常是累积到3个Store File后执行。通过参数hbase.hstore,compactionThreadhold配置。

小合并的大致步骤为:

  • 分别读取出待合并的StoreFile文件的KeyValues,并顺序地写入到位于./tmp目录下的临时文件中
  • 将临时文件移动到对应的Region目录中
  • 将合并的输入文件路径和输出路径封装成KeyValues写入WAL日志,并打上compaction标记,最后强制自行sync
  • 将对应region数据目录下的合并的输入文件全部删除,合并完成

这种小合并一般速度很快,对业务的影响也比较小。本质上,小合并就是使用短时间的IO消耗以及带宽消耗换取后续查询的低延迟。

 2.大合并(MajorCompaction)

所谓的大合并,就是将一个Region下的所有StoreFile合并成一个StoreFile文件,在大合并的过程中,之前删除的行和过期的版本都会被删除,拆分的母Region的数据也会迁移到拆分后的子Region上。大合并一般一周做一次,控制参数为hbase.hregion.majorcompaction。大合并的影响一般比较大,尽量避免统一时间多个Region进行合并,因此Hbase通过一些参数来进行控制,用于防止多个Region同时进行大合并。该参数为:hbase.hregion.majorcompaction.jitter


 布隆过滤器

在讨论布隆过滤器在HBase中的应用之前,先介绍一下HBase的块索引机制。块索引是HBase固有的一个特性,因为HBase的底层数据是存储在HFile中的,而每个HFile中存储的是有序的<key, value>键值对,HFile文件内部由连续的块组成[1],每个块中存储的第一行数据的行键组成了这个文件的块索引,这些块索引信息存储在文件尾部。当HBase打开一个HFile时,块索引信息会优先加载到内存;HBase首先在内存的块索引中进行二分查找,确定可能包含给定键的块,然后读取磁盘块找到实际想要的键。

但实际应用中,仅仅只有块索引满足不了需求,这是因为,块索引能帮助我们更快地在一个文件中找到想要的数据,但是我们可能依然需要扫描很多文件。而布隆过滤器就是为解决这个问题而生。因为布隆过滤器的作用是,用户可以立即判断一个文件是否包含特定的行键,从而帮我们过滤掉一些不需要扫描的文件。

布隆过滤器原理

布隆过滤器需要的是一个位数组和k个映射函数(和Hash表类似),在初始状态时,对于长度为m的位数组array,它的所有位都被置为0,如下图:

对于有n个元素的集合S={s1,s2......sn},通过k个映射函数{f1,f2,......fk},将集合S中的每个元素sj(1<=j<=n)映射为k个值{g1,g2......gk},然后再将位数组array中相对应的array[g1],array[g2]......array[gk]置为1:

如果要查找某个元素item是否在S中,则通过映射函数{f1,f2.....fk}得到k个值{g1,g2.....gk},然后再判断array[g1],array[g2]......array[gk]是否都为1,若全为1,则item在S中,否则item不在S中。这个就是布隆过滤器的实现原理。

即使array[g1],array[g2]......array[gk]都为1,能代表item一定在集合S中吗?不一定,因为有这个可能:就是集合中的若干个元素通过映射之后得到的数值恰巧包括g1,g2,.....gk,那么这种情况下可能会造成误判,但是这个概率很小,一般在万分之一以下。所有,布隆过滤器的误判率和这K个映射函数的设计有关。

HBase中的布隆过滤器

布隆过滤器允许你对存储在每个数据块的数据做一个反向测试。当某行被请求时,先检查布隆过滤器看看该行是否不在这个数据块。布隆过滤器要么确定回答该行不在,要么回答它不知道。这就是为什么我们称它是反向测试。布隆过滤器也可以应用到行里的单元上。当访问某列标识符时先使用同样的反向测试。   

 布隆过滤器也不是没有代价。存储这个额外的索引层次占用额外的空间。布隆过滤器随着它们的索引对象数据增长而增长,所以行级布隆过滤器比列标识符级布隆过滤器占用空间要少。当空间不是问题时,它们可以帮助你榨干系统的性能潜力。  
你可以在列族上打开布隆过滤器,如下所示:  

hbase(main):007:0> create 'mytable', {NAME => 'colfam1', BLOOMFILTER => 'ROWCOL'} 

BLOOMFILTER参数的默认值是NONE。一个行级布隆过滤器用ROW打开,列标识符级布隆过滤器用ROWCOL打开。行级布隆过滤器在数据块里检查特定行键是否不存在,列标识符级布隆过滤器检查行和列标识符联合体是否不存在。ROWCOL布隆过滤器的开销高于ROW布隆过滤器。 

Bloomfilter在HBase中的作用

 HBase利用Bloomfilter来提高随机读(Get)的性能,对于顺序读(Scan)而言,设置Bloomfilter是没有作用的(0.92以后,如果设置了bloomfilter为ROWCOL,对于指定了qualifier的Scan有一定的优化,但不是那种直接过滤文件,排除在查找范围的形式)

Bloomfilter在HBase中的开销

 Bloomfilter是一个列族(cf)级别的配置属性,如果你在表中设置了Bloomfilter,那么HBase会在生成StoreFile时包含一份bloomfilter结构的数据,称其为MetaBlock;MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以,开启bloomfilter会有一定的存储及内存cache开销。 

Bloomfilter如何提高随机读(Get)的性能

对于某个region的随机读,HBase会遍历读memstore及storefile(按照一定的顺序),将结果合并返回给客户端。如果你设置了bloomfilter,那么在遍历读storefile时,就可以利用bloomfilter,忽略某些storefile。 

HBase中的Bloomfilter的类型及使用? 

a) ROW, 根据KeyValue中的row来过滤storefile
举例:假设有2个storefile文件sf1和sf2, 
sf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v) 
sf2包含kv3(r3 cf:q1 v)、kv4(r4 cf:q1 v) 
如果设置了CF属性中的bloomfilter为ROW,那么get(r1)时就会过滤sf2,get(r3)就会过滤sf1 
b) ROWCOL,根据KeyValue中的row+qualifier来过滤storefile 
举例:假设有2个storefile文件sf1和sf2, 
sf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v) 
sf2包含kv3(r1 cf:q2 v)、kv4(r2 cf:q2 v) 
如果设置了CF属性中的bloomfilter为ROW,无论get(r1,q1)还是get(r1,q2),都会读取sf1+sf2;而如果设置了CF属性中的bloomfilter为ROWCOL,那么get(r1,q1)就会过滤sf2,get(r1,q2)就会过滤sf1 

结论如下: 
1.任何类型的get(基于rowkey和基于row+col)bloomfilter都能生效,关键是get的类型要匹配bloomfilter的类型 
2.基于row的scan是没办法优化的 
scan是一个范围,如果是row的bloomfilter不命中只能说明该rowkey不在此storefile中,但next rowkey可能在。 
3.row+col+qualify的scan可以去掉不存在此qualify的storefile,也算是不错的优化了,而且指明qualify也能减少流量,因此scan尽量指明qualify。 
而rowcol的bloomfilter就不一样了,如果rowcol的bloomfilter没有命中表明该qualifiy不在这个storefile中,因此这次scan就不需要scan此storefile了


提高HBase表读写效率的方法

1.建表技巧

(1) 合理设计列族

 一张HBase表的列族数量最好控制在三个以内,因为当一个列族的MemStore中的数据量达到阈值时,会引起同一个region的所有StoreFile的MemStore进行flush操作,即使其中某些列族MemStore中的数据量还很小。因此,如果有很多列族的话,会产生许多小文件,可能会引起很多不必要的flush和compact操作,导致不必要的I/O负载。因此,在设计表结构时,尽可能使用一个列族,除非每次查询的时候,查询粒度是列,而不是行。

如果有一个数据量小的列族经常要做全表扫描,那么这个列族最好不要和数据量大的列族放在同一张表。因为不同列族的分区数是一致的,如果有一个列族的数据量很大,导致表被分割成了多个分区,那么数据量很小列族的数据会分布在很多分区,导致做该列族做全表扫描的时候,效率低下。如果大部分是随机取的情况,数据量小的列和数据量大的列尽量不要放在同一列族中,尤其在我们经常做列查找,而不是行查找的情况下。

举个例子,列A和列B属于同一列族,其中列B的数据量远大于列A,当客户端查找列A中某一行的所有记录,迫使HBase扫描所有底层文件以找到所有属于该行的记录,因为同一列族中的数据会被写入同一个底层文件,HBase在扫描列A的同时,实际上也扫描了列B的所有数据,做了大量无用功。

(2) 使用块缓存

HBase的所有存储文件都被划分成了若干个小存储块(HFile的结构),存储块的默认大小是64KB。当HBase顺序地读取一个数据块到内存缓存中时,其读取相邻的数据时就可以在内存中读取,而不需要再次读取磁盘,可以有效减少磁盘I/O的次数,提高了I/O效率。这个参数默认为true,意味着每次读取的块都会缓存到内存中。

(3) in-memory

除了利用缓存块来提高连续访问的效率以外,还有一个在内存中(in-memory)标志,默认值为false。当这个参数设置true时,并不意味着整个列族的所有存储块都会被加载到内存中,也不意味这内存中的数据会被长期保留,而代表一种高优先级的承诺。在正常的数据读取过程中,块数据会被加载到缓存区并长期驻留在内存中,除非堆压力过大,才会从内存中强制卸载这部分数据。需要注意的是,这个参数通常适合数据量较小的列族,例如保存登陆账号和密码的用户表,将这个参数设置为true有利于提升这个环节的处理速度。

(4) 布隆过滤器

布隆过滤器属于HBase系统中的高级功能,能够减少特定访问模式下的查询时间。但是,布隆过滤器加重了内存和存储的负担,因此默认情况下是关闭的状态。

(5) 生存期TTL

HBase不仅可以设置每个值能保存的最大版本数,也支持处理版本数据保存时间。在major合并过程中,时间戳被判定为超过TTL的数据会被删除。

(6) 使用压缩

压缩的作用是减小存储文件的大小,一方面可以节省存储空间,更重要的是,因为文件变小了,文件读入内存的速度也提高了。当然,压缩和解压也会耗费一定的时间(也会导致compact操作的时间变长),因此需要小心衡量性能瓶颈是在IO还是在CPU。不同的算法压缩率也有所不同,压缩率越高的算法,需要耗费的时间也越长,因此应该根据实际情况,选择合适的压缩算法。

在这里补充一点,对一个已经存在的表,可以通过alter table的方式,开启压缩或者更改压缩算法。然而,这个操作并不会立刻起作用,因为之前产生的存储文件依然保持更改之前的状态。若想强制重写这些文件,可以通过major_compact强制进行major合并,新产生的文件自然是我们想要的格式。

(7) 预拆分Region

HBase可以自动管理region拆分,当region中的数据量达到一定阈值,HBase会将其拆分成两个region继续工作。然而,设想当用户所有的region以相同的速率增长,最后它们会在同一时间发生region拆分,拆分过程中需要重写底层文件(compaction),引起磁盘I/O上升,影响集群响应速度,这就是俗称的“split/compaction storms”(拆分/合并风暴)。

为了避免这个问题,解决方案之一是,关闭拆分功能(将拆分阈值设成无限大),然后由用户手动调用命令进行region拆分。这样做的好处是,用户可以控制拆分的时间,拆分可以在集群或者表空闲的时间进行,并且如果不同的region在不同时间拆分,能够分散I/O负载。第二种方法是,在创建表的时候,就预先将表按照给定的行键,划分成多个分区。这样做的好处是,当用户能预先估计到表会增长到很大规模时,预先拆分成多个分区,可以有效避免拆分合并风暴;另一方面,多个分区会交给多个regionServer管理,通过在行键前面添加分区号,可以将读写压力均匀地分配到多个server,可以解决读写热点问题。

2.查询优化

(1) 多并发读写提升吞吐量

HBase的优势不在于对单条请求的响应速度,而在于整个集群的吞吐量高。因此,倘若想提高客户端读写速度,最直接的一个方法就是多并发读写。

(2) 批量处理请求

和其它数据库类似,HBase提供了批量处理操作的API,批量处理请求可以利用好RPC时间,提高单个客户端的处理效率。

(3) 全表扫描时关闭块缓存功能

HBase提供了读缓存,通过scanner.setCacheBlocks()方法控制。当读取一条记录时,会将对应的块读到读内存中。对于某些频繁访问的行,这个功能可以提高读取速度。然而,当用户需要做全表扫描时,应记得关闭这个功能,因为全表扫描,每条记录只读取以便,如果使用块缓存,会降低扫描效率。

(4) 扫描时使用扫描缓存

HBase的扫描器在获取数据时,会为每行数据生成一个单独的RPC请求,即使用户显式指定了要获取n行的数据,扫描器也会向服务器发送n个RPC请求。为了一次RPC请求可以获取多行数据,用户必须显式开启扫描器的扫描缓存功能。

(5) 严格限制查找范围

使用行键查找对应的值无疑是效率最高的查找方式。不过,如果在查找请求中添加一些额外的条件,也有助于提高查找效率:

限定列族可以避免扫描其它列族的存储文件;限定时间戳在4小时以内,可以跳过一些最后修改时间在4小时之前的文件;限定列名可以控制返回给客户端的数据量,降低传输时间和网络流量。

// 指定开始行和列
scanner.setStartRow(startRow); scanner.setEndRow(endRow);
// 指定列族和列 scanner.addColumn(cf,q)

(6)关闭ResultScanner

如果ResultScanner不关闭,可能会出现服务端在一段时间内一直保存连接,资源无法释放,从而导致服务器端某些资源的不可用,所以在使用完该类后,需要执行关闭操作。

(7)优化行键查询

对表进行全表scan时,如果仅需要行键,不需要列族、列名、值等信息,可以使用过滤器减少服务器端返回的数据量。使用方法是:使用Scan的setFilter()方法,添加MUST_PASS_ALL操作参数,使用FilterList封装两个过滤器。一个是FirstKeyOnlyFilter,另一个是KeyOnlyFilter。

(8)使用Coprocessor统计行数

对于表的行数统计,主要有两种常用方法:

  • MapReduce。借助HTableInputFormat实现对于RowKey的划分,并行访问表数据,但是占用的资源很多,对于生产环境的影响比较大。同时这种方法延时比较高,不适合应用频繁访问。
  • Scan+FirstKeyOnlyFilter。在RegionServer端进行统计,可以减轻client端压力,但是表较大时,遍历一个Region的时间也非常长,延迟高。

基于上述两种方法,可以使用协处理器实现行数实时统计,使用org.apache.hadoop.hbase.coprocessor.AggregateImplementation。

3. 其他

1. 定期触发major_compact

太频繁地major_compact会给集群带来I/O压力,不过当表中有太多的已删除数据影响了查找速度的话,执行major_compact彻底删除这些数据无疑是最佳选择。目前,我们在使用的是hbase-1.0.0-cdh5.4.5版本,默认的major_compact周期是7天,用户也可以通过命令行或者客户端API手动触发compact操作。

2. 尽量使用简短的列名

HBase的数据存储是key-value的形式,也就是<行键-列族-列-时间戳,值>。换句话说,为每一个单元格,HBase都存储了列名,因此使用较简短的列名也是一个好习惯。

*注:[ 1 ] * 注意,HFile的块和HDFS的块没有直接关系。HDFS的块用于拆分大文件以提供分布式存储,另外便于MR框架进行并行计算;而HBase的块主要用于高效加载和缓存数据,并不依赖于HDFS的块大小,并且只用于HBase内部。

 

4.写入优化

(1)关闭写WAL日志

如果应用可以容忍一定的数据丢失风险,可以尝试在更新数据时,关闭写WAL,该方法的风险是,当RegionServer宕机时,可能写入的数据会出现丢失的情况,且无法恢复。关闭WAL的方法:
Put put = new Put(Bytes.toBytes("rowkey1"));
put.add(Bytes.toBytes("cf1"), Bytes.toBytes("q1"), Bytes.toBytes("val1"));

put.setWriteToWAL(false);

(2)设置autoFlush

HTable有一个属性是AutoFlush,该属性用于支持客户端的批量更新,默认值是true,即客户端每收到一条数据,立刻发送给服务端。如果该属性设置为false,客户端提交put时,将请求在客户端缓存,知道数据达到某个阈值的容量时(该参数有由hbase.client.write.buffer决定),才向RegionServer提交请求。

(3)预分区

(4)延迟日志flush

 默认情况下,写入操作开启WAL日志,并会在很短时间内写入HDFS,默认时间是1秒,可以通过hbase.regionserver.optionallogflushinterval配置,如果增大这个值,WAL日志会留在内存中,知道RegionServer维护的该周期性flush操作执行,这样做的优势时能减少WAL日志到HDFS同步的次数,提升写入效率,缺点是一旦RegionServer发生宕机,会造成更多数据丢失。

(5)使用批量写入。

 
 
 
 
 
 
 
 

猜你喜欢

转载自www.cnblogs.com/yn-huang/p/10853946.html