HBase最佳实践

本文致力于从架构原理、集群部署、性能优化与使用技巧等方面,阐述在如何基于HBase构建容纳大规模数据,支撑高并发、毫秒响应、稳定高效的OLTP实时系统

1. 架构原理

1.1 基本架构

在这里插入图片描述

从上往下可以看到HBase架构中的角色分配为:

  • Client
  • Zookeeper
  • HMaster
  • RegionServer
  • HDFS

1.1.1 Client

Client是执行查询/写入等对HBase表数据进行增删改查的使用方,可以是使用HBase Client API编写的程序。也可以是其他开发好的HBase客户端应用。

1.1.2 Zookeeper

同HDFS一样,HBase使用Zookeeper作为集群协调和管理系统。

在HBase中,其主要的功能和职责为:

  • 存储整个集群HMaster和RegionServer的运行状态
  • 实现HMaster的故障恢复自动切换
  • 为Client提供元数据表的存储信息

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

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

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

扫描二维码关注公众号,回复: 12653881 查看本文章

协调过程如下:

在这里插入图片描述

除了作为集群中的协调者,Zookeeper还为Client提供了hbase:mate表的存储信息

客户端要访问hbase中的数据,只要知道Zookeeper集群的连接信息,访问步骤如下:

  • 客户端将从Zookeeper(/hbase/meta-region-server)获得hbase:meta表存储在哪个RegionServer,缓存该位置信息。
  • 查询该RegionServer上的hbase:meta表数据,查找要操作的rowkey所在的Region存储在哪个RegionServer中,缓存该位置信息。
  • 在具体的RegionServer上根据rowkey检索该Region数据

可以看到,客户端操作数据过程并不需要HMaster的参与,通过Zookeeper间接访问RegionServer来操作数据。

第一次请求将会产生3次RPC,之后使用相同的rowkey时客户端将直接使用缓存下来的位置信息,直接访问RegionServer,直至缓存失效(Region失效、迁移等原因)。

通过Zookeeper的读写流程如下:

在这里插入图片描述

1.1.3 hbase:meta表

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

表结构如下:
在这里插入图片描述

  • Rowkey格式:tableName,regionStartKey,regionId

    • 第一个region的regionStartKey为空

      示例:ns1:testTable,xxxxregionId

  • 只有一个列蔟iinfo,包含三列:

    • regionInfo:RegionInfo的proto序列化格式,包含regionId,tableName,startKey,endKey,offline,split,replicaId等信息
    • server:RegionServer对应的server:port
    • serverstartcode:RegionServer的启动时间戳

简单总结Zookeeper在HBase集群的作用如下:

  • 对于服务端:是实现集群协调与控制的重要依赖
  • 对于客户端:是查询与操作数据必不可少的一部分

1.1.4 HMaster

在这里插入图片描述

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

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

如前面所说的,HMaster通过Zookeeper实现对集群中各个RegionServer的监控与管理,在RegionServer发生故障时可以发现节点宕机并转移Region至其他节点,保证服务的可用性

但是HBase的故障转移并不是无感知的,相反故障转移过程中,可能会直接影响线上请求的稳定性,造成短时间内的大量延迟。

在分布式系统的CAP定理中(Consistency一致性、Availability可用性、Partition tolerance分区容错性),分布式数据库基本都会实现P,但是不同的数据对于A和C各有取舍。如HBase选择了C,而通过Zookeeper这种方式来辅助实现A(虽然会有一定缺陷),而Cassandra选择了A,通过其他辅助措施实现了C,各有优劣。

对于HBase集群来说,HMaster是一个内部管理者,除了DDL操作并不对外(客户端)开放,因而HMaster的负载是比较低的。

造成HBase集群压力大的情况可能是集群中存在多个(两个或者三个以上)HMaster,备用的Master会定期与Active Master通信以获取最新的状态信息,以保证故障切换时自身的数据状态是最新的,因而Active Master可能会收到大量来自备用Master的数据请求。

1.1.5 RegionServer

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

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

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

RegionServer内部结构如下:

在这里插入图片描述

1.1.6 BlockCache

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

RegionServer处理客户端读请求过程:

1. 在BlockCache中查询是否命中缓存
2. 缓存未命中则定位到存储数据的Region
3. 检索Region MemStore中是否有所需的数据
4. 任一过程查询成功则将数据返回给客户端并缓存至BlockCache

