前言:本篇主要梳理了HBase的架构设计,更多关于HBase的基础知识请参照《HBase学习笔记 - 基础篇》或HBase官方文档:
http://hbase.apache.org/book.html
除了官方文档,本文也引用了其他大神的观点,并结合自己的思路和理解输出到本文中。因为内容很长,写到后面,有些模块有点偷懒了,后续有机会补上。
如有疑问,欢迎留言一起探讨,共同进步。
一、名词解释
名称 | 释义 |
Master |
|
RegionServer |
|
Zookeeper |
|
Table | 类似于关系型数据库中的表,用于存储一系列逻辑相关的数据。 |
Region | 将Table基于RowKey区间进行水平拆分,每部分即为一个Region,类似于关系型数据库中的分表。Region是表可用性及分布性的基本单位。 |
二、HBase架构图
-
简化架构图
-
完整架构图
-
架构解读
- HBase是基于文件系统存储的,生产环境一般选用HDFS,它可以保障分布式环境下系统的高可用性。
- Zookeeper纵跨HDFS和HBase,提供分布式环境下的协调服务,关于Zookeeper在HBase中的应用,后面会详细说到。
- Master和RegionServer是HBase集群中的两类重要服务器角色。Master负责协调RegionServer,不负责数据的存储。RegionServer是实际存储数据的服务器。
- RegionServer包含多个Region,每个Region可包含多个Store(取决于列族数),Store由MemStore和多个StoreFile组成。RegionServer还包含一个全局的HLog,负责记录各Region的数据变更记录以支持HBse的容灾。
- 客户端Client只与Zookeeper和RegionServer交互,不会直接与Master交互,因此Master的负载很低。
二、HBase的读写流程
客户端对HBase的操作类型包括四种:Get/Scan/Put/Delete。Get用于单行查询,Scan用于范围查询,Get底层是基于Scan实现的。Put用于新增或更新,当RowKey不存在时,Put表现为新增,否则表现为更新。Delete用于删除。
在HBase中,因为底层存储媒介为文件系统,为了保证执行效率,更新及删除有别于传统的关系型数据库(如MySQL)。对于更新操作,HBase不会在相应行上直接进行更新,而是新插入一条记录,并基于时间戳实现数据的多版本,在通过RowKey查询数据时,返回数据的最新版本即可;对于删除操作,HBase也不会真正地对相应行执行物理删除,而是对该行数据打上"deleted"标签,在通过RowKey查询数据时,过滤掉"deleted"标签的数据即可,真正的物理删除发生在Major Compaction阶段。由此可知,读流程实际要比写流程复杂得多。
HBase数据的读写流程不需要HMaster的参与,HMaster仅维护Table和Region的元数据信息,负载很低。客户端不需要知道HMaster或RegionServer的地址,只需配置Zookeeper的地址即可。
在HBase 0.96之前,-ROOT- 和 .META.表(或称hbase:meta表)是与读写流程相关的两张表,这两张表与其他用户自定义的HBase表无异。-ROOT- 表不会分裂,且仅有一个Region,存储 .META. 表的Region信息。.META. 表可分裂为多个Region,存储用户表的Region信息。在之后的HBase版本中,-ROOT- 表已被移除,仍用 .META. 表存储用户表的Region信息,而 .META. 表的位置信息存于Zookeeper中。鉴于目前使用的版本大多在HBase 1.0+,以下的讨论均基于HBase 0.96+,即不涉及 -ROOT- 表的HBase版本。
参考官方文档,.META. 表的结构如下:
-
RegionServer存储架构图
架构解读:
- 客户端通过KeyValue对象向RegionServer发送读写请求;
- HBase基于文件系统存储数据,为了优化读写性能,在StoreFile(磁盘存储)之上,提供了MemStore(内存存储)。MemStore中数据是有序的(基于RowKey的字典序),当MemStore中的数据达到阈值时,数据会被异步刷写到StoreFile中,StoreFile中的数据也是基于RowKey有序的。
- 考虑RegionServer可能有宕机的情况,此时MemStore中来不及刷写到文件系统中的数据存在丢失的风险,体现为客户端的部分操作请求没有生效。HBase采用了WAL机制(即HLog)以规避这种情况的发生。数据的每一次变更都需要先写入Hlog,然后才是MemStore,HLog写入失败则代表整个请求的失败,这样当RegionServer出现宕机的情况时,依旧可以通过HLog重播数据的变更记录,保障了系统的可靠用。
- HBase支持关闭HLog,以达到性能的进一步提升,但会有数据丢失的风险。官方文档推荐仅当做批量导入时才考虑关闭此特性。
-
HBase的读流程
- 客户端请求Zookeeper获取 .META. 表所在的RegionServer信息;
- 客户端查询 .META. 表(存储用户表的Region信息),并将 .META. 表的数据缓存到客户端本地;
- 客户端通过 .META. 表,确定待检索RowKey所在的RegionServer信息;
- 客户端向该RegionServer发送真正的数据读取请求;
- RegionServer的内存分MemStore和BlockCache两部分,MemStore主要用于写请求,BlockCache用于读请求。数据读取请求优先从MemStore中查找数据,其次从BlockCache中查找,若都查不到,则从StoreFile中查找,并将查询结果写入到BlockCache中。
附注:客户端最少需要一次RPC(本地已缓存 .META. 表的情况),最多需要3次RPC(本地未缓存 .META. 表的情况),可以完成一次HBase读请求。
-
HBase的写流程
- 客户端请求Zookeeper获取 .META. 表所在的RegionServer信息;
- 客户端查询 .META. 表(存储用户表的Region信息),并将 .META. 表的数据缓存到客户端本地;
- 客户端通过 .META. 表,确定待操作RowKey所在的RegionServer信息;
- 客户端向该RegionServer发送真正的写请求;
- 数据写请求优先写HLog,其次写MemStore;
- 当MemStore达到设定的阈值后,HBase会将数据异步刷写到StoreFile中;
- 当StoreFile数量达到设定的阈值后,HBase会触发StoreFile合并,得到更少、更大的StoreFile,以提升检索性能;(这一步同时会触发数据的版本合并及物理删除)
- 当StoreFile大小达到设定的阈值后,HBase会触发Region分裂,由HMaster通过负载均衡将分裂后的Region分配到RegionServer中。
附注:客户端最少需要一次RPC(本地已缓存 .META. 表的情况),最多需要3次RPC(本地未缓存 .META. 表的情况),可以完成一次HBase写请求。
三、MemStore
-
什么是MemStore?
首先我们看下HBase官方文档提供的表存储相关对象的层级关系图:
将表基于RowKey区间进行水平拆分,每部分即为一个Region,Region是表可用性和分布性的基本单位。Region可包含多个Store,其数量取决于表的列族数量。Store由MemStore(内存存储)和StoreFile(磁盘存储)组成,对于每个Store,MemStore只有一个,StoreFile可以有多个。Block则是StoreFile的基本存储单位。由此也可以看出,HBase是基于列族存储的。
-
为什么需要MemStore?
我们知道,HBase底层是基于文件系统(如HDFS)做数据存储的,由此会带来以下问题:
- HBase读写依赖于磁盘IO,性能很差;
- HBase不直接支持二级索引,其查询性能依赖于RowKey的有序性,而在文件系统中做排序又是一个难题;
因此,HBase在StoreFile(磁盘存储)之上提供了MemStore(内存存储)。数据都先写入MemStore,并基于RowKey做内存排序,当MemStore大小达到设定的阈值时,再由异步任务将数据刷写到StoreFile中。由此便解决了以上两个问题。
-
MemStore刷写的时机
- MemStore级别的限制:当MemStore达到hbase.hregion.memstore.flush.size指定的大小时,会触发MemStore刷写;
- Region级别的限制:当Region中所有MemStore的大小总和达到hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size时,会触发MemStore刷写;
- RegionServer级别的限制:当RegionServer中所有MemStore的大小总和达到hbase.regionserver.global.memstore.upperLimit * hbase_heapsize时,会按MemStore由大到小触发MemStore刷写;
- RegionServer级别的限制:当一个RegionServer中HLog的数量达到hbase.regionserver.maxlogs的指定值时,为了舍弃最早的HLog,HBase会从最早的HLog记录的数据变更中选取一个或多个Region执行MemStore刷写,随后才能删除HLog;
- HBase的MemStore定期刷写机制,以避免MemStore长时间没有持久化,默认周期为一小时。且为了避免大量MemStore同时触发磁盘刷写,定期触发的MemStore刷写有20000ms左右的随机延迟;
- 通过shell命令 flush ‘tablename’ 或 flush ‘region name’ 对表或Region手动触发刷写。
附注:HLog记录的是整个Region的MemStore刷写状态,而非单个MemStore的刷写状态。因此MemStore刷写的最小单元是Region,而非单个MemStore。不难得知,若表具有较多的列族,即对应多个MemStore,则MemStore刷写的开销也会随之增加,这也是HBase官方推荐表不要太多列族的原因之一(一般保持在1 ~ 3个列族)。
-
MemStore刷写的流程
- prepare阶段:遍历当前Region中的所有MemStore,为MemStore中的数据集生成一个快照snapshot,并新建一个新的数据集kvset。为了避免写操作影响了快照的数据一致性,这个过程需要加锁,但因为无耗时操作,持锁时间很短。在这之后,写操作都走新的数据集kvset,而读操作首先检索新的数据集kvset,其次是snapshot,其次是BlockCache,最后才是StoreFile。
- flush阶段:遍历当前Region中的所有MemStore,将prepare阶段生成的snapshot持久化为临时文件,并存到tmp目录下。此过程涉及磁盘IO,较为耗时。
- commit阶段:遍历当前Region中的所有MemStore,将flush阶段生成的临时文件移到列族对应的数据目录下。然后生成StoreFile和Reader对象,并把StoreFile添加到对应Store的StoreFile列表中。最后删除prepare阶段生成的snapshot。
四、WAL
-
什么是WAL?
HBase的WAL(Write Ahead Log),即预写日志,提供了一种高并发、持久化的日志保存与回放机制。
-
为什么需要WAL?
HBase的存储基于文件系统,因而磁盘IO是HBase读写的性能瓶颈。为了提升读写性能,HBase引入了MemStore(内存存储),数据变更写入StoreFile之前,需要先写入MemStore,后续由HBase通过异步任务刷写到磁盘上。这种设计又会面临新的问题,当RegionServer宕机时,MemStore中的数据可能仍未刷写到磁盘中,这时便会出现数据丢失的风险。HLog是HBase中WAL的实现,正是为了解决上述问题引入的。每个HBase写操作(Put/Delete)执行前,均需要先写入WAL,写WAL成功后方能写MemStore,这样便保证了RegionServer宕机后,仍可以通过WAL重播数据的变更记录。
这里可能会有个误区,如果没有WAL,那么写操作是直接写入StoreFile(一次磁盘IO),而引入WAL后,写操作是先写WAL(一次磁盘IO),然后再写缓存MemStore。这样看来,引入WAL后,写操作甚至多了一次写缓存的开销,事实真的是这样吗?显然不是的,原因有二:
- HBase为了提升检索性能,StoreFile中的记录基于RowKey有序,若没有MemStore,是无法直接基于文件系统排序的。
- MemStore作为StoreFile之上的内存存储,可以大幅提升HBase的读取性能。
五、Compaction
-
什么是Compaction?
HBase将Store中较多、较小的StoreFile文件合并为较少、较大的StoreFile文件的过程称为Compaction,Compaction是以Store为单位进行的。
-
为什么需要Compaction?
Compaction的引入大致是为了解决以下问题:
- 如前所述,针对HBase的写操作(Put/Delete),需要先写WAL,其次写MemStore,当MemStore中的数据达到配置的阈值时,由HBase的异步任务将数据刷写到磁盘上,得到一个StoreFile文件。随着时间推移,Store(Region的列族存储空间)中的StoreFile越来越多,且这个过程中可能对同一RowKey的数据进行了多次更新(实质为添加新记录)或执行了删除(标记为"deleted"),因而HBase的读请求可能需要查询多个StoreFile的同时,还需要能正确处理这些数据的版本变更,于是HBase的读请求变得越来越复杂。Compaction通过将多个StoreFile合并为较大的StoreFile,同时清除过期和删除的数据,以达到优化查询性能的目的。
- HDFS的NameNode需要维护StoreFile的元信息,大量StoreFile文件会加重HDFS的NameNode负载。
-
Compaction的分类
- Minor Compaction:选取一些小的、相邻的StoreFile,并将它们合并成一个较大StoreFile的过程。这个过程不会清理删除、过期或多余版本的数据(Minor Compaction仅掌握部分StoreFile信息,事实上也无法做到脏数据的清理)。
- Major Compaction:将一个Store中的所有StoreFile文件合并成一个StoreFile的过程。这个过程中会清理三类脏数据:已删除的数据、已过期的数据、超过最大设定版本数的数据。一般情况下,Major Compaction会消耗较多系统资源,对上层业务有较大的影响,因此线上业务一般会选择关闭自动触发Major Compaction的功能,并选择在业务低峰时间段手动执行。HBase默认七天左右自动触发一次Major Compaction。
-
Compaction的触发时机
- HBase后台线程周期性触发。可通过将hbase.hregion.majorcompaction设置为0以关闭自动触发Major Compaction的功能;
- MemStore Flush:StoreFile产生的缘由,因此每次执行完此过程,HBase都会判断是否需要执行Compaction操作。需要说明的是,虽然Compaction是以Store为单位进行的,但MemStore Flush却是以Region为单位进行的,因此MemStore Flush可能触发Region中的多个Store同时执行Compaction;
- 通过compact或major_compact手动触发。
附注:除了上述的第三种情况(明确指明执行Minor Compaction/Major Compaction),其他两种情况下,HBase会优先判断是否符合Minor Compaction的条件,其次才考虑Major Compaction。具体的触发条件较为复杂,这里不展开说明,有需要请参阅其他资料。
-
Compaction的流程
Compaction的大致流程如图所示,这里我们主要围绕StoreFile的合并过程展开进一步的讨论,StoreFile合并的流程如下:
- 分别读取待合并StoreFile的数据,并将数据写入到tmp目录下的临时文件中。若是Major Compaction,则在此过程中,删除、过期及超过设定版本数的数据均被过滤和清理;
- 将临时文件移动到对应Region的数据目录下。直到执行第3步前,HBase的读写请求仍访问到待合并的StoreFile中;
- 将合并后的新文件替换到StoreFileManager管理的StoreFile中,替换过程中需要加短暂的写锁,完成StoreFile替换后,由合并后的新的StoreFile对外提供服务,将本次Compaction的信息写入到WAL中;
- 将对应Region目录下的被合并的原文件删除,完成整个Compaction流程。
附注:整个Compaction流程有很强的容错性及幂等性。若在HBase读写切换到合并后的StoreFile之前发生异常,本次Compaction被视为失败,不影响下一次Compaction,唯一的影响是多了一份冗余的数据;若HBase读写切换到合并后的StoreFile,且写WAL成功之后发生异常,只需要在WAL重播的时候删除合并前的StoreFile即可。
-
Compaction的作用及副作用
下图为脱离Compaction,StoreFile数量的增加对HBase读延迟的影响
下图为引入Compaction,StoreFile数量的增加对HBase读延迟的影响
作用:随着数据写入,StoreFile的数量越来越多,HBase读性能随之越来越差。而Compaction可以使StoreFile数量基本保持稳定,使HBase的读延迟可以稳定在一定的范围内。可以认为,Compaction是以短时间的IO及带宽消耗换取后续查询操作的低延迟。
副作用:从上面图二可以看出,引入Compaction后,存在一定程度的读延迟毛刺,这是Compaction过程中消耗较多的系统资源所致。另外,在极端的高并发写入场景下,可能存在Compaction的速度跟不上StoreFile生成速度的情况,这样会使StoreFile不断增加最终影响到读性能。为了避免这种情况,在StoreFile数量过多的时候需要限制HBase的写入请求,即在执行MemStore Flush之前,若发现StoreFile数量超过hbase.hstore.blockingStoreFiles的配置值,则会阻塞MemStore Flush操作hbase.hstore.blockingWaitTime配置的时间,随后才能执行MemStore Flush。因而,Compaction在特定情况下,也会造成一定程度的写阻塞。
六、Region拆分
-
为什么需要Region拆分?
通过上面的讨论,我们知道,HBase写操作会先写MemStore,当MemStore中的数据量达到设定的阈值时,触发MemStore Flush,生成StoreFile文件。随着写操作的进行,StoreFile文件不断增加继而影响读性能,HBase通过Compaction机制将StoreFile文件合并,使StoreFile文件稳定在一定的数量,以此提升读性能。但随着Compaction的进行,Store中StoreFile变得越来越大,由此又会带来以下问题:
- 数据分布不均匀。随着StoreFile的不断增大,其所属的RegionServer承载的读请求也会越来越多,尤其对于热点数据,若未及时分散压力,必然会导致严重的性能问题。
- Compaction性能损耗严重。Compaction本质上是一个排序合并的操作,需要占用大量的内存,文件越大,内存占用越多。另外,Compaction操作可能需要迁移远程数据至本地进行处理,文件越大,迁移的带宽成本越高。
- 大文件本身对读性能的影响。
- 在高并发写入的场景中,若未及时对Region进行拆分和重新调整负载均衡,那么单台RegionServer的资源将很快耗尽。
基于以上考虑,HBase引入了Region拆分机制,微观上体现为Region中的每个StoreFile拆分。类似于传统关系型数据库的大数据表的扩展方式,HBase天然支持自动分库分表,这是由HBase的Region Split及Rebalance实现的。
-
Region拆分的触发时机
- MemStore Flush后会生成一个新的StoreFile,并判断是否需要执行Region拆分;
- Region中的Store触发Compaction操作后,会判断是否需要执行Region拆分;
- 通过HBase命令手动执行Regino拆分。
-
Region拆分的流程
prepare阶段:
RegionServer在内存中初始化两个子Region,同时生成一个 transaction journal,这个对象用来记录Region拆分的进展。
execute阶段:
a.)创建一个Zookeeper节点/hbase/region-in-transition/region-name,并将其状态置为
SPLITTING;
b.)Master通过对region-in-transition节点的监听,检测到Region的状态变化;
c.)RegionServer在父Region的HDFS数据目录下创建一个名为.splits的子目录;
d.)RegionServer将父Region关闭,并在RegionServer的本地数据结构中将父Region标记为下线状态,并将Region中的数据强制刷写到磁盘上。此后,客户端对此Region发送的HBase读写请求都将抛出NotServingRegionException,客户端需要进行重试,直到新的Region上线;
e.)RegionServer在HDFS的.splits目录下创建daughterA和daughterB的子目录,并分别在两个目录下创建reference引用文件,分别指向父Region的StoreFile的一部分;
f.)RegionServer在HDFS上创建实际的子Region目录,即上述.splits目录下的daughterA和daughterB的子目录,并将引用文件分别放入对应子Region的目录中;
g.)RegionServer发送写请求到.META.表,将父Region的状态置为下线状态,并将子Region的信息添加到.META.表中,如上图.META.表第一行,此时,.META.表还没有单独的子Region条目,即没有上图.META.表的第二、第三行。若RegionServer执行这个过程失败,则Master和下一个分配此Region的RegionServer会将本次Region拆分的脏数据清理掉;
h.)RegionServer并行打开两个子Region:daughterA和daughterB。此时子Region已准备好随时可以对外提供服务;
i.)RegionServer发送写请求到.META.表,将daughterA和daughterB添加到.META.表,并将其置为上线状态。这样,子Region便能被客户端发现了,可正式对外提供服务。
j.)RegionServer将Zookeeper节点/hbase/region-in-transition/region-name的状态置为SPLIT,同时Master可以监听到Region拆分的状态变化,Region拆分事务就此结束。
若有需要,Master会重新调整Region在HBase集群中的负载均衡。
rollback阶段:
整个Region拆分的execute阶段被分为很多子流程,若在execute阶段出现异常,则通过transaction journal执行rollback操作,清理相应阶段产生的脏数据。
附注:为了减少Region拆分对上层业务的影响,Region拆分过程中并不涉及数据的迁移操作,子Region仅保存父Region数据文件的引用,整个Region拆分可以在秒级完成。父Region中的数据最终会迁移到子Region中,这一过程发生在子Region中的Store触发Major Compaction的时候(Region拆分后会触发子Region的Major Compaction)。当HBase的定时任务检测到父Region的数据文件未被引用,HBase会将其删除。
-
Region拆分的事务性保证
Region拆分是一个复杂的流程,需要保证其事务性,即要么拆分成功,要么回滚为拆分前的状态。HBase使用了状态机(见SplitTransaction类)的方式保存各步骤的状态,一旦发生异常,HBase可根据当前所处的状态决定是否回滚及如何回滚。但在HBase2.0之前,Region拆分的状态信息保存在RegionServer的内存中,一旦RegionServer出现宕机,就可能出现中间状态,需要手动解决。HBase2.0之后,通过HLog保存其中间状态,规避了上述的问题。
-
Region拆分的影响
- 如上图所示,当父Region关闭而子Region未开启时,客户端的请求会抛出NotServingRegionException异常,客户端需针对这种场景应用重试机制;
- 父Region的数据文件实际写入子Region数据文件发生在子Region的Major Compaction阶段,这一过程会消耗大量的IO及带宽资源。
-
Region拆分的最佳实践
- 对于预估数据量较大的表,需要在表创建的时候根据RowKey进行预分区,由此可以减少热点数据剧增时导致的性能问题;
- 生产环境建议关闭Region的自动拆分,通过运维手动去维护。
Region合并
-
为什么需要Region合并?
顾名思义,Region拆分和Region合并是两个相反的过程,根据上面的分析,Region拆分是必要的,那么是否意味着Region合并是多余的呢?显然不是的。随着Region拆分,HBase集群中的Region个数不断增加,由此又会带来一系列的性能问题,因此需要引入Region合并。
-
Region合并的流程
附注:Region合并需要Master和RegionServer的参与,在RegionServer受到Region合并请求后,具体的本地Region合并流程类似于Region拆分过程,这里不再累赘。
Region的状态机
-
Region的状态
HBase为每个Region记录了状态并保存在.META.表(即hbase:meta表)中,.META.表本身的状态保存在Zookeeper中。可通过HBase的Master Web UI查看Region的状态,以下是可能的Region状态:
OFFLINE:下线状态;
OPENING:Region正在打开;
OPEN:Region处于打开状态,且RegionServer已通知Master;
FAILED_OPEN:Region打开失败;
CLOSING:Region正在关闭;
CLOSED:Region处于关闭状态,且RegionServer已通知Master;
FAILED_CLOSE:Region关闭失败;
SPLITTING:Region正在拆分;
SPLIT:Region拆分成功,且RegionServer已通知Master;
SPLITTING_NEW:Region拆分产生的新的Region;
MERGING:Region正在合并;
MERGED:Region合并成功,且RegionServer已通知Master;
MERGING_NEW:Region合并产生的新的Region;
-
Region的状态转换
附注:具体的状态流转这里就不详细展开了,需要了解的请自行参照HBase官方文档。
RegionServer的自动故障转移
当RegionServer出现故障的时候,Zookeeper通过心跳检测将RegionServer的状态上报给Master,由Master将RegionServer上的Region重新分配到其他RegionServer上,这种可能性建立在RegionServer未发生物理宕机的条件下。