欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

HBase堆外内存使用解析

时间:2023-05-12
背景

JVM需要申请一块内存用于进程的使用,类、对象、方法等数据均保存在JVM堆栈也就是申请的这块内存之中,JVM也会负责帮我们管理和回收再利用这块内存。

相对的,堆外内存就是直接调用系统malloc分配的内存,这部分内存不属于JVM直接管理,也不受JVM最大内存的限制,通过引用指向这段内存。

堆外内存可以带来更小的GC压力,更少的内存碎片,以及零拷贝方面的性能优化。Hbase在读写路径上都进行了堆外内存的优化。

目前Hbase的堆外内存主要应用在4部分:MSLAB,Bucket cache,RPC,HDFS读写数据。

但是堆外内存需程序主动管理和释放。所以我们需要了解Hbase堆外内存的使用和配置,以尽可能少的减少堆外内存配置不当带来的问题。

Hbase内存管理整体架构

下面主要详细解释Hbase的堆外内存使用部分,按图中的标号分别描述。

Hbase堆外内存分配器

Hbase的堆外内存分两种类型,一种是Netty的NIO ByteBuffer,一种是Java的DirectByteBuffer。(测试性能后者较好)

堆外内存分配器分为3种:

Hbase自定义的仿Netty的堆外内存分配器ByteBuffAllocator,采用引用计数归零后重复利用的机制;

直接用JVM分配的一大块DirectByteBuffer,内部按一定的大小分页,采用淘汰后再重复利用的机制;

采用Netty堆外内存池PooledByteBufAllocator.DEFAULT分配的NIO ByteBuffer,由Netty管理;

1、Hbase自定义的ByteBuffAllocator

目前是RS级别的内存分配器,返回包装了的Java ByteBuffer内存的分配池,分配NIO堆外内存和堆内内存。

如需申请堆外内存块,会优先从池子中拿,池子中不够时创建,已有内存块数量达到最大时,就是申请堆内的内存了。

1.1 应用

目前使用ByteBuffAllocator分配内存的地方有两处:

1、从HFile读出data block和RAM cache中的block。每个data block会尽可能存储在一个堆外内存块中,以高效利用hadoop native lib的checksum。

而不足最小存储堆外内存大小的数据,会在堆内分配。如果表自定义的data block大小大于这里定义的堆外内存块大小,则每个data block可能占用多个堆外内存块和一部分堆内内存。

2、RPC请求response中的数据。读请求过滤出需要读取的cell block后,会用这里的堆外内存块存储数据,所以,这里的堆外内存其实是支持NIO的,

Netty的OutputChannel在返回数据时,不需要复制,可以直接读取这部分堆外内。

3、如果使用SimpleRPCServer,RPC请求request中的数据会也由ByteBuffAllocator分配内存存储。

1.2 引用计数

这里分配的堆外内存,在两个地方会增加引用计数:

创建call;

创建WalEntry;

相应的,堆外内存的生命周期基本与server端处理RPC call及刷wal的生命周期一致。如果没设堆外bucket cache,Call结束后及wal写入文件后,会释放所有内存引用,将内存块重新放到池子中。

1.3 相关配置

hbase.server.allocator.buffer.size:堆外内存块大小。默认是65KB(64KB table设定block大小 + delta,delta取决于最后一条数据的大小)

hbase.server.allocator.max.buffer.count:最大可分配堆外内存块的数量。

hbase.server.allocator.minimal.allocate.size:用最小可分配堆外内存数据的大小。默认为堆外内存块的1/6。

2、Memstore ChunkPool

Hbase基于LSM树模型实现,所有的数据写入操作首先会顺序写入日志HLog,再写入MemStore,当MemStore中数据大小超过阈值之后再将这些数据批量写入磁盘,生成一个新的HFile文件。

MemStore从本质上来看就是一块缓存,可以称为写缓存。先说说memstore在堆上的问题,和由此引发的它的进化。

众所周知在Java系统中,大内存系统总会面临GC问题,MemStore本身会占用大量内存,因此GC的问题不可避免。

不仅如此,Hbase中MemStore工作模式的特殊性更会引起严重的内存碎片,存在大量内存碎片会导致系统看起来似乎还有很多空间,但实际上这些空间都是一些非常小的碎片,已经分配不出一块完整的可用内存,这时会触发长时间的Full GC。

为了优化这种内存碎片可能导致的Full GC,Hbase借鉴了线程本地分配缓存(Thread-Local Allocation Buffer,TLAB)的内存管理方式,通过顺序化分配内存、内存数据分块等特性使得内存碎片更加粗粒度,有效改善Full GC情况。具体实现步骤如下:

1)每个MemStore会实例化得到一个MemStoreLAB对象。

2)MemStoreLAB会申请一个2M大小的Chunk数组,同时维护一个Chunk偏移量,该偏移量初始值为0。

3)当一个KeyValue值插入MemStore后,MemStoreLAB会首先通过KeyValue.getBuffer()取得data数组,并将data数组复制到Chunk数组中,之后再将Chunk偏移量往前移动data.length。

4)当前Chunk满了之后,申请一个新的Chunk。

这种内存管理方式,使得flush之后残留的内存碎片更加粗粒度,极大降低Full GC的触发频率。但是频繁的申请Chunk容易造成新生代Eden区满掉,触发YGC。于是重复利用Chunk的思想应运而生,

即使用Chunk pool来管理所有未被引用的Chunk,这些Chunk就不会再被JVM当作垃圾回收。如果需要申请新的Chunk来存储KeyValue,首先从Chunk Pool中获取,如果能够获取得到就重复利用,否则就重新申请一个新的Chunk。

而Chunk pool的大小=hbase.hregion.memstore.chunkpool.maxsize([0,1])* Memstore Size。这里Memstore Size就可以选择配置成堆外或堆内的大小。

