Block Cache

HBase提供了两种不同的BlockCache实现,用于缓存从HDFS读出的数据。这两种分别为:

  1. 默认的,存在于堆内存的(on-heap)LruBlockCache
  2. 存在堆外内存的(off-heap)BucketCache

下面我们会讨论每种方法的优点和缺点、如何对两种方式做选择,以及这两种类型的相关配置。

 

Cache Choices

LruBlockCache是最初始的实现,并且全部存在Java堆内存中。BucketCache是另一个选择,主要用于将block cache的数据存在off-heap(堆外内存),不过BlockCache也可以作为一种文件备份式的缓存。

当开启了BucketCache后,便启用了两级缓存的系统。以前我们会用“L1”和“L2”来描述这两个等级,但是现在这个术语已经在hbase-2.0.0后被弃用了。现在“L1” cache 直接指的是LruBlockCache,“L2”指的是一个off-heap的BucketCache。(hbase-2.0.02之后)当BucketCache启用后,所有数据块(DATA block)会被存在BucketCache 层,而meta 数据块(INDEX 以及BLOOM块)被存在on-heap的LruBlockCache中。管理这两层缓存,以及指示数据块如何在它们之间移动的策略,由CombinedBlockCache完成。

 

Cache的常规配置

除了缓存它自己的实现以外,我们也可以设置一些常规的配置选项,用于控制cache的行为。具体可以参考CacheConfig的文档:

https://hbase.apache.org/devapidocs/org/apache/hadoop/hbase/io/hfile/CacheConfig.html

在设置或修改了任何属性后,需要重启HBase集群以让配置文件生效。若是遇到异常,可以进一步查看HBase中的报错。

 

LruBlockCache的设计

LruBlockCache是一个LRU缓存,包括三种优先级,以适应于于:scan-resistance 以及 in-memory ColumnFamilies场景。三种优先级分别为:

  1. Single Access 优先级:当一个数据块第一次从HDFS读取时,它会具有这种优先级,并且在缓存空间需要被回收(置换)时,它属于优先被考虑范围内。它的优点在于:一般被扫描(scanned)读取的数据块,相较于之后会被用到的数据块,更应该被优先清除
  2. Multi Access优先级:如果一个数据块,属于Single Access优先级,但是之后被再次访问,则它会升级为Multi Access优先级。在缓存里的内容需要被清除(置换)时,这部分内容属于次要被考虑的范围
  3. In-memory Access优先级:如果数据块族被配置为“in-memory”,则会具有这种优先级,并且与它被访问的次数无关。HBase Catalog便是被配置的这个优先级。在缓存里的内容需要被置换时,这部分内容属于最后被考虑的范围。若是需要将一个列族标注为此优先级:
  1. 在Java中可以调用: HColumnDescriptor.setInMemory(true);
  2. 在hbase shell 中创建或修改一个表时,可以使用 IN_MEMORY => true,例如:create ‘t’, {NANME => ‘f’, IN_MEMORY => ‘true’}

若是想了解更具体的信息,可以参考LruBlockCache 源码

 

LruBlockCache 的使用

一般来说,BlockCache在所有用户表中默认是开启的,也就是说,任何的读操作均会加载LRU Cache。这个方案可能适用于大部分场景,但是,如果需要达到更优的performance,仍需要做一些调整。其中一个很重要的概念是working set size(或WSS),它的意思是:为了解决一个问题所需要的资源(内存)大小。对于一个网站来说,这个便是在短时间内响应请求所需要的数据量。计算在HBase中到底有多少内存可供cache的方法为:

number of region servers * heap size * hfile.block.cache.size * 0.99

block.cache的默认值是0.4,表示可用堆内存的40%。最后一个值(99%)是:在缓存内存收回开始后,默认可被回收的比率(这里若是值是100%,则不太现实,因为在置换时也会有新块写入?)。下面是使用这个公式的一些例子:

  1. 一个region server,设置了1GB的堆内存,使用默认的block.cache参数,则会有405MB的block cache可用
  2.  20个region server,设置了8GB大小的堆内存,使用默认的block.cache参数,则会有63GB大小的block cache可用(20 × 8 × 0.4 × 0.99)
  3. 100 个region server,设置了24GB 大小的堆内存,使用block.cache=0.5,则会有1.16TB的可用block cache

