HBase依赖于Zookeeper
和Hadoop
的,所以在启动HBase前需要启动Zookeeper和Hadoop。
HMaster
用于管理整个HBase集群,即管理每个HRegionServer
,它掌握着整个集群的元数据信息,同时会将相应的数据存储到Zookeeper
(元数据信息、高可用信息等)。
HMaster
的职责:
1)管理用户对Table
的增、删、改、查操作;
2)记录region
在哪台 Hregion server
上;
3)在Region
Split 后,负责新Region
的分配;
4)新机器加入时,管理 HRegion Server
的负载均衡,调整Region分布;
5)在 HRegion Server
宕机后,负责失效 HRegion Server
上的Regions
迁移。
HRegionServer
是每台机器上的一个Java进程(一台机器只有一个HRegionServer
进程),用来处理客户端的读写请求(和Hadoop的DataNode
类似),并维护着元数据信息。
HRegionServer
的职责:
1) HRegion Server
主要负责响应用户I/O请求,向HDFS文件系统中读写数据,是 HBASE中最核心的模块。
2) HRegion Server
管理了很多 table 的分区,也就是region
。
每个HRegionServer
有一个HLog(有且仅有一个)
。HLog
是操作日志,用来做灾难恢复的,当客户端发起一个写请求时,会先往HLog
中写再往Memory Store
中写。假设没有HLog
,我们进行一个写请求,会首先写到Memory Store
上,等到Memory Store
到达一定容量后,才会flush
到StoreFile
中。但是如果在这之前主机断电了呢?那这部分操作的数据全丢失了。这显然不是我们想到的结果,于是就有了HLog
。
每个HRegionServer
里面有多个HRegion
,一个HRegion
对应于HBase
的一张表(也可能是表的一部分,因为表太大了会切分,表和HRegion
的对应关系是一对多),当这张表到一定大小的时候会进行切分,切分成两个HRegion
,切分出来的新的HRegion
会保存到另一台机器上。每个HRegionServer
里面有多个HRegion,可以理解为有多张表。
每个HRegion
里面有多个Store
(一张表中有多个列族),一个Store
对应于HBase一张表的一个列族
,。按照这个原理,我们在设计列族的时候,可以把经常查询的列放在同一个列族,这样可以提高效率,因为同一个列族在同一个文件里面(不考虑切分)。
每个Store
有一个内存级别的存储Memory Store(有且仅有一个)
。当Memory Store达到一定大小或一定时间后会进行数据刷写(flush
),写到磁盘中(即HFile
)。
每个Store
有多个磁盘级别的存储StoreFile
,Memory Store
每刷写一次就形成一个StoreFile
,HFile是StoreFile在HDFS上的存储格式
。
HBase
读原理在上图中,我们模拟一下客户端读取数据过程,假设Zookeeper存放的meta
表在RS1机器上,meta表存放的内容如下,Student表行键范围在1-100的存放在RS4上,在101~200的存放在RS3上,等等。
meta表:
_________________________________________________
| 表名 | rowkey范围 | 所在位置 |
|____________|________________|_________________|
| Student | 1 ~ 100 | RS4 |
|____________|________________|_________________|
| Student | 101 ~ 200 | RS3 |
|____________|________________|_________________|
| Teacher | 1 ~ 500 | RS1 |
|____________|________________|_________________|
| ··· |
|_______________________________________________|
客户端现在要读取Student表的第100行,具体步骤如下:
客户端向Zookeeper
发起请求,请求元数据所在RegionServer
,Zookeeper集群存放的是HBase的meta
表所在位置。
Zookeeper返回给客户端元数据所在RegionServer(即RS1)
。
客户端收到应答后去请求RS1,请求查询Student表的rowkey=100数据所在位置。
在RS1上查询meta表可知该数据在RS4机器上,即返回给客户端rowkey
所在位置(RS4)。
客户端收到应答后去请求RS4读数据。
RS4查询数据返回给客户端。查询时先去内存(MemStore
)查找,因为内存是最新的数据,如果找到了就返回结果,如果没找到则去缓存(blockcache(每个HRegionServer只有一个)
)找,如果找到了就返回结果,如果还没找到就去磁盘(StoreFile
)找,如果在磁盘找到了,则先将结果写入缓存(blockcache),再返回给客户端,写入缓存是为了下次查询提高效率。
注:blockcache中存储数据来源即Hfile的标记,在读blockcache后,再进行读取未标记的Hfile与MemStore进行数据合并取对应版本数据,返回并保存至blockcache,再返回至client
在整个读过程中HMaster
并没有参与,即读流程与HMaster
无关,所以如果HMaster挂了,也是可以读数据的。
HBase
写原理HBase的写是比读快的,为什么呢,看下面的写过程,同样假设Zookeeper存放的meta表在RS1机器上,meta表存放的内容如下,Student表行键范围在1100的存放在RS4上,在101200的存放在RS3上,等等。
meta表:
meta表:
_________________________________________________
| 表名 | rowkey范围 | 所在位置 |
|____________|________________|_________________|
| Student | 1 ~ 100 | RS2 |
|____________|________________|_________________|
| Student | 101 ~ 200 | RS1 |
|____________|________________|_________________|
| Teacher | 1 ~ 500 | RS1 |
|____________|________________|_________________|
| ··· |
|_______________________________________________|
客户端现在要插入数据给Student表,其中rowkey=100,具体步骤如下:
客户端向Zookeeper发起请求,请求元数据所在RegionServer,Zookeeper集群存放的是HBase的meta表所在位置。
Zookeeper返回给客户端元数据所在RegionServer(即RS1)。
客户端收到应答后去请求RS1,请求查询Student表的rowkey=100数据所在位置。
在RS1上查询meta表可知该数据在RS2机器上,即返回给客户端rowkey所在位置(RS2)。
客户端收到应答后去请求RS4写入数据。
RS2收到请求,先将数据写入HLog,再将数据写入MemStore,写入MemStore后就返回给客户端写入成功信息,此时,客户端的写流程完成了。
因为写入内存就结束了写流程,不用访问磁盘,所以总体比读流程是快一点的。
同样,在整个写流程中HMaster也没有参与,所以如果HMaster挂了,也是可以进行写数据的。但是,如果时间长了,表的大小一直变大,而HMaster却挂了,即不会触发Region切分,这样就会导致数据倾斜,系统就变得不安全了。
flush
刷写过程在hbase-default.xml
配置文件中有这么几项配置(见下面)
1.
hbase.hregion.memstore.flush.size
默认值:128M
MemStore 级别限制,当 Region 中任意一个 MemStore 的大小(压缩后的大小)达到了设定值,会触发 MemStore flush。
2.
hbase.regionserver.optionalcacheflushinterval
默认值:3600000
HBase 定期刷新 MemStore,默认周期为1小时,确保 MemStore 不会长时间没有持久化。为避免所有的 MemStore 在同一时间都进行 flush,定期的 flush 操作有 20000 左右的随机延时。
3.
hbase.hregion.memstore.block.multiplier
默认值:2 (3.0版本是4)
Region 级别限制,当 Region 中所有 MemStore 的大小总和达到了设定值(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默认 2* 128M = 256M),会触发 MemStore flush,禁止当前Region读写
4.
hbase.regionserver.global.memstore.upperLimit
默认值:0.4
Region Server 级别限制,当一个 Region Server 中所有 MemStore 的大小总和达到了设定值(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默认 0.4 * RegionServer堆内存大小),会触发全部 MemStore flush,不管MemStore有多小。而且regionserver级别的flush会阻塞客户端读写。
5.
hbase.regionserver.global.memstore.lowerLimit
默认值:0.38
与 hbase.regionserver.global.memstore.upperLimit 类似,区别是:当一个 Region Server 中所有 MemStore 的大小总和达到了设定值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认 0.38 * RS堆内存大小),会触发部分 MemStore flush。
Flush 顺序是按照 Region 的总 MemStore 大小,由大到小执行,先操作 MemStore 最大的 Region,再操作剩余中最大的 Region,直至总体 MemStore 的内存使用量低于设定值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize)。
6.
hbase.regionserver.maxlogs
默认值:32
当一个 Region Server 中 HLog 数量达到设定值,系统会选取最早的一个 HLog 对应的一个或多个 Region 进行 flush。
当增加 MemStore 的大小以及调整其他的 MemStore 的设置项时,也需要去调整 HLog 的配置项。否则,WAL的大小限制可能会首先被触发。因而,将利用不到其他专门为Memstore而设计的优化。
需要关注的 HLog 配置是 HLog 文件大小,由参数 hbase.regionserver.hlog.blocksize 设置(默认512M),HLog 大小达到上限,或生成一个新的 HLog
通过WAL限制来触发Memstore的flush并非最佳方式,这样做可能会会一次flush很多Region,尽管“写数据”是很好的分布于整个集群,进而很有可能会引发flush“大风暴”。
7.
手动触发
用户可以通过shell命令一下分别对一个 Table 或者一个 Region 进行 flush:
hbase> flush ‘TABLENAME’
hbase> flush ‘REGIONNAME’
8.其他
执行 Compact 和 Split 之前,会进行一次 flush。
需要注意的是HBase的最小flush单元是HRegion
而不是单个MemStore
。
Flush是由HMaste
r触发的,Flush顺序是按照Memstore由大到小执行,先Flush Memstore最大的Region,再执行次大的,直至总体Memstore内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit
)。
1.出现上述(2)的情况,Region 下所有 Memstore 的总大小超过了 MemStore 默认大小的倍数,该 Region 在 flush 完成前会 block 新的更新请求。
2.出现上述(3)的情况,RegionServer 所有 MemStore 占整个堆的最大比例超过 hbase.regionserver.global.memstore.upperLimit 设置值,该 RegionServer 的更新请求会被 block,一直到 MemStore 恢复阈值一下。
更新被阻塞对单个节点和整个集群的影响都很大,需要关注 MemStore 的大小和 Memstore Flush Queue 的长度。
为了减少 flush 过程对读写的影响,HBase 采用了类似于两阶段提交的方式,将整个 flush 过程分为三个阶段:
prepare 阶段:遍历当前 Region 中的所有 MemStore,将 MemStore 中当前数据集 kvset 做一个快照 snapshot,然后再新建一个新的 kvset,后期的所有写入操作都会写入新的 kvset 中。整个 flush 阶段读操作读 MemStore 的部分,会分别遍历新的 kvset 和 snapshot。prepare 阶段需要加一把 updateLock 对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
flush 阶段:遍历所有 MemStore,将 prepare 阶段生成的 snapshot 持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时。
commit 阶段:遍历所有的 MemStore,将 flush 阶段生成的临时文件移到指定的 Column family 目录下,生成对应的 Storefile(HFile) 和 Reader,把 Storefile 添加到 HStore 的 Storefiles 列表中,最后再清空 prepare 阶段生成的 snapshot。
由于在flush过程中,可能会产生很多小文件(这很好理解,比如有两个MemStore,一个很大,一个很小,然后就触发了flush操作,那么那个小的就形成了小文件),我们都知道,HDFS不适合存储小文件,所以在写入HDFS之前会进行合并操作。
在hbase-default.xml
配置文件中有这么几项配置:
hbase.hregion.majorcompaction
:一个region进行 major compaction合并的周期,在这个点的时候, 这个region下的所有hfile会进行合并,默认是7天,major compaction非常耗资源,建议生产关闭(设置为0),在应用空闲时间手动触发。
hbase.hstore.compactionThreshold
:一个store里面允许存的hfile的个数,超过这个个数会被写到新的一个hfile里面 也即是每个region的每个列族对应的memstore在fulsh为hfile的时候,默认情况下当超过3个hfile的时候就会对这些文件进行合并重写为一个新文件,设置个数越大可以减少触发合并的时间,但是每次合并的时间就会越长。
在我们利用shell命令或者API删除数据的时候,数据并没有被删除,而是被打上标记
,而是在这里的compaction合并过程中才会被完全删除。
转自:https://blog.csdn.net/hzj1998/article/details/99116931
Region自动切分是HBase能够拥有良好扩张性的最重要因素之一,也必然是所有分布式系统追求无限扩展性的一副良药。
Region切分触发策略 在最新稳定版(1.2.6)中,HBase已经有多达6种切分触发策略。当然,每种触发策略都有各自的适用场景,用户可以根据业务在表级别选择不同的切分触发策略。常见的切分策略如下图:
ConstantSizeRegionSplitPolicy
:0.94版本前默认切分策略
。这是最容易理解但也最容易产生误解的切分策略,从字面意思来看,当region大小大于某个阈值(hbase.hregion.max.filesize)之后就会触发切分,实际上并不是这样,真正实现中这个阈值是对于某个store
来说的,即一个region中最大store的大小大于设置阈值之后
才会触发切分。store大小为压缩后的文件大小
(采用压缩的场景)。
弊 端: 切分策略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1个,这对业务来说并不是什么好事。如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的region,这对于集群的管理、资源使用、failover来说都不是一件好事。
IncreasingToUpperBoundRegionSplitPolicy
:0.94版本~2.0版本默认切分策略
。这种切分策略微微有些复杂,总体来看和ConstantSizeRegionSplitPolicy思路相同,一个region中最大store大小大于设置阈值就会触发切分。但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是会在一定条件下不断调整,调整规则和region所属表在当前regionserver上的region个数有关系 :(#regions) * (#regions) * (#regions) * flush size * 2
,当然阈值并不会无限增大,最大值为用户设置的MaxRegionFileSize
。
优点: 这种切分策略很好的弥补了ConstantSizeRegionSplitPolicy的短板,能够自适应大表和小表。而且在大集群条件下对于很多大表来说表现很优秀,但并不完美。
弊 端: 这种策略下很多小表会在大集群中产生大量小region,分散在整个集群中。而且在发生region迁移时也可能会触发region分裂。
SteppingSplitPolicy
:2.0版本默认切分策略
。这种切分策略的切分阈值又发生了变化,相比IncreasingToUpperBoundRegionSplitPolicy简单了一些,依然和待分裂region所属表在当前regionserver上的region个 数有关系,如果region个数等于1,切分阈值为flush size * 2,否则为MaxRegionFileSize。这种切分策略对于大集群中的大表、小表会比IncreasingToUpperBoundRegionSplitPolicy更加友好,小表不会再产生大量的小region,而是适可而止。
另外, 还有一些其他分裂策略, 比如使用
DisableSplitPolicy
: 可以禁止region 发生分裂; 而KeyPrefixRegionSplitPolicy ,DelimitedKeyPrefixRegionSplitPolicy 对 于 切 分 策 略 依 然 依 据 默 认 切 分 策 略 , 但 对 于 切 分 点 有 自 己 的 看 法 , 比 如KeyPrefixRegionSplitPolicy要求必须让相同的PrefixKey待在一个region中。
在用法上,一般情况下使用默认切分策略即可,也可以在cf级别设置region切分策略,命令为: create’table’{NAME=>‘cf’,SPLIT_POLICY=>‘org.apache.hadoop.hbase.regionserver. ConstantSizeRegionSpli
region切分策略会触发region切分,切分开始之后的第一件事就是寻找切分点-splitpoint。所有默认切分策略,无论是ConstantSizeRegionSplitPolicy、IncreasingToUpperBoundRegionSplitPolicy抑或是SteppingSplitPolicy,对于切分点的定义都是一致的。当然,用户手动执行切分时是可以指定切分点进行切分的,这里并不讨论这种情况。
那切分点是如何定位的呢?整个region中最大store中的最大文件中最中心的一个block的首个rowkey。这是一句比较消耗脑力的语句,需要细细品味。另外,HBase还规定,如果定位到的rowkey是整个文件的首个rowkey或者最后一个rowkey的话,就认为没有切分点。
什么情况下会出现没有切分点的场景呢?最常见的就是一个文件只有一个block,执行split的时候就会发现无法切分。很多新同学 在测试split的时候往往都是新建一张新表,然后往新表中插入几条数据并执行一下flush,再执行split,奇迹般地发现数据表并没有真正执行切分。原因就在这里,这个时候仔细的话你翻看debug日志是可以看到这样的日志滴:
HBase将整个切分过程包装成了一个事务,意图能够保证切分事务的原子性。整个分裂事务过程分为三个阶段:prepare – execute– (rollback) ,操作模版如下:
prepare阶段: 在内存中初始化两个子region,具体是生成两个HRegionInfo对象,包含tableName、regionName、startkey、endkey等。同时会生成一个transaction journal,这个对象用来记录切分的进展,具体见rollback阶段。
execute阶段:切分的核心操作。见下图(来自Hortonworks):
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 [StoreOpene-0155388346c3c919d3f05d7188e885e0-1] regionserver.StoreFileInfo: reference’hdfs://hdfscluster/hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698143e5c4d9dc00 其中reference文件名为d24415c4fb44427b8f698143e5c4d9dc.00bb6239169411e4d0ecb6ddfdbacf66,格式看起来比较特殊,那这种文件名具体什么含义呢?那来看看该reference文件指向的父region文件,根据日志可以看到,切分的父region是00bb6239169411e4d0ecb6ddfdbacf66,对应的切分文件是d24415c4fb44427b8f698143e5c4d9dc,可见reference文件名是个信息量很大的命名方式,如下所示:
除此之外,还需要关注reference文件的文件内容,reference文件是一个引用文件(并非linux链接文件),文件内容很显然不是用户数据。文件内容其实非常简单, 主要有两部分构成: 其一是切分点splitkey, 其二是一个boolean类型的变量( true 或者false),true表示该reference文件引用的是父文件的上半部分(top),而false表示引用的是下半部分 (bottom)。为什么存储的是这两部分内容?且听下文分解。
看官可以使用hadoop命令亲自来查看reference文件的具体内容: hadoopdfs-cat/hbase-rsgroup/data/default/music/0155388346c3c919d3f05d7188e885e0/cf/d24415c4fb44427b8f698 6. 父region分裂为两个子region后,将daughter A、daughter B拷贝到HBase根目录下,形成两个新的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在可用性方面最值得期待的一个亮点!!!
Region切分对其他模块的影响
通过region切分流程的了解,我们知道整个region切分过程并没有涉及数据的移动,所以切分成本本身并不是很高,可以很快完成。切分后子region的文件实际没有任何用户数据,文件中存储的仅是一些元数据信息-切分点rowkey等,那通过引用文件如何查找数据呢?子region的数据实际在什么时候完成真正迁移?数据迁移完成之后父region什么时候会被删掉?
这里就会看到reference文件名、文件内容的实际意义啦。整个流程如下图所示:
(1)根据reference文件名(region名+真实文件名)定位到真实数据所在文件路径。
(2)定位到真实数据文件就可以在整个文件中扫描待查KV了么?非也。因为reference文件通常都只引用了数据文件的一半数据, 以切分点为界,要么上半部分文件数据,要么下半部分数据。那到底哪部分数据?切分点又是哪个点?还记得上文又提到reference 文件的文件内容吧,没错,就记录在文件中。
答案是子region发生major_compaction时,我们知道compaction的执行实际上是将store中所有小文件一个KV一个KV从小到大读出来之后再顺序写入一个大文件,完成之后再将小文件删掉,因此compaction本身就需要读取并写入大量数据。子region执行major_compaction后会将父目录中属于该子region的所有数据读出来并写入子region目录数据文件中。可见将数据迁移放到compaction这个阶段来做,是一件顺便的事。
实际上HMaster会启动一个线程定期遍历检查所有处于splitting状态的父region,确定检查父region是否可以被清理。检测线程首先会在meta表中揪出所有split列为true的region,并加载出其分裂后生成的两个子region(meta表中splitA列和splitB列),只需要检查此两个子region是否还存在引用文件,如果都不存在引用文件就可以认为该父region对应的文件可以被删除。现在再来看看上文中父目录在meta表中的信息,就大概可以理解为什么会存储这些信息了:
4. split模块在生产线的一些坑?
有些时候会有同学反馈说集群中部分region处于长时间RIT,region状态为spliting。通常情况下都会建议使用hbck看下什么报错, 然后再根据hbck提供的一些工具进行修复,hbck提供了部分命令对处于split状态的rit region进行修复,主要的命令如下: -fixSplitParents Try to force offline split parents to be online.-removeParents Try to offline and sideline lingering parents and keep daughter regions.-fixReferenceFiles Try to offline lingering reference store files
其中最常见的问题是 : ERROR:Foundlingeringreferencefilehdfs://mycluster/hbase/news_user_actions/3b3ae24c65fc5094bc2acfebaa7a56de/ 简单解释一下,这个错误是说reference文件所引用的父region文件不存在了,如果查看日志的话有可能看到如下异常:java.io.IOException: java.io.IOException: java.io.FileNotFoundException: File does not exist:/hbase/news_user_actions/b7
父region文件为什么会莫名其妙不存在?经过和朋友的讨论,确认有可能是因为官方bug导致,详见HBASE-13331。这个jira是说HMaster在确认父目录是否可以被删除时,如果检查引用文件(检查是否存在、检查是否可以正常打开)抛出IOException异常, 函数就会返回没有引用文件,导致父region被删掉。正常情况下应该保险起见返回存在引用文件,保留region,并打印日志手工介入查看。如果大家也遇到类似的问题,可以看看这个问题,也可以将修复patch打到线上版本或者升级版本。