Hbase内部探险
前言
以下内容我将从Hbase的数据存储、region等这些方面入手,进行一个比较全面的总结
在进入Hbase之前,我们先拓展几个api
mutation方法
根据前面提到的方法,如果我们想在一行种添加一列的同时删除一列。可以新建一个Put来添加,新建一个Delete来删除。不过这两行代码分两步执行,并不是一个原子操作。那么我要保证这两个操作同时成功或失败,该怎么做呢?
Table接口提供了一个mutateRow方法来把两个操作作为一个原子操作。为什么不叫方法Mutation呢?顾名思义,两个操作必须是同一rowkey才可以。而我们发现Mutation这个类是Put、Delete、Append、Increment类的父 类。
checkAndMutate方法
put有checkAndPut,delete有checkAndDelete,自然的mutate肯定 也有checkAndMutate方法。只是checkAndMutate不会针对每个操作都去 check一次,所有的操作只会在一开始check一次给出的value跟数据库 中现有的value是否一致(即两个value的关系是否一致,可以是非等关系)
批量操作
当需要一次性操作很多条数据的时候,很多人的第一反应应该是循 环调用put、get、delete方法。不过Hbase给我们提供了一个批量操作的方法batch。因为Put、Get、Delete都实现了Row接口。
当使用batch()功能执行批量put操作时,Put实例不会被写入到客户端缓冲区中,batch()请求是同步的,会把操作直接发往服务器,注意不要将对一行的操作放到一个batch中,因为batch中的操作是无顺序的。
早期的batch方法中可以不传results接收返回结果的集合。不过这种方法在执行的过程中,如果发生异常,则没有返回结果。我们提倡的是batch(actions, results),只传actions的已经被废弃了。
显示调用行锁(Row Lock)的方法已经被废弃,所以我们不提这个。
我们也可以给put、get、delete传入一个list的操作集合来实现批量操作,这个也是基于batch实现的。
BufferedMutator方法
说到BufferedMutator必须得先提一下客户端写缓冲区,是把 多个Put操作攒到一起通过单个RPC请求发送给客户端,目的是节省网络 握手带来的IO消耗。调用 HTable.setAutoFlush(false)来开启。不过0.99版本后,这个特性已经被废弃了。新的模式鼓励大家创建一个新的Table实例,用完即释放(轻量级)。
但是客户端的写缓冲区还是存在,不过转向了BufferedMutator。这个类更多地是被HBase内部调用,大部分情况下我们不需要 直接调用到BufferedMutator。交给客户端自己判断就可以了。
内部探险
Hbase是怎么存储数据的
预写日志
数据到达Region的时候是先写入WAL,然后再被加载到 Memstore的。就算Region的机器宕掉了,由于WAL的数据是存储在HDFS 上的,所以数据并不会丢失。
-
WAL默认打开,可以调用Mutation的setDurability(Durability.SKIP_WAL)来关闭WAL,put、get、delete也有这个方法,但最好不要这样做。
-
延迟、同步写入WAL
除了关闭WAL来提高性能,还可以异步提交。也是使用setDurability这个方法。默认异步提交的时间间隔是1s。如果你的系统对一致性要求不大,且系统瓶颈出现在WAL上时可以使用。
-
WAL滚动
WAL的检查间隔由hbase.regionserver.logroll.period定义,默认 值为1小时。检查的内容是把当前WAL中的操作跟实际持久化到HDFS上的 操作比较,看哪些操作已经被持久化了,被持久化的操作就会被移动到.oldlogs文件夹内(这个文件夹也是在HDFS上的)。WAL文件的最大数量通过hbase.regionserver.maxlogs(默认是32)参数来定义
其他触发滚动条件:
1)当WAL所在的块快要满的时候。
2)当WAL所占的空间大于某个阙值的时候(默认是0.95)
-
WAL文件创建出来存在哪
WAL文件被创建出来后会放在/hbase/.log下(这里说的路径都是基 于HDFS),一旦WAL文件被判定为要归档,则会被移动 到/hbase/.oldlogs文件夹。当.oldlogs没有引用时会删除日志文件.
目前有两种进程会引用WAL,一种是TTL超时(默认10分钟),一种是备份(指不同集群之间的备份)
Memstore
数据在进入HFile之前已经被存储到HDFS一次了,为什么还需要放入Memstore?这是因为HDFS上的文件只能创建、追加、删除,不能修改。对一个数据库来说,按顺序存放数据是非常重要的,这是性能的保证。所以我们需要在内存中进行整理再写入磁盘。所以Memstor是作用不是提高性能,而是维持数据按照rowkey排列,并不是一个缓存。
Memstore是LSM存储设计的必要组件。LSM数是B+树的改进,它的核心是“尽量保证数据的顺序性,并且会有频率的对数据进行整理,而顺序性就可以保证读取数据的最大稳定性”。
HFile
HFile是数据存储的实际载体。它模仿了BigTable的SStable格式。HFile是由一个一个块组成的,每个块的大小为64kb。
HFile的块类型如下:
Data:数据块。每个HFile有多个Data块。我们存储在HBase表中 的数据就在这里。Data块其实是可选的,但是几乎很难看到不包 含Data块的HFile。
Meta:元数据块。Meta块是可选的,Meta块只有在文件关闭的时 候才会写入。Meta块存储了该HFile文件的元数据信息,在v2之 前布隆过滤器(Bloom Filter)的信息直接放在Meta里面存储, v2之后分离出来单独存储。不过你现在暂时不需要了解布隆过滤 器是什么东西。
FileInfo:文件信息,其实也是一种数据存储块。FileInfo是 HFile的必要组成部分,是必选的。它只有在文件关闭的时候写 入,存储的是这个文件的信息,比如最后一个Key(Last Key),平均的Key长度(Avg Key Len)等。 DataIndex:存储Data块索引信息的块文件。索引的信息其实也 就是Data块的偏移值(offset)。DataIndex也是可选的,有 Data块才有DataIndex。
MetaIndex:存储Meta块索引信息的块文件。MetaIndex块也是可 选的,有Meta块才有MetaIndex。
Trailer:必选的,它存储了FileInfo、DataIndex、MetaIndex 块的偏移值。
Data块中第一位存的是块类型。后面存储的是多个KeyValue键值对,也是单元格(Cell)的实现类。Cell是一个接口,KeyValue是它的实现类。
那么HFile和StoreFile的关系是什么?其实这两种说法都对,HFile是物理存储的叫法,而StoreFile就是HFile的抽象类而已。
KeyValue类
一个KeyValue类中最后一部分是存储数据的Value,前面的都是存储和单元格有关的元数据信息。我们可以通过适当的压缩算法来节省空间。
增删改查的真面目
我们知道Hbase是一个随机读写的数据库,而它基于的HDFS是一个不能修改的系统。那么Hbase是怎么实现数据的增删改查的?其实Hbase总是在做增加操作。当你删除时,还是会增加一条数据,只是这条数据没有Value。我们把它叫做墓碑标记。
由于数据库在使用的过程中由很多增删改查操作,数据的连续性和顺序性必然会被破坏。所以每隔一段时间,Hbase会进行一次合并(Compact),合并对象为HFile。合并分两种,MInor和Major。合并的时候会忽略被打上墓碑标记的这条记录(墓碑标记和数据不在一块)。
Region的定位
Region就是HBase架构的灵魂。HBase的大部分工作基本都围绕着 Region展开。现在我们来详细说说Client在读写的时候是怎么定位到 RegionServer的。
0.96之前,Region的查找被称为三层架构。 -ROOT => .META => Region
.META是元数据表,记录了Region的起始行、结束行和该Region的连接信息。存在RegionServer上
-ROOT是存储.META的一张表。存在zk上
从0.96版本后这个-ROOT被去掉了,把META所在RegionServer的信息存到了zk的的/hbase/meta-region-server上,再后来引入了namespace,META改名为了hbase:meta。在客户端读取的时候hbase:meta会缓存起来。
维护工具即调优
维护工具管理
Admin还提供了维护工具的管理。包括均衡器、规整器、目录管理器。
均衡器
均衡器用于移动Region到不同RegionServer上,用以平摊压力。最早的内置均衡器是SimpleLoadBalancer,后来被StochasticLoadBalancer取代了。
主要的参数:
hbase.balancer.period:均衡器执行周期,默认值为300000毫 秒,即5分钟。均衡器会启动一个叫BalancerChore的线程,该线 程会定时去扫描是否有RegionServer需要做重均衡 (rebalance)。这个定时的间隔就是由hbase.balancer.period 来定义。
hbase.regions.slop:均衡容忍值。这个参数用来判 断RegionServer需要被均衡 (公式为average + (average * slop)region)slop默认0.001
规整器
规整器用于规整Region的尺寸。将与平均值差异较大的Region进行规整。不过这可能会引发拆分/合并风暴。
目录管理器
目录指hbase:meta中存储的region信息。拆分/合并时都会保留原有的目录信息,直到拆分/合并结束后使用目录管理器清除旧的region信息。
Hbase的性能优化
Master和RegionServer的JVM调优
默认的RegionServer的内存为1G,而Memstore占40%。在实际场景下很容易就写阻塞了。我们可以通过指定HBASE_HEAPSIZE来调大Region实例(Master和Region)的内存。不过建议单独调整它们的内存
如果jdk为1.8之前版本的hbase内存泄漏,我们可以调整PermSize和MaxPermSize的参数(128m已经很大了)。
Full GC
随着内存的加大,有一个不容忽视的问题也出现了,那就是JVM的 堆内存越大,Full GC的时间越久。Full GC有时候可以达到好几分钟。 GC的时候JVM会停止响应任何的请求(STW)当zk长时间没有收到regionServer的心跳时会将它标记为宕机,当regionserver结束gc向zk查看时会发现自己已经“被宕机”,这时regionserver会自杀(防止脑裂)。
解决方案
-
ParallelGC和CMS的组合方案
修改$HBASE_HOME/conf/hbase-env.sh,在我们前 面修改xms和xmx的地方加上-XX:+UseParNewGCXX:+UseConcMarkSweepGC
2. G1GC方案
regionserver内存小于4g不考虑使用,最好是32g以上。G1GC策略通过把堆内存划分为多个Region,然后对各个Region单独 进行GC,这样整体的Full GC可以被最大限度地避免。也可以指定最长full gc的时间。
Memstore的专属JVM策略MSLAB
JVM为了避免Full GC带来的问题有一个基于线程的解决方案,叫 TLAB(Thread-Local allocation buffer)。当你使用TLAB的时候,每 一个线程都会分配一个固定大小的内存空间,专门给这个线程使用,当 线程用完这个空间后再新申请的空间还是这么大,这样下来就不会出现
特别小的碎片空间,基本所有的对象都可以有地方放。缺点就是无论你 的线程里面有没有对象都需要占用这么大的内存,其中有很大一部分空 间是闲置的,内存空间利用率会降低。不过能避免Full GC,这些都是 值得的。
Hbase不能直接使用,所以它提供了MSLAB
Region的拆分
自动拆分
1.ConstantSizeRegionSplitPolicy
0.94版本之前hbase只有ConstantSizeRegionSplitPolicy这种方案,Region大小超过10g自动拆分。
2.IncreasingToUpperBoundRegionSplitPolicy(默认)
0.94版本后,增加了IncreasingToUpperBoundRegionSplitPolicy,意为限制不断增长的文件尺寸的策略
3.KeyPrefixRegionSplitPolicy
我们可以自定义拆分点,保证相同前缀的数据分到一个region里。
4.BusyRegionSplitPolicy
如果你的系统经常出现热点问题,而你对性能的要求高,则可以使用这种策略。可以将热点的region拆分开来。
5.DisabledRegionSplitPolicy
永不拆分,不过你可以还是可以手动指定来拆分region。我们需要的话可以关闭自动拆分,手动拆分。手动拆分分为预拆分、强制拆分
预拆分
预拆分(pre-splitting)就是在建表的时候就定义好了拆分点的 算法,所以叫预拆分。使用 org.apache.hadoop.hbase.util.RegionSplitter类来创建表,并传入 拆分点算法就可以在建表的同时定义拆分点算法。
1.HexStringSplit
根据拆分的数量来拆分
2.UniformSplit
根据范围来拆分
3.手动拆分
通过SPLITS参数来手动拆分
强制拆分
除了预拆分和自动拆分以外,你还可以对运行了一段时间的Region 进行强制地手动拆分(forced splits)。方法是调用hbase shell的 split方法
Region的合并
Region可以被拆分,也可以被合并。不过Region的合并(merge) 并不是为了性能考虑的,而更多地是出于维护的目的被创造出来的
1.通过Merge类合并Region(冷合并)
2.热合并
hbase shell提供了一个命令叫online_merge,通过这个方法可以 进行热合并(online_merge)
WAL的优化
一个Region只有一个WAL实例。WAL实例启动后在内存 中维护了一个ConcurrentNavigableMap。ConcurrentNavigableMap中包含了很多各个WAL的引用
优化参数如下
hbase.regionserver.maxlogs:Region中的最大WAL文件数量, 默认值是32。 后来官方自动计算了这个值,取默认值和计算值之间最大的那个值
hbase.regionserver.hlog.blocksize:HDFS块大小,没有默认值,如果你不设定该值,HBase就会直接调用HDFS的API去获取。
hbase.regionserver.logroll.multiplier:WAL文件大小因子。 每一个WAL文件所占的大小通过HDFS块大小*WAL文件大小因子得 出。默认值是0.95。
BlockCache的优化
原理为,读请求到HBase之 后先尝试查询BlockCache,如果获取不到就去HFile(StoreFile)和 Memstore中去获取。如果获取到了则在返回数据的同时把Block块缓存 到BlockCache中。设置hfile.block.cache.size的时候要注意在HBase的内存使用上 有一个规则那就是Memstore + BlockCache的内存占用比例不能超过 0.8(即80%),否则就要报错。因为必须要留20%作为机动空间。
优化方案
1.LRUBlock Cache
淘汰最近最少使用的BlockCache
2.SlabCache
使用堆外内存,SlabCahce把堆外内存按照80%和20%的比例划分为 两个区域:
(1)存放大小约等于1个BlockSize默认值的Block。
(2)存放大小约等于2个BlockSize默认值的Block。
3.Bucket Cache
也使用了堆外内存,不过将内存划分为14块区域。
4.组合策略
前面说了BucketCache那么多好处,那么是不是BucketCache就完爆 LRUCache了?答案是没有,在很多情况下倒是LRUCache完爆 BucketCache。虽然后面有了SlabCache和 BucketCache,但是这些 Cache从速度和可管理性上始终无法跟完全基于内存的LRUCache相媲 美。虽然LRUCache有严重的Full GC问题,HBase一直都没有放弃 LRUCache。所以,还是那句话,不要不经过测试比较就直接换策略
Memstore的优化
如果你开启了BlockCache,那么读取数据的时候会先查询 BlockCache。当BlockCache查询失败后,则会去查询Memstore + HFile 的数据。由于有些数据还未被刷写到HFile中,所以Memstore + HFile 才是所有数据的集合。
Memstore的实现目的不是加速数据写入,而是维持数据结构。
Memstore的刷写
Memstore在以下5种情况下会触发刷写
1.大小达到刷写阙值
当Memstore占用的内存大小达到 hbase.hregion.memstore.flush.size的配置值的时候就会触发一次刷写,
如果你的数据增长得太快了,在还未到达检 查时间之前,数据就达到了hbase.hregion.memstore.flush.size的好 几倍,那么会触发阻塞机制,默认大小512m。
2.整个RegionServer的memstore总和达到阀值
早先时候,控制全局阻塞写入的时候曾经有一个参数叫 hbase.regionserver.global.memstore.upperLimit,这个值要设置的 比lower.limit高一点,当达到这个值的时候阻塞。后来这个参数被废 弃了,HBase直接用hbase.regionserver.global.memstore.size来控制 阻塞阈值了。
3.WAL的数量大于maxLogs
当WAL文件的数量大于maxLogs的时候,也会触发一次刷写。不过这 个时候WAL会报警一下,不过不会阻塞写入
4.Memstore达到刷写时间间隔
hbase.regionserver.optionalcacheflushinterval:memstore刷 写间隔,默认值为3600000,即1个小时
5.手动触发flush
可以通过admin类或者hbase shell来手动触发
HFile的合并
每次memstore的刷写都会产生一 个新的HFile,而HFile毕竟是存储在硬盘上的东西,凡是读取存储在硬 盘上的东西都涉及一个操作:寻址,如果是传统硬盘那就是磁头的移动 寻址,这是一个很慢的动作。当HFile一多,你每次读取数据的时候寻 址的动作就多了,效率就低了。所以为了防止寻址的动作过多,我们要 适当地减少碎片文件,所以需要继续合并操作。
合并的策略(Minor Compaction和Major Compaction)
Minor Compaction:将Store中多个HFile合并为一个HFile。在 这个过程中达到TTL的数据会被移除,但是被手动删除的数据不 会被移除。这种合并触发频率较高。 Major Compaction:合并Store中的所有HFile为一个HFile。在 这个过程中被手动删除的数据会被真正地移除。同时被删除的还 有单元格内超过MaxVersions的版本数据。这种合并触发频率较 低,默认为7天一次。不过由于Major Compaction消耗的性能较 大,你不会想让它发生在业务高峰期,建议手动控制Major
compaction的吞吐量限制参数
由于compaction机制经常给HBase“搞事”。用户在使用的过程中 常常抱怨会莫名地出现IO突然降低的情况,而调查起来一般都是 compaction造成的。但是compaction本身又是不可或缺的,没有 compaction性能更差,而且被删除的数据还不能真正清除。面对这个矛 盾,HBase提供了一个简单的处理方案:通过配置来限制compaction时 占用的IO性能。
合并过程
(1)获取需要合并的HFile列表。
(2)由列表创建出StoreFileScanner。
(3)把数据从这些HFile中读出,并放到tmp目录(临时文件 夹)。
(4)用合并后的HFile来替换合并前的那些HFile
Major Compaction
Minor Compaction的目的是增加读性能,而majorCompaction在 minorCompaction的目的之上还增加了1点:真正地从磁盘上把用户删除 的数据(带墓碑标记的数据)删除掉。ttl超时的数据例外,因为根本没有写入文件
Major的性能消耗特别大,所以建议在非高峰期手动合并。
-----总结自《Hbase不睡觉书》
来源:CSDN
作者:takeuheart
链接:https://blog.csdn.net/takeuherat/article/details/103835129