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的最终执行者。
KeyValue中Key由RowKey,ColumnFamily,Qualifier ,TimeStamp,KeyType等5部分组成,HBase设定Key大小:
最直接的解释是scan的结果需要由小到大输出给用户,当然,这并不全面,最合理的解释是只有由小到大排序才能使得scan效率最高。举个简单的例子,HBase支持数据多版本,假设用户只想获取最新版本,那只需要将这些数据由最新到最旧进行排序,然后取队首元素返回就可以。那么,如果不排序,就只能遍历所有元素,查看符不符合用户查询条件。这就是排队的意义。
下图是一张表的逻辑视图,该表有两个列族cf1和cf2(我们只关注cf1),cf1只有一个列name,表中有5行数据,其中每个cell基本都有多个版本。cf1的数据假如实际存储在三个区域,memstore中有r2和r4的最新数据,hfile1中是最早的数据。现在需要查询RowKey=r2的数据,按照上文的理论对应的Scanner指向就如图所示:
这三个Scanner组成的heap为
基于HBase的架构,一条数据可能存在RegionServer中的三个不同位置:
RegionServer接收到的一条数据查询请求,只需要从以上三个地方检索到数据即可,在HBase中的检索顺序依次是:BlockCache -> Memstore -> HFiles。
其中,BlockCache、Memstore都是直接在内存中进行高性能的数据检索。
而HFiles则是真正存储在HDFS上的数据:
如何在大量的HFile中快速找到所需要的数据呢?
为了提高检索HFiles的性能,HBase支持使用 Bloom Fliter 对HFiles进行快读定位。
Bloom Filter(布隆过滤器)是一种数据结构,常用于大规模数据查询场景,其能够快速判断一个元素一定不在集合中,或者可能在集合中。
HBase中支持使用以下两种Bloom Filter:
两者的区别仅仅是:是否使用列信息作为Bloom Filter的条件。
使用ROWCOL时,可以让指定列的查询更快,因为其通过Rowkey与列信息来过滤不存在数据的HFile,但是相应的,产生的Bloom Filter数据会更加庞大。
而只通过Rowkey进行检索的查询,即使指定了ROWCOL也不会有其他效果,因为没有携带列信息。
通过Bloom Filter(如果有的话)快速定位到当前的Rowkey数据存储于哪个HFile之后(或者不存在直接返回),通过HFile携带的 Data Block Index 等元数据信息可快速定位到具体的数据块起始位置,读取并返回(加载到缓存中)。
这就是Bloom Filter在HBase检索数据的应用场景:
当然,如果没有指定创建Bloom Filter,RegionServer将会花费比较多的力气一个个检索HFile来判断数据是否存在。
服务器端RegionServer接收到客户端的写入请求后,首先会反序列化为Put对象,然后执行各种检查操作,比如检查region是否是只读、memstore大小是否超过blockingMemstoreSize等。检查完成之后,就会执行如下核心操作:
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 个字节,越短越好。
原因有两点:
RowKey 散列原则
设计的 RowKey 应均匀的分布在各个 HBase节点上。
拿时间戳举例,假如 RowKey 是按系统时间戳的方式递增,时间戳信息作为RowKey 的第一部分,将造成所有新数据都在一个 RegionServer 上堆积,也就是通常说的 Region 热点问题。
热点发生在大量的 client 直接访问集中在个别 RegionServer 上(访问可能是读,写或者其他操作),导致单个 RegionServer 机器自身负载过高,引起性能下降甚至 Region 不可用,常见的是发生 jvm full gc 或者显示 region too busy 异常情况,当然这也会影响同一个 RegionServer 上的其他 Region。
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。
hbase:meta 表存储了集群中所有Region的位置信息。
HBase中可以通过设置WAL的持久化等级决定是否开启WAL机制、以及HLog的落盘方式。WAL的持久化等级分为如下四个等级:
用户可以通过客户端设置WAL持久化等级,代码:put.setDurability(Durability. SYNC_WAL );
HBase整体架构中HMaster的功能与职责如下:
RegionServer在HBase集群中的功能与职责:
一个RegionServer中存储并管理者多个Region,是HBase集群中真正 存储数据、接受读写请求 的地方,是HBase架构中最核心、同时也是最复杂的部分。
BlockCache为RegionServer中的 读缓存,一个RegionServer共用一个BlockCache。
HFile是HDFS上的文件(或大或小都有可能),现在HBase面临的一个问题就是如何在HFile中 快速检索获得指定数据?
HBase随机查询的高性能很大程度上取决于底层HFile的存储格式,所以这个问题可以转化为 HFile的存储格式该如何设计,才能满足HBase 快速检索 的需求。
Memstore内存中的数据在刷写到磁盘时,将会进行以下操作:
至此,已经完成了第一个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已经生成,如下图所示:
生成HFile之后该如何使用呢?
HFile的索引数据(包括 Bloom Filter索引和数据索引信息)会在 Region Open 的时候被加载到读缓存中,之后数据检索经过以下过程:
同时,得益于 分层索引 与 分块存储,在Region Open加载索引数据的时候,再也不必和老版本(0.9甚至更早,HFile只有一层数据索引并且统一存储)一样加载所有索引数据到内存中导致启动缓慢甚至卡机等问题。
数据分布不均匀。同一 region server 上数据文件越来越大,读请求也会越来越多。一旦所有的请求都落在同一个 region server 上,尤其是很多热点数据,必然会导致很严重的性能问题。
compaction性能损耗严重。compaction本质上是一个排序合并的操作,合并操作需要占用大量内存,因此文件越大,占用内存越多。另一方面,compaction有可能需要迁移远程数据到本地进行处理(balance之后的compaction就会存在这样的场景),如果需要迁移的数据是大文件的话,带宽资源就会损耗严重。
太大文件读取效率也会受到影响。
通过切分,一个region变为两个近似相同大小的子region,再通过balance机制均衡到不同 region server上,才能使系统资源使用更加均衡。
一是用户通过 Api 请求 split,HBase Admin 手动执行 split 命令时,会触发 Split;
二是非用户请求:
这两类操作之后都会针对相应region生成一个requestSplit请求,requestSplit首先会执行checkSplit,检测store size是否达到阈值(具体算法见下面分析),如果超过阈值,就进行切分。
目前已经的支持触发策略多达6种,每种触发策略都有各自的适用场景,可以根据业务在表级别(Column family 级别)选择不同的切分触发策略。一般情况下使用默认切分策略即可。
将一个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阶段。
region server 更改ZK节点 /region-in-transition 中该region的状态为SPLITING。
master通过watch节点/region-in-transition检测到region状态改变,并修改内存中region的状态,在master页面RIT模块就可以看到region执行split的状态信息。
region 在存储目录下新建临时文件夹.split保存split后的daughter region信息。
parent region关闭数据写入并触发flush操作,将写入region的数据全部持久化到磁盘,此后短时间内客户端落在父region上的请求都会抛出异常NotServingRegionException。
核心分裂步骤:在.split文件夹下新建两个子文件夹,称之为daughter A、daughter B,并在文件夹中生成引用文件,分别指向父region中对应文件。生成reference文件名如下所示:
reference文件是一个引用文件(并非linux链接文件),文件内容很显然不是用户数据。文件内容其实非常简单, 主要有两部分构成: 其一是切分点splitkey, 其二是一个boolean类型的变量( true 或者false),true表示该reference文件引用的是父文件的上半部分(top),而false表示引用的是下半部分 (bottom)。
将daughter A、daughter B拷贝到HBase根目录下,形成两个新的region。
parent region通知修改 hbase.meta 表后下线,不再提供服务。下线后parent region在meta表中的信息并不会马上删除, 而是标注split列、offline列为true,并记录两个子region。
开启daughter A、daughter B两个子region。
通知修改 hbase.meta 表,正式对外提供服务。
3)rollback阶段:如果execute阶段出现异常,则执行rollback操作。为了实现回滚,整个切分过程被分为很多子阶段,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据。代码中使用 JournalEntryType 来表征各个子阶段,具体见下图:
根据文件名来判断是否是 reference 文件:
执行 Region Split 过程不涉及数据的移动,所以可以很快完成。新生成的子 Region 文件中没有任何用户数据,而是一个 reference 文件,文件中存储的是一些元数据信息,包括切分点的 Rowkey 等。引入了以下问题:
Compaction 会从一个region的一个store中选择一些hfile文件进行合并。合并说来原理很简单,先从这些待合并的数据文件中读出KeyValues,再按照由小到大排列后写入一个新的文件中。
HBase Compaction操作是为了数据读取做的优化,总的来说是以牺牲磁盘io来换取读性能的基本稳定。Compaction操作分为minor compaction与major compaction,其中major compaction消耗资源较大、对读写请求有一定影响,因此一般是禁用自动周期性执行而选择业务低峰期时手动执行。
HBase 是基于一种LSM-Tree(Log-Structured Merge Tree)存储模型设计的,写入路径上是先写入WAL(Write-Ahead-Log)即预写日志,再写入 memstore 缓存,满足一定条件后执行 flush 操作将缓存数据刷写到磁盘,生成一个 HFile 数据文件。随着数据不断写入,磁盘 HFile 文件就会越来越多,文件太多会影响HBase查询性能,主要体现在查询数据的 io 次数增加。为了优化查询性能,HBase 会合并小的 HFile 以减少文件数量,这种合并 HFile 的操作称为 Compaction,这也是为什么要进行 Compaction 的原因。
HBase Compaction分为两种:Minor Compaction 与 Major 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后触发,但不会对线上读写请求造成太大延迟影响。
相对于Minor Compaction 只合并选择的一部分HFile合并、合并时只简单合并数据文件的特点,Major Compaction:指将所有的 StoreFile 合并成一个 StoreFile(会过滤到达 maxSize 的 HFile),这个过程会清理三类没有意义的数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。
另外,一般情况下,major compaction时间会持续比较长,整个过程会消耗大量系统资源,对上层业务有比较大的影响。因此线上业务都会将关闭自动触发major compaction 功能,改为手动在业务低峰期触发。
Major Compaction定期执行的条件由以下两个参数控制:
集群中各个RegionServer将会在 hbase.hregion.majorcompaction ± hbase.hregion.majorcompaction * hbase.hregion.majorcompaction.jitter 的区间浮动进行Major Compaction,以避免过多RegionServer同时进行,造成较大影响。
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会不做很多自动化检查,直接执行合并。
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设置。
一旦触发,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 对象当中。
这部分具体操作被封装在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。
因此,通过设置hbase.hregion.majorcompaction = 0可以关闭CompactionChecke触发的major compaction,但是无法关闭用户调用级别的mc。
**过滤对于大文件进行Compaction 操作。**判断fileToCompact队列中的文件是否超过了maxCompactSize,如果超过,则过滤掉该文件,避免对于大文件进行compaction。
如果确定 Minor Compaction 方式执行,会检查经过过滤过的fileToCompact的大小是否满足minFilesToCompact 最低标准,如果不满足,忽略本次操作。确定执行的Minor Compaction的操作时,会使用一个smart算法,从filesToCompact当中选出匹配的storefiles。
通过selectCompaction 选出的文件,加入到filesCompacting队列中。
创建compactionRequest,提交请求。
第一步:获取需要合并的HFile列表
获取列表的时候需要排除掉带锁的HFile。
锁分两种:写锁(write lock)和读锁(read lock)。当HFile正在进行以下操作的时候会上锁:
第二步:由列表创建出StoreFileScanner
HRegion会创建出一个Scanner,用这个Scanner来读取本次要合并的所有StoreFile上的数据。
第三步:把数据从这些HFile中读出,并放到tmp目录(临时文件夹)
HBase会在临时目录中创建新的HFile,并使用之前建立的Scanner从旧HFile上读取数据,放入新HFile。以下两种数据不会被读取出来:如果数据过期了(达到TTL所规定的时间),那么这些数据不会被读取出来。
如果是 Major Compaction,那么数据带了墓碑标记也不会被读取出来。
第四步:用合并后的HFile来替换合并前的那些HFile
最后用临时文件夹内合并后的新HFile来替换掉之前的那些HFile文件。过期的数据由于没有被读取出来,所以就永远地消失了。如果本次合并是Major Compaction,那么带有墓碑标记的文件也因为没有被读取出来,就真正地被删除掉了。
其实HBase一直拖到majorCompaction的时候才真正把带墓碑标记的数据删掉,并不是因为性能要求,而是之前真的做不到。
HBase是建立在HDFS这种只有增加删除而没有修改的文件系统之上的,所以就连用户删除这个动作,在底层都是由新增实现的:
现在会遇到一个问题:当用户删除数据的时候之前的数据已经被刷写到磁盘上的另外一个HFile了。这种情况很常见,也就是说,墓碑标记和原始数据这两个KeyValue 压根可能就不在同一个HFile上。
在查询的时候Scan指针其实是把所有的HFile都看过了一遍,它知道了有这条数据,也知道它有墓碑标记,而在返回数据的时候选择不把数据返回给用户,这样在用户的Scan操作看来这条数据就是被删掉了。
如果你可以带上RAW=>true参数来Scan,你就可以查询到这条被打上墓碑标记的数据。
这是因为当数据达到TTL的时候,并不需要额外的一个KeyValue来记录。合并时创建的Scan在查询数据的时候,根据以下公式来判断cell是否过期:
当前时间 - cell 的 timestamp > TTL
如果过期了就不返回这条数据。这样当合并完成后,过期的数据因为没有被写入新文件,自然就消失了。
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。
对于Region的大小,HBase官方文档推荐单个在10G-30G 之间,单台RegionServer的数量控制在20-300之间(当然,这仅仅是参考值)。
Region过大过小都会有不良影响:
规划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。
得到以下配置项:
RegionServer中的BlockCache有两种实现方式:
我们可以根据可用内存大小来判断使用哪种内存模式。
1)先看 超小内存(假设8G以下) 和 超大内存(假设128G以上) 两种极端情况:
对于超小内存来说,即使可以使用BucketCache来利用堆外内存,但是使用堆外内存的主要目的是避免GC时不稳定的影响,堆外内存的效率是要比堆内内存低的。由于内存总体较小,即使读写缓存都在堆内内存中,GC时也不会造成太大影响,所以可以直接选择LRUBlockCache。
对于超大内存来说,在超大内存上使用LRUBlockCache将会出现我们所担忧的情况:GC时对线上造成很不稳定的延迟影响。这种场景下,应该尽量利用堆外内存作为读缓存,减小堆内内存的压力,所以可以直接选择BucketCache。
2)在两边的极端情况下,我们可以根据内存大小选择合适的内存模式,那么如果内存大小在合理、正常的范围内该如何选择呢?
此时我们应该主要关注业务应用的类型。
当业务主要为写多读少型应用时,写缓存利用率高,应该使用LRUBlockCache尽量提高堆内写缓存的使用率。
当业务主要为写少读多型应用时,读缓存利用率高(通常也意味着需要稳定的低延迟响应),应该使用BucketCache尽量提高堆外读缓存的使用率。
3)对于不明确或者多种类型混合的业务应用,建议使用BucketCache,保证读请求的稳定性同时,堆内写缓存效率并不会很低。
如果 HBase可使用的内存高达153G,故将选择BucketCache的内存模型来配置HBase,该模式下能够最大化利用内存,减少GC影响,对线上的实时服务较为有利。
得到配置项:
从 HBase集群规划 引入一个Disk / JavaHeap Ratio 的概念来帮助我们设置内存相关的参数。
理论上我们假设 最优 情况下 硬盘维度下的Region个数 和 JavaHeap维度下的Region个数 相等。
相应的计算公式为:
其中默认配置下:
现在我们已知条件 硬盘维度和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的硬盘大小最合适。
BucketCache模式下,RegionServer的内存划分如下图:
简化版:
从架构原理中知道,Memstore有6种情况的Flush,需要我们关注的是 Memstore、Region和RegionServer级别的刷写。
其中Memstore和Region级别的刷写并不会对线上造成太大影响,但可以控制其阈值和刷写频次来进一步提高性能。
而RegionServer级别的刷写将会阻塞请求直至刷写完成,对线上影响巨大,需要尽量避免。
配置项:
读缓存由 堆内的LRU元数据 与 堆外的数据缓存 组成,两部分占比一般为 1:9(经验值)
而对于总体的堆内内存,存在以下限制,如果超出此限制则应该调低比例:
LRUBlockCache + MemStore < 80% * JVM_HEAP
配置堆外缓存涉及到的相关参数如下: