Hbase底层解析
hfile+compaction
原理
用户数据写入先写WAL,再写缓存,满足一定条件后缓存数据会执行flush操作真正落盘,形成一个数据文件HFile。太多数据文件会导致数据查询IO次数增多,因此HBase尝试着不断对这些文件进行合并,这个合并过程称为Compaction。
Compaction过程会有以下作用:
(1)合并文件
(2)清除删除、过期、多余版本的数据
(3)提高读写数据的效率
Compaction会从一个region的一个store中选择一些hfile文件进行合并,分为MinorCompaction和MajorCompaction。
- MinorCompaction是指选取一些小的、相邻的StoreFile将他们合并成一个更大的StoreFile,在这个过程中不会处理已经Deleted或Expired的Cell。一次Minor Compaction的结果是更少并且更大的StoreFile。
- MajorCompaction是指将所有的StoreFile合并成一个StoreFile,这个过程还会清理三类无意义数据:被删除的数据、TTL过期数据、版本号超过设定版本号的数据。
触发compaction
HBase中可以触发compaction的因素有很多,最常见的因素有这么三种:Memstore Flush、后台线程周期性检查、手动触发。
- Memstore Flush: 应该说compaction操作的源头就来自flush操作,memstore flush会产生HFile文件,文件越来越多就需要compact。因此在每次执行完Flush操作之后,都会对当前Store中的文件数进行判断,一旦文件数# > ,就会触发compaction。需要说明的是,compaction都是以Store为单位进行的,而在Flush触发条件下,整个Region的所有Store都会执行compact,所以会在短时间内执行多次compaction。
- 后台线程周期性检查:后台线程CompactionChecker定期触发检查是否需要执行compaction,检查周期为:hbase.server.thread.wakefrequencyhbase.server.compactchecker.interval.multiplier。和flush不同的是,该线程优先检查文件数#是否大于,一旦大于就会触发compaction。如果不满足,它会接着检查是否满足major compaction条件,简单来说,如果当前store中hfile的最早更新时间早于某个值mcTime,就会触发major compaction,HBase预想通过这种机制定期删除过期数据。上文mcTime是一个浮动值,浮动区间默认为[7-70.2,7+7*0.2],其中7为hbase.hregion.majorcompaction,0.2为hbase.hregion.majorcompaction.jitter,可见默认在7天左右就会执行一次major compaction。用户如果想禁用major compaction,只需要将参数hbase.hregion.majorcompaction设为0
- 手动触发:一般来讲,手动触发compaction通常是为了执行major compaction,原因有三,其一是因为很多业务担心自动major compaction影响读写性能,因此会选择低峰期手动触发;其二也有可能是用户在执行完alter操作之后希望立刻生效,执行手动触发major compaction;其三是HBase管理员发现硬盘容量不够的情况下手动触发major compaction删除大量过期数据;无论哪种触发动机,一旦手动触发,HBase会不做很多自动化检查,直接执行合并。
Compaction流程
一旦触发,HBase会将该Compaction交由一个独立的线程处理,该线程会执行以下操作:
- 首先会从对应store中选择合适的hfile文件进行合并,这一步是整个Compaction的核心,选取文件需要遵循很多条件,比如文件数不能太多、不能太少、文件大小不能太大等等,最理想的情况是,选取那些承载IO负载重、文件小的文件集,实际实现中,HBase提供了多个文件选取算法:RatioBasedCompactionPolicy、ExploringCompactionPolicy和StripeCompactionPolicy等,用户也可以通过特定接口实现自己的Compaction算法。
- 选出待合并的文件后,HBase会根据这些hfile文件总大小挑选对应的线程池处理。
- 最后对这些文件执行具体的合并操作。
Exception
1. compaction操作重写文件会带来很大的带宽压力以及短时间IO压力
2. 对 hbase 进行短时间大批量数据导入时,hbase 很容易触发 regionserver 的 compaction 操作,在 compaction 过程中,会获得region too busy 的 Exception。
org.apache.hadoop.hbase.client.RetriesExhaustedWithDetailsException: Failed 1 action: RegionTooBusyException
......
......
对此类Exception的处理,目前程序中所使用的方法是
- 使用try..catch..得到此类错误的时候让当前线程sleep一段时间,再重新导入,直至没有catch到此类错误。(办法总比困难多,希望以后有更好的处理方法)
Rowkey特性
字符串类型
虽然行键在HBase中是以byte[]字节数组的形式存储的,但是建议在系统开发过程中将其数据类型设置为String类型,保证通用性;如果在开发过程中将RowKey规定为其他类型,譬如Long型,那么数据的长度将可能受限于编译环境等所规定的数据长度。
常用的行键字符串有以下几种:
纯数字字符串,譬如9559820140512;
数字+特殊分隔符,譬如95598-20140512;
数字+英文字母,譬如city20140512;
数字+英文字母+特殊分隔符,譬如city_20140512。
有明确意义
RowKey的主要作用是为了进行数据记录的唯一性标示,但是唯一性并不是其全部,具有明确意义的行键对于应用开发、数据检索等都具有特殊意义。譬如上面的数字字符串9559820140512,其实际意义是这样:95598(电网客服电话)+20140512(日期)。
行键往往由多个值组合而成,而各个值的位置顺序将影响到数据存储和检索效率,所以在设计行键时,需要对日后的业务应用开发有比较深入的了解和前瞻性预测,才能设计出可尽量高效率检索的行键。
有序性
RowKey是按照字典序存储,因此,设计RowKey时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为RowKey的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE – timestamp作为RowKey,这样能保证新写入的数据在读取时可以被快速命中。
定长性
行键具有有序性的基础便是定长,譬如20140512080500、20140512083000,这两个日期时间形式的字符串是递增的,不管后面的秒数是多少,我们都将其设置为14位数字形式,如果我们把后面的0去除了,那么201405120805将大于20140512083,其有序性发生了变更。所以我们建议,行键一定要设计成定长的。
计数器
两种方法
- RMW(read-modify-write)
- increment
优劣比较
以前人的比较结果进行描述:
- 单线程环境下进行的RMW速率比increment快
- 多线程环境下的RMW需要对列进行加锁解锁操作,效率比increment慢
原理简述
RMW操作中的主要瓶颈在read操作,在对多列进行数据获取的情况下,效率会明显变慢,且多线程环境下,更是需要增加加锁解锁操作。
increment是hbase自带的计数器API,线程安全,操作原子性。