BlockCache有两种实现方式,有不同的应用场景,各有优劣:

  • On-Heap的LRUBlockCache

    优点:直接在java堆内存取,响应速度快

    缺点:容易受GC影响,响应延迟不稳定,特别是在堆内存巨大的情况下。

    适用场景:写多读少型、小内存等场景

  • Off-Heap的BucketCache

    优点:无GC影响,延迟稳定

    缺点:从堆外内存获取数据,性能略差于堆内内存。

    适用场景:读多写少型、大内存等场景

我们将在【性能优化】一节具体讨论如何判断使用哪种内存模式。

1.1.7 WAL HLog

全称 Write Ahead Log,是RegionServer中的预写日志。

所有写入数据默认情况下都会先写入WAL中,以保证RegionServer宕机重启之后可以通过WAL来恢复数据,一个RegionServer共用一个WAL。

RegionServer的写流程如下:

1. 将数据写入WAL中
2. 根据TableName,RowKey和ColumnFaily将数据写入对应的MemStore中。
3. MemStore通过特定算法将内存中的数据刷写成Storefile写入磁盘,并标记WAL sequence值。
4. Storefile定期合并小文件

WAL会通过日志滚动的操作定期对日志文件进行清理(已写入HFile中的数据可以清除),对应HDFS上的存储路径为**/hbase/WALs/${HRegionServer_Name}**。

1.1.8 Region

一个Table由一个或多个Region组成,**一个Region中可以看成是Table按行切分且有序的数据快,**每个Region都有自身的StartKey、EndKey。

一个Region由一个或多个Store组成,每个Store存储该Table对应Region中一个列蔟的数据,相同的列蔟的列存储在同一个Store中。

同一个Table的Region会分布在集群中不同的RegionServer上,以实现读写请求的负载均衡。所以一个RegionServer中将存储来自不同Table的N个Region。

Store、Region与Table的关系可以表述如下:多个Store(列蔟)组成Region,多个Region(行数据块)组成完整的Table

其中,Store由MemStore(内存)、StoreFile(磁盘)两部分组成。

在RegionServer中,MemStore可以看成指定Table、Region、Store的写缓存(正如BlockCache小节中所诉,MemStore还承载了一些读缓存的功能),以RowKey、Cloumn Family、Cloumn、Tiemstamp进行排序。如下图所示:
在这里插入图片描述

写请求到RegionServer之后并没有立刻写入磁盘,而是先写入内存中的MemStore(内存中数据丢失问题可以通过回放WAL解决)以提升写入性能。

Region中的Memstore会根据特定算法将内存中的数据刷写到磁盘形成StoreFile文件,因为数据在Memstore中已经排序,顺序写入磁盘性能高、速度快。

在这种日志结构合并树(Log-Structured Merge Tree)架构随机写入HBase拥有相当高的性能。

MemStore刷磁盘形成的Store以HFile格式存储HBase的KV数据在HDFS上。

1.1.9 HDFS

HDFS为HBase提供底层存储系统,通过HDFS的高可用、高可靠性等特性,保障了HBase的数据安全、容灾与备份

1.2 写数据与 Memstore Flush

对于客户端来说,将请求发送到需要写入的RegionServer中,等待RegionServer写入WAL、Memstore之后返回写入成功的ack信号。

对于RegionServer来说,写入的数据还需要经过一系列的处理步骤。

首先我们知道MemStore是在内存中的,将数据存放在内存中可以得到优异的读写性能,但是同样也会带来麻烦:

  • 内存中的数据如何防止断电丢失
  • 将数据存储于内存的代价是高昂的,空间是有限的

对于第一个问题,虽然可以通过WAL机制在重启的时候进行数据回放,但对于第二个问题,则必须将内存中的数据持久化到磁盘中

在不同情况下,RegionServer通过不同级别的刷写策略对MemStore中的数据进行持久化,根据触发刷写动作的时机以及影响范围,可以分为不同的几个级别:

  • Memstore级别:Region中任意一个MemStore达到了hbase.hregion.memstore.flush.size控制的上限(默认128M),会触发Memstore的flush。
  • Region级别:Region中MemStore大小之和达到了hbase.hregion.memstore.multiplier * hbase.hregion.memstore.flush.size控制的上限(默认2 * 128M = 256M),会触发Memstore的flush。
  • RegionServer级别:RegionServer中所有Region的Memstore大小总和达到了hbase.regionserver.global.memstore.upperLimt * hbase_heapsize 控制的上限(默认0.4,即RegionServer 40%的JVM内存),将会按Memstore由大到小进行flush。直至总体Memstore内存使用量低于hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize 控制的下限(默认0.38,即RegionServer 38%的JVM内存)。
  • RegionServer中HLog数量达到上限:将会选取最早的HLog对应的一个或多个Region进行flush(通过参数hbase.regionserver.maxlogs配置)。
  • HBase定期flush:确保Memstore不会长时间没有持久化,默认周期为1小时。为避免所有Memstore在同一时间都进行flush导致的问题,定期flush操作都会有20秒左右的随机延迟。
  • 手动执行flush:用户可以通过shell命令 flush ‘tableName’ 或者flush 'region name’分别对表或者一个region进行flush。

Memstore刷写时会阻塞线上的请求响应,由此可以看到,不同级别的刷写对线上的请求会造成不同程度的影响的延迟:

  • 对Memstore与Region级别的刷写,速度是比较快的,并不会造成太大影响
  • 对于RegionServer级别的刷写,将会阻塞发生到该RegionServer上的所有请求,直至Memstore刷写完毕,会产生较大影响。

数据在经过Memstore刷写到磁盘时,对应的会写入WAL sequence的相关信息,已经持久化到磁盘的数据就没必要通过WAL记录

RegionServer会根据这个sequence值对WAL日志进行滚动清理,防止WAL日志数量太多,RegionServer启动时加载太多数据信息。

同样,在Memstore的刷写cel中可以看到,为了防止WAL日志数量太多,达到指定阈值之后将会选择WAL记录中最早的一个或多个Region进行刷写。

1.3 读数据 与 Bloom Filter

经过前文的了解,我们现在可以知道HBase中一条完整的读操作流程中,Client和Zookeeper、RegionServer等发生多次交互请求。

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

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

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

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

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

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

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

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

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

Bloom Filter由 一个长度为m的位数组k个哈希函数组成。

其工作原理如下:

  1. 原始集合写入一个元素时,Bloom Filter同时将该元素经过k个哈希函数映射成k个数字,并以这些数字为下标,将位数组中对应下标的元素标记为1

  2. 当需要判断一个元素是否存在于原始集合中,只需要将该元素经过同样的k个哈希函数得到k个数字

    取 位数组 中对应下标的元素,如果都是为1,则表示元素可能存在

    如果存在其中一个元素为0则该元素不可能存在于元素集合中

  3. 因为哈希碰撞问题,不同的元素经过相同的哈希函数之后可能得到相同的值

    对于集合外的一个元素,如果经过k个函数得到的k个数字,对应位数组中的元素都为1,可能是该元素存在集合中,也有可能是集合中的其他元素“碰巧”让这些下标对应的元素都标记为1,所以只能说其可能存在。

  4. 对于集合中不同元素,如果经过k个哈希函数得到的k个数字中,任意重复位数组中对应的下标的元素会被覆盖,此时该下标的元素不能被删除(即归零)。删除可能会导致其他多个元素在Bloom Filter表示不“存在”

由此可见,Bloom Filter中:

  • 位数组的长度m越大,误差率越小,而存储代价越大
  • 哈希函数的个数k越多,误差率越小,而性能越低
    在这里插入图片描述

Hbase中支持使用一下两种Bloom Filter:

  • ROW:基于RowKey创建的Bloom Filter。
  • ROWCOL:基于RowKey+Cloumn创建的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来判断数据是否存在。

1.4 HFile存储格式

通过Bloom Filter快速定位到需要检索的数据所在的HFile之后的操作自然是从HFile中读取出数据并返回。

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

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

1.4.1 生成一个HFile

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

  • 会先在内存中创建空的Data Block数据块包含预留的Header空间。然后,将Memstore中的KVs一个个顺序写满该Block(一般默认大小为64kb)。
  • 如果指定了压缩或者解密算法,Block数据写满之后将会对整个数据区做相应的压缩或者加密处理。
  • 随后在预留的Header写入该Block的元数据信息,如压缩前后大小,上一个block的offset,checksum等。
  • 内存中的准备工作完成之后,通过HFile Write输出流将数据写入到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 + TREE的结果。得益于多层索引,HBase可以在不读整个文件的情况下查找数据。

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

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

至此,一个完整的HFile已经生成,如下图所示:
在这里插入图片描述

1.4.2 检索HFile

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

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

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

可以看到,在HFile的数据检索过程中,一次读请求只有真正确认数据存在且需要读取硬盘数据的时候才会执行硬盘查询操作

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

在这里插入图片描述

1.5 HFile Compaction

Bloom Filter解决了了如何在大量的HFile中快速定位数据所在的HFile文件,虽然有了Bloom Filter的帮助大大提升了检索效率,但对于RegionServer来说,要检索的HFile数量并没有减少

