较早之前的内部分享。
Phoenix是Apache基金的顶级项目。基于JDBC API操作HBase的开源SQL引擎。
基本架构
二级索引
Global Indexes 全局索引
全局索引擅长读多写少的场景,索引的所有性能损失都发生在写入时。
每个全局索引都会建一个单独的HBase表存储索引数据,也即数据和索引是分别存储在不同表中不同region中。
写数据时,通过数据表region的CP去写索引数据。
读数据时,数据和索引不在一个region甚至不在一个RS中,所以client先读索引表,然后根据索引再读数据表。
索引表rowkey的组成:{索引列的值,可以是多列} {数据表rowkey}。
Local Indexes 局部索引
局部索引擅长写多读少的场景。
局部索引将索引数据存储于同一数据表中不同CF,无需独立表,索引数据存储于L#列族里。
索引表rowkey组成是:{region的startKey} {index_id} {索引值的值,可以多列} {数据表rowkey}。
rowkey以region的startkey做前缀,保证了数据和索引必然在同一个region里,避免了写数据时的网络开销。
但在读数据时,因为没法确定索引数据的确切位置,所以必须检查每个region,所以局部索引的读开销比较大,大表尤甚。
Covered Indexes 覆盖索引
索引表中除了必须的索引列数据,还可以放其他列的数据在索引表中,减少查询时的回表操作,提高读性能。
scss
CREATE INDEX my_index ON my_table (v1,v2) INCLUDE(v3)
v1,v2是索引列,多存储了v3列
这个应该是对Global Indexes更适用的。
Functional Indexes 函数式索引
支持在任意的表达式上建索引。当对表中数据使用固定表达式做查询时,就可以用这种索引,对表达式的计算结果做索引,避免直接查数据表计算表达式结果。
sql
CREATE INDEX UPPER_NAME_IDX ON EMP (UPPER(FIRST_NAME||' '||LAST_NAME))
把FIRST_NAME和LAST_NAME两列表示全名的表达式做索引
SELECT EMP_ID FROM EMP WHERE UPPER(FIRST_NAME||' '||LAST_NAME)='JOHN DOE'
Index Population
索引创建默认是同步执行的,对于大数据表不友好。4.5版本开始,索引初始化可以异步执行。
sql
CREATE INDEX async_index ON my_schema.my_table (v) ASYNC
加ASYNC关键字异步初始化索引
然后手动使用MR任务去生成索引表
shell
${HBASE_HOME}/bin/hbase org.apache.phoenix.mapreduce.index.IndexTool
--schema MY_SCHEMA --data-table MY_TABLE --index-table ASYNC_IDX
--output-path ASYNC_IDX_HFILES
一致性保证(数据和索引之间)
- 全局索引:因为索引和数据分在不同表中,所以出现异常无法保证一致性。
- 局部索引:4.8版本后,可以保证索引和数据的一致性。
- 事务表的全局索引:总可以保证一致性。要求数据表开启事务,由Tephra保证跨表事务一致性。
总结
- 全局事务和局部事务各有优劣,对应使用场景不同。对于普通用户来说,理解和使用上可能会有障碍。
- 数据和索引的一致性需要依赖事务来保证。不一致之后,会根据配置产生不同后果:
- 索引失效,直至数据表和索引表一致。
- 数据表不可写。
Tephra
Apache基金会孵化器项目。被用于Phoenix为HBase提供跨行跨表的全语义ACID事务支持。
基于HBase原生提供的多版本并发控制,Tephra实现了快照隔离的并发事务支持。
架构
Tephra Components
分为3部分组件
-
Client
- 和Tx Manager交互,协调事务生命周期
- 在请求中注入事务信息,甚至改写请求
- 包装HBase原生API,读写HBase
-
Tx Manager,单独的Server,一主一备,利用ZK选主探活。
- 管理分配事务ID,保证全局唯一
- 管理事务状态
- 冲突检查
-
Coprocessor
- 在读请求里加Filter,过滤掉不可见数据
- 清理无效数据
事务生命周期
注:事务支持两大类:
- long-running:特指像MR任务这样的大量修改的事务,容易出现事务冲突,所以并不维护change keys,也不支持回滚。如果abort了,会一直处于对其他事务不可见的状态。
- short:会有个timeout,超时如果还没结束,会被置为invalided状态。下面主要讨论的就是这类。
java
TransactionContext context = new TransactionContext(client, transactionAwareHTable);
try {
context.start();
transactionAwareHTable.put(new Put(Bytes.toBytes("row"));
// ...
context.finish();
} catch (TransactionFailureException e) {
context.abort();
}
start tx
生成一个新事务Transaction,事务对象的主要要素:
- 一个写指针:事务中所有写操作使用的版本号。基本可以认为是事务开始的时间戳。认为也是事务ID。
- 一个读指针:事务中所有读操作不超过这个版本。已提交的事务中的最大写针指值。
- excluded versions的集合:其他事务的写版本,本事务的读操作不可见,因为这些事务可能还未完成或没有回滚。还有些invalid状态的事务也不可见。
这个Transaction对象会在Tx Manager、Client、Coprocessor之间传输。
如果在now这个时间点开启一个新事物,3个属性的值应该分别是:
- write pointer: now。
- read pointer: t1。Committed状态的事务中,写指针最大值。
- excluded versions: t2, t4。Invalid和In Progress状态的事务不可见。
do work
读写操作。
-
所有操作都会加上attribute,序列化的Transaction对象,用于RS的Coprocessor识别。
-
Put操作,使用写指针值改写时间戳
-
Delete操作,被改写成Put操作。这里对原生API的读写会有影响。
- DELETE_FAMILY,改成了CF,Column都是byte[0]的Put
- DELETE_COLUMN,改成了Column是byte[0]的Put
- 我理解这里的目的是为了方便的生成undo操作,用于回滚。Delete操作很难undo,但Put操作则可以通过生成Delete来做undo。
-
所有写操作都会被记录,生成change keys。用于回滚和冲突检查。更具冲突级别分为两种:
- NONE,ROW:table+rowkey+CF
- COLUMN:table+rowkey+CF+column
try commit
将事务ID和change keys交给Tx Manager,做冲突检查。检查逻辑主要是:在该事务开始后,有没有其他已经commit的事务,如果有,change keys有没有重复。
try abort
尝试回滚。
主要是client端对所有写操作做undo,将Put转成Delete。
invalidate
主要是把事务ID加到invalidTxList。其他事物会把invalidTxList加到excluded versions里,避免读到非法数据。
变成invalid的事务的处理方法:
- 手动处理,把事务ID从invalidTxList删掉,事务产生的数据仍然可能在hbase里,而且删掉之后就对其他事物可见了。
- 依靠major compact,invalid数据被compact删掉之后,定时任务检查到了,从invalidTxList删掉。
快照隔离
总结
优点
- 用于Phoenix,实现了跨行、跨表的完备的ACID语义。
- 性能损失不大。
缺点
- 删除操作对HBase数据有影响。
- Tx Manager如果故障或重启,期间事务功能可能用不了。Tx Manager的状态数据使用了snapshot+WAL的方式来恢复,failover可能会比较慢。
- 对于大集群、大事务可能支持的并不好。(不确定)
- 事务的change keys都由tx manager内存存储一份
- invalided状态事务的清理,依赖major compact。
- 不同版本的hbase client对应不同版本的tephra client,原生无法兼容,集群升级对用户影响大。可能使用Phoenix可以解决。
- 项目好像还在孵化器,社区不活跃。基本处于只是维护适配新hbase release版本的状态。