本文2014年7月份发表于InfoQ,HBase的PMC成员Ted Yu先生参与了审稿并于给予了确定。该方案设计之初仅寄但愿于经过二级索引提高查询性能,因为在前期架构时充分考虑了通用性以及对复杂条件的支持,在后来的演变中逐渐被剥离出来造成了一个通用的查询引擎。Ted Yu对“查询决策器”表示了关心,他指出相似的组件同时也是Phoenix, Impala用于支持SQL查询的核心组件,可是这类组件很难引入到HBase中,由于HBase专一于byte[]的操做。对此,方案在设计时避开了“SQL解析”和“在各类数据类型与byte[]之间进行转化”的棘手问题,而是使用了一组能够描述查询的Query API,这与Hibernate中提供Criteria接口的作法很是类似,在Hibernate中既支持HQL语句的查询又支持使用Criteria接口以编程方式描述的查询,对于咱们来讲选择相似后者的作法实现起来要快速和容易的多,而查询条件中的值在构造之初就以byte[]的形式传递,避免了决策器解析时的类型断定和转化问题。本文原文出处:http://blog.csdn.net/bluishglc/article/details/31799255 严禁任何形式的转载,不然将委托CSDN官方维护权益!前端
——索引的实质是另外一种编排形式的数据冗余,高效的检索源自于面向查询特别设计的编排形式,若是再辅以分布式的计算框架,就能够支撑起高性能的大数据查询。数据库
Apache HBase™是一个分布式、可伸缩的NoSQL数据库,它构建在Hadoop基础设施之上,依托于Hadoop的迅猛发展,HBase在大数据领域的应用愈来愈普遍,成为目前NoSQL数据库中表现最耀眼,呼声最高的产品之一。像其余NoSQL数据库同样,HBase也有其适用范围,就应对复杂条件的查询来讲,通常认为它并非很是适合[i],熟悉HBase的开发人员对此应该有必定的体会,可是基于广泛的需求,开发者们但愿HBase在保持高性能优点的同时能对复杂条件的查询给予必定的支持,而本文将要介绍的正是一种在HBase现行机制下以非侵入式实现的基于二级多列索引的高性能复杂条件查询引擎。编程
目前HBase主要应用在结构化和半结构化的大数据存储上,其在插入和读取上都具备极高的性能表现,这与它的数据组织方式有着密切的关系,在逻辑上,HBase的表数据按RowKey进行字典排序, RowKey其实是数据表的一级索引(Primary Index),因为HBase自己没有二级索引(Secondary Index)机制,基于索引检索数据只能单纯地依靠RowKey,为了能支持多条件查询,开发者须要将全部可能做为查询条件的字段一一拼接到RowKey中,这是HBase开发中极为常见的作法,可是不管怎样设计,单一RowKey固有的局限性决定了它不可能有效地支持多条件查询。一般来讲,RowKey只能针对条件中含有其首字段的查询给予使人满意的性能支持,在查询其余字段时,表现就差强人意了,在极端状况下某些字段的查询性能可能会退化为全表扫描的水平,这是由于字段在RowKey中的地位是不等价的,它们在RowKey中的排位决定了它们被检索时的性能表现,排序越靠前的字段在查询中越具备优点,特别是首位字段具备特别的先发优点,若是查询中包含首位字段,检索时就能够经过首位字段的值肯定RowKey的前缀部分,从而大幅度地收窄检索区间,若是不包含则只能在全体数据的RowKey上逐一查找,由此能够想见二者在性能上的差距。后端
受限于单一RowKey在复杂查询上的局限性,基于二级索引(Secondary Index)的解决方案成为最受关注的研究方向,而且开源社区已经在这方面已经取得了必定的成果,像ITHBase、IHBase以及华为的hindex项目,这些产品和框架都按照本身的方式实现了二级索引,各自具备不一样的优点,同时也都有必定局限性,本文阐述的方案借鉴了它们的一些优势,在确保非侵入的前提下,以高性能为首要目标,经过创建二级多列索引实现了对复杂条件查询的支持,同时经过提供通用的查询API,以及彻底基于配置的索引结构,彻底封装了索引的建立和使用细节,使之成为一种通用的查询引擎。缓存
“二级多列索引”是针对目标记录的某个或某些列创建的“键-值”数据,以列的值为键,以记录的RowKey为值,当以这些列为条件进行查询时,引擎能够经过检索相应的“键-值”数据快速找到目标记录。因为HBase自己并无索引机制,为了确保非侵入性,引擎将索引视为普通数据存放在数据表中,因此,如何解决索引与主数据的划分存储是引擎第一个须要处理的问题,为了能得到最佳的性能表现,咱们并无将主数据和索引分表储存,而是将它们存放在了同一张表里,经过给索引和主数据的RowKey添加特别设计的Hash前缀,实现了在Region切分时,索引可以跟随其主数据划归到同一Region上,即任意Region上的主数据其索引也一定驻留在同一Region上,这样咱们就能把从索引抓取目标主数据的性能损失下降到最小。与此同时,特别设计的Hash前缀还在逻辑上把索引与主数据进行了自动的分离,当全体数据按RowKey排序时,排在前面的都是索引,咱们称之为索引区,排在后面的均为主数据,咱们称之为主数据区。最后,经过给索引和主数据分配不一样的Column Family,又在物理存储上把它们隔离了起来。逻辑和物理上的双重隔离避免了将两类数据存放在同一张表里带来的反作用,防止了它们之间的相互干扰,下降了数据维护的复杂性,能够说这是在性能和可维护性上达到的最佳平衡。架构
图1:Sample表Region 1的数据逻辑视图并发
让咱们经过一个示例来详细了解一下二级多列索引表的结构,假定有一张Sample表,使用四位数字构成Hash前缀[ii],范围从0000到9999,规划切分100个Region,则100个Region的RowKey区间分别为[0000,0099],[0100,0199],……,[9900,9999],以第一个Region为例,请看图1,全部数据按RowKey进行字典排序,自动分红了索引区和主数据区两段,主数据区的Column Family是d,下辖q1,q2,q3等Qualifier,为了简单起见,咱们假定q1,q2,q3的值都是由两位数字组成的字符串,索引区的Column Family是i,它不含任何Qualifier,这是一个典型的“Dummy Column Family“,做为区别于d的另外一个Column Family,它的做用就是让索引独立于主数据单独存储。接下来是最重要的部分,即索引和主数据的RowKey,咱们先看主数据的RowKey,它由四位Hash前缀和原始ID两部分组成,其中Hash前缀是由引擎分配的一个范围在0000到9999之间的随机值,经过这个随机的Hash前缀可让主数据均匀地散列到全部的Region上,咱们看图1,由于Region 1的RowKey区间是[0000,0099],因此没有任何例外,凡是且必须是前缀从0000到0099的主数据都被分配到了Region 1上。接下来看索引的RowKey,它的结构要相对复杂一些,格式为:RegionStartKey-索引名-索引键-索引值,与主数据不一样,索引RowKey的前缀部分虽然也是由四位数字组成,但却不是随机分配的,而是固定为当前Region的StartKey,这是很是重要而巧妙的设计,一方面,这个值处在Region的RowKey区间以内,它确保了索引一定跟随其主数据被划分到同一个Region里;另外一方面,这个值是RowKey区间内的最小值,这保证了在同一Region里全部索引会集中排在主数据以前。接下来的部分是“索引名”,这是引擎给每类索引添加的一个标识,用于区分不一样类型的索引,图1中展现了两种索引:a和b,索引a是为字段q1和q2设计的两列联合索引,索引b是为字段q2和q3设计的两列联合索引,依次类推,咱们能够根据须要设计任意多列的联合索引。再接下来就是索引的键和值了,索引键是由目标记录各对应字段的值组成,而索引值就是这条记录的RowKey。框架
如今,假定须要查询知足条件q1=01 and q2=02的Sample记录,分析查询字段和索引匹配状况可知应使用索引a,也就是说咱们首先肯定了索引名,因而在Region 1上进行scan的区间将从主数据全集收窄至[0000-a, 0000-b),接着拼接查询字段的值,咱们获得了索引键:0102,scan区间又进一步收窄为[0000-a-0102, 0000-a-0103),因而咱们能够很快地找到0000-a-0102-0000|63af51b2这条索引,进而获得了索引值,也就是目标数据的RowKey:0000|63af51b2,经过在Region内执行Get操做,最终获得了目标数据。须要特别说明的是这个Get操做是在本Region上执行的,这和经过HTable发出的Get有很大的不一样,它专门用于获取Region的本地数据,其执行效率是很是高的,这也是为何咱们必定要将索引和它的主数据放在同一张表的同一个Region上的缘由。分布式
在了解了引擎的工做原理以后来咱们来看一下它的总体架构:工具
图2:引擎的总体架构
引擎构建在HBase的Coprocessor机制之上,由Client端和Server端两部分构成,对于查询而言,查询请求从Client端经由HTable的coprocessorExec方法推送到全部的RegionServer上,RegionServer接收到查询请求后使用“查询决策器”分析查询条件,比对索引元数据,在找到适合该查询的最优索引后,解析索引区间,而后委托“索引查询器”基于给定的最优索引和解析区间进行数据检索,若是没有找到合适的索引则委托“全表查询器”进行全表扫描。当各RegionServer的局部查询结果返回以后,引擎的Client端还负责对它们并进行合并汇总和排序,从而获得最终的结果集。对于插入而言,当主数据试图写入时会被Coprocessor拦截,委托“索引构造器”根据“索引配置文件”建立指向当前主数据的全部索引,而后一同插入到数据表中。
让咱们来深刻了解一下引擎的几个核心组件。对于引擎的客户端来说,最重要的组件是一套用于表达复杂查询请求的Query API,在这套API的设计上咱们借鉴了IHBase的一些作法,经过对查询条件(Condition)进行抽象和建模,获得一套典型的基于“复合模式”(Composite Pattern)的Class Hierarchy,使之可以优雅地表达基于AND和OR的多重复合条件。以图1所示的Sample表为例,使用Query API构造一个查询条件为“(q1=01 and q2<02) or (q1=03 and q2>04)”的Java代码以下:
图3:引擎客户端的Query API示意代码
查询请求到达Server端之后,由Coprocessor委派查询决策器进行分析以肯定使用何种查询策略应对,这是查询处理流程上的一个关键结点。查询决策器须要分析查询请求的各项细节,包括条件字段、排序字段和排序,而后和索引的元数据进行比对找出性能最优的索引,有时候对于一个查询请求可能会有多个适用索引,可是查询性能却有高下之分,所以须要对每个候选索引进行性能评估,找出最优者,性能评估的方法是看哪一个索引能最大限度地收窄检索区间。索引的元数据来自于索引配置文件,图4展现了一份简单的索引配置,配置中描述的正是图1中使用的索引a和b的元数据,索引元数据主要是由索引名和一组field组成,filed描述的是索引针对的目标列(ColumnFamily:Qualifier)。实际的索引配置一般比咱们看到的这份要复杂,由于在生成索引时有不少细节须要经过索引配置给出指引,好比如何处理不定长字段,目标列使用正序仍是倒序(例如时间数据在HBase中常常须要按补值进行倒序处理),是否须要使用自定义格式化器对目标列的值进行格式化等等,彻底配置化的索引元数据使建立和维护索引的成本大大下降,为上层应用根据实际需求灵活设计索引提供了保障。
图4:一份简单的索引配置文件
在肯定最优索引以后,查询决策器开始基于最优索引对查询条件进行解析,解析的结果是一组索引区间,区间内的数据未必都知足查询条件,但倒是经过计算所能获得的最小区间,索引查询器就在这些区间上进行检索,经过配备的专用Filter对区间内的每一条数据进行最后的匹配判断。图5展现了一个条件为q1=01 and 01<=q2<=03的查询请求在Sample表Region 1上的解析和执行过程。
图5 :查询请求q1=01 and 01<=q2<=03在Sample表Region 1上的解析和执行过程示意
对于那些找不到索引的查询请求来讲,查询决策器将委派全表查询器处理,全表查询器将跳过索引区,从主数据区开始经过配备的专用Filter进行全表扫描。显然,相对于索引查询,全表扫描的执行效率是很低的,它的存在是为了在全部索引都不适用的状况下起“托底”做用,以此保证任意复杂条件的查询都能获得处理,因此这里引出一个很是重要的问题,就是在索引查询和全表扫描之间的选择与权衡问题。一般人们老是但愿全部的查询都越快越好,虽然从理论上讲创建覆盖任意条件查询的索引是可能的,但这是不现实的,由于建立索引是有代价的,除了占用大量的存储空间以外还会影响到数据插入的性能,因此不能无节制地建立索引,理性的作法是分析并筛选出最为经常使用的查询,针对这些查询创建相应的索引,优化查询性能,而对于那些较为“生僻”的查询则使用全表扫描的方式进行处理,以此在存储成本、插入性能和查询性能之间找到一种理想的平衡。最后要补充说明的是,无论是使用索引查询仍是进行全表扫描,这些动做都是经过Coprocessor机制分发到全部Region上去并发执行的,即便是全表扫描其性能也将远超过HBase原生的Scan操做!
因为引擎设计之初就以非侵入性为前提,因此引擎的部署与集成就与引入第三方类库无异,惟一须要上层应用提供的是面向数据表的索引配置文件。设计索引主要以业务需求为导向,先分析并梳理出经常使用的查询用例,而后针对查询用例所涉及的字段和排序要求按类似性进行分组,尽量让单个索引同时支持多种相近的查询,减小索引的种类和数量,提高索引复用率。在这方面以下设计原则可供参考(注:如下原则均以“不考虑排序”为前提):
假如某表有A、B、C、D四个字段,在不考虑排序的前提下,若是要用索引支持以任意字段或字段组合为条件的查询,则索引的设计方法以下:四字段索引只须要一个,假定取ABCD(它将同时支持ABCD、ABC、AB和A四种查询)。三字段索引分别以A、B、C、D开头向后循环取足三个字段,获得:ABC、BCD(它将同时支持BCD、BC和B三种查询)、CDA(它将同时支持CDA、CD和C三种查询)和DAB(它将同时支持DAB、DA和D三种查询),其中ABC是ABCD的前缀,故舍弃。按照一样的方法,两字段索引要分别从保留下来的三个三字段索引中依次以每个字段开头取足两个字段,而后去除重复和前缀重叠的索引,最终获得DB(它将同时支持DB和D两种查询)和AC(它将同时支持AC和A两种查询),总计是6个索引,最后能够再根据实际需求剪裁掉不须要的索引。
在上述原则的表述中特别注明了“不考虑排序“这个前提,对于索引来讲,”排序“是一个很“敏感”的要求,索引自己只有一种排序(即按索引首字段进行的字典排序),若是查询请求的排序与索引排序不一样,则索引直接出局,即便它们的字段彻底匹配,也就是说排序会极大地消弱索引的复用度,对于咱们的引擎来讲,排序字段应该受到严格的控制。实际上,不少大数据系统都须要对排序进行限制,好比淘宝上的商品检索,可供排序的字段只有人气,销量,信用和价格,由于排序须要针对数据全集进行计算,若是不是针对有限的排序字段创建索引或是离线计算并缓存结果,按任意字段排序的查询是很难在线返回的。
综合前文所述,方案主要有以下几个显著的优点:
限于HBase自身的特色,方案自己也有必定的局限性,一是它不能随意地支持任意的条件查询,这一点前文已经给出了分析和建议,二是在插入主数据时须要伴随插入多份索引从而对写入性能产生了必定的影响,如何控制写入和查询的竞争关系须要根据系统的读写比进行权衡,对于数据写入实时性要求不高或者数据是离线导入的系统来讲,能够考虑使用批量导入工具,特别是以直接生成HFile的方式导入的话能够在很大程度上消除引入索引后的写入压力。
[i]理论上基于HBase的 Filter机制能够实现任意复杂条件的查询,可是那样作就完全放弃了RowKey做为索引的利用价值,大多数查询的性能都将变得很是差。
[ii]Hash前缀的长度和Region数量有着密切的关系,因为索引和主数据的分配高度依赖RowKey前缀和Region的RowKey区间,引擎严禁Region进行自动切分,开发人员须要在前期对Region数量和前缀长度进行规划,本例中取四位前缀意味着最多能够支持10000个Region。
转:http://blog.csdn.net/bluishglc/article/details/31799255
相关阅读: