Hbase 知识要点归总

一、HBase Get 流程

1.1、客户端流程解析

  1. 客户端首先会根据配置文件中zookeeper地址连接zookeeper,并读取//meta-region-server节点信息,该节点信息存储HBase元数据(hbase:meta)表所在的RegionServer地址以及访问端口等信息。用户可以通过zookeeper命令(get //meta-region-server)查看该节点信息。
  2. 根据hbase:meta所在RegionServer的访问信息,客户端会将该元数据表加载到本地并进行缓存。然后在表中确定待检索rowkey所在的RegionServer信息。
  3. 根据数据所在RegionServer的访问信息,客户端会向该RegionServer发送真正的数据读取请求。
  4. 如果集群发生某些变化导致hbase:meta元数据更改,客户端再根据本地元数据表请求的时候就会发生异常,此时客户端需要重新加载一份最新的元数据表到本地。

1.2、服务器端流程解析

RegionServer接收到客户端的get/scan请求之后,先后做了两件事情:构建scanner体系(排序放入堆中),在此体系基础上一行一行检索。

scanner体系的核心在于三层scanner:RegionScanner、StoreScanner以及StoreFileScanner。三者是层级的关系,一个RegionScanner由多个StoreScanner构成,一张表由多个列族组成,就有多少个StoreScanner负责该列族的数据扫描。一个StoreScanner又是由多个StoreFileScanner组成。每个Store的数据由内存中的MemStore和磁盘上的StoreFile文件组成,相对应的,StoreScanner对象会雇佣一个MemStoreScanner和N个StoreFileScanner来进行实际的数据读取,每个StoreFile文件对应一个StoreFileScanner,注意:StoreFileScanner和MemstoreScanner是整个scan的最终执行者。

  1. RegionScanner 会根据列族构建StoreScanner,有多少列族就构建多少StoreScanner,用于负责该列族的数据检索
    • 构建StoreFileScanner:每个StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner,用于实际执行对应文件的检索。同时会为对应Memstore构造一个MemstoreScanner,用于执行该Store中Memstore的数据检索。
    • 过滤淘汰StoreFileScanner:根据Time Range以及RowKey Range对StoreFileScanner以及MemstoreScanner进行过滤,淘汰肯定不存在待检索结果的Scanner。
    • Seek rowkey:所有StoreFileScanner开始做准备工作,在负责的HFile中定位到满足条件的起始Row。Seek过程(此处略过Lazy Seek优化)是一个很核心的步骤,它主要包含下面三步:
      • 定位Block Offset:在Blockcache中读取该HFile的索引树结构,根据索引树检索对应RowKey所在的Block Offset和Block Size
      • Load Block:根据BlockOffset首先在BlockCache中查找Data Block,如果不在缓存,再在HFile中加载
      • Seek Key:在Data Block内部通过二分查找的方式定位具体的RowKey
    • StoreFileScanner合并构建最小堆:将该Store中所有StoreFileScanner和MemstoreScanner合并形成一个heap(最小堆),所谓heap是一个优先级队列,队列中元素是所有scanner,排序规则按照scanner seek到的keyvalue大小由小到大进行排序。
  2. 构建Scanner体系是为了更好地执行scan查询。scan查询总是一行一行查询的,先查第一行的所有数据,再查第二行的所有数据,但每一行的查询流程却没有什么本质区别。

1.3、不同 KeyValue 之间如何进行大小比较?

KeyValue中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等5部分组成,HBase设定Key大小:

  • 首先比较RowKey,RowKey越小Key就越小;
  • RowKey如果相同就看CF,CF越小Key越小;
  • CF如果相同看Qualifier,Qualifier越小Key越小;
  • Qualifier如果相同再看Timestamp,Timestamp越大表示时间越新,对应的Key越小;
  • 如果Timestamp还相同,就看KeyType,KeyType按照DeleteFamily -> DeleteColumn -> Delete -> Put 顺序依次对应的Key越来越大。

1.4、为什么 Scanner 需要由小到大排序?

最直接的解释是scan的结果需要由小到大输出给用户,当然,这并不全面,最合理的解释是只有由小到大排序才能使得scan效率最高。举个简单的例子,HBase支持数据多版本,假设用户只想获取最新版本,那只需要将这些数据由最新到最旧进行排序,然后取队首元素返回就可以。那么,如果不排序,就只能遍历所有元素,查看符不符合用户查询条件。这就是排队的意义。

1.5、Get 实例说明

下图是一张表的逻辑视图,该表有两个列族cf1和cf2(我们只关注cf1),cf1只有一个列name,表中有5行数据,其中每个cell基本都有多个版本。cf1的数据假如实际存储在三个区域,memstore中有r2和r4的最新数据,hfile1中是最早的数据。现在需要查询RowKey=r2的数据,按照上文的理论对应的Scanner指向就如图所示:

这三个Scanner组成的heap为,Scanner由小到大排列。查询的时候首先pop出heap的堆顶元素,即MemstoreScanner,得到keyvalue = r2:cf1:name:v3:name23的数据,拿到这个keyvalue之后,需要进行如下判定:

  1. 检查该KeyValue的KeyType是否是Deleted/DeletedCol等,如果是就直接忽略该列所有其他版本,跳到下列(列族)
  2. 检查该KeyValue的Timestamp是否在用户设定的Timestamp Range范围,如果不在该范围,忽略
  3. 检查该KeyValue是否满足用户设置的各种filter过滤器,如果不满足,忽略
  4. 检查该KeyValue是否满足用户查询中设定的版本数,比如用户只查询最新版本,则忽略该cell的其他版本;反正如果用户查询所有版本,则还需要查询该cell的其他版本。

1.6、读数据 与 Bloom Filter

基于HBase的架构,一条数据可能存在RegionServer中的三个不同位置:

  • 对于刚读取过的数据,将会被缓存到BlockCache
  • 对于刚写入的数据,其存在Memstore
  • 对于之前已经从Memstore刷写到磁盘的,其存在于HFiles

RegionServer接收到的一条数据查询请求,只需要从以上三个地方检索到数据即可,在HBase中的检索顺序依次是:BlockCache -> Memstore -> HFiles

其中,BlockCache、Memstore都是直接在内存中进行高性能的数据检索

而HFiles则是真正存储在HDFS上的数据:

  • 检索HFiles时会产生真实磁盘的IO操作
  • Memstore不停刷写的过程中,将会产生大量的HFile

如何在大量的HFile中快速找到所需要的数据呢?

为了提高检索HFiles的性能,HBase支持使用 Bloom Fliter 对HFiles进行快读定位。

Bloom Filter(布隆过滤器)是一种数据结构,常用于大规模数据查询场景,其能够快速判断一个元素一定不在集合中,或者可能在集合中。

HBase中支持使用以下两种Bloom Filter:

  • ROW:基于 Rowkey 创建的Bloom Filter
  • ROWCOL:基于 Rowkey+Column 创建的Bloom Filter

两者的区别仅仅是:是否使用列信息作为Bloom Filter的条件

使用ROWCOL时,可以让指定列的查询更快,因为其通过Rowkey与列信息来过滤不存在数据的HFile,但是相应的,产生的Bloom Filter数据会更加庞大

而只通过Rowkey进行检索的查询,即使指定了ROWCOL也不会有其他效果,因为没有携带列信息。

通过Bloom Filter(如果有的话)快速定位到当前的Rowkey数据存储于哪个HFile之后(或者不存在直接返回),通过HFile携带的 Data Block Index 等元数据信息可快速定位到具体的数据块起始位置,读取并返回(加载到缓存中)。

这就是Bloom Filter在HBase检索数据的应用场景:

  1. 高效判断key是否存在
  2. 高效定位key所在的HFile

当然,如果没有指定创建Bloom Filter,RegionServer将会花费比较多的力气一个个检索HFile来判断数据是否存在。

二、HBase Put 流程

2.1、客户端流程解析

  1. 用户提交put请求后,HBase客户端会将put请求添加到本地buffer中,符合一定条件就会通过AsyncProcess异步批量提交。HBase默认设置autoflush=true,表示put请求直接会提交给服务器进行处理;用户可以设置autoflush=false,这样的话put请求会首先放到本地buffer,等到本地buffer大小超过一定阈值(默认为2M,可以通过配置文件配置)之后才会提交。很显然,后者采用group commit机制提交请求,可以极大地提升写入性能,但是因为没有保护机制,如果客户端崩溃的话会导致提交的请求丢失。
  2. 在提交之前,HBase会在元数据表.meta.中根据rowkey找到它们归属的region server,这个定位的过程是通过HConnection 的locateRegion方法获得的。如果是批量请求的话还会把这些rowkey按照HRegionLocation分组,每个分组可以对应一次RPC请求。
  3. HBase 会为每个HRegionLocation 构造一个远程RPC 请求MultiServerCallable , 然后通过rpcCallerFactory. newCaller()执行调用,忽略掉失败重新提交和错误处理,客户端的提交操作到此结束。

2.2、服务器端流程解析

服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为Put对象,然后执行各种检查操作,比如检查region是否是只读、memstore大小是否超过blockingMemstoreSize等。检查完成之后,就会执行如下核心操作:

  1. 获取行锁、Region更新共享锁: HBase中使用行锁保证对同一行数据的更新都是互斥操作,用以保证更新的原子性,要么更新成功,要么失败。
  2. 开始写事务:获取 write number,用于实现 MVCC,实现数据的非锁定读,在保证读写一致性的前提下提高读取性能。
  3. 写缓存 memstore:HBase中每个列族都会对应一个store,用来存储该列族数据。每个store都会有个写缓存memstore,用于缓存写入数据。HBase并不会直接将数据落盘,而是先写入缓存,等缓存满足一定大小之后再一起落盘。
  4. Append HLog:HBase使用WAL机制保证数据可靠性,即首先写日志再写缓存,即使发生宕机,也可以通过恢复HLog还原出原始数据。该步骤就是将数据构造为WALEdit对象,然后顺序写入HLog中,此时不需要执行sync操作。
  5. 释放行锁以及共享锁
  6. Sync HLog真正sync到HDFS,在释放行锁之后执行sync操作是为了尽量减少持锁时间,提升写性能。如果Sync失败,执行回滚操作将memstore中已经写入的数据移除。
  7. 结束写事务:此时该线程的更新操作才会对其他读请求可见,更新才实际生效。
  8. flush memstore:当写缓存满128M之后,会启动flush线程将数据刷新到硬盘。

三、HBase 架构设计

3.1、Hbase的rowKey的设计原则

RowKey 的唯一原则
由于在 HBase 中数据存储是 Key-Value 形式,若 HBase 中同一表插入相同Rowkey,则原先的数据会被覆盖掉(如果表的 version 设置为1的话),所以务必保证 Rowkey 的唯一性。

RowKey 的排序原则
HBase 的 RowKey 是按照 ASCII 有序设计的,在设计 RowKey 时要充分利用这点。比如视频网站上的弹幕信息,这个弹幕是按照时间倒排序展示视频里,这个时候设计的 Rowkey 要和时间顺序相关,可以使用 “Long.MAX_VALUE - 弹幕发表时间” 的 long 值作为 Rowkey 的前缀。

RowKey 的长度原则
RowKey 是一个二进制,RowKey 的长度建议设计在 10~100 个字节,越短越好。

原因有两点:

  • HBase 的持久化文件 HFile 是按照 KeyValue 存储的,如果 RowKey 过长,比如 500 个字节,1000 万列数据光 RowKey 就要占用500*1000 万 = 50 亿个字节,将近 1G 数据,这会极大影响HFile的存储效率;
  • MemStore 缓存部分数据到内存,如果 RowKey 字段过长内存的有效利用率会降低,系统无法缓存更多的数据,这会降低检索效率。
  • 其实不仅 RowKey 的长度是越短越好,列族名、列名等也应该尽量使用短名字,因为 HBase 属于列式数据库,这些名字都是会写入到 HBase 的持久化文件 HFile 中去,过长的 RowKey、列族、列名都会导致整体的存储量成倍增加。

RowKey 散列原则
设计的 RowKey 应均匀的分布在各个 HBase节点上。

拿时间戳举例,假如 RowKey 是按系统时间戳的方式递增,时间戳信息作为RowKey 的第一部分,将造成所有新数据都在一个 RegionServer 上堆积,也就是通常说的 Region 热点问题。

热点发生在大量的 client 直接访问集中在个别 RegionServer 上(访问可能是读,写或者其他操作),导致单个 RegionServer 机器自身负载过高,引起性能下降甚至 Region 不可用,常见的是发生 jvm full gc 或者显示 region too busy 异常情况,当然这也会影响同一个 RegionServer 上的其他 Region。

