HBase_HBase的Region过大导致写数慢与Compact导致CPU使用率陡增问题实战

HBase_HBase的Region过大导致写数慢与Compact导致CPU使用率陡增问题实战

一、Region过大导致写数慢问题

1.问题解决步骤

1.1 问题背景

在数仓到HBase集群的数据同步工具datapicker中使用了MapReduce和BufferedMutator来进行数据批量写入,没有任何变更的某一天,某个HBase表的推数任务就变得特别慢,数据写入命令经常报超时错误。

1.2 平台分析

平台通过监控和数据分析,给我们的结论就是该HBase表中存在一个特别大的region,导致写数组件中多个reduce并发写同一个region,该region的写入速度太快,导致memstore阻塞严重,整体写入任务也被阻塞。

1.3 解决方法

紧急方法是通过对大region手动切分,写入速度就恢复了。长期方案平台侧推荐降低BufferedMutator的writeBufferSize,降低写入速度减小吞吐量,但是这个方案会导致整体推数速度变慢,且更容易超请求次数quota告警,所以最终没有实施该最终方案。

1.4 类似问题规避

此处是单个region过大,也就是数据倾斜导致单个region上数据写入速度过快,反而导致数据写入被阻塞。那么同样的原理,大value和列过多场景下,当写入行数速度保持不变,也会导致数据量写入过快,导致HLog文件写入频繁切换、memstore flush和compaction频繁触发,这些操作都会导致写锁被阻塞情况更严重。

2.HBase中的锁机制

造成上述问题的一个重要原因就是,HBase中的memstore刷写时的读写锁阻塞了写入请求,接下来介绍一下HBase中主要有哪些锁。

2.1 HBase中的事务控制

HBase和传统数据库一样提供了事务的概念,只是HBase的事务是行级事务,可以保证行级数据的原子性、一致性、隔离性以及持久性(ACID特性)。为了实现行级数据的事务特性,HBase采用了各种并发控制策略,主要有如下三种:

1)基于CountDownLatch实现的行数据更新时的互斥锁,也叫行锁,用于确保行级数据的一致性;

2)基于ReentrantReadWriteLock实现的读写锁,用于锁实现store或region级别操作的并发控制;

3)基于MVCC机制实现数据的高效并发读写以及读写数据一致性。

2.2 HBase的行锁

HBase采用行锁实现更新的原子性,要么全部更新成功,要么失败。所有对HBase行级数据的更新操作,都需要首先获取该行的行锁,并且在更新完成之后释放,等待其他线程获取。因此,HBase中对同一行数据的更新操作都是串行操作。行锁并不会阻塞读请求。

2.3 HBase的读写锁

HBase采用读写锁实现store或region级别操作的并发控制:

1)Region更新读写锁:HBase在将memstore数据flush落盘时会加一把Region级别的写锁(独占锁),数据更新操作线程(Put操作、Append操作、Delete操作)都会阻塞等待至该写锁释放。

2)Region Close保护锁:HBase在执行close或split操作时会首先加一把Region级别的写锁(独占锁),阻塞对region的其他操作,比如compact操作、flush操作以及其他更新操作。

3)Store snapshot保护锁:HBase在执行flush memstore的过程中首先会基于memstore做snapshot,这个阶段会加一把store级别的写锁(独占锁),用以阻塞其他线程对该memstore的各种更新操作;清除snapshot时也相同,会加一把写锁阻塞其他对该memstore的更新操作。

2.4 HBase的MVCC

MVCC即多版本并发控制技术,HBase使用行锁+MVCC保证高效的并发读写以及读写数据一致性。

上图中简单地表述了数据更新流程,可以分为如下几个阶段:获取行锁、更新WAL、数据写入本地缓存memstore、释放行锁。

如上图所示,前后分别有两次对同一行数据的更新操作。假如第二次更新过程在将列簇cf1更新为t2_cf1之后中有一次读请求进来,此时读到的第一列数据将是第二次更新后的数据t2_cf1,然而第二列数据却是第一次更新后的数据t1_cf2,很显然,只针对更行操作加行锁会产生读取数据不一致的情况。最简单的数据不一致解决方案是读写线程公用一把行锁,这样可以保证读写之间互斥,但是读写线程同时抢占行锁必然会极大地影响性能。

