二级索引与索引Join是多数业务系统要求存储引擎提供的基本特性,RDBMS早已支持,NOSQL阵营也在摸索着符合自身特色的最佳解决方案。
这篇文章会以HBase作为对象来讨论如何基于Hbase构建二级索引与实现索引join。文末同时会列出目前已知的包括0.19.3版secondary index, ITHbase, Facebook方案和官方Coprocessor的介绍。html
理论目标
在HBase中实现二级索引与索引Join须要考虑三个目标:
1,高性能的范围检索。
2,数据的低冗余(存储所占的数据量)。
3,数据的一致性。git
性能与数据冗余,一致性是相互制约的关系。
若是你实现了高性能地范围检索,必然须要靠冗余索引数据来提高性能,而数据冗余会致使更新数据时难以实现一致性,特别是分布式场景下。
若是你不要求高效地范围检索,那么能够不考虑产生冗余数据,一致性问题也能够间接避免,毕竟share nothing是公认的最简单有效的解决方案。github
理论结合实际,下文会以实例的方式来阐述各个方案是如何选择偏重点。
这些方案是通过笔者资料查阅和同事的不断交流后得出的结论,若有错误,欢迎指正:apache
1,按索引建表
每个索引创建一个表,而后依靠表的row key来实现范围检索。row key在HBase中是以B+ tree结构化有序存储的,因此scan起来会比较效率。
单表以row key存储索引,column value存储id值或其余数据 ,这就是Hbase索引表的结构。服务器
如何Join?
多索引(多表)的join场景中,主要有两种参考方案:app
1,按索引的种类扫描各自独立的单索引表,最后将扫描结果merge。
这个方案的特色是简单,可是若是多个索引扫描结果数据量比较大的话,merge就会遇到瓶颈。框架
好比,如今有一张1亿的用户信息表,建有出生地和年龄两个索引,我想获得一个条件是在杭州出生,年龄为20岁的按用户id正序排列前10个的用户列表。
有一种方案是,系统先扫描出生地为杭州的索引,获得一个用户id结果集,这个集合的规模假设是10万。
而后扫描年龄,规模是5万,最后merge这些用户id,去重,排序获得结果。less
这明显有问题,如何改良?
保证出生地和年龄的结果是排过序的,能够减小merge的数据量?但Hbase是按row key排序,value是不能排序的。
变通一下 – 将用户id冗余到row key里?OK,这是一种解决方案了,这个方案的图示以下:分布式
merge时提取交集就是所须要的列表,顺序是靠索引增长了_id,以字典序保证的。ide
2, 按索引查询种类创建组合索引。
在方案1的场景中,想象一下,若是单索引数量多达10个会怎么样?10个索引,就要merge 10次,性能可想而知。
解决这个问题须要参考RDBMS的组合索引实现。
好比出生地和年龄须要同时查询,此时若是创建一个出生地和年龄的组合索引,查询时效率会高出merge不少。
固然,这个索引也须要冗余用户id,目的是让结果天然有序。结构图示以下:
这个方案的优势是查询速度很是快,根据查询条件,只须要到一张表中检索便可获得结果list。缺点是若是有多个索引,就要创建多个与查询条件一一对应的组合索引,存储压力会增大。
在制定Schema设计方案时,设计人员须要充分考虑场景的特色,结合方案一和二来使用。下面是一个简单的对比:
单索引 | 组合索引 | |
检索性能 | 优异 | 优异 |
存储 | 数据不冗余,节省存储。 | 数据冗余,存储比较浪费。 |
事务性 | 多个索引保证事务性比较困难。 | 多个索引保证事务性比较困难。 |
join | 性能较差 | 性能优异 |
count,sum,avg,etc | 符合条件的结果集全表扫描 | 符合条件的结果集全表扫描 |
从上表中能够得知,方案1,2都存在更新时事务性保证比较困难的问题。若是业务系统能够接受最终一致性的话,事务性会稍微好作一些。不然只能借助于复杂的分布式事务,好比JTA,Chubby等技术。
count, sum, avg, max, min等聚合功能,Hbase只能经过硬扫的方式,而且很悲剧,你可能须要作一些hack操做(好比加一个CF,value为null),不然你在扫描时可能须要往客户端传回全部数据。
固然你能够在这个场景上作一些优化,好比增长状态表等,但复杂性带来的风险会更高。
还有一种终极解决方案就是在业务上只提供上一页和下一页,这或许是最简单有效的方案了。
2,单张表多个列族,索引基于列
Hbase提供了列族Column Family特性。
列索引是将Column Family作为index,多个index值散落到Qualifier,多个column值依据version排列(CF, Qualifer, Version Hbase会保证有序,其中CF和Qualifier正序,Version倒序)。
举个典型的例子,就是用户卖了不少商品,这些商品的标题title须要支持like %title%查询。传统基于RDMBS就是模糊查询,基于search engine就是分词+倒排表。
在HBase中,模糊查询显然不知足咱们的要求,接下来只能经过分词+倒排的方式来存储。基于CF的倒排表索引结构见下图:
取数据的时候,只须要根据用户id(row key)定位到一个row,而后根据分词定位到qualifier,再经过version的有序list,取top n条记录便可。不过你们可能会发现个问题,version list的总数量是须要scan全version list才能知道的,这里须要业务系统自己作一些改进。
如何join?
实现方式同方案1里的join,多个CF列索引扫描结果后,须要走merge,将多个索引的查询结果conjunction。
两个方案的对比彷佛变化就是一个表,一个列,但其实这个方案有个最大的好处,就是解决了事务性的问题,由于全部的索引都是跟单个row key绑定的,咱们知道单个row的更新,在hbase中是保证原子更新的,这就是这个方案的自然优点。当你在考虑单索引时,使用基于列的索引会比单表索 引有更好的适用性。
而组合索引在以列为存储粒度的方案里,也一样能够折中实现。理解这种存储模式的同窗可能已经猜到了,就是基于qualifier。
下表对比了表索引和列索引的优缺点:
列索引 | 表索引 | |
检索性能 | 检索数据须要走屡次scan,第一次scan row key,第二次scan qualifier,第三次scan version。 | 只须要走一次row key的scan便可。 |
存储 | 在没有组合索引时,存储较节省 | 在没有组合索引时,存储较节省 |
事务性 | 容易保证 | 保证事务性比较困难 |
join | 性能较差,只有在创建组合条件Qualifier的时候性能会有所改善 | 性能较差,只有在创建组合表索引的时候性能会有所改善 |
额外的问题 | 1,同一个row里每一个qualifier的version是有大小限制的,不能超过Int的最大值。(别觉得这个值很大,对于海量数据存储,上亿很日常) 2,version的count总数须要额外作处理获取。 3,单个row数据超过split大小时,会致使不能compaction或compaction内存吃紧,增长风险。 |
|
count,sum,avg,etc | 符合条件的结果集全表扫描 | 符合条件的结果集全表扫描 |
虽然列索引缺点这么多,可是存储节省带来的成本优点有时仍是值得咱们去这么作的,况且它还解决了事务性问题,须要用户本身去权衡。
值得一提的是,Facebook的消息应用服务器就是基于相似的方案来实现的。
3,ITHBase
方案一中的多表,解决了性能问题,同时带来了存储冗余和数据一致性问题。这两个问题中,只要解决其中一项,其实也就知足了大多数业务场景。
本方案中,着重关注的是数据一致性。ITHbase的全称是 Indexed Transactional HBase,从名字中就能看出,事务性是它的重要特性。
ITHBase的事务原理简介
建一张事务表__GLOBAL_TRX_LOG__,每次开启事务时,在表中记录状态。由于是基于Hbase的HTable,事务表一样会写WAL用于恢复,不过这个日志格式被ITHbase改造过,它称之为THLog。
客户端对多张表更新时,先启动事务,而后每次PUT,将事务id传递给HRegionServer。
ITHbase经过继承HRegionServer和HReogin类,重写了大多数操做接口方法,好比put, update, delete, 用于获取transactionalId和状态。
当server收到操做和事务id后,先确认服务端收到,标记当前事务为待写入状态(须要再发起一次PUT)。当全部表的操做完成后,由客户端统一作commit写入,作二阶段提交。
4,Map-reduce
这个方案没什么好说的,存储节省,也不须要建索引表,只须要靠强大的集群计算能力便可导出结果。但通常不适合online业务。
5,Coprocessor协处理器
官方0.92.0新版正在开发中的新功能-Coprocessor,支持region级别索引。详见:
https://issues.apache.org/jira/browse/HBASE-2038
协处理器的机制能够理解为,server端添加了一些回调函数。这些回调函数以下:
The Coprocessor interface defines these hooks:
The RegionObserver interface is defines these hooks:
利用这些hooks能够实现region级二级索引,实现count, sum, avg, max, min等聚合操做而不须要返回全部的数据,详见 https://issues.apache.org/jira/browse/HBASE-1512。
二级索引的原理猜想
由于coprocessor的最终方案还未公布,就提供的这些hooks来讲,二级索引的实现应该是拦截同一个region的put, get, scan, delete等操做。与此同时在同一个reigon里维护一个索引CF,创建对应的索引表。
基于region的索引表其实有不少局限性,好比全局排序就很难作。
不过我以为Coprocessor最大的好处在于其提供了server端的彻底扩展能力,这对于Hbase来讲是一个大的跃进。
如何join?
目前还未发布,不过就了解很难从本质上有所突破。解决方案无非就是merge和composite index,一样事务性是须要解决的难题之一。
0.19.3版Secondary Index
一直关注HBase的同窗,或许知道,早在HBase 0.19.3版发布时,曾经加入过secondary index的功能,Issue详见这里。
它的使用例子也很简单:http://blog.rajeevsharma.in/2009/06/secondary-indexes-in-hbase.html
0.19.3版Secondary Index经过将列值以row key方法存储,提供索引scan。
但HBase早期的需求主要来自Hadoop。事务的复杂性以及当时发现hadoop-core里有个很难解决的与ITHBase兼容的问题,导致官方在0.20.0版将其核心代码移出了hbase-core,改成contrib第三方扩展,Issue详见这里。
Transactional tableindexed-ITHBase
这个方案就是在0.19.3版被官方剥离出核心的第三方扩展,它的方案上面已经介绍过了。目前支持最新的Hbase 0.90。
是否具有工业强度的稳定性是用户选择它的主要障碍。
https://github.com/hbase-trx/hbase-transactional-tableindexed
Facebook方案
facebook采用的是单表多列索引的解决方案,上面已经提到过了。很完美地解决了数据一致性问题,这主要跟他们的使用场景有关。
感兴趣的同窗能够看下这篇blog,本文不做详述:
HBase官方方案 0.92.0 版开发中 – Coprocessor协处理器
还未发布,不过hbase官方blog有篇介绍:http://hbaseblog.com/2010/11/30/hbase-coprocessors
Lily Hbase indexing Library
这是一个索引构建,查询,管理的框架。结构上,就是经过一张indexmeta表管理多张indexdata索引表。
特色是,有一套很是完善的针对int, string, utf-8, decimal等类型的row key排序机制。这个机制在这篇博文中有详细介绍:
此外,框架针对join场景(原理=merge),提供了封装好的conjunction和disjunction工具类。
针对索引构建场景,Hbase indexing library也提供了很方便的接口。
IHbase
Feature | ITHBase | IHBase | Comment |
---|---|---|---|
global ordering | yes | no | IHBase has an index for each region. The flip side of not having global ordering is compatibility with the good old HRegion: results are coming back in row order (and not value order as in ITHBase) |
Full table scan? | no | no | THbase does a partial scan on the index table. ITHBase supports specifying start/end rows to limit the number of scanned regions |
Multiple Index Usage | no | yes | IHBase can take advantage of multiple indexes in the same scan. IHBase IdxScan object accepts an Expression which allows intersection/unison of several indexed column criteria |
Extra disk storage | yes | no | IHBase indexes are created when the region starts/flushes and do not require any extra storage |
Extra RAM | yes | yes | IHBase indexes are in memory and hence increase the memory overhead. THBbase indexes increase the number of regions each region server has to support thus costing memory too |
Parallel scanning support | no | yes | In ITHBase the index table needs to be consulted and then GETs are issued for each matching row. The behavior of IHBase (as perceived by the client) is no different than a regular scan and hence supports parallel scanning seamlessly. parallel GET can be implemented to speedup THbase scans |