为了再一次提升HFile的检索效率,同时避免大量小文件的产生造成性能低下,RegionServer会通过Compaction机制对HFile进行合并操作

常见的Compaction触发方式有:

  • Memstore flush检测条件执行
  • RegionServer定期检测执行
  • 用户手动触发执行

1.5.1 Minor Compaction

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

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

  • 首先排除掉文件大小大于 hbase.hstore.compaction.max.size值的HFile
  • 将HFile安装文件年龄排序(older to younger),并从 older file开始选择
  • 如果改文件小于 hbase.hstore.compaction.min则加入Minor Compaction中
  • 如果该文件大小小于 后续 hbase.hstore.compaction.max个HFile大小之和 * hbase.hstore.compaction.ratio,则将该文件加入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后触发,但不会对线上读写请求造成太大延迟影响。

在这里插入图片描述

1.5.2 Major Compaction

相对于Minor Compaction只合并选择的一部分HFile文件、合并时只简单合并数据文件的特点,Major Compaction则会把Store中的所有HFile合并成一个大文件,将会产生较大的IO操作

同时将会清理三类无意义的数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据,Region Split过程中产生的Reference文件也会在此时被清理。

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同时进行,造成较大影响。

Major Compaction执行机制触发后,简单来说如果当前Store中HFile的最早更新时间早于某个时间值,就会执行Major Compaction,该时间值为hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter

手动触发的情况下将会直接执行Compaction。

在这里插入图片描述

1.5.3 Compaction的优缺点

HBase通过Compaction机制使底层HFile文件保持在一个稳定的范围,减少一次请求产生的IO次数、文件Seek(寻址)次数,确保HFile文件检索效率,从而实现高效处理线上请求。

如果没有Compaction机制,随着Memstore刷写的数据越来越多,HFile文件数量将会持续上涨,一次读请求产生的IO操作、Seek文件的次数将会越来越多,反馈到线上就是读请求延迟越来越大

然而,在Compaction执行过程中,不可避免的仍然会对线上造成影响。

  • 对于Major Compaction来说,合并过程将会占用大量宽带、IO资源,此时线上的读延迟将会增大

  • 对于Minor Compaction来说,如果Memstore写入的数据量太多,刷写越来越频繁,超出了HFile的合并速度

    即使不停的合并,但是HFile文件仍然越来越多,读延迟会越来越大

    HBase通过hbase.hstore.blockingStoreFiles(默认7)来控制Store中的HFile数量

    超过配置值时,将会阻塞Memstore flush的操作,阻塞超时时间为hbase.store.blockingWaitTime

    阻塞Memstore flush操作将会使Memstore的内存占用率越来越高,可能导致完全无法写入

简而言之,Compaction机制保证了HBase的读请求一直保持低延迟状态,但付出的代价是Compaction执行期间大量的读延迟毛刺和一定的写阻塞(写入量巨大的情况下)。

1.6 Region Split

HBase通过LSM-Tree架构提供了高性能的随机写,通过缓存、Bloom Filter、HFile与Compaction等机制提供了高性能的随机读

至此,HBase已经具备了作为一个高性能读写数据库的基本条件。如果HBase仅仅到此为止的话,那么其也只是在架构上和传统的数据有所区别的数据库而已,作为一个高性能读写的分布式数据库来说,其拥有近乎可以无限扩展的特性

支持HBase进行自动扩展、负载均衡的是Region Split机制

1.6.1 Split策略与触发条件

在HBase中,提供了多种split策略,不同的策略触发条件各不相同。

在这里插入图片描述

如上图所示,不同版本中使用的默认策略在变化。

  • ConstantSizeRegionSplitPolicy

    固定值策略,阈值默认大小hbase.hregion.max.filesize(1G)

    优点:实现简单

    缺陷:考虑片面,小表不切分、大表切分成很多个region,线上使用弊端多

  • IncreasingToUpperBoundRegionSplitPolicy

非固定阈值

  • 计算公式min(R^2 * memstore.flush.size,region.split.size)
  • R为Region所在的Table在当前RegionServer上Region的个数
  • 最大大小hbase.hregion.max.filesize

优点:自动适应大小表,对于Region个数多阈值大,Region个数少的阈值小

缺陷:对于小表来说会产生很多小的Region

  • SteppingSplitPolicy

非固定阈值

  • 如果Region个数为1,则阈值为memstore.flush.size * 2(128M * 2 = 256M)
  • 否则为region.split.size(128M)

优点:对大小表更加友好,小表会一直产生小Region

缺点:控制力度比较粗

