HBase 的一些总结

最近接手了一个新的项目,因为单表都是涉及到几十亿的数据而且每天是不停的增长单机无法承载,调研后项目使用了 HBase 做存储,下面对 HBase 做一个总结。

与 MySQL、Mongo 等数据库不同,HBase 没有除 rowKey 之外的索引,所有数据的查询都是依赖与 rowKey,在表设计层面,rowKey 的设计非常重要。HBase 天然就是一个分布式数据库,有点类似于 Mongo 的 Sharding 机制,Mongo 的 configserver 决定数据落到哪一个分区上,而 HBase 通过 Zookeeper 来获取 root 表所在地址,通过 root 表得到相应的 meta 表信息最终定位数据存储的 region 位置。

基本概念

HBase 底层依赖于 HDFS 来做存储,有几个比较重要的概念:

  1. Master: 通常有 2 个,以主备模式运行,基于 Zookeeper 竞争,未竞选成功的称为 Backup Master,可以有多个。Master 负责协调集群工作,不处理数据。
  2. RegionServer: 一个集群中有很多 RegionServer,是处理数据请求的实际角色。每个 RegionServer 负责特定数量的 Region(通常会自动均衡)。

HBase 中的 Table 概念上可以类比为 MySQL 的 Table,只是数据结构有差异。并且没有索引的概念,所有的查询都是基于 RowKey 进行的,RowKey 相当于主键。根据主键的范围,一个 Table 会被划分为 N 个 Region(相当于 MySQL 的分表),N 取决于系统配置的 Region 大小自动切分,或者手动切分。每个 Region 会被分配到一个 RegionServer 提供服务。

HBase 存储基于 HDFS,由 HDFS 提供可靠性和扩展性。通常 RegionServer 要和 HDFS DataNode 一起部署,以获得较好的性能。这是因为 RegionServer 在存储数据的时候会优先选择本机的 DataNode 节点存储一个副本(通常 HDFS 设置 3 副本),这样没有网络开销。这个特性叫做 Locality,如果值太低(每个 Region 单独计算,最高为 1)可能会影响读写性能。

HBase 在写入数据(更新也是写入)的时候,首先会写入到内存中,称为 memstore,然后定期或者写入达到一定量之后刷盘产生一个文件,RegionServer 会在某些情况下进行整理数据的操作以提高效率,该操作被称为 Compact。Compact 又分为 minor 和 major 两种:minor_compact 由复杂的条件决定,小范围的进行文件合并,清理,对服务影响较小;major_compact 定时进行或者手动触发,会对 Table 的所有文件进行整理、合并、清洗,删除旧版本数据等,对性能影响较大,所以通常安排在业务低峰期进行。

当一个 Region 的数据越来越多,达到某个设定的阈值之后会进行 Split 操作,即将一个 Region 切分成 2 个或者多个 Region,以提高服务性能。Split 操作可以手动进行,通常情况下大部分时间不需要关注。

每个 Region 对应数个 HDFS 上的文件,文件由 HDFS 多副本机制保证可靠性。如果 RegionServer 宕机,Master 会协调将宕机 RegionServer 上分配的 Region 重新分配给其他 RegionServer,此过程可能持续数分钟,持续时间内受影响的 Region 不可访问,其他 Region 不受影响。重新分配候 Locality 会降低。

通过增加 HDFS DataNode 可以增加存储层的吞吐量,增加 RegionServer 可以提高应用程序访问的吞吐量。理论上是可以任意扩展的。

HBase 给外部 client 提供的就是 zk 的地址,zk 中存有 root 表所在的 rs 地址,从 root 表可以获取 meta 表信息,根据 meta 表可以获取 region 在 rs 上的分布地址,client 获得这个地址后向该地址发送数据请求。

我们可以思考一个问题,既然 zk 可以保存 root 表信息那为什么不直接把 meta 表信息保存都 zk 中?主要是数据量太大,zk 不宜保存大量数据,而 meta 表主要是保存 region 和 region server 的映射信息,在内存允许的方位 region 的数量可以无限多,如果保存在 zk 中,zk 的压力会很大。

另外对于 zk=>root=>meta 表这个流程,client 端是有缓存的,第一次查询到相应的 region 所在 rs 后,这个信息会被缓存到客户端,以后每次访问都直接从缓存中获取 rs 地址即可。region 所在的 rs 可能发生变化,比如被 rebalance 到其他 rs 上了,这个时候缓存信息可能会有错误会抛出异常,在这种情况下,client 只需要重新走一遍上面的流程获取新的 rs 地址即可。

HLog

通过 zk=>root=>meta 或者缓存中的信息获取到 rs 地址后,直接向 rs 上对应的 region 写入数据,HBase 的数据写入采用 wal 的形式,先写 log 后写数据,HBase 是一个 append 类型的数据库,没有关系型数据库那么复杂的操作,所以记录 HLog 的操作都是简单的 put 操作,delete/update 操作都被转化为 put 进行。

HLog 是 HBase WAL 产生的日志信息,内部是一个简单的顺序日志,每个 RS 上的 Region 都共享一个 HLog,所有对于该 Region 的修改都被记录在这个 HLog 中。HLog 的主要作用就是在 RS 出现意外崩溃的时候,可以尽量多的恢复数据,这里说是尽量多,因为在一般情况下,客户端为了提高性能,会把 HLog 的 auto flush 关掉,这样 HLog 日志的落盘全靠操作系统保证,如果出现意外崩溃,短时间内没有被 fsync 的日志会被丢失。