为此,HBase采用MVCC解决方案避免读线程去获取行锁,数据更新操作时和读操作都进行了一定的修正,主要新增了一个写序号和读序号,其实就是数据的版本号。修正后的更新操作时序示意图为:

如上图所示,修正后的更新操作主要新增了‘获取写序号’和’结束写序号’两个步骤,并且每个cell数据写memstore操作都会携带该写序号。修正后的读请求需要在开始时分配一个读序号,称为读取点,读取点的值是所有的写操作完成序号中的最大整数,一次读操作的结果就是读取点对应的所有cell值的集合。这样就解决了读写操作并发时读取数据不一致的问题了。

二、Compact导致CPU使用率陡增问题

1.问题解决步骤

1.1 问题背景

平台侧对HBase集群服务端进行版本升级后,HBase集群大部分节点的CPU陡增至60%以上,导致持续报警,持续近一个月,暂无业务读取和推数性能影响。

1.2 平台分析

平台通过查看监控,发现在集群版本升级时间点之后,Compaction队列长度陡增,且下降缓慢。所以得出结论是由于版本升级导致集群全量数据进行major compaction,全量region进入compact队列,然后每天新推数又引发compaction,导致compact队列下降特别缓慢,CPU使用率持续高位。

1.3 解决方案

平台侧在跟进调整compact策略尽快消除compact队列。

2.HBase的Compaction机制

2.1 基本原理

Compaction是从一个Region的一个Store中选择部分HFile文件进行合并,即Compaction是以Store为单位的。先从待合并的数据文件中依次读出KeyValue,再由小到大排序后写入一个新的文件。

根据合并规模将Compaction分为两类:

1)Minor是指选取部分小的、相邻的HFile(老文件在前,新文件在后,此外BulkLoad进来的文件总是排在hbase内部生成的文件之前),将它们合并成一个更大的HFile,在这个过程中达到TTL的数据会被移除,但是DELETE类型数据不会被移除,这种合并触发频率较高。

2)Major是指将一个Store中所有的HFile合并成一个HFile,这个过程还会完全清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。这种合并触发频率较低,默认为7天一次。

Compaction带来的好处有:合并小文件,减少文件数,稳定随机读延迟;提高数据的本地化率;清除无效数据,减少数据存储量。但是Compaction是一个比较消耗带宽和IO资源的操作,Minor资源消耗较小,major资源消耗就比较大了,最好选择线上业务较少时进行Compaction操作。

2.2 Compaction触发时机

HBase 中触发 Compaction 的时机有很多,最常见的时机有如下三种:MemStore Flush、后台线程周期性检查以及手动触发。

1)MemStore Flush:MemStore Flush会产生 HFile,文件越来越多就需要执行Compaction。因此在每次执行完flush操作之后,都会对当前Store中的文件数进行判断,一旦Store中总文件数大于hbase.hstore.compactionThreshold,就会触发Compaction。

2)后台线程周期性检查:RegionServer会在后台启动一个线程CompactionChecker,定期触发检查对应Store 是否需要执行Compaction,大概2小时40分左右检查一次,如不满足minor条件则检查是否满足major条件,major则还需要检查是否满足7天时间。

3)手动触发:手动触发Compaction大多是为了执行Major,使用手动触发Major的原因通常有三个:其一,因为很多业务担心自动Major影响读写性能,直接关闭自动major,选择低峰期手动触发;其二,在执行完alter操作之后希望立刻生效,手动触发Major;其三,为删除大量过期数据,手动触发Major。

2.3 Compaction优化建议

1)Minor Compaction设置:hbase.hstore.compaction.min设置不能太小,也不能太大,建议设置为5~6;hbase.hstore.compaction.max.size = RegionSize / hbase.hstore.compaction.min;

2)Major Compaction设置:大Region读延迟敏感业务( 100G以上)通常不建议开启自动Major,手动低峰期触发。小Region或者延迟不敏感业务可以开启Major Compaction,但建议限制流量。