[图片上传失败…(image-45a47b-1645000372947)]

2.1 相关配置 hbase.regionserver.offheap.global.memstore.size:这个值为大于0的值开启堆外,表示RegionServer中所有MemStore可以使用的堆外内存总大小。 2.2 引用计数

数据在写入memstore chunk之前,有两种存储方式:

ByteBuffAllocator分配的堆外/堆内内存;

Netty分配的堆外内存。

这两种堆外内存的数据,在RPC call结束之前都不会被释放,而拷贝到chunk的过程处于RPC call处理的过程中,所以拷贝之前,并不需要额外增加对堆外内存的引用。

3、Bucket cache

Hbase 2.0.0之后,默认采用两级缓存CombinedBlockCache。将Index/Bloom block放在堆内缓存(LRUBlockCache层),而将data block放入BucketCache层。

BucketCache通过配置可以工作在三种模式下:heap,offheap和file。heap模式表示这些Bucket是从JVM Heap中申请,offheap模式使用DirectByteBuffer技术实现堆外内存存储管理,而file模式使用类似SSD的高速缓存文件存储数据块。

3.1 基础知识

Bucket cache属于一种block cache,只缓存解码后的data block。而Hbase中Block分为四种类型:Data Block,Index Block,Bloom Block和meta Block。

其中Data Block用于存储实际数据,通常情况下每个Data Block可以存放多条KeyValue数据对;Index Block和Bloom Block都用于优化随机读的查找路径,其中Index Block通过存储索引数据加快数据查找,而Bloom Block通过一定算法可以过滤掉部分一定不存在待查KeyValue的数据文件,减少不必要的IO操作;meta Block主要存储整个HFile的元数据。

3.2 堆外内存管理

Bucket cache是用来存储data block,用BucketAllocator管理分配内存,直接使用JVM DirectByteBuffer分配,内存块的单元是2MB。

无论工作在那种模式下,BucketCache都在初始化的时候申请14个带有固定大小标签的Bucket,一种Bucket存储一种指定BlockSize的数据块,而且即使在某一种Bucket空间不足的情况下,系统也会从其他Bucket空间借用内存使用,不会出现内存使用率低的情况。

[图片上传失败…(image-cd4b30-1645000372947)]

但是data block在进入bucket cache之前,是存在于RAM cache的,同样也是有可能在堆外内存中。因为RAM cache的内存是上面第1部分的ByteBuffAllocator分配管理的(它分配的内存有可能在堆外,也可能在堆内)。

然后由单独的bucket cache writer定期去处理RAM cache队列,将block拷贝进bucket cache。

3.3 Block缓存写入、读取流程

下图是block写入缓存以及从缓存中读取block的流程示意图,图中主要包括5个模块,其中RAMCache是一个存储blockkey和block对应关系的HashMap;WriteThead是整个block写入的中心枢纽,主要负责异步的写入block到内存空间;IOEngine是具体的内存管理模块,主要实现将block数据写入对应地址的内存空间;BackingMap也是一个HashMap,用来存储blockKey与对应物理内存偏移量的映射关系,用来根据blockkey定位具体的block;其中绿线表示cache block流程,蓝色虚线表示get block流程。

[图片上传失败…(image-3cb218-1645000372947)]

3.4 引用计数

接上面,使用了堆外的bucket cache后,在data block被写入bucket cache之前,RAM cache中会保留一个堆外内存的引用;写入bucket cache之后,由ByteBuffAllocator分配的堆外内存即可释放。

而bucket cache堆外内存的释放,由LRU触发data block淘汰时调用。

3.5 相关配置

hbase.bucketcache.ioengine:使用的存储介质,可选值为heap, offheap, file。不设置的话,默认为offheap。

hbase.bucketcache.combinedcache.enabled:是否打开组合模式(CombinedBlockCache),默认是true

hbase.bucketcache.size:BucketCache所占的大小,如果设置为0.0-1.0 ,则代表占堆内存的百分比; 如果大于1,则代表实际的BucketCache的大小,单位为MB。 默认值为0.0,即关闭BucketCache

hbase.bucketcache.bucket.sizes:定义所有Block种类,默认是14种,默认值为(4+1)K、(8+1)K、(16+1)K、(32+1)K、(40+1)K、(48+1)K、(56+1)K、(64+1)K、(96+1)K … (512+1)K

4、Netty NIO ByteBuffer

Hbase中有直接使用Netty分配的堆外内存,主要在3个地方:

使用NettyRPCServer,RPC请求传入的数据会直接存在Netty的堆外内存中;

FanoutOutputStream在把数据向HDFS DN channel写入之前,需要申请Netty的堆外内存,来暂存序列化后的数据;

大的Cell如果无法拷贝进入memstore chunk,也会一直存在Netty的堆外内存中;(这部分怀疑有问题,并没有加引用,但是目前线上没出现这类问题)

这部分Netty的NIO ByteBuffer大小需用系统变量配置,一般采用默认值,但最好预留足够宽松的大小,一般预留堆外内存总量的1/3。

5、其他

short-circuit reading(读数据不经过DataNode,客户端直接读文件),RegionServer上的DFSclient会分配direct memory buffers。

DFSClient会使用的内存大小并不容易量化;它是由打开的HFile文件数量 × hbase.dfs.client.read.shortcircuit.buffer.size 决定。

总结

在启动JVM时,确保 -XX:MaxDirectMemorySize 的设置(在hbase-env.sh)考虑到了offheap BlockCache、offheap memstore size,DFS读写的使用量,以及RPC端的ByteBuffPool的最大总和大小。

Direct memory 的大小应该比 offheap BlockCache + max ByteBufferPool + max offheap memstore size的大小更大。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。