HLog 的大量写入会造成 HLog 占用存储空间会越来越大,HBase 会将过期的 HLog 进行清理,每个 RS 内部都有一个 HLog 监控线程在运行,其周期可以通过 hbase.master.cleaner.interval 进行配置。HLog 在数据从 memstore flush 到底层存储上后,说明该段 HLog 已经不再被需要,就会被移动到 .oldlogs 这个目录下,HLog 监控线程监控该目录下的 HLog,当该文件夹下的 HLog 达到 hbase.master.logcleaner.ttl 设置的过期条件后,监控线程立即删除过期的 HLog。

wal 的顺序写可以极大地提高性能,并且在一个事务内是可回滚的,后续总结 HBase 读写一条数据的流程时将会提及。

Memstore

memstore 是 region 内部缓存,其大小通过 HBase 参数 hbase.hregion.memstore.flush.size 进行配置。RS 在写完 HLog 以后,数据写入的下一个目标就是 region 的 memstore,memstore 在 HBase 内部通过 LSM-tree 结构组织,所以能够合并大量对于相同 rowkey 上的更新操作。

正是由于 memstore 的存在,HBase 的数据写入都是异步的,而且性能非常不错,写入到 memstore 后,该次写入请求就可以被返回,HBase 即认为该次数据写入成功。

Memstore 中的数据在一定条件下会进行刷写操作,使数据持久化到相应的存储设备上。memstore 整体内存占用上限通过参数 hbase.regionserver.global.memstore.upperLimit 进行设置,当然在达到上限后,memstore 的刷写也不是一直进行,在内存下降到 hbase.regionserver.global.memstore.lowerLimit 配置的值后,即停止 memstore 的刷盘操作。这样做,主要是为了防止长时间的 memstore 刷盘,会影响整体的性能。除了这个之外,memstore 的大小通过 hbase.hregion.memstore.flush.size 进行设置,当 region 中 memstore 的数据量达到该值时,会自动触发 memstore 的刷盘操作。

memstore 的数据刷盘,对 region 的直接影响就是:在数据刷盘开始到结束这段时间内,该 region 上的访问都是被拒绝的,这里主要是因为在数据刷盘结束时,RS 会对该 region 做一个 snapshot,同时 HLog 做一个 checkpoint 操作,通知 ZK 哪些 HLog 可以被移到 .oldlogs 下。在 memstore 写盘开始,相应 region 会被加上 UpdateLock 锁,写盘结束后该锁被释放。

StoreFile

memstore 在触发刷盘操作后会被写入底层存储,每次 memstore 的刷盘就会相应生成一个存储文件 HFile,storeFile 即 HFile 在 HBase 层的轻量级分装。

数据量的持续写入,造成 memstore 的频繁 flush,每次 flush 都会产生一个 HFile,这样底层存储设备上的 HFile 文件数量将会越来越多。

Compact

大量 HFile 的产生,会消耗更多的文件句柄,同时会造成 RS 在数据查询等的效率大幅度下降,HBase 为解决这个问题,引入了 compact 操作,RS 通过 compact 把大量小的 HFile 进行文件合并,生成大的 HFile 文件。RS 上的 compact 根据功能的不同,可以分为两种不同类型,即:minor compact 和 major compact。

minor compact 在 RS 运行过程中会频繁进行,主要通过参数 hbase.hstore.compactionThreshold 进行控制,该参数配置了 HFile 数量在满足该值时,进行 minor compact,minor compact 只选取 region 下部分 HFile 进行 compact操作,并且选取的 HFile 大小不能超过 hbase.hregion.max.filesize 参数设置。

major compact 会对整个 region 下相同列簇的所有 HFile 进行 compact,也就是说 major compact 结束后,同一个列簇下的 HFile 会被合并成一个。major compact 是一个比较长的过程,对底层 I/O 的压力相对较大。

major compact 除了合并 HFile 外,另外一个重要功能就是清理过期或者被删除的数据。前面提到过,HBase 的 delete 操作也是通过 append 的方式写入,一旦某些数据在 HBase 内部被删除了,在内部只是被简单标记为删除,真正在存储层面没有进行数据清理,只有通过 major compact 对 HFile 进行重组时,被标记为删除的数据才能被真正的清理。compact 操作都有特定的线程进行,一般情况下不会影响 RS 上数据写入的性能,当然也有例外:在 compact 操作速度跟不上 region 中 HFile 增长速度时,为了安全考虑,RS 会在 HFile 达到一定数量时,对写入进行锁定操作,直到 HFile 通过 compact 降到一定的范围内才释放锁。

Split

compact 将多个 HFile 合并单个 HFile 文件,随着数据量的不断写入,单个 HFile 也会越来越大,大量小的 HFile 会影响数据查询性能,大的 HFile 也会,HFile 越大,相对的在 HFile 中搜索的指定 rowkey 的数据花的时间也就越长,HBase 同样提供了 region 的 split 方案来解决大的 HFile 造成数据查询时间过长问题。

split 只是简单的把 region 从逻辑上划分成两个,并没有涉及到底层数据的重组,split 完成后,Parent region 并没有被销毁,只是被做下线处理,不再对外部提供服务。而新产生的 region Daughter A 和 Daughter B,内部的数据只是简单的指向 Parent region 数据的索引,Parent region 数据的清理在 Daughter A 和 Daughter B 进行 major compact 以后,发现已经没有到其内部数据的索引后,Parent region 才会被真正的清理。