HBase最佳实践

一、架构原理

1.1 基本架构

客户端要访问HBase中的数据,只需要知道Zookeeper集群的连接信息,访问步骤如下:

  • 客户端将从Zookeeper(/hbase/meta-region-server)获得 hbase:meta 表存储在哪个RegionServer,缓存该位置信息
  • 查询该RegionServer上的 hbase:meta 表数据,查找要操作的 rowkey所在的Region存储在哪个RegionServer中,缓存该位置信息
  • 在具体的RegionServer上根据rowkey检索该Region数据

可以看到,客户端操作数据过程并不需要HMaster的参与,通过Zookeeper间接访问RegionServer来操作数据。

第一次请求将会产生3次RPC,之后使用相同的rowkey时客户端将直接使用缓存下来的位置信息,直接访问RegionServer,直至缓存失效(Region失效、迁移等原因)。

通过Zookeeper的读写流程如下:

  • Rowkey格式:tableName,regionStartKey,regionId
    • 第一个region的regionStartKey为空
    • 示例:ns1:testTable,xxxxreigonid
  • 只有一个列族info,包含三个列:
    • regioninfo:RegionInfo的proto序列化格式,包含regionId,tableName,startKey,endKey,offline,split,replicaId等信息
    • server:RegionServer对应的server:port
    • serverstartcode:RegionServer的启动时间戳

简单总结Zookeeper在HBase集群中的作用如下:

  • 对于服务端,是实现集群协调与控制的重要依赖
  • 对于客户端,是查询与操作数据必不可少的一部分

HMaster

BlockCache

BlockCache为RegionServer中的 读缓存,一个RegionServer共用一个BlockCache。

RegionServer处理客户端读请求的过程:

BlockCache有两种实现方式,有不同的应用场景,各有优劣:

  • On-Heap的LRUBlockCache
    • 优点:直接中Java堆内内存获取,响应速度快
    • 缺陷:容易受GC影响,响应延迟不稳定,特别是在堆内存巨大的情况下
    • 适用于:写多读少型、小内存等场景
  • Off-Heap的BucketCache
    • 优点:无GC影响,延迟稳定
    • 缺陷:从堆外内存获取数据,性能略差于堆内内存
    • 适用于:读多写少型、大内存等场景

我们将在「性能优化」一节中具体讨论如何判断应该使用哪种内存模式。

WAL

全称 Write Ahead Log ,是 RegionServer 中的预写日志。

所有写入数据默认情况下都会先写入WAL中,以保证RegionServer宕机重启之后可以通过WAL来恢复数据,一个RegionServer中共用一个WAL。

RegionServer的写流程如下:

WAL会通过 日志滚动 的操作定期对日志文件进行清理(已写入HFile中的数据可以清除),对应HDFS上的存储路径为 /hbase/WALs/${HRegionServer_Name}

Region

一个Table由一个或者多个Region组成,一个Region中可以看成是Table按行切分且有序的数据块,每个Region都有自身的StartKey、EndKey。

一个Region由一个或者多个Store组成,每个Store存储该Table对应Region中一个列簇的数据,相同列簇的列存储在同一个Store中。

同一个Table的Region会分布在集群中不同的RegionServer上以实现读写请求的负载均衡。故,一个RegionServer中将会存储来自不同Table的N多个Region。

Store、Region与Table的关系可以表述如下:多个Store(列簇)组成Region,多个Region(行数据块)组成完整的Table。

其中,Store由Memstore(内存)、StoreFile(磁盘)两部分组成。