2.4 Compaction相关参数汇总

  • hbase.hstore.compactionThreshold:默认值为3,如果在任何一个Store中存在超过这个数量的HFile,运行Compaction将所有HFile重写为单个HFile。将该参数设置成较大的值会延迟Compaction,但是当Compaction运行时,则需要更长的时间。
  • hbase.server.thread.wakefrequency:默认值为10000ms,作为服务线程(如日志roller)的睡眠时间间隔。
  • hbase.server.compactchecker.interval.multiplier:默认值为1000,用于决定周期任务频率的值,以确定是否需要Compaction。通常情况下,Compaction是在一些事件(如memstore刷新)之后执行的,但是如果Region在一段时间内没有收到很多写操作,或者由于不同的Compaction策略,可能需要定期检查它。
  • hbase.hregion.majorcompaction:默认值为604800000ms,即7天,表示两次Major Compaction之间的时间间隔。设置为0可以禁用基于时间的自动Major Compaction,但手动触发和基于Store中HFile文件数量的Major Compaction仍将运行。
  • hbase.hregion.majorcompaction.jitter:默认值为0.5,这个值乘以hbase.hregion.majorcompaction使Compaction在给定的时间窗口中以某种随机的时间开始。该参数的值越小,Major Compaction就会越接近hbase.hregion.majorcompaction区间。
  • hbase.hstore.compaction.max.size:默认值:Long.MAX_VALUE,以字节表示,大于这个大小的HFile文件将被排除在Compaction之外。当Compaction发生得太频繁时,可以尝试提高这个值。
  • hbase.hstore.compaction.max:默认值为10,执行单次Minor Compaction过程可以被选择的HFile的最大数量,而与符合条件的HFile的数量无关。实际上,该参数的值控制完成一次Compaction所需的时间长度。将其设置得更大意味着Compaction中包含更多的HFile文件。对于大多数情况,默认值是合适的。
  • hbase.hstore.compaction.ratio:默认值为1.2F,对于Minor Compaction,这个比率用于确定给定的HFile文件大于等于hbase.hstore.compaction.min.size情况下是否适合进行Compaction。它的作用是限制对较大的HFile的进行Compaction。推荐取值范围为1.0和1.4之间的中等值。对于大多数情况,默认值是合适的。
  • hbase.hstore.compaction.ratio.offpeak:默认值为5.0F,工作原理同hbase.hstore.compaction.ratio参数,但是该值用于非高峰时间段内的Mimor Compaction。默认情况下非高峰时间段是关闭的,因此该参数的值不会生效。
  • hbase.offpeak.start.hour:默认值为-1,非高峰时间的开始,表示为0到23之间的整数。设置为-1禁用非峰值,即默认禁用。
  • hbase.offpeak.end.hour:默认值为-1,非高峰时间的结束,表示为0到23之间的整数。设置为-1禁用非峰值,即默认禁用。
  • hbase.hstore.compaction.min:默认为3,在运行Compaction之前必须符合Compaction条件的HFile文件的最小数量。调优该参数的的目的是避免产生太多需要Compaction的小HFile文件。将该值设置为2将导致每次在一个Store中有两个HFile文件时进行Minor Compaction,这可能不合适。如果您将该值设置得过高,则需要相应地调整所有其他值。对于大多数情况,默认值是合适的。在HBase的早期版本中,该参数被命名为hbase.hstore.compactionThreshold。
  • hbase.hstore.compaction.min.size:默认值为134217728,即128M,小于此大小的HFile文件将始终适合进行Minor Compaction。大于等于此大小的HFiles文件由hbase.hstore.compact.ratio评估,以确定它们是否符合条件。
  • hbase.regionserver.thread.compaction.throttle:默认值为2684354560,即2G,Compaction有两个不同的线程池,一个用于大型Compaction,另一个用于小型Compaction。这有助于保持精简表(如hbase:meta)的快速Compaction。如果Compaction大于此阈值,则将进入大型Compaction池。在大多数情况下,默认值是合适的。默认值是2 * hbase.hstore.compaction.max * hbase.hregion.memstore.flush.size。

参考文献

HBase Compaction原理及相关参数