HBase 优化
JVM 调优
内存调优
一般安装好的 HBase 集群,默认配置是给 Master 和 RegionServer 1G 的内存,而 Memstore 默认占 0.4,也就是 400MB。显然 RegionServer 给的 1G 真的太少了。
export HBASE_MASTER_OPTS="$HBASE_MASTER_OPTS -Xms2g -Xmx2g"
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g"
这里只是举例,并不是所有的集群都是这么配置。
== 要牢记至少留 10% 的内存给操作系统来进行必要的操作 ==
如何给出一个合理的 JVM 内存大小设置,举一个 ambari 官方提供的例子吧。
比如你现在有一台 16GB 的机器,上面有 MapReduce 服务、RegionServer 和 DataNode(这三位一般都是装在一起的),那么建议按 照如下配置设置内存:
- 2GB:留给系统进程。
- 8GB:MapReduce 服务。平均每 1GB 分配 6 个 Map slots + 2 个 Reduce slots。
- 4GB:HBase 的 RegionServer 服务
- 1GB:TaskTracker
- 1GB:DataNode
如果同时运行 MapReduce 的话,RegionServer 将是除了 MapReduce 以外使用内存最大的服务。如果没有 MapReduce 的话,RegionServer 可以调整到大概一半的服务器内存。
Full GC 调优
由于数据都是在 RegionServer 里面的,Master 只是做一些管理操作,所以一般内存问题都出在 RegionServer 上。
JVM 提供了 4 种 GC 回收器:
- 串行回收器(SerialGC)。
- 并行回收器(ParallelGC),主要针对年轻带进行优化(JDK 8 默认策略)。
- 并发回收器(ConcMarkSweepGC,简称 CMS),主要针对年老带进 行优化。
- G1GC 回收器,主要针对大内存(32GB 以上才叫大内存)进行优化。
一般会采取两种组合方案
- ParallelGC 和 CMS 的组合方案
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseParNewGC -XX:+UseConMarkSweepGC"
- G1GC 方案
export HBASE_REGIONSERVER_OPTS="$HBASE_REGIONSERVER_OPTS -Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
怎么选择呢?
一般内存很大(32~64G)的时候,才会去考虑用 G1GC 方案。
如果你的内存小于 4G,乖乖选择第一种方案吧。
如果你的内存(4~32G)之间,你需要自行测试下两种方案,孰强孰弱靠实践。测试的时候记得加上命令 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintAdaptiveSizePolicy
MSLAB 和 In Memory Compaction(HBase2.X 才有)
HBase 自己实现了一套以 Memstore 为最小单元的内存管理机制,称为 MSLAB(Memstore-Local Allocation Buffers)
跟 MSLAB 相关的参数是:
- hbase.hregion.memstore.mslab.enabled:设置为 true,即打开 MSLAB,默认为 true。
- hbase.hregion.memstore.mslab.chunksize:每个 chunk 的大 小,默认为 2048 * 1024 即 2MB。
- hbase.hregion.memstore.mslab.max.allocation:能放入 chunk 的最大单元格大小,默认为 256KB,已经很大了。
- hbase.hregion.memstore.chunkpool.maxsize:在整个 memstore 可以占用的堆内存中,chunkPool 占用的比例。该值为一个百分 比,取值范围为 0.0~1.0。默认值为 0.0。hbase.hregion.memstore.chunkpool.initialsize:在 RegionServer 启动的时候可以预分配一些空的 chunk 出来放到 chunkPool 里面待使用。该值就代表了预分配的 chunk 占总的 chunkPool 的比例。该值为一个百分比,取值范围为 0.0~1.0,默认值为 0.0。
在 HBase2.0 版本中,为了实现更高的写入吞吐和更低的延迟,社区团队对 MemStore 做了更细粒度的设计。这里,主要指的就是 In Memory Compaction。
开启的条件也很简单。
hbase.hregion.compacting.memstore.type=BASIC # 可选择 NONE/BASIC/EAGER
具体这里不介绍了。
Region 自动拆分
Region 的拆分分为自动拆分和手动拆分。自动拆分可以采用不同的策略。
拆分策略
ConstantSizeRegionSplitPolicy
0.94 版本的策略方案
hbase.hregion.max.filesize
通过该参数设定单个 Region 的大小,超过这个阈值就会拆分为两个。
IncreasingToUpperBoundRegionSplitPolicy(默认)
文件尺寸限制是动态的,依赖以下公式来计算
Math.min(tableRegionCount^3 * initialSize, defaultRegionMaxFileSize)
- tableRegionCount:表在所有 RegionServer 上所拥有的 Region 数量总和。
- initialSize:如果你定义了 hbase.increasing.policy.initial.size,则使用这个数值;如果没有定义,就用 memstore 的刷写大小的 2 倍,即 hbase.hregion.memstore.flush.size * 2。
- defaultRegionMaxFileSize:ConstantSizeRegionSplitPolicy 所用到的 hbase.hregion.max.filesize,即 Region 最大大小。
假如 hbase.hregion.memstore.flush.size 定义为 128MB,那么文件 尺寸的上限增长将是这样:
- 刚开始只有一个文件的时候,上限是 256MB,因为 1^3 1282 = 256MB。
- 当有 2 个文件的时候,上限是 2GB,因为 2^3 128 2 2048MB。
- 当有 3 个文件的时候,上限是 6.75GB,因为 3^3 128 2 = 6912MB。
- 以此类推,直到计算出来的上限达到 hbase.hregion.max.filesize 所定义的 10GB。
KeyPrefixRegionSplitPolicy
除了简单粗暴地根据大小来拆分,我们还可以自己定义拆分点。KeyPrefixRegionSplitPolicy 是 IncreasingToUpperBoundRegionSplitPolicy 的子类,在前者的基础上增加了对拆分点(splitPoint,拆分点就是 Region 被拆分处的 rowkey)的定义。它保证了有相同前缀的 rowkey 不会被拆分到两个不同的 Region 里面。这个策略用到的参数是 KeyPrefixRegionSplitPolicy.prefix_length rowkey: 前缀长度
那么它与 IncreasingToUpperBoundRegionSplitPolicy 区别,用两张图来看。
默认策略为
KeyPrefixRegionSplitPolicy 策略
如果你的前缀划分的比较细,你的查询就比较容易发生跨 Region 查询的情况,此时采用 KeyPrefixRegionSplitPolicy 较好。
所以这个策略适用的场景是:
- 数据有多种前缀。
- 查询多是针对前缀,比较少跨越多个前缀来查询数据。
DelimitedKeyPrefixRegionSplitPolicy
该策略也是继承自 IncreasingToUpperBoundRegionSplitPolicy,它也是根据你的 rowkey 前缀来进行切分的。唯一的不同就是:KeyPrefixRegionSplitPolicy 是根据 rowkey 的固定前几位字符来进行判断,而 DelimitedKeyPrefixRegionSplitPolicy 是根据分隔符来判断的。在有些系统中 rowkey 的前缀可能不一定都是定长的。
使用这个策略需要在表定义中加入以下属性:
DelimitedKeyPrefixRegionSplitPolicy.delimiter:前缀分隔符
比如你定义了前缀分隔符为_,那么 host1_001 和 host12_999 的前缀就分别是 host1 和 host12。
BusyRegionSplitPolicy
如果你的系统常常会出现热点 Region,而你对性能有很高的追求,那么这种策略可能会比较适合你。它会通过拆分热点 Region 来缓解热点 Region 的压力,但是根据热点来拆分 Region 也会带来很多不确定性因 素,因为你也不知道下一个被拆分的 Region 是哪个。
DisabledRegionSplitPolicy
这种策略就是 Region 永不自动拆分。
如果你事先就知道这个 Table 应该按 怎样的策略来拆分 Region 的话,你也可以事先定义拆分点(SplitPoint)。所谓拆分点就是拆分处的 rowkey,比如你可以按 26 个 字母来定义 25 个拆分点,这样数据一到 HBase 就会被分配到各自所属的 Region 里面。这时候我们就可以把自动拆分关掉,只用手动拆分。
手动拆分有两种情况:预拆分(pre-splitting)和强制拆分(forced splits)。
推荐方案
一开始可以先定义拆分点,但是当数据开始工作起来后会出现热点 不均的情况,所以推荐的方法是:
- 用预拆分导入初始数据。
- 然后用自动拆分来让 HBase 来自动管理 Region。
== 建议:不要关闭自动拆分。==
Region 的拆分对性能的影响还是很大的,默认的策略已经适用于大 多数情况。如果要调整,尽量不要调整到特别不适合你的策略
BlockCache 优化
一个 RegionServer 只有一个 BlockCache。
BlockCache 的工作原理:读请求到 HBase 之后先尝试查询 BlockCache,如果获取不到就去 HFile(StoreFile)和 Memstore 中去获取。如果获取到了则在返回数据的同时把 Block 块缓存到 BlockCache 中。它默认是开启的。
如果你想让某个列簇不使用 BlockCache, 可以通过以下命令关闭它。
alter 'testTable', CONFIGURATION=>{NAME => 'cf',BLOCKCACHE=>'false'}
BlockCache 的实现方案有
- LRU BLOCKCACHE
- SLAB CACHE
- Bucket CACHE
LRU BLOCKCACHE
在 0.92 版本 之前只有这种 BlockCache 的实现方案。LRU 就是 Least Recently Used,即近期最少使用算法的缩写。读出来的 block 会被放到 BlockCache 中待 下次查询使用。当缓存满了的时候,会根据 LRU 的算法来淘汰 block。LRUBlockCache 被分为三个区域,
看起来是不是很像 JVM 的新生代、年老代、永久代?没错,这个方案就是模拟 JVM 的代设计而做的。
Slab Cache
SlabCache 实际测试起来对 Full GC 的改善很小,所以这个方案最后被废弃了。不过它被废弃还有一个更大的原因,这就是有另一个更好的 Cache 方案产生了,也用到了堆外内存,它就是 BucketCache。
Bucket Cache
- 相比起只有 2 个区域的 SlabeCache,BucketCache 一上来就分配了 14 种区域。注意:我这里说的是 14 种区域,并不是 14 块区域。这 14 种区域分别放的是大小为 4KB、8KB、16KB、32KB、40KB、48KB、56KB、64KB、96KB、128KB、192KB、256KB、384KB、512KB 的 Block。而且这个种类列表还是可以手动通过设置 hbase.bucketcache.bucket.sizes 属性来定义(种类之间用逗号 分隔,想配几个配几个,不一定是 14 个!),这 14 种类型可以分 配出很多个 Bucket。
- BucketCache 的存储不一定要使用堆外内存,是可以自由在 3 种存 储介质直接选择:堆(heap)、堆外(offheap)、文件(file, 这里的文件可以理解成 SSD 硬盘)。通过设置 hbase.bucketcache.ioengine 为 heap、offfheap 或者 file 来配置。
- 每个 Bucket 的大小上限为最大尺寸的 block 4,比如可以容纳的最大的 Block 类型是 512KB,那么每个 Bucket 的大小就是 512KB 4 = 2048KB。
- 系统一启动 BucketCache 就会把可用的存储空间按照每个 Bucket 的大小上限均分为多个 Bucket。如果划分完的数量比你的种类还少,比如比 14(默认的种类数量)少,就会直接报错,因为每一种类型的 Bucket 至少要有一个 Bucket。
Bucket Cache 默认也是开启的,如果要关闭的话
alter 'testTable', CONFIGURATION=>{CACHE_DATA_IN_L1 => 'true'}
它的配置项:
- hbase.bucketcache.ioengine:使用的存储介质,可选值为 heap、offheap、file。不设置的话,默认为 offheap。
- hbase.bucketcache.combinedcache.enabled:是否打开组合模 式(CombinedBlockCache),默认为 true
- hbase.bucketcache.size:BucketCache 所占的大小
- hbase.bucketcache.bucket.sizes:定义所有 Block 种类,默认 为 14 种,种类之间用逗号分隔。单位为 B,每一种类型必须是 1024 的整数倍,否则会报异常:java.io.IOException: Invalid HFile block magic。默认值为:4、8、16、32、40、48、56、64、96、128、192、256、384、512。
- -XX:MaxDirectMemorySize:这个参数不是在 hbase-site.xml 中 配置的,而是 JVM 启动的参数。如果你不配置这个参数,JVM 会按 需索取堆外内存;如果你配置了这个参数,你可以定义 JVM 可以获得的堆外内存上限。显而易见的,这个参数值必须比 hbase.bucketcache.size 大。
在 SlabCache 的时代,SlabCache,是跟 LRUCache 一起使用的,每一 个 Block 被加载出来都是缓存两份,一份在 SlabCache 一份在 LRUCache,这种模式称之为 DoubleBlockCache。读取的时候 LRUCache 作为 L1 层缓存(一级缓存),把 SlabCache 作为 L2 层缓存(二级缓存)。
在 BucketCache 的时代,也不是单纯地使用 BucketCache,但是这回 不是一二级缓存的结合;而是另一种模式,叫组合模式(CombinedBlockCahce)。具体地说就是把不同类型的 Block 分别放到 LRUCache 和 BucketCache 中。
Index Block 和 Bloom Block 会被放到 LRUCache 中。Data Block 被直 接放到 BucketCache 中,所以数据会去 LRUCache 查询一下,然后再去 BucketCache 中查询真正的数据。其实这种实现是一种更合理的二级缓 存,数据从一级缓存到二级缓存最后到硬盘,数据是从小到大,存储介质也是由快到慢。考虑到成本和性能的组合,比较合理的介质是:LRUCache 使用内存 ->BuckectCache 使用 SSD->HFile 使用机械硬盘。
总结
关于 LRUBlockCache 和 BucketCache 单独使用谁比较强,曾经有人做 过一个测试。
- 因为 BucketCache 自己控制内存空间,碎片比较少,所以 GC 时间 大部分都比 LRUCache 短。
- 在缓存全部命中的情况下,LRUCache 的吞吐量是 BucketCache 的 两倍;在缓存基本命中的情况下,LRUCache 的吞吐量跟 BucketCache 基本相等。
- 读写延迟,IO 方面两者基本相等。
- 缓存全部命中的情况下,LRUCache 比使用 fiile 模式的 BucketCache CPU 占用率低一倍,但是跟其他情况下差不多。
从整体上说 LRUCache 的性能好于 BucketCache,但由于 Full GC 的存在,在某些时刻 JVM 会停止响应,造成服务不可用。所以适当的搭配 BucketCache 可以缓解这个问题。
HFile 合并
合并分为两种操作:
- Minor Compaction:将 Store 中多个 HFile 合并为一个 HFile。在 这个过程中达到 TTL 的数据会被移除,但是被手动删除的数据不 会被移除。这种合并触发频率较高。
- Major Compaction:合并 Store 中的所有 HFile 为一个 HFile。在 这个过程中被手动删除的数据会被真正地移除。同时被删除的还 有单元格内超过 MaxVersions 的版本数据。这种合并触发频率较 低,默认为 7 天一次。不过由于 Major Compaction 消耗的性能较 大,你不会想让它发生在业务高峰期,建议手动控制 Major Compaction 的时机。
Compaction 合并策略
RatioBasedCompactionPolicy
从旧到新地扫描 HFile 文件,当扫描到某个文件,该文件满足以下条件:
该文件大小 < 比它更新的所有文件的大小总和 * hbase.store.compation.ratio(默认 1.2)
实际情况下的 RatioBasedCompactionPolicy 算法效果很差,经常引 发大面积的合并,而合并就不能写入数据,经常因为合并而影响 IO。所 以 HBase 在 0.96 版本之后修改了合并算法。
ExploringCompactionPolicy
0.96 版本之后提出了 ExploringCompactionPolicy 算法,并且把该 算法作为了默认算法。
算法变更为
该文件大小 <(所有文件大小总和 - 该文件大小)* hbase.store.compation.ratio(默认 1.2)
如果该文件大小小于最小合并大小(minCompactSize),则连上面那个公式都不需要套用,直接进入待合并列表。最小合并大小的配置项:hbase.hstore.compaction.min.size。如果没设定该配置项,则使用 hbase.hregion.memstore.flush.size。
被挑选的文件必须能通过以上提到的筛选条件,并且组合内含有的文件数必须大于 hbase.hstore.compaction.min,小于 hbase.hstore.compaction.max。
文件太少了没必要合并,还浪费资源;文件太多了太消耗资源,怕 机器受不了。
挑选完组合后,比较哪个文件组合包含的文件更多,就合并哪个组 合。如果出现平局,就挑选那个文件尺寸总和更小的组合。
FIFOCompactionPolicy
这个合并算法其实是最简单的合并算法。严格地说它都不算是一种合并算法,是一种删除策略。
FIFOCompactionPolicy 策略在合并时会跳过含有未过期数据的 HFile,直接删除所有单元格都过期的块。最终的效果是:
- 过期的块被整个删除掉了。
- 没过期的块完全没有操作。
这个策略不能用于什么情况
- 表没有设置 TTL,或者 TTL=FOREVER。
- 表设置了 MIN_VERSIONS,并且 MIN_VERSIONS > 0
DateTieredCompactionPolicy
DateTieredCompactionPolicy 解决的是一个基本的问题:最新的数据最 有可能被读到。
配置项
- hbase.hstore.compaction.date.tiered.base.window.millis:基本的时间窗口时长。默认是 6 小时。拿默认的时间窗口举例:从现在到 6 小时之内的 HFile 都在同一个时间窗口里 面,即这些文件都在最新的时间窗口里面。
- hbase.hstore.compaction.date.tiered.windows.per.tier:层 次的增长倍数。分层的时候,越老的时间窗口越宽。在同一个窗口里面的文件如果达到最小合并数量(hbase.hstore.compaction.min)就会进行合并,但不 是简单地合并成一个,而是根据 hbase.hstore.compaction.date.tiered.window.policy.class 所定义的合并规则来合并。说白了就是,具体的合并动作 使用的是用前面提到的合并策略中的一种(我刚开始看到 这个设计的时候都震撼了,居然可以策略套策略),默认是 ExploringCompactionPolicy。
- hbase.hstore.compaction.date.tiered.max.tier.age.millis:最老的层次时间。当文件太老了,老到超过这里所定义的时间范 围(以天为单位)就直接不合并了。不过这个设定会带来一个缺 点:如果 Store 里的某个 HFile 太老了,但是又没有超过 TTL,并 且大于了最老的层次时间,那么这个 Store 在这个 HFile 超时被删 除前,都不会发生 Major Compaction。没有 Major Compaction,用户手动删除的数据就不会被真正删除,而是一直占着磁盘空间。
配置项好像很复杂的样子,举个例子画个图就清楚了。
假设基本窗口宽度(hbase.hstore.compaction.date.tiered.base.window.millis) = 1。最小合并数量(hbase.hstore.compaction.min) = 3。层次增长倍数(hbase.hstore.compaction.date.tiered.windows.per.tier) = 2。
这个策略非常适用于什么场景
- 经常读写最近数据的系统,或者说这个系统专注于最新的数据。
- 因为该策略有可能引发不了 Major Compaction,没有 Major Compaction 是没有办法删除掉用户手动删除的信息,所以更适用 于那些基本不删除数据的系统。
这个策略比较适用于什么场景
- 数据根据时间排序存储。
- 数据的修改频率很有限,或者只修改最近的数据,基本不删除数据。
这个策略不适用于什么场景
- 数据改动很频繁,并且连很老的数据也会被频繁改动。
- 经常边读边写数据。
StripeCompactionPolicy
该策略在读取方面稳定。
那么什么场景适合用 StripeCompactionPolicy
- Region 要够大:这种策略实际上就是把 Region 给细分成一个个 Stripe。Stripe 可以看做是小 Region,我们可以管它叫 sub- region。所以如果 Region 不大,没必要用 Stripe 策略。小 Region 用 Stripe 反而增加 IO 负担。多大才算大?作者建议如果 Region 大 小小于 2GB,就不适合用 StripeCompactionPolicy。
- Rowkey 要具有统一格式,能够均匀分布。由于要划分 KeyRange,所以 key 的分布必须得均匀,比如用 26 个字母打头来命名 rowkey,就可以保证数据的均匀分布。如果使用 timestamp 来做 rowkey,那么数据就没法均匀分布了,肯定就不适合使用这个策略。
总结
请详细地看各种策略的适合场景,并根据场景选择策略。
- 如果你的数据有固定的 TTL,并且越新的数据越容易被读到,那么 DateTieredCompaction 一般是比较适合你的。
- 如果你的数据没有 TTL 或者 TTL 较大,那么选择 StripeCompaction 会比默认的策略更稳定。
- FIFOCompaction 一般不会用到,这只是一种极端情况,比如用于 生存时间特别短的数据。如果你想用 FIFOCompaction,可以先考虑使用 DateTieredCompaction。
附录
- HBase Region Split 策略