在RegionServer中,Memstore可以看成指定Table、Region、Store的写缓存(正如BlockCache小节中所述,Memstore还承载了一些读缓存的功能),以RowKey、Column Family、Column、Timestamp进行排序。如下图所示:

  • ROW:基于 Rowkey 创建的Bloom Filter

  • ROWCOL:基于 Rowkey+Column 创建的Bloom Filter

  • 两者的区别仅仅是:是否使用列信息作为Bloom Filter的条件

    使用ROWCOL时,可以让指定列的查询更快,因为其通过Rowkey与列信息来过滤不存在数据的HFile,但是相应的,产生的Bloom Filter数据会更加庞大

    而只通过Rowkey进行检索的查询,即使指定了ROWCOL也不会有其他效果,因为没有携带列信息。

    通过Bloom Filter(如果有的话)快速定位到当前的Rowkey数据存储于哪个HFile之后(或者不存在直接返回),通过HFile携带的 Data Block Index 等元数据信息可快速定位到具体的数据块起始位置,读取并返回(加载到缓存中)。

    这就是Bloom Filter在HBase检索数据的应用场景:

    1. 高效判断key是否存在
    2. 高效定位key所在的HFile

    当然,如果没有指定创建Bloom Filter,RegionServer将会花费比较多的力气一个个检索HFile来判断数据是否存在。

    1.4 HFile存储格式

    通过Bloom Filter快速定位到需要检索的数据所在的HFile之后的操作自然是从HFile中读出数据并返回。

    据我们所知,HFile是HDFS上的文件(或大或小都有可能),现在HBase面临的一个问题就是如何在HFile中 快速检索获得指定数据/p>

    HBase随机查询的高性能很大程度上取决于底层HFile的存储格式,所以这个问题可以转化为 HFile的存储格式该如何设计,才能满足HBase 快速检索 的需求。

    生成一个HFile

    Memstore内存中的数据在刷写到磁盘时,将会进行以下操作:

    • 会先现在内存中创建 空的Data Block数据块 包含 预留的Header空间。而后,将Memstore中的KVs一个个顺序写满该Block(一般默认大小为64KB)。
    • 如果指定了压缩或者加密算法,Block数据写满之后将会对整个数据区做相应的压缩或者加密处理。
    • 随后在预留的Header区写入该Block的元数据信息,如 压缩前后大小、上一个block的offset、checksum 等。
    • 内存中的准备工作完成之后,通过HFile Writer输出流将数据写入到HDFS中,形成磁盘中的Data Block。
    • 为输出的Data Block生成一条索引数据,包括 {startkey、offset、size} 信息,该索引数据会被暂时记录在内存中的Block Index Chunk中。

    至此,已经完成了第一个Data Block的写入工作,Memstore中的 KVs 数据将会按照这个过程不断进行 写入内存中的Data Block -> 输出到HDFS -> 生成索引数据保存到内存中的Block Index Chunk 流程。

    值得一提的是,如果启用了Bloom Filter,那么 Bloom Filter Data(位图数据)Bloom元数据(哈希函数与个数等) 将会和 KVs 数据一样被处理:写入内存中的Block -> 输出到HDFS Bloom Data Block -> 生成索引数据保存到相对应的内存区域中

    由此我们可以知道,HFile写入过程中,Data Block 和 Bloom Data Block 是交叉存在的

    随着输出的Data Block越来越多,内存中的索引数据Block Index Chunk也会越来越大。

    达到一定大小之后(默认128KB)将会经过类似Data Block的输出流程写入到HDFS中,形成 Leaf Index Block (和Data Block一样,Leaf Index Block也有对应的Header区保留该Block的元数据信息)。

    同样的,也会生成一条该 Leaf Index Block 对应的索引记录,保存在内存中的 Root Block Index Chunk

    Root Index -> Leaf Data Block -> Data Block 的索引关系类似 B+树 的结构。得益于多层索引,HBase可以在不读取整个文件的情况下查找数据。

    随着内存中最后一个 Data Block、Leaf Index Block 写入到HDFS,形成 HFile 的 Scanned Block Section

    Root Block Index Chunk 也会从内存中写入HDFS,形成 HFile 的 Load-On-Open Section 的一部分。

    至此,一个完整的HFile已经生成,如下图所示:

    1.5 HFile Compaction

    Bloom Filter解决了如何在大量的HFile中快速定位数据所在的HFile文件,虽然有了Bloom Filter的帮助大大提升了检索效率,但是对于RegionServer来说 要检索的HFile数量并没有减少

    为了再次提高HFile的检索效率,同时避免大量小文件的产生造成性能低下,RegionServer会通过 Compaction机制 对HFile进行合并操作

    常见的Compaction触发方式有:

    • Memstore Flush检测条件执行
    • RegionServer定期检查执行
    • 用户手动触发执行

    Minor Compaction

    Minor Compaction 只执行简单的文件合并操作,选取较小的HFiles,将其中的数据顺序写入新的HFile后,替换老的HFiles。

    但是如何在众多HFiles中选择本次Minor Compaction要合并的文件却有不少讲究:

    • 首先排除掉文件大小 大于 hbase.hstore.compaction.max.size 值的HFile
    • 将HFiles按照 文件年龄排序(older to younger),并从older file开始选择
    • 如果该文件大小 小于 hbase.hstore.compaction.min 则加入Minor Compaction中
    • 如果该文件大小 小于 后续hbase.hstore.compaction.max 个HFile大小之和 * hbase.hstore.compaction.ratio,则将该文件加入Minor Compaction中
    • 扫描过程中,如果需要合并的HFile文件数 达到 hbase.hstore.compaction.max(默认为10) 则开始合并过程
    • 扫描结束后,如果需要合并的HFile的文件数 大于 hbase.hstore.compaction.min(默认为3) 则开始合并过程
    • 通过 hbase.offpeak.start.hour、hbase.offpeak.end.hour 设置高峰、非高峰时期,使 hbase.hstore.compaction.ratio的值在不同时期灵活变化(高峰值1.2、非高峰值5)

    可以看到,Minor Compaction不会合并过大的HFile,合并的HFile数量也有严格的限制,以避免产生太大的IO操作,Minor Compaction经常在Memstore Flush后触发,但不会对线上读写请求造成太大延迟影响。

    Compaction的优缺点

    HBase通过Compaction机制使底层HFile文件数保持在一个稳定的范围,减少一次读请求产生的IO次数、文件Seek次数,确保HFiles文件检索效率,从而实现高效处理线上请求。

    如果没有Compaction机制,随着Memstore刷写的数据越来越多,HFile文件数量将会持续上涨,一次读请求生产的IO操作、Seek文件的次数将会越来越多,反馈到线上就是读请求延迟越来越大

    然而,在Compaction执行过程中,不可避免的仍然会对线上造成影响。

    • 对于Major Compaction来说,合并过程将会占用大量带宽、IO资源,此时线上的读延迟将会增大
    • 对于Minor Compaction来说,如果Memstore写入的数据量太多,刷写越来越频繁超出了HFile合并的速度
      • 即使不停地在合并,但是HFile文件仍然越来越多,读延迟也会越来越大
      • HBase通过 hbase.hstore.blockingStoreFiles(默认7) 来控制Store中的HFile数量
      • 超过配置值时,将会堵塞Memstore Flush阻塞flush操作 ,阻塞超时时间为 hbase.hstore.blockingWaitTime
      • 阻塞Memstore Flush操作将会使Memstore的内存占用率越来越高,可能导致完全无法写入

    简而言之,Compaction机制保证了HBase的读请求一直保持低延迟状态,但付出的代价是Compaction执行期间大量的读延迟毛刺和一定的写阻塞(写入量巨大的情况下)。

    1.6 Region Split

    HBase通过 LSM-Tree架构提供了高性能的随机写,通过缓存、Bloom Filter、HFile与Compaction等机制提供了高性能的随机读

    至此,HBase已经具备了作为一个高性能读写数据库的基本条件。如果HBase仅仅到此为止的话,那么其也只是个在架构上和传统数据库有所区别的数据库而已,作为一个高性能读写的分布式数据库来说,其拥有近乎可以无限扩展的特性

    支持HBase进行自动扩展、负载均衡的是Region Split机制

    Split策略与触发条件

    在HBase中,提供了多种Split策略,不同的策略触发条件各不相同。

    1. RegionServer在Zookeeper上的 /hbase/region-in-transition 节点中标记该Region状态为SPLITTING
    2. HMaster监听到Zookeeper节点发生变化,在内存中修改此Region状态为RIT
    3. 在该Region的存储路径下创建临时文件夹 .split
    4. 父Region close,flush所有数据到磁盘中,停止所有写入请求
    5. 在父Region的 .split文件夹中生成两个子Region文件夹,并写入reference文件
      • reference是一个特殊的文件,体现在其文件名与文件内容上
      • 文件内容:[splitkey]切分点rowkey[toptrue/false,true为top上半部分,false为bottom下半部分
      • 根据reference文件名,可以快速找到对应的父Region、其中的HFile文件、HFile切分点,从而确认该子Region的数据范围
      • 数据范围确认完毕之后进行正常的数据检索流程(此时仍然检索父Region的数据
    6. 将子Region的目录拷贝到HBase根目录下,形成新的Region
    7. 父Regin通知修改 hbase:meta 表后下线,不再提供服务
      • 此时并没有删除父Region数据,仅在表中标记split列、offline列为true,并记录两个子region
    8. 两个子Region上线服务
    9. 通知 hbase:meta 表标记两个子Region正式提供服务

    rollback阶段

    如果execute阶段出现异常,则执行rollback操作,保证Region切分整个过程是具备事务性、原子性的,要么切分成功、要么回到未切分的状态。

    region切分是一个复杂的过程,涉及到父region切分、子region生成、region下线与上线、zk状态修改、元数据状态修改、master内存状态修改 等多个子步骤,回滚程序会根据当前进展到哪个子阶段清理对应的垃圾数据

    为了实现事务性,HBase设计了使用**状态机(SplitTransaction类)**来保存切分过程中的每个子步骤状态。这样一来一旦出现异常,系统可以根据当前所处的状态决定是否回滚,以及如何回滚。

    但是目前实现中,中间状态是存储在内存中,因此一旦在切分过程中RegionServer宕机或者关闭,重启之后将无法恢复到切分前的状态。即Region切分处于中间状态的情况,也就是RIT

    由于Region切分的子阶段很多,不同阶段解决RIT的处理方式也不一样,需要通过hbck工具进行具体查看并分析解决方案。

    好消息是HBase2.0之后提出了新的分布式事务框架Procedure V2,将会使用HLog存储事务中间状态,从而保证事务处理中宕机重启后可以进行回滚或者继续处理,从而减少RIT问题产生。

    父Region清理

    从以上过程中我们可以看到,Region的切分过程并不会父Region的数据到子Region中,只是在子Region中创建了reference文件,故Region切分过程是很快的。

    只有进行Major Compaction时才会真正(顺便)将数据切分到子Region中,将HFile中的kv顺序读出、写入新的HFile文件。

    RegionServer将会定期检查 hbase:meta 表中的split和offline为true的Region,对应的子Region是否存在reference文件,如果不存在则删除父Region数据。

    负载均衡

    Region切分完毕之后,RegionServer上将会存在更多的Region块,为了避免RegionServer热点,使请求负载均衡到集群各个节点上,HMaster将会把一个或者多个子Region移动到其他RegionServer上。

    移动过程中,如果当前RegionServer繁忙,HMaster将只会修改Region的元数据信息至其他节点,而Region数据仍然保留在当前节点中,直至下一次Major Compaction时进行数据移动。

    HBase最佳实践
    至此,我们已经揭开了HBase架构与原理的大部分神秘面纱,在后续做集群规划、性能优化与实际应用中,为什么这么调整以及为什么这么操作 都将一一映射到HBase的实现原理上。

    如果你希望了解HBase的更多细节,可以参考《HBase权威指南》。

    二、集群部署

    经过冗长的理论初步了解过HBase架构与工作原理之后,搭建HBase集群是使用HBase的第一个步骤。

    需要注意的是,HBase集群一旦部署使用,再想对其作出调整需要付出惨痛代价(线上环境中),所以如何部署HBase集群是使用的第一个关键步骤。

    2.1 集群物理架构

    硬件混合型+软件混合型集群

    硬件混合型 指的是该集群机器配置参差不齐,混搭结构

    软件混合型 指的是该集群部署了一套类似CDH全家桶套餐

    如以下的集群状况:

    • 集群规模:30
    • 部署服务:HBase、Spark、Hive、Impala、Kafka、Zookeeper、Flume、HDFS、Yarn等
    • 硬件情况:内存、CPU、磁盘等参差不齐,有高配有低配,混搭结构

    这个集群不管是规模、还是服务部署方式相信都是很多都有公司的「标准」配置。

    那么这样的集群有什么问题呢/p>

    如果仅仅HBase是一个非「线上」的系统,或者充当一个历史冷数据存储的大数据库,这样的集群其实一点问题也没有,因为对其没有任何苛刻的性能要求。

    但是如果希望HBase作为一个线上能够承载海量并发、实时响应的系统,这个集群随着使用时间的增加很快就会崩溃。

    硬件混合型 来说,一直以来Hadoop都是以宣称能够用低廉、老旧的机器撑起一片天。

    这确实是Hadoop的一个大优势,然而前提是作为离线系统使用。

    离线系统的定义,即跑批的系统,如:Spark、Hive、MapReduce等,没有很强的时

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2019年9月9日
    下一篇 2019年9月9日

    相关推荐