可以看到,不同的切分策略其实只是在寻找切分Region时的阈值,不同的策略对阈值有不同的定义

1.6.2 切分点

切分阈值确认之后,首先要做的是寻找切分Region的切分点。

HBase对Region的切分点定义如下:

  • Region中最大的Store中的最大的HFile中心的block里面首个Rowkey
  • 如果最大的HFile只有一个block,那么不切分(没有middle key)

得到切分点之后,核心的切分流程分为prepare -> execute -> rollback 三个阶段

1.6.3 prepare 阶段

在内存中初始化两个子Region(HRegionInfo对象),准备进行切分操作。

1.6.4 execute 阶段

在这里插入图片描述

execute阶段执行过程较为复杂,具体实施步骤为:

  1. RegionServer在Zookeeper上的**/hbase/region-in-transtion节点中标记该Region状态为SPLITTING**。

  2. HMaster监听到Zookeeper节点发生变化,在内存中修改此Region状态为RIT

  3. 在该Region的存储路径下创建临时文件夹**.split**

  4. 父Region close,flush所有数据到磁盘中,停止所有写入请求

  5. 在父Region的 .split 文件夹中生成两个Region文件夹,并写入reference文件

    reference是一个特殊的文件,体现在其文件名和文件内容上

    文件名组成:父Region对应切分点所在的HFile文件.{ 父Region}

    文件内容:[splitkey]切分点rowkey,[top?]true/false,true为top上半部分,false为bottom下半部分

    根据reference文件名,可以快速找到对应的父Region、其中的HFile文件、HFile切分点,从而确认该子Region的数据范围

    数据范围确认完毕之后进行正常的数据检索流程(此时仍然检索父Region的数据)

  6. 将子Region的目录拷贝到HBase根目录下,形成新的Region

  7. 父Region通知修改hbase:meta表后下线,不在提供服务

    此时并没有删除父Region数据,仅在表中标记split列、offline列为true,并记录两个子region

  8. 两个子Region上线服务

  9. 通知hbase:meta表标记两个子Region正式提供服务

1.6.5 rollback 阶段

如果execute阶段出现异常,则执行rollback操作,保证Region切分整个过程是具备事务性、原子性的,要么切分成功,要么回到为切分的状态。

region切分是一个复杂的过程,涉及到父region切分、子region生成、region下线和上线、zk状态修改、元数据状态修改、master内存状态修改等多个子步骤,回滚程序会根据当前进展到哪个字阶段清理对应的垃圾数据

为了实现事务性,HBase设计了使用**状态机(SplitTransation类)**来保存切分过程的每个子步骤状态。这样一来一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。

但是目前实现中,中间状态是存储在内存中,因此一旦在切分过程中RegionServer宕机或者关闭,重启之后将无法恢复到切分之前的状态。即Region切分处于中间状态的情况,也就是RIT

由于Region切分的子阶段很多,不同阶段解决RIT的处理方式也不一样,需要通过hbck工具进行具体查看并分析解决方案。

好消息是HBase2.0之后提出了新的分布式事务框架Procedure V2,将会使用HLog存储事务中间状态,从而保证事务处理中宕机重启后可以进行回滚或者继续处理,从而减少RIT问题产生。

1.6.6 父Region清理

从以上过程中我们可以看到,Region的切分过程并不把会父Region的数据写入到子Region中,只是在子Region中创建了reference文件,故Region切分过程是很快的。

只有进行Major Compaction时才会真正(顺便)将数据切分到子Region中,将HFile中的kv顺序读出、写入新的HFile文件。

RegionServer将会定期检查hbase:meta表中的split和offline为true的Region,对应的子Region是否存在reference文件,不过不存在则删除父region数据。

1.6.7 负载均衡

Region切分完毕之后,RegionServer上将会存在更多的Region块,为了避免RegionServer热点,使请求负载均衡到集群各个节点上,HMaster将会把一个或多个子Region移动到其他RegionServer上。

移动过程中,如果当前RegionServer繁忙,HMaster将只会修改Region的元数据信息至其他节点,而Region数据仍然保留在当前节点中,直至下一次Major Compaction时进行数据移动。

在这里插入图片描述

至此,我们已经揭开了HBase架构与原理的大部分神秘面纱,在后续做集群规划、性能优化与实际应用中,为什么这么调整以及为什么这么操作都将一一映射到HBase的实现原理上。

如果你希望了解HBase的更多细节,可以参考《HBase权威指南》。

猜你喜欢

转载自blog.csdn.net/dwjf321/article/details/110200892