当然,被存在block cache中的并不仅仅只是你的数据,下面是其他一些需要被考虑到的地方:

  1. Catalog :hbase:meta 表被强制载入block cache,并且具有in-memory 优先级,也就是说,它的内容几乎很少会被从缓存移除。(根据regions的数量,hbase:meta 表会占据一小部分内存)
  2. HFile 索引:HFile是HBase用于在HDFS上存储数据数据文件格式。它包含多级索引,可以让HBase在不需要读入整个文件的情况下找到目标数据。决定这些索引大小的因素是:数据块大小(默认是64KB),你的key大小,以及存储数据的量。对于大数据集来说,一个region server 的cache中包含大约1GB的索引大小也属正常,尽管不是所有的索引均会被放入cache(因为LRU会将那些不常用的索引移除)。
  3. Keys:The values that are stored are only half the picture, since each value is stored along with its keys (row key, family qualifier, and timestamp).
  4. Bloom Filter:如HFile的索引一样,那些数据结构(若是在enabled之后),会被存在LRU

当前来说,衡量HFile索引以及Bloom filter 大小的一种推荐的方法是:查看region server UI,并查看相关指标。对于键来说,获取抽样时,可以使用HFile的命令行工具,并查看键的平均大小。从HBase 0.98.3以后,可以在UI界面中的Block Cache部分,查看BlockCache的详细状态以及指标。

一般来说,如果当前可用内存并不足以支持WSS,则不建议使用block caching。举个例子:假设在集群中一共有40GB的可用内存分布于各个region server的block cache,但是你需要处理1TB的数据,则这种场景不太适合使用block caching。其中一个原因是:回收(置换)缓存会打乱内存分布,并触发更多本没必要的垃圾回收。下面是两个场景:

  1. 完全随机的读模式:这种场景一般是,在短时间内,应用几乎不会重复读取表中同一行的内容,所以在这种情况下,命中cache的机会基本接近于0。在这个表中设置block caching属于浪费内存以及CPU的时间片。不仅如此,它还会产生更多的JVM垃圾回收事件。
  2. Mapping a table:比如在某个MapReduce任务中,任务的输入是一张表。每一行仅会被读取一次,所以就没必要将这些数据放入block cache。在Java中,Scan对象有一个关闭block cache的功能:setCaching(设置为false)。当然,如果你需要快速的随机读访问,也可以在这个表上保持开启block caching功能。

仅缓存META数据块(DATA 数据块在fscache

一个有趣的设置是:仅缓存 META数据块,每次在读取数据时,均去访问DATA数据块。在这种情况下如果DATA数据块适应于fscache,且当访问数据的操作在一个很大的集群中完全是随机时,这种设置是可取的。若要开启这个设置,可以直接修改表:对某个列族,设置BLOCKCACHE => ‘false’。这样就关闭了对这个列族的BlockCache。不过,META数据块的block caching无法被关闭,即使它的被关闭了,META数据块也仍会被载入缓存。

 

堆外(Off-heapBlock Cache

如何开启BucketCache

一个通常的部署BucketCache方式是通过一个管理类,它设置两级缓存:一个堆内的缓存,由LruBlockCache实现;以及第二层缓存,由BucketCache实现。默认管理的类为CombinedBlockCache。简单的说,这个类实现的缓存规则是:将meta数据块(INDEX以及BLOOM)放在堆内缓存(LruBlockCache层),而将DATA数据放入BucketCache层。

在HBase-2.00 版本之前

在hbase 2.00 版本以前,从BucketCache取数据时都会比较慢(对比使用堆内存的LruBlockCache)。然而,从表现来看,读操作的延迟时间基本趋于稳定。因为在使用BucketCache时,会有较少的垃圾回收(BucketCache管理BlockCache的分配,而不是GC)。如果BucketCache被部署为堆外(off-heap)模式,则这部分内存根本不会被GC管理。这就是为什么你在2.0.0版本之前的HBase使用BucketCache时,延迟时间基本趋于稳定,并可以减轻GC以及堆内存碎片的影响,这样可以安全的使用更多内存。如果你希望缓存不被GC管理,可以使用BucketCache。

在2.0.0版本前,在配置了BucketCache后,可以减少LruBlockCache置换的影响。所有数据以及index块首先被缓存在L1。当L1中发生缓存清除(置换)时,被置换出的数据块会被移动到L2。在Java中,可以通过HColumnDescriptor.setCacheDataInL1(true)设置cacheDataInL1;在hbase shell中可以设置CACHE_DATA_IN_L1 为true,例如:create ‘t1’, {NamE => ‘t1’, CONFIGURATION => {CACHE_DATA_IN_L1 => ‘true’}}

HBase-2.0.0 版本之后

HBASE-11425改变了HBase的数据读取路径,实现了直接从堆外读取数据。off-heap的延迟可以接近于on-heap的延迟,因为off-heap并不会引起GC操作。

从HBase 2.0.0 开始,L1与L2的概念便被弃用。当BucketCache启用时,数据块(DATA blocks)会一直保存于BucketCache;INDEX/BLOOM块会保存于LRUBlockCache的堆内存。cacheDetaInL1 的配置也被移除。

BucketCache的块缓存可以被部署为off-heap,文件,或mmaped文件这三种模式(通过hbase.bucketcache.ioengine配置)。设置为offheap会让BucketCache在堆外内存管理block cache。设置为file:PATH_TO_FILE(EMR里默认为files:/mnt/hbase/bucketcache),会直接让BucketCache使用文件缓存(如果卷是SSD之类的高速盘的话,会比较有用)。从2.0.0 开始,使用多个文件路径也是可以的。若是在需要较大Cache 的场景下,这个配置很有用。在设置时的基本配置为:files:PATH_TO_FILE1, PATH_TO_FILE2, PATH_TO_FILE3。BucketCache可以也可以配置为使用一个mmapped文件。配置ioengine为mmap:PATH_TO_FILE即可。

在hbase 2.0.0之前,也可以设置多级缓存(绕过CombinedBlockCache策略),将BucketCache设置为严格的L2 缓存,LruBlockCache为L1缓存。在这种配置中,设置 hbase.bucketcache.combinedcache.enable 为false即可。在这种模式下,当L1缓存内容被清除(置换)时,会将置换出的块放入L2。当一个块被缓存时,首先被缓存在L1。当我们去查询一个缓存块时,首先在L1查,若是没找到,则再搜索L2。我们将此部署方法称为Raw L1+L2。需要注意的是,这个L1+L2模式已经在hbase 2.0.0 以后被移除了。当BucketCache被使用时,它会严格的将DATA块缓存放入BucketCache,而INDEX/META块则被放入LruBlockCache。

其他BucketCache的配置包括:指定cache被持久化的路径以在重启后仍存在、写cache的线程数量,等等。在检查它是否开启时,可以查看日志内容,会包含cache的设置;它会详细的记录BucketCache被部署的信息。也可以通过UI,它可以看到详细的cache层级以及它们的配置。

 

BucketCache示例配置

这个例子提供了为一个4GB堆外BucketCache、1GB堆内缓存的配置。配置过程在RegionServer上实施。

设置hbase.bucketcache.ioengine,并设置 hbase.bucketcache.size > 0,开启CombinedBlockCache。这里我们假设当前RegionServer配置了5GB的堆内存(HBASE_HEAPSIZE=5g)

  1. 首先,编辑RegionServer的hbase-env.sh 文件,并设置HBASE_OFFHEAPSIZE,需要高于所需的off-heap的大小。在这个案例中需要的off-heap为4GB,所以我们设置此值为5GB。这里4GB被用于我们的off-heap缓存,剩余的1G被其他用户使用(因为会有其他用户也会使用off-heap内存;例如RegionServer中的DFSClient会使用堆外内存,参考下面的Direct Memory Usage in HBase)。HBASE_OFFHEAPSIZE=5G
  2. 然后,在hbase-site.xml 下设置以下配置:

<property>

  <name>hbase.bucketcache.ioengine</name>

  <value>offheap</value>

</property>

<property>

  <name>hfile.block.cache.size</name>

  <value>0.2</value>

</property>

<property>

  <name>hbase.bucketcache.size</name>

  <value>4196</value>

</property>

  1. 重启集群,若出现任何问题,检查日志

上面的配置文件中,我们设置了BucketCache为4G,配置on-heap LruBlockCache为20%的RegionServer的堆内存(0.2 × 5G = 1G)。

HBASE-10641以后,HBase 0.98及以后的版本,引入了可以为BucketCache配置多个bucket以及它们的大小的功能。配置多个bucket sieze,可以设置hbase.bucketcache.bucket.sizes为一系列块大小设置,从小到大。它的目的是根据你数据访问模式,优化bucket sizes。下面是一个示例配置,大小从4096到8192:

<property>

  <name>hbase.bucketcache.bucket.sizes</name>

  <value>4096,8192</value>

</property>

 

Direct Memory Usage in HBase

默认最大可以使用的direct memory因JVM的不同而不同。传统下是64M,或者通过直接分配堆内存(-Xmx),或完全没有限制(如JDK7)。HBase服务器使用direct memory,特别是short-circuit reading(读数据不经过DataNode,客户端直接读文件),RegionServer上的DFSclient会分配direct memory buffers。DFSClient会使用的内存大小并不容易量化;它是由:打开的HFile文件数量 × hbase.dfs.client.read.shortcircuit.buffer.size 决定。hbase.dfs.client.read.shortcircuit.buffer.size在HBase中设置为128k(参考hbae-default.xml默认配置)。如果需要使用off-heap block caching,则需要使用到直接内存(direct memory)。在RPC Server中,也会使用一个ByteBuffer池,从hbase 2.0.0开始,这些缓冲区为off-heap ByteBuffers。在启动JVM时,确保 -XX:MaxDirectMemorySize 的设置(在hbase-env.sh)考虑到了off-heap BlockCache(hbase.bucketcache.size)、DFSClient的使用量,以及RPC端的ByteBufferPool的最大总和大小。Direct memory 的大小应该比 off-heap BlockCache + max ByteBufferPool 的大小更大。在一般情况下,可以在基于所需的direct memory大小情况下,额外再分配多1-2GB的空间。Direct memory属于Java进程堆的一部分,与对象堆(由-Xmx分配)分离。MaxDirectMemorySize的大小必须小于物理的RAM大小,并且小于所有可用的RAM大小(由于内存的其他用处,以及系统的限制)。

你可以在UI中的Server Metrics: Memory 栏看到一个RegionServer配置的内存量(on-heap以及off-heap/direct memory)。这部分数据也可以通过JMX获取。

 

BlockCache 压缩

HBASE-11331引入了lazy BlockCache decompression。在开启此功能后,DATA(以及ENCODED_DATA)数据块会以它们on-disk的形式缓存到BlockCache。与默认的模式不同点在于:默认情况下,在缓存一个数据块时,会先解压缩、解密,然后存入缓存(因为数据块是从HDFS取)。而lazy BlockCache decompression 直接将数据块存入缓存。

如果一个RegionServer存储的数据过多,无法适当的将大部分数据放入缓存,则开启这个功能(使用SNAPPY压缩)后,实验证明:可以提升50%的throughput,30%的平均延迟上升,增加80%垃圾回收,以及2%的整体CPU负载。

对于一个RegionServer,如果它的数据量已经适合cache的大小,或者你的应用对额外的CPU或GC的负载格外敏感,则这个选项不会有太大用处。

默认情况下,这个功能是关闭的,若要开启,可以在所有的RegionServer中的hbase-site.xml文件里设置 hbase.block.data.cachecompressed 为 true