HBase 二级索引与Join

不羁岁月 提交于 2020-04-06 13:16:57

二级索引与索引Join是Online业务系统要求存储引擎提供的基本特性。RDBMS支持得比较好,NOSQL阵营也在摸索着符合自身特点的最佳解决方案。
这篇文章会以HBase做为对象来探讨如何基于Hbase构建二级索引与实现索引join。文末同时会列出目前已知的包括0.19.3版secondary index,?ITHbase, Facebook和官方Coprocessor方案的介绍。

理论目标
在HBase中实现二级索引与索引Join需要考虑三个目标:
1,高性能的范围检索。
2,数据的低冗余(存储所占的数据量)。
3,数据的一致性。

性能与数据冗余,一致性是相互制约的关系。
如果想实现了高性能地范围检索,必然需要依赖冗余索引数据来提升性能,而数据冗余会导致更新数据时难以实现一致性,特别是分布式场景下。
如果对范围检索的性能要求不高,那么可以不考虑冗余数据,一致性问题也可以间接避免,毕竟share nothing是公认的最简单有效的解决方案。

理论结合实际,下文会以实例的方式来阐述各个方案是如何选择偏重点。
这些方案是经过笔者资料查阅和同事的不断交流后得出的结论,如有错误,欢迎指正:

 

1,按索引建表
每一个索引建立一个表,然后依靠表的row key来实现范围检索。row key在HBase中是以B+ tree结构化有序存储的,所以scan起来会比较效率。
单表以row key存储索引,column value存储id值或其他数据 ,这就是Hbase索引表的结构。

如何Join?
多索引(多表)的join场景中,主要有两种参考方案:

1,按索引的种类扫描各自独立的单索引表,最后将扫描结果merge。
这个方案的特点是简单,但是如果多个索引扫描结果数据量比较大的话,merge就会遇到瓶颈。

比如,现在有一张1亿的用户信息表,建有出生地和年龄两个索引,我想得到一个条件是在杭州出生,年龄为20岁的按用户id正序排列前10个的用户列表。
有一种方案是,系统先扫描出生地为杭州的索引,得到一个用户id结果集,这个集合的规模假设是10万。
然后扫描年龄,规模是5万,最后merge这些用户id,去重,排序得到结果。

这明显有问题,如何改良?
保证出生地和年龄的结果是排过序的,可以减少merge的数据量?但Hbase是按row key排序,value是不能排序的。
变通一下 – 将用户id冗余到row key里?OK,这是一种解决方案了,这个方案的图示如下:

merge时提取交集就是所需要的列表,顺序是靠索引增加了_id,以字典序保证的。

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:

  • preOpen, postOpen: Called before and after the region is reported as online to the master.
  • preFlush, postFlush: Called before and after the memstore is flushed into a new store file.
  • preCompact, postCompact: Called before and after compaction.
  • preSplit, postSplit: Called after the region is split.
  • preClose and postClose: Called before and after the region is reported as closed to the master.

The RegionObserver interface is defines these hooks:

  • preGet, postGet: Called before and after a client makes a Get request.
  • preExists, postExists: Called before and after the client tests for existence using a Get.
  • prePut and postPut: Called before and after the client stores a value.
  • preDelete and postDelete: Called before and after the client deletes a value.
  • preScannerOpen postScannerOpen: Called before and after the client opens a new scanner.
  • preScannerNext, postScannerNext: Called before and after the client asks for the next row on a scanner.
  • preScannerClose, postScannerClose: Called before and after the client closes a scanner.
  • preCheckAndPut, postCheckAndPut: Called before and after the client calls checkAndPut().
  • preCheckAndDelete, postCheckAndDelete: Called before and after the client calls checkAndDelete().

利用这些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,本文不作详述:

blog.huihoo.com/?p=688

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排序机制。这个机制在这篇博文中有详细介绍:

http://brunodumon.wordpress.com/2010/02/17/building-indexes-using-hbase-mapping-strings-numbers-and-dates-onto-bytes/

此外,框架针对join场景(原理=merge),提供了封装好的conjunction和disjunction工具类。
针对索引构建场景,Hbase indexing library也提供了很方便的接口。

IHbase

IHBase非常类似ITHBase。
IHBase同样从HBase源码级别进行了扩展了,重新定义和实现了一些Server,Client端处理逻辑,所以,它是具备强侵入性的。
不幸的是,这个工具在fix完Hbase 0.20.5版兼容bug以后再也没更新。是否支持0.90以上版本,笔者还未尝试。
IHBase与ITHBase的一个对比(仁者见仁)
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
原理简介
在Memstore满了以后刷磁盘时,IHBase会进行拦截请求并为这个memstore的数据构建索引。索引另一个CF的方式存储在表内。不过只支持region级别(类似coprocessor)
scan的时候,IHBase会结合索引列中的标记,来加速scan。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!