3.2、Zookeeper 在 Hbase 中作用

  • 保证任何时候集群中只有一个活跃的Master,实现HMaster的故障恢复与自动切换
  • 存储所有的Region的寻址入口,知道哪个Region在哪台机器上,为Client提供元数据表的存储信息。
  • 实时监控RegionServer的状态,将RegionServer的上下线的信息汇报给HMaster,RegionServer不直接向HMaster汇报信息,减轻HMaster的压力,而是通过向ZK发送信息。
  • 存储 HBase 的元数据结构(schema),知道集群中有哪些Table,每个Table有哪些Column Family。

HMaster、RegionServer 启动之后将会在 Zookeeper 上注册并创建节点(/hbasae/master 与 /hbase/rs/*),同时 Zookeeper 通过 Heartbeat 的心跳机制来维护与监控节点状态,一旦节点丢失心跳,则认为该节点宕机或者下线,将清除该节点在 Zookeeper 中的注册信息。

当 Zookeeper 中任一 RegionServer 节点状态发生变化时,HMaster 都会收到通知,并作出相应处理,例如 RegionServer 宕机,HMaster 重新分配 Regions 至其他 RegionServer 以保证集群整体可用性。

当 HMaster 宕机时(Zookeeper监测到心跳超时),Zookeeper 中的 /hbasae/master 节点将会消失,同时 Zookeeper 通知其他备用 HMaster 节点,重新创建 /hbasae/master 并转化为 active master。

3.3、hbase:meta 表

hbase:meta 表存储了集群中所有Region的位置信息

  • Rowkey格式:tableName,regionStartKey,regionId
  • 第一个region的regionStartKey为空
  • 示例:ns1:testTable,xxxxreigonid
  • 只有一个列族info,包含三个列:
    • regioninfo:RegionInfo的proto序列化格式,包含regionId,tableName,startKey,endKey,offline,split,replicaId等信息
    • server:RegionServer对应的server:port
    • serverstartcode:RegionServer的启动时间戳

3.4、HBase WAL 持久化等级

HBase中可以通过设置WAL的持久化等级决定是否开启WAL机制、以及HLog的落盘方式。WAL的持久化等级分为如下四个等级:

  • SKIP_WAL:只写缓存,不写HLog日志。这种方式因为只写内存,因此可以极大的提升写入性能,但是数据有丢失的风险。在实际应用过程中并不建议设置此等级,除非确认不要求数据的可靠性。
  • ASYNC_WAL:异步将数据写入HLog日志中。
  • SYNC_WAL:同步将数据写入日志文件中,需要注意的是数据只是被写入文件系统中,并没有真正落盘。
  • FSYNC_WAL:同步将数据写入日志文件并强制落盘。最严格的日志写入等级,可以保证数据不会丢失,但是性能相对比较差。
  • USER_DEFAULT:默认如果用户没有指定持久化等级,HBase使用SYNC_WAL等级持久化数据。

用户可以通过客户端设置WAL持久化等级,代码:put.setDurability(Durability. SYNC_WAL );

3.5、HMaster

HBase整体架构中HMaster的功能与职责如下:

  • 管理RegionServer,监听其状态,保证集群负载均衡且高可用。
  • 管理Region,如新Region的分配、RegionServer宕机时该节点Region的分配与迁移。
  • 接收客户端的DDL操作,如创建与删除表、列簇等信息。
  • 权限控制。

3.6、RegionServer

RegionServer在HBase集群中的功能与职责:

  • 根据HMaster的region分配请求,存放和管理Region
  • 接受客户端的读写请求,检索与写入数据,产生大量IO

一个RegionServer中存储并管理者多个Region,是HBase集群中真正 存储数据、接受读写请求 的地方,是HBase架构中最核心、同时也是最复杂的部分。

BlockCache为RegionServer中的 读缓存,一个RegionServer共用一个BlockCache。

3.7、HFile 存储格式

HFile是HDFS上的文件(或大或小都有可能),现在HBase面临的一个问题就是如何在HFile中 快速检索获得指定数据

HBase随机查询的高性能很大程度上取决于底层HFile的存储格式,所以这个问题可以转化为 HFile的存储格式该如何设计,才能满足HBase 快速检索 的需求。

3.7.1、生成一个HFile

Memstore内存中的数据在刷写到磁盘时,将会进行以下操作:

  • 会先现在内存中创建 空的Data Block数据块 包含 预留的Header空间。而后,将Memstore中的KVs一个个顺序写满该Block(一般默认大小为64KB)。
  • 如果指定了压缩或者加密算法,Block数据写满之后将会对整个数据区做相应的压缩或者加密处理。
  • 随后在预留的Header区写入该Block的元数据信息,如 压缩前后大小、上一个block的offset、checksum 等。
  • 内存中的准备工作完成之后,通过HFile Writer输出流将数据写入到HDFS中,形成磁盘中的Data Block。
  • 为输出的Data Block生成一条索引数据,包括 {startkey、offset、size} 信息,该索引数据会被暂时记录在内存中的Block Index Chunk中。

至此,已经完成了第一个Data Block的写入工作,Memstore中的 KVs 数据将会按照这个过程不断进行 写入内存中的Data Block -> 输出到HDFS -> 生成索引数据保存到内存中的Block Index Chunk 流程。

如果启用了Bloom Filter,那么 Bloom Filter Data(位图数据)Bloom元数据(哈希函数与个数等) 将会和 KVs 数据一样被处理:写入内存中的Block -> 输出到HDFS Bloom Data Block -> 生成索引数据保存到相对应的内存区域中

由此可以知道,HFile写入过程中,Data Block 和 Bloom Data Block 是交叉存在的

随着输出的Data Block越来越多,内存中的索引数据Block Index Chunk也会越来越大。

达到一定大小之后(默认128KB)将会经过类似Data Block的输出流程写入到HDFS中,形成 Leaf Index Block (和Data Block一样,Leaf Index Block也有对应的Header区保留该Block的元数据信息)。

同样的,也会生成一条该 Leaf Index Block 对应的索引记录,保存在内存中的 Root Block Index Chunk

Root Index -> Leaf Data Block -> Data Block 的索引关系类似 B+树 的结构。得益于多层索引,HBase可以在不读取整个文件的情况下查找数据。

随着内存中最后一个 Data Block、Leaf Index Block 写入到HDFS,形成 HFile 的 Scanned Block Section

Root Block Index Chunk 也会从内存中写入HDFS,形成 HFile 的 Load-On-Open Section 的一部分。

至此,一个完整的HFile已经生成,如下图所示:

3.7.2、检索HFile

生成HFile之后该如何使用呢?

HFile的索引数据(包括 Bloom Filter索引和数据索引信息)会在 Region Open 的时候被加载到读缓存中,之后数据检索经过以下过程:

  • 所有的读请求,如果读缓存和Memstore中不存在,那么将会检索HFile索引
  • 通过Bloom Filter索引(如果有设置Bloom Filter的话)检索Bloom Data以 快速定位HFile是否存在 所需数据
  • 定位到数据可能存在的HFile之后,读取该HFile的 三层索引数据,检索数据是否存在
  • 存在则根据索引中的 元数据 找到具体的 Data Block 读入内存,取出所需的KV数据

同时,得益于 分层索引分块存储,在Region Open加载索引数据的时候,再也不必和老版本(0.9甚至更早,HFile只有一层数据索引并且统一存储)一样加载所有索引数据到内存中导致启动缓慢甚至卡机等问题。

四、HBase Split

4.1、为什么需要 Split?

数据分布不均匀。同一 region server 上数据文件越来越大,读请求也会越来越多。一旦所有的请求都落在同一个 region server 上,尤其是很多热点数据,必然会导致很严重的性能问题。

compaction性能损耗严重。compaction本质上是一个排序合并的操作,合并操作需要占用大量内存,因此文件越大,占用内存越多。另一方面,compaction有可能需要迁移远程数据到本地进行处理(balance之后的compaction就会存在这样的场景),如果需要迁移的数据是大文件的话,带宽资源就会损耗严重。

太大文件读取效率也会受到影响。

通过切分,一个region变为两个近似相同大小的子region,再通过balance机制均衡到不同 region server上,才能使系统资源使用更加均衡。

4.2、Region Split 触发条件

一是用户通过 Api 请求 split,HBase Admin 手动执行 split 命令时,会触发 Split;

二是非用户请求:

  1. HBase中写请求会先写入memstore,region server 会为每个 region 分配一个默认大小为 128M(可通过hbase.hregion.memstore.flush.size参数配置) 的 memstore,当memstore写满之后,会启动flush刷新到磁盘。flush完成之后会判断是否需要进行切分操作。
  2. 系统在很多场景下执行压缩操作(compaction操作),将多个小文件合并成一个大文件,执行完成之后也会判断是否需要进行切分操作。

这两类操作之后都会针对相应region生成一个requestSplit请求,requestSplit首先会执行checkSplit,检测store size是否达到阈值(具体算法见下面分析),如果超过阈值,就进行切分。

4.3、Region 切分策略

目前已经的支持触发策略多达6种,每种触发策略都有各自的适用场景,可以根据业务在表级别(Column family 级别)选择不同的切分触发策略。一般情况下使用默认切分策略即可。

  • ConstantSizeRegionSplitPolicy:0.94版本前默认切分策略。
    一个 Region 中最大 Store 的大小大于设置阈值之后才会触发切分,Store 大小为压缩后的文件大小(启用压缩的场景)。切分策略对于大表和小表没有明显的区分。
  • IncreasingToUpperBoundRegionSplitPolicy:0.94版本~2.0版本默认切分策略。
    和 ConstantSizeRegionSplitPolicy 思路相同,一个 Region 中最大 Store 大小大于设置阈值就会触发切分,区别是这个阈值并不像 ConstantSizeRegionSplitPolicy 是一个固定的值,而是会在不断调整。
    调整规则和 Region 所属表在当前 RegionServer 上的 Region 个数有关系 :(#regions) * (#regions) * (#regions) * flush_size * 2,最大值为用户设置的 MaxRegionFileSize。
    能够自适应大表和小表,这种策略下很多小表会在大集群中产生大量小 Region,分散在整个集群中
  • SteppingSplitPolicy:2.0版本默认切分策略。
    相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂 Region 所属表在当前 RegionServer 上的 Region 个数有关系:如果 Region 个数等于1,切分阈值为 flush_size * 2,否则为 MaxRegionFileSize。
  • KeyPrefixRegionSplitPolicy:切分策略依然依据默认切分策略,根据 Rowkey 指定长度的前缀来切分 Region,保证相同的前缀的行保存在同一个 Region 中。由 KeyPrefixRegionSplitPolicy.prefix_length 属性指定 Rowkey 前缀长度。按此长度对splitPoint进行截取。
    此种策略比较适合有固定前缀的 Rowkey。当没有设置前缀长度,切分效果等同与 IncreasingToUpperBoundRegionSplitPolicy。
  • DelimitedKeyPrefixRegionSplitPolicy:切分策略依然依据默认切分策略,同样是保证相同 RowKey 前缀的数据在一个Region中,但是是以指定分隔符前面的前缀为来切分 Region。

4.4、Region 切分流程

将一个region切分为两个近似大小的子region,首先要确定切分点。切分操作是基于region执行的,每个region有多个store(对应多个column famliy)。系统首先会遍历所有store,找到其中最大的一个,再在这个store中找出最大的HFile,定位这个文件中心位置对应的rowkey,作为region的切分点。

找到切分点之后,切分线程会初始化一个SplitTransaction对象,从字面上就可以看出来split流程是一个类似‘事务’的过程,之所以称为类似’事务’,因为它不满足事务的很多特性,比如隔离性。但它却尝试使用写journal的方式实现数据的原子性和一致性。整个过程分为三个阶段:prepare - execute - (rollback) ,操作模版如下:

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

2)execute阶段:切分的核心操作。
  1. region server 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。

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

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

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

  5. 核心分裂步骤:在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成引用文件,分别指向父region中对应文件。生成reference文件名如下所示:

    reference文件是一个引用文件(并非linux链接文件),文件内容很显然不是用户数据。文件内容其实非常简单, 主要有两部分构成: 其一是切分点splitkey, 其二是一个boolean类型的变量( true 或者false),true表示该reference文件引用的是父文件的上半部分(top),而false表示引用的是下半部分 (bottom)。

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

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


  8. 开启daughter A、daughter B两个子region。

  9. 通知修改 hbase.meta 表,正式对外提供服务。

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


4.5、通过 reference 文件如何查找到对应的数据

根据文件名来判断是否是 reference 文件:

  1. reference 文件的命名规则为前半部分为父 Region 对应的 HFile 的文件名,后半部分是父 Region 的名称,因此读取的时候也根据前半部分和后半部分来定位文件。
  2. 根据 reference 文件的内容来确定扫描的范围,reference 的内容包含两部分:一部分是切分点 splitkey,另一部分是 boolean 类型的变量,如果为 true 则扫描文件的上半部分,反之则扫描文件的下半部分
  3. 接下来确定了扫描的文件,以及文件的扫描范围,那就按照正常的文件检索了

4.6、Split 对其他模块的影响

执行 Region Split 过程不涉及数据的移动,所以可以很快完成。新生成的子 Region 文件中没有任何用户数据,而是一个 reference 文件,文件中存储的是一些元数据信息,包括切分点的 Rowkey 等。引入了以下问题:

  1. 父 Region 的数据什么时候会迁移到子 Region 目录
    子 Region 发生 major_compaction 时。将父 Region 目录中属于该子 Region 的所有数据读出来并写入子 Region 数据文件目录中,这一操作符合 compaction 本身的处理逻辑,因此在 compaction 中操作。
  2. 父 Region 什么时候会被删除
    HMaster 会启动一个线程定期检查所有处于 splitting 状态的父 Region,确定其是否可以被清理。检测线程首先会在 .META. 表中找到 splitting region,并找出其生成的两个子 Region(.META. 表中 splitA 和 splitB 列)。然后检查两个子 Region 是否保留引用文件,如果都不存在就认为该 splitting region 可以被删除和下线。

五、HBase Compaction

Compaction 会从一个region的一个store中选择一些hfile文件进行合并。合并说来原理很简单,先从这些待合并的数据文件中读出KeyValues,再按照由小到大排列后写入一个新的文件中。

HBase Compaction操作是为了数据读取做的优化,总的来说是以牺牲磁盘io来换取读性能的基本稳定。Compaction操作分为minor compaction与major compaction,其中major compaction消耗资源较大、对读写请求有一定影响,因此一般是禁用自动周期性执行而选择业务低峰期时手动执行。

5.1、为什么要执行 Compaction?

HBase 是基于一种LSM-Tree(Log-Structured Merge Tree)存储模型设计的,写入路径上是先写入WAL(Write-Ahead-Log)即预写日志,再写入 memstore 缓存,满足一定条件后执行 flush 操作将缓存数据刷写到磁盘,生成一个 HFile 数据文件。随着数据不断写入,磁盘 HFile 文件就会越来越多,文件太多会影响HBase查询性能,主要体现在查询数据的 io 次数增加。为了优化查询性能,HBase 会合并小的 HFile 以减少文件数量,这种合并 HFile 的操作称为 Compaction,这也是为什么要进行 Compaction 的原因。

5.2、Compaction 分类

HBase Compaction分为两种:Minor Compaction 与 Major Compaction:

5.2.1、Minor Compaction

Minor Compaction(0.96版本后默认使用ExploringCompactionPolicy策略):是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经 Deleted 或 Expired 的 Cel。默认情况,Minor操作只用来做部分文件的合并操作以及包括minVersion=0并且设置ttl的过期版本清理,不做任何删除数据、多版本数据的清理工作。

Minor Compaction 只执行简单的文件合并操作,选取较小的HFiles,将其中的数据顺序写入新的HFile后,替换老的HFiles。

但是如何在众多HFiles中选择本次Minor Compaction要合并的文件却有不少讲究:

  • 首先排除掉文件大小 大于 hbase.hstore.compaction.max.size 值的HFile

  • 将HFiles按照 文件年龄排序(older to younger),并从older file开始选择

  • 如果该文件大小 小于 hbase.hstore.compaction.min.size 则直接加入Minor Compaction中

  • 如果该文件大小 小于其后面 hbase.hstore.compaction.max(默认为10)个 HFile大小之和 * hbase.hstore.compaction.ratio(默认为1.2),则将该文件加入Minor Compaction中

  • 扫描过程中,如果需要合并的 HFile 文件数达到 hbase.hstore.compaction.max(默认为10) 则开始合并过程

  • 扫描结束后,如果需要合并的HFile的文件数 大于 hbase.hstore.compaction.min(默认为3) 则开始合并过程

  • 通过 hbase.offpeak.start.hour、hbase.offpeak.end.hour 设置高峰、非高峰时期,使 hbase.hstore.compaction.ratio的值在不同时期灵活变化(高峰值1.2、非高峰值5)

Minor Compaction不会合并过大的HFile,合并的HFile数量也有严格的限制,以避免产生太大的IO操作,Minor Compaction经常在Memstore Flush后触发,但不会对线上读写请求造成太大延迟影响。

5.2.2、Major Compaction

相对于Minor Compaction 只合并选择的一部分HFile合并、合并时只简单合并数据文件的特点,Major Compaction:指将所有的 StoreFile 合并成一个 StoreFile(会过滤到达 maxSize 的 HFile),这个过程会清理三类没有意义的数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。

另外,一般情况下,major compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发major compaction 功能,改为手动在业务低峰期触发。

Major Compaction定期执行的条件由以下两个参数控制:

  • hbase.hregion.majorcompaction:默认7天
  • hbase.hregion.majorcompaction.jitter:默认为0.2

集群中各个RegionServer将会在 hbase.hregion.majorcompaction ± hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 的区间浮动进行Major Compaction,以避免过多RegionServer同时进行,造成较大影响。

5.3、Compaction 的触发时机

HBase触发Compaction的条件有三种:memstore Flush、后台线程周期性检查、手动触发。

Memstore Flush:

应该说 compaction 操作的源头就来自 flush 操作,memstore flush 会产生 HFile 文件,文件越来越多就需要 compact。因此在每次执行完 Flush 操作之后,都会对当前 Store 中的文件数等条件进行判断,一旦满足条件 ,就会触发 compaction。需要说明的是,compaction 都是以 Store 为单位进行的,而在 Flush 触发条件下,整个 Region 的所有 Store 都会执行 compact,所以会在短时间内执行多次compaction。

后台线程周期性检查:

后台线程 CompactionChecker 会定期检查是否需要执行compaction,检查周期为hbase.server.thread.wakefrequency * hbase.server.compactchecker.interval.multiplier,这里主要考虑的是一段时间内没有写入请求仍然需要做compact检查。

其中参数 hbase.server.thread.wakefrequency 默认值 10000 即 10s,是HBase服务端线程唤醒时间间隔,用于log roller、memstore flusher等操作周期性检查;参数 hbase.server.compactchecker.interval.multiplier 默认值1000,是compaction操作周期性检查乘数因子。10 * 1000 s 时间上约等于2hrs, 46mins, 40sec。

和 Memstore flush 不同的是,该线程优先检查文件数,一旦达到阈值就会触发 minor compaction。

  public boolean needsCompaction(final Collection storeFiles,
      final List filesCompacting) {
    int numCandidates = storeFiles.size() - filesCompacting.size();
    return numCandidates >= comConf.getMinFilesToCompact();
  }

如果不满足,它会接着检查是否满足 major compaction 条件,简单来说,如果当前 store 中 hfile 的最早更新时间早于某个值 mcTime,就会触发 major compaction,HBase 预想通过这种机制定期删除过期数据。上文mcTime 是一个浮动值,浮动区间默认为[7-7*0.5,7+7*0.5],其中7为hbase.hregion.majorcompaction,0.5为hbase.hregion.majorcompaction.jitter,可见默认在7天左右就会执行一次major compaction。用户如果想禁用major compaction,只需要将参数hbase.hregion.majorcompaction设为0。

手动触发:

一般来讲,手动触发compaction通常是为了执行major compaction,原因有三:

其一是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发;

其二也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major compaction;

其三是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据;无论哪种触发动机,一旦手动触发,HBase会不做很多自动化检查,直接执行合并。

5.4、Compaction 参数解析

Major Compaction 参数

Major Compaction涉及的参数比较少,主要有大合并时间间隔与一个抖动参数因子,如下:

1.hbase.hregion.majorcompaction

Major compaction周期性时间间隔,默认值604800000,单位ms。表示major compaction默认7天调度一次,HBase 0.96.x及之前默认为1天调度一次。设置为 0 时表示禁用自动触发major compaction。需要强调的是一般major compaction持续时间较长、系统资源消耗较大,对上层业务也有比较大的影响,一般生产环境下为了避免影响读写请求,会禁用自动触发major compaction。

2.hbase.hregion.majorcompaction.jitter

Major compaction抖动参数,默认值0.5。这个参数是为了避免major compaction同时在各个regionserver上同时发生,避免此操作给集群带来很大压力。 这样节点major compaction就会在 + 或 - 两者乘积的时间范围内随机发生。

Minor Compaction 参数

Minor compaction涉及的参数比major compaction要多,各个参数的目标是为了选择合适的HFile,具体参数如下:

1.hbase.hstore.compaction.min

一次minor compaction最少合并的HFile数量,默认值 3。表示至少有3个符合条件的HFile,minor compaction才会启动。一般情况下不建议调整该参数。

如果要调整,不建议调小该参数,这样会带来更频繁的压缩,调大该参数的同时其他相关参数也应该做调整。早期参数名称为 hbase.hstore.compactionthreshold。

2.hbase.hstore.compaction.max

一次minor compaction最多合并的HFile数量,默认值 10。这个参数也是控制着一次压缩的时间。一般情况下不建议调整该参数。调大该值意味着一次compaction将会合并更多的HFile,压缩时间将会延长。

3.hbase.hstore.compaction.min.size

文件大小 < 该参数值的HFile一定是适合进行minor compaction 的文件,默认值 128M(memstore flush size)。意味着小于该大小的HFile将会自动加入(automatic include)压缩队列。一般情况下不建议调整该参数。

但是,在write-heavy就是写压力非常大的场景,可能需要微调该参数、减小参数值,假如每次memstore大小达到1~2M时就会flush生成HFile,此时生成的每个HFile都会加入压缩队列,而且压缩生成的HFile仍然可能小于该配置值会再次加入压缩队列,这样将会导致压缩队列持续很长。

4.hbase.hstore.compaction.max.size

文件大小 > 该参数值的HFile将会被排除,不会加入minor compaction,默认值Long.MAX_VALUE,表示没有什么限制。一般情况下也不建议调整该参数。

5.hbase.hstore.compaction.ratio

这个ratio参数的作用是判断文件大小 > hbase.hstore.compaction.min.size 的HFile是否也是适合进行 minor compaction 的,默认值1.2。更大的值将压缩产生更大的HFile,建议取值范围在1.0~1.4之间。大多数场景下也不建议调整该参数。

6.hbase.hstore.compaction.ratio.offpeak

此参数与 compaction ratio 参数含义相同,是在原有文件选择策略基础上增加了一个非高峰期 的ratio 控制,默认值5.0。这个参数受另外两个参数 hbase.offpeak.start.hour 与 hbase.offpeak.end.hour 控制,这两个参数值为[0, 23]的整数,用于定义非高峰期时间段,默认值均为-1表示禁用非高峰期ratio设置。

5.5、Compaction 流程

一旦触发,HBase 会将该 Compaction 交由一个独立的线程处理,该线程首先会从对应 store 中选择合适的 hfile 文件进行合并,这一步是整个 Compaction 的核心,选取文件需要遵循很多条件,比如文件数不能太多、不能太少、文件大小不能太大等等,最理想的情况是,选取那些承载 IO 负载重、文件小的文件集,实际实现中,HBase提供了多个文件选取算法:RatioBasedCompactionPolicy、ExploringCompactionPolicy 和 StripeCompactionPolicy 等,用户也可以通过特定接口实现自己的 Compaction 算法;选出待合并的文件后,HBase会根据这些hfile文件总大小挑选对应的线程池处理,最后对这些文件执行具体的合并操作。可以通过下图简单地梳理上述流程:

流程详解:

CompactionChecker 是 RS 上的工作线程(Chore),设置执行周期是通过threadWakeFrequency 指定,大小通过 hbase.server.thread.wakefrequency 配置(默认10000),然后乘以默认倍数multiple(1000),毫秒时间转换为秒。因此,在不做参数修改的情况下,CompactionChecker大概是2hrs, 46mins, 40sec执行一次。

首先,对于HRegion里的每个HStore进行一次判断,needsCompaction()判断是否足够多的文件触发了Compaction的条件。

条件为:HStore 中 StoreFIles 的个数 – 正在执行 Compacting 的文件个数 > minFilesToCompact

操作:以最低优先级提交Compaction申请。

步骤1:选出待执行 Compact 的 storefiles。由于在 Store 中的文件可能已经在进行 Compacting,因此,这里取出未执行 Compacting 的文件,将其加入到 Candidates 中。

步骤2:执行 compactSelection 算法,在Candidates 中选出需要进行 compact 的文件,并封装成 CompactSelection 对象当中。

  1. 选出过期的 store files。过滤minVersion=0,并且 storefile.maxTimeStamp + store.ttl < now_timestamp。这意味着整个文件最大的时间戳的kv,都已经过期了,从而证明整个storefile都已经过期了。CompactSelection如果发现这样的storefile,会优先选择出来,作为Min然后提交给Store进行处理。

这部分具体操作被封装在ScanQueryMatcher下的ColumnTracker中,在StoreScanner的遍历过程,ScannerQueryMatcher负责kv的过滤。这里的ScanType包括(MAJOR_COMPACT,MINOR_COMPACT,USER_SCAN),compact操作是对选出的文件执行一次标识ScanType为MAJOR_COMPACT或者MINOR_COMPACT类型的scan操作,然后将最终符合标准的kv存储在一个新的文件中。

应用重要参考:根据应用的需求设置ttl,并且设置minVersions=0,根据selectCompation优选清理过期不保留版本的文件的策略,这样会使得这部分数据在CompactionChecker的周期内被清理。

误区:在CompactSplitThread有两个配置项

hbase.regionserver.thread.compaction.large:配置largeCompactions线程池的线程个数,默认个数为1。

hbase.regionserver.thread.compaction.small:配置smallCompactions线程池的线程个数,默认个数为1。

这两个线程池负责接收处理CR(CompactionRequest),这两个线程池不是根据CR来自于Major Compaction和Minor Compaction来进行区分,而是根据一个配置hbase.regionserver.thread.compaction.throttle的设置值(一般在hbase-site.xml没有该值的设置),而是采用默认值2 * minFilesToCompact * memstoreFlushSize,如果cr需要处理的storefile文件的大小总和,大于throttle的值,则会提交到largeCompactions线程池进行处理,反之亦然。

应用重要参考:可以稍微调大一些largeCompactions和smallCompactions线程池内线程的个数,建议都设置成5。

  1. 判断是否需要进行majorCompaction,这是很多判断条件的合成,其中最为重要的一个是
    hbase.hregion.majorcompaction设置的值,也就是判断上次进行majorCompaction到当前的时间间隔,如果超过设置值,则满足一个条件,同时另外一个条件是compactSelection.getFilesToCompact().size() < this.maxFilesToCompact。

因此,通过设置hbase.hregion.majorcompaction = 0可以关闭CompactionChecke触发的major compaction,但是无法关闭用户调用级别的mc。

  1. 过滤对于大文件进行Compaction 操作。判断fileToCompact队列中的文件是否超过了maxCompactSize,如果超过,则过滤掉该文件,避免对于大文件进行compaction。

  2. 如果确定 Minor Compaction 方式执行,会检查经过过滤过的fileToCompact的大小是否满足minFilesToCompact 最低标准,如果不满足,忽略本次操作。确定执行的Minor Compaction的操作时,会使用一个smart算法,从filesToCompact当中选出匹配的storefiles。

  3. 通过selectCompaction 选出的文件,加入到filesCompacting队列中。

  4. 创建compactionRequest,提交请求。

5.6、Compaction HBase做了什么?

第一步:获取需要合并的HFile列表

获取列表的时候需要排除掉带锁的HFile。

锁分两种:写锁(write lock)和读锁(read lock)。当HFile正在进行以下操作的时候会上锁:

  • 用户正在scan查询:上Region读锁(region read lock)。
  • Region正在切分(split):此时Region会先关闭,然后上Region写锁(region write lock)。
  • Region关闭:上Region写锁(region write lock)。
  • Region批量导入:上Region写锁(region write lock)。

第二步:由列表创建出StoreFileScanner

HRegion会创建出一个Scanner,用这个Scanner来读取本次要合并的所有StoreFile上的数据。

第三步:把数据从这些HFile中读出,并放到tmp目录(临时文件夹)

HBase会在临时目录中创建新的HFile,并使用之前建立的Scanner从旧HFile上读取数据,放入新HFile。以下两种数据不会被读取出来:如果数据过期了(达到TTL所规定的时间),那么这些数据不会被读取出来。

如果是 Major Compaction,那么数据带了墓碑标记也不会被读取出来。

第四步:用合并后的HFile来替换合并前的那些HFile

最后用临时文件夹内合并后的新HFile来替换掉之前的那些HFile文件。过期的数据由于没有被读取出来,所以就永远地消失了。如果本次合并是Major Compaction,那么带有墓碑标记的文件也因为没有被读取出来,就真正地被删除掉了。

5.7、Major Compaction 真正删除数据

其实HBase一直拖到majorCompaction的时候才真正把带墓碑标记的数据删掉,并不是因为性能要求,而是之前真的做不到。

HBase是建立在HDFS这种只有增加删除而没有修改的文件系统之上的,所以就连用户删除这个动作,在底层都是由新增实现的:

  • 用户增加一条数据就在HFile上增加一条KeyValue,类型是PUT。
  • 用户删除一条数据还是在HFile上增加一条KeyValue,类型是DELETE,这就是墓碑标记。所以墓碑标记没有什么神秘的,它也就只是另外一个KeyValue,只不过value没有值,而类型是DELETE。

现在会遇到一个问题:当用户删除数据的时候之前的数据已经被刷写到磁盘上的另外一个HFile了。这种情况很常见,也就是说,墓碑标记和原始数据这两个KeyValue 压根可能就不在同一个HFile上。

在查询的时候Scan指针其实是把所有的HFile都看过了一遍,它知道了有这条数据,也知道它有墓碑标记,而在返回数据的时候选择不把数据返回给用户,这样在用户的Scan操作看来这条数据就是被删掉了。

如果你可以带上RAW=>true参数来Scan,你就可以查询到这条被打上墓碑标记的数据。

5.8、TTL 的数据被 Minor Compaction 删除

这是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来记录。合并时创建的Scan在查询数据的时候,根据以下公式来判断cell是否过期:

当前时间 - cell 的 timestamp > TTL

如果过期了就不返回这条数据。这样当合并完成后,过期的数据因为没有被写入新文件,自然就消失了。

六、HBase Flush

6.1、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* 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.maxlogs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush

5、HBase定期刷新Memstore:默认周期为1小时,确保Memstore不会长时间没有持久化。为避免所有的MemStore在同一时间都进行flush导致的问题,定期的flush操作有20000左右的随机延时。

6、手动执行flush:用户可以通过shell命令 flush ‘tablename’或者flush ‘region name’分别对一个表或者一个Region进行flush。

七、性能优化

7.1、Region规划

对于Region的大小,HBase官方文档推荐单个在10G-30G 之间,单台RegionServer的数量控制在20-300之间(当然,这仅仅是参考值)。

Region过大过小都会有不良影响:

  • 过大的Region
    • 优点:迁移速度快、减少总RPC请求
    • 缺点:compaction的时候资源消耗非常大、可能会有数据分散不均衡的问题
  • 过小的Region
    • 优点:集群负载平衡、HFile比较少compaction影响小
    • 缺点:迁移或者balance效率低、频繁flush导致频繁的compaction、维护开销大

规划Region的大小与数量时可以参考以下算法:

0. 计算HBase可用磁盘空间(单台RegionServer)
1. 设置region最大与最小阈值,region的大小在此区间选择,如10-30G
2. 设置最佳region数(这是一个经验值),如单台RegionServer 200个
3. 从region最小值开始,计算 HBase可用磁盘空间 / (region_size * hdfs副本数) = region个数
4. 得到的region个数如果 > 200,则增大region_size(step可设置为5G),继续计算直至找到region个数最接近200的region_size大小
5. region大小建议不小于10G

比如当前可用磁盘空间为18T,选择的region大小范围为10-30G,最佳region个数为300。

那么最接近 最佳Region个数300的 region_size 值为30G。

得到以下配置项:

  • hbase.hregion.max.filesize=30G
  • 单节点最多可存储的Region个数约为300

7.2、内存规划

RegionServer中的BlockCache有两种实现方式:

  • LRUBlockCache:On-Heap
  • BucketCache:Off-Heap

我们可以根据可用内存大小来判断使用哪种内存模式。

1)先看 超小内存(假设8G以下)超大内存(假设128G以上) 两种极端情况:

对于超小内存来说,即使可以使用BucketCache来利用堆外内存,但是使用堆外内存的主要目的是避免GC时不稳定的影响,堆外内存的效率是要比堆内内存低的。由于内存总体较小,即使读写缓存都在堆内内存中,GC时也不会造成太大影响,所以可以直接选择LRUBlockCache

对于超大内存来说,在超大内存上使用LRUBlockCache将会出现我们所担忧的情况:GC时对线上造成很不稳定的延迟影响。这种场景下,应该尽量利用堆外内存作为读缓存,减小堆内内存的压力,所以可以直接选择BucketCache

2)在两边的极端情况下,我们可以根据内存大小选择合适的内存模式,那么如果内存大小在合理、正常的范围内该如何选择呢?

此时我们应该主要关注业务应用的类型

当业务主要为写多读少型应用时,写缓存利用率高,应该使用LRUBlockCache尽量提高堆内写缓存的使用率

当业务主要为写少读多型应用时,读缓存利用率高(通常也意味着需要稳定的低延迟响应),应该使用BucketCache尽量提高堆外读缓存的使用率

3)对于不明确或者多种类型混合的业务应用,建议使用BucketCache保证读请求的稳定性同时,堆内写缓存效率并不会很低

如果 HBase可使用的内存高达153G,故将选择BucketCache的内存模型来配置HBase,该模式下能够最大化利用内存,减少GC影响,对线上的实时服务较为有利。

得到配置项:

  • hbase.bucketcache.ioengine=offheap: 使用堆外缓存

7.3、内存与磁盘比

从 HBase集群规划 引入一个Disk / JavaHeap Ratio 的概念来帮助我们设置内存相关的参数。

理论上我们假设 最优 情况下 硬盘维度下的Region个数JavaHeap维度下的Region个数 相等。

相应的计算公式为:

  • 硬盘容量维度下Region个数: DiskSize / (RegionSize * ReplicationFactor)
  • JavaHeap维度下Region个数: JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )

其中默认配置下:

  • RegionSize = 10G:Region大小,配置项:hbase.hregion.max.filesize
  • ReplicationFactor = 3:HDFS的副本数,配置项:dfs.replication
  • HeapFractionForMemstore = 0.4:JavaHeap写缓存大小,即RegionServer内存中Memstore的总大小,配置项:hbase.regionserver.global.memstore.lowerLimit
  • MemstoreSize = 128M:Memstore刷写大小,配置项:hbase.hregion.memstore.flush.size

现在我们已知条件 硬盘维度和JavaHeap维度相等,求 1 bytes的JavaHeap大小需要搭配多大的硬盘大小

已知:

DiskSize / (RegionSize * ReplicationFactor) = JavaHeap * HeapFractionForMemstore / (MemstoreSize / 2 )

求:

DiskSize / JavaHeap

进行简单的交换运算可得:

DiskSize / JavaHeap = RegionSize / MemstoreSize * ReplicationFactor * HeapFractionForMemstore * 2

10G / 128M * 3 * 0.4 * 2 = 192,即理想状态下 RegionServer上 1 bytes的Java内存大小需要搭配192bytes的硬盘大小最合适

7.4、内存布局

BucketCache模式下,RegionServer的内存划分如下图:

简化版:

7.4.1、写缓存

从架构原理中知道,Memstore有6种情况的Flush,需要我们关注的是 Memstore、Region和RegionServer级别的刷写。

其中Memstore和Region级别的刷写并不会对线上造成太大影响,但可以控制其阈值和刷写频次来进一步提高性能

而RegionServer级别的刷写将会阻塞请求直至刷写完成,对线上影响巨大,需要尽量避免

配置项:

  • hbase.hregion.memstore.flush.size=256M: 控制的Memstore大小默认值为128M,太过频繁的刷写会导致IO繁忙,刷新队列阻塞等。
    设置太高也有坏处,可能会较为频繁的触发RegionServer级别的Flush,这里设置为256M。
  • hbase.hregion.memstore.block.multiplier=3: 控制的Region flush上限默认值为2,意味着一个Region中最大同时存储的Memstore大小为2 * MemstoreSize ,如果一个表的列族过多将频繁触发,该值视情况调整。

7.4.2、读缓存

读缓存由 堆内的LRU元数据堆外的数据缓存 组成,两部分占比一般为 1:9(经验值)

而对于总体的堆内内存,存在以下限制,如果超出此限制则应该调低比例:

LRUBlockCache + MemStore < 80% * JVM_HEAP

配置堆外缓存涉及到的相关参数如下:

  • hbase.bucketcache.size=111 * 1024M: 堆外缓存大小,单位为M
  • hbase.bucketcache.percentage.in.combinedcache=0.9: 堆外读缓存所占比例,剩余为堆内元数据缓存大小
  • hfile.block.cache.size=0.15: 校验项,+upperLimit需要小于0.8

你可能感兴趣的:(Hbase 知识要点归总)