varchar 用什么索引类型 varchar建立索引_apache

目录导读

  • 目录导读
  • 1. 前言
  • 2. 二级索引的分类
  • 3. 配置 HBase 支持 Phoenix 二级索引
  • 4. 实战
  • 4.1 全局索引测试
  • 4.2 本地索引测试
  • 4.3 异步构建索引
  • 4.4 继续本地索引的测试
  • 5. 主表数据的不同更新方式对索引表数据的影响
  • 6. 索引性能调优
  • 7. 最后
  • 8. 参考链接
  • 9. 模拟测试数据的脚本

1. 前言

本文是 Phoenix 系列的第二篇文章,在此我将着重介绍使用 Phoenix 来构建二级索引。文中涉及到的理论介绍以及案例分享,大部分都直接选自知乎大佬栋公子的文章,phoenix 构建二级索引,但在此基础上我又做了大量额外的补充和说明,尽可能做到全面而详实地记录 Phoenix 二级索引的特性和使用细节。

对于 HBase 而言,如果想精确定位到某行记录,唯一的办法是通过 rowkey 来查询,如果不通过 rowkey 来查找数据,就必须逐行地比较每一列的值,即全表扫瞄。

对于数据量较大的表,全表扫描的代价是不可接受的。但是,在很多情况下,我们又不得不需要从多个维度来查询数据。例如,在定位某个人的时候,可以通过姓名、身份证号、学籍号等不同的维度来查询,可要想把这么多维度的数据都放到 rowkey 中几乎不可能(业务的灵活性不允许,对 rowkey 长度的要求也不允许)。所以需要 secondary index(二级索引)来完成这件事。secondary index 的原理很简单,但是如果自己来维护二级索引的话则会麻烦一些。现在,Phoenix 已经提供了对 HBase secondary index 的支持。

有关我测试所用的 Phoenix 的环境,请参见我 Phoenix 系列一的第一篇文章。

2. 二级索引的分类

二级索引分为全局索引和本地索引。

「Global Indexing」Global Indexing,即全局索引,适用于读多写少的业务场景。

使用 Global Indexing 在写数据的时候开销很大,因为所有对数据表的更新操作(DELETE, UPSERT VALUES and UPSERT SELECT),都会引起索引表的更新,而索引表是分布在不同的数据节点上的,跨节点的数据传输带来了较大的性能消耗。

在读数据的时候 Phoenix 会选择优先查询索引表来降低查询消耗的时间。在默认情况下如果待查询的字段不是索引字段的话,索引表是不会被使用的,也就是说不会带来查询速度上的提升。

「Local Indexing」Local Indexing,即本地索引,适用于写操作频繁以及空间受限制的场景。

与 Global Indexing 一样,Phoenix 会自动判定在进行查询的时候是否使用索引。使用 Local indexing 时,索引数据和数据表的数据存放在相同的服务器中,这样避免了在写操作的时候往不同服务器的索引表中写索引带来的额外开销。使用 Local indexing 的时候即使查询的字段不是索引字段索引表也会被使用,这会带来查询速度的提升,这点跟 Global indexing 不同。对于 Local Indexing,一个数据表的所有索引数据都存储在一个单一的独立的可共享的表中。

「Immutable Index」Immutable Index,不可变索引,适用于数据只增加不更新并且按照时间先后顺序存储(time-series data)的场景,如保存日志数据或者事件数据等。

不可变索引的存储方式是 write one,append only。当在 Phoenix 使用 create table 语句时指定 IMMUTABLE_ROWS = true 表示该表上创建的索引将被设置为不可变索引。Phoenix 默认情况下如果在 create table 时不指定 IMMUTABLE_ROWS = true 时,表示该表为 mutable。不可变索引分为 Global immutable index 和 Local immutable index 两种。

CREATE TABLE my_table (k VARCHAR PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true;

CREATE TABLE my_table (k VARCHAR PRIMARY KEY, v VARCHAR) IMMUTABLE_ROWS=true;

「mutable index」mutable index,可变索引,适用于数据有增删改的场景。

Phoenix 默认情况创建的索引都是可变索引,除非在 create table 的时候显式地指定 IMMUTABLE_ROWS = true。可变索引同样分为 Global mutable index 和 Local mutable index 两种。(这里原文其实有笔误)

3. 配置 HBase 支持 Phoenix 二级索引

「修改配置文件」如果要启用 Phoenix 的二级索引功能,需要修改 HBase 的配置文件 hbase-site.xml,在 hbase 集群的 conf/hbase-site.xml 文件中添加以下内容。

不同的版本对 Phoenix 的配置要求还不太一样,大家可以按需配置,更具体的细节可以参考其官网文档。http://phoenix.apache.org/secondary_indexing.html

<property>
          <name>hbase.regionserver.wal.codecname>
          <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodecvalue>
property>



<property>
           <name>hbase.region.server.rpc.scheduler.factory.classname>
           <value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactoryvalue>
property>
 <property>
         <name>hbase.rpc.controllerfactory.classname>
         <value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactoryvalue>
 property>

<property>
          <name>hbase.master.loadbalancer.classname>
          <value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancervalue>
property>
<property>
          <name>hbase.coprocessor.master.classesname>
          <value>org.apache.phoenix.hbase.index.master.IndexMasterObservervalue>
property>
<property>
          <name>hbase.coprocessor.regionserver.classesname>
          <value>org.apache.hadoop.hbase.regionserver.LocalIndexMergervalue>
property>


<property>
          <name>hbase.regionserver.wal.codecname>
          <value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodecvalue>
property>



<property>
           <name>hbase.region.server.rpc.scheduler.factory.classname>
           <value>org.apache.hadoop.hbase.ipc.PhoenixRpcSchedulerFactoryvalue>
property>
 <property>
         <name>hbase.rpc.controllerfactory.classname>
         <value>org.apache.hadoop.hbase.ipc.controller.ServerRpcControllerFactoryvalue>
 property>

<property>
          <name>hbase.master.loadbalancer.classname>
          <value>org.apache.phoenix.hbase.index.balancer.IndexLoadBalancervalue>
property>
<property>
          <name>hbase.coprocessor.master.classesname>
          <value>org.apache.phoenix.hbase.index.master.IndexMasterObservervalue>
property>
<property>
          <name>hbase.coprocessor.regionserver.classesname>
          <value>org.apache.hadoop.hbase.regionserver.LocalIndexMergervalue>
property>

在 CDH 中做如下配置修改:

varchar 用什么索引类型 varchar建立索引_varchar 索引_02

然后加入如下配置:

varchar 用什么索引类型 varchar建立索引_apache_03

配置修改完成之后需要重启集群。

4. 实战

实战中引用的小例子也来自大佬的博客,只是大佬博客里测试数据的网盘路径已经失效了,然后我只能按照原文中的数据格式,自己 mock 了 500w 条数据。模拟数据的 python 脚本,贴在文末,就不占用此处的空间。测试数据准备好之后,就可以进行接下来的实战了。

首先,在 Phoenix 中创建一个 user 表。

create table user (
"session_id" varchar(100) not null primary key,
"f"."cookie_id" varchar(100),
"f"."visit_time" varchar(100),
"f"."user_id" varchar(100),
"f"."age" Integer,
"f"."sex" varchar(100),
"f"."visit_url" varchar(100),
"f"."visit_os" varchar(100),
"f"."browser_name" varchar(100),
"f"."visit_ip" varchar(100),
"f"."province" varchar(100),
"f"."city" varchar(100),
"f"."page_id" varchar(100),
"f"."goods_id" varchar(100),
"f"."shop_id" varchar(100)) column_encoded_bytes=0;

create table user (
"session_id" varchar(100) not null primary key,
"f"."cookie_id" varchar(100),
"f"."visit_time" varchar(100),
"f"."user_id" varchar(100),
"f"."age" Integer,
"f"."sex" varchar(100),
"f"."visit_url" varchar(100),
"f"."visit_os" varchar(100),
"f"."browser_name" varchar(100),
"f"."visit_ip" varchar(100),
"f"."province" varchar(100),
"f"."city" varchar(100),
"f"."page_id" varchar(100),
"f"."goods_id" varchar(100),
"f"."shop_id" varchar(100)) column_encoded_bytes=0;

然后导入数据,该 CSV 文件中有 500 万条记录,存储的路径/data/leo_jie/user.csv,导入的命令为:

phoenix-psql -t USER localhost:2181 /data/leo_jie/user.csv

phoenix-psql -t USER localhost:2181 /data/leo_jie/user.csv

备注:因为我安装的是 CDH 版的 Phoenix,所以命令会和原文略有不同。这种方式导入数据的效率挺慢的,数据量大的话还是推荐使用之前提到的 bulkload CSV 文件的方式。500 万的测试数据大概三个 G,导入耗时 15 分钟左右。

4.1 全局索引测试

在为 USER 表创建 secondary index 之前,先来看看查询一条数据所需的时间:

select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

查询耗时:

varchar 用什么索引类型 varchar建立索引_varchar 索引_04

可见,对名为 cookie_id 的列进行按值查询需要 31 秒左右。

然后执行下逻辑计划:

explain select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

explain select * from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';


varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_05

explainz

由图可知,上述查询会进行全表扫描,然后再通过过滤器来筛选出目标数据,显然这种查询方式的效率是很低的。

此时我们来创建 Global Indexing 的二级索引,在 cookie_id 列上面创建二级索引。其实这里隐含了一个坑,如果你的 Phoenix 表数据量过大,而你直接就以下面的这种方式去创建索引,十有八九会遇到超时异常发生,甚至创建的索引表可能也不是完整的。要想避免这个问题,就需要用到异步索引创建了,这个在下文中会有详细说明。

create index USER_COOKIE_ID_INDEX on USER ("f"."cookie_id");

create index USER_COOKIE_ID_INDEX on USER ("f"."cookie_id");

等索引创建的命令运行结束,再来查看当前所有的表,你会发现多了一张名叫 USER_COOKIE_ID_INDEX 的索引表,查询下该索引表的数据。

varchar 用什么索引类型 varchar建立索引_数据_06

USER_COOKIE_ID_INDEX 这个索引表,其实就是 cookie_id 和 session_id 的对应表,这个 session_id 就是 hbase 中的 rowkey.这个时候查询会根据 cookie_id 先找到 session_id,然后 HBase 会利用布隆过滤器来查询。

在来运行下上述的查询命令:

varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_07

你会发现查询用时跟之前一样,依旧是几十秒的时间,难道是索引表未生效?莫急,再来运行如下语句。

select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';


varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_08

fast

0.063 秒,是不是快了。看到这里大家也许会在心里暗骂,有毛病啊,我以这个条件去查这个条件?原来,在 Phoenix 中,我们建立的是 cookie_id 列的二级索引,这里只能查询 cookie_id 列的时候索引机制才会起作用,再来看一下该 SQL 的执行计划。

explain select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';

explain select "cookie_id" from user where "cookie_id" = 'b80da72c-c6e4-4c79-9fc5-08e7253f6596';


varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_09

explain

可以从描述中看到,上述的查询扫描的是 USER_COOKIE_ID_INDEX 索引表来进行查询的。单独查询这一列是没有问题,可如果只能查询这一列,显然太不合理了。我想再增加一列,比如如下的 SQL 语句。

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

查询计划和执行计划如下图:

varchar 用什么索引类型 varchar建立索引_apache_10

耗时四十多秒,依旧是全扫描,可见,只要是涉及到非索引列的查询,就会发生全表扫描。到这里先不继续往下延伸,我们先不管全局索引,我们来测试下本地索引。

4.2 本地索引测试

给 user_id 列上面创建二级索引,本地索引比全局索引在创建语法上多了一个 local。

create local index USER_USER_ID_INDEX on USER ("f"."user_id");

create local index USER_USER_ID_INDEX on USER ("f"."user_id");

在等待本地索引命令创建完毕的过程中,果不其然,在客户端遇到了超时异常,异常信息大致如下所示:

Caused by: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call to rs-server1:16020 failed on local exception: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call id=150214, waitTime=60016, rpcTimeout=60000

Caused by: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call to rs-server1:16020 failed on local exception: org.apache.hadoop.hbase.ipc.CallTimeoutException: Call id=150214, waitTime=60016, rpcTimeout=60000

看一下待建索引的状态:

varchar 用什么索引类型 varchar建立索引_二级索引_11

BUILDING 意味着索引还在构建中,如果此时你查询下 USER_USER_ID_INDEX 索引表的数据,里面虽然可以查得到数据,但我想,该索引表的数据十有八九是不完整的。

不信的话,运行一下如下 SQL 语句。

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

varchar 用什么索引类型 varchar建立索引_二级索引_12

上图中所看到的结果,并不像栋公子文章里描述的那样,查询语句在毫秒级返回结果。这可能跟我们的索引表构建失败有关,所以这里有必要先提前说明下异步索引的创建,然后再回过头来继续我们的索引表测试。

4.3 异步构建索引

上述创建索引的语句,默认以同步的方式运行,如果表的数据量过大,服务端构建索引的过程耗时过长,极大可能会造成客户端操作超时,使待建的索引表陷入 BUILDING 的状态。不太确定这种状态过一段时间会不会自己消失,在这里我的做法是,删除索引表,异步创建索引,然后用 IndexTool 的 MR 任务来构建索引数据。

「删除索引表」索引表的删除不能像 drop view 那样直接删除,而是需要用如下的删除语句。

drop index USER_USER_ID_INDEX on "USER";

drop index USER_USER_ID_INDEX on "USER";

删除时虽然也遇到了客户端超时,但查看所有表时,索引表已经消失了。

「异步创建索引」直接上语句,跟之前相比只是多了一个关键字 async 而已。

create local index USER_USER_ID_INDEX on USER ("f"."user_id") async;

create local index USER_USER_ID_INDEX on USER ("f"."user_id") async;


varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_13

create index

对于全局异步索引表,亦是相同的创建方式。上述语句瞬间返回结果,只是当前索引表的状态还是 BUILDING,并且索引表里是没有数据的。

「IndexTool 构建索引表数据」

hbase org.apache.phoenix.mapreduce.index.IndexTool --data-table USER --index-table USER_USER_ID_INDEX --output-path USER_USER_ID_INDEX

hbase org.apache.phoenix.mapreduce.index.IndexTool --data-table USER --index-table USER_USER_ID_INDEX --output-path USER_USER_ID_INDEX

如果你的索引表有 schema,请参考如下创建语句。

${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

${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

上述命令运行成功后,IndexTool 的 MR 任务会帮助我们构建好索引数据,同时会把索引表的当前状态置为 ACTIVE,然后就可以使用这个索引表进行数据查询了。


varchar 用什么索引类型 varchar建立索引_varchar 索引_14

index build 查看所有表

varchar 用什么索引类型 varchar建立索引_二级索引_15

4.4 继续本地索引的测试

再一次执行如下 SQL:

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

select * from user where "user_id"='590335014';
explain select * from user where "user_id"='590335014';

看结果:

varchar 用什么索引类型 varchar建立索引_数据_16

同样的查询语句,前后用时的差距十分明显。

接下来我们就讨论一下怎么建立索引表一定就可以使用索引。

第一种方式是使用 local indexing 这种本地索引来建立二级索引。

第二种方式是创建 converted index。如果在某次查询中,查询项或者查询条件中包含有被索引列之外的列(主键除外)。默认情况下,该查询就会触发 full table scan(全表扫描),但是使用 covered index 则可以避免全表扫描。 创建包含某个字段的覆盖索引,创建方式如下:

create index USER_COOKIE_ID_AGE_INDEX on USER ("f"."cookie_id") include("f"."age");

 create index USER_COOKIE_ID_AGE_INDEX on USER ("f"."cookie_id") include("f"."age");

查看当前所有表,会发现多出来一张名为 USER_COOKIE_ID_AGE_INDEX 的索引表,再以如下 SQL 查询下 USER 表的数据。

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select "cookie_id","age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

毫秒级响应查询。

varchar 用什么索引类型 varchar建立索引_apache_17

第三种方式是在查询的时候提示其使用索引。 在 select 和 column_name 之间加上/+ Index( )/,通过这种方式强制使用索引。 例如:

select /*+ index(user,USER_COOKIE_ID_AGE_INDEX) */ "age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select /*+ index(user,USER_COOKIE_ID_AGE_INDEX) */ "age" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

如果 sex 是索引字段,那么就会直接从索引表中查询,如果 sex 不是索引字段,那么将会进行全表扫描,所以当用户明确知道表中数据较少且符合检索条件时才适用,此时的性能才是最佳的。

比如如下 SQL,走的还是全表扫描,因为性别列并不包含在索引表中。

select /*+ index(user, USER_COOKIE_ID_AGE_INDEX) */"age","sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select /*+ index(user, USER_COOKIE_ID_AGE_INDEX) */"age","sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

到这里难道就真的可以收尾了吗?栋公子文章中的演示也是到此为止了,那么请各位看官继续看下面的 SQL。

select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age", "sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age", "sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age", "sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';
explain select /*+ index(user USER_COOKIE_ID_AGE_INDEX) */"age", "sex" from user where "cookie_id"='b80da72c-c6e4-4c79-9fc5-08e7253f6596';

执行结果贴图:

varchar 用什么索引类型 varchar建立索引_数据_18

同样的 SQL,细微的差别,最终的效果却天壤之别,怎么来解释这种情况呢?(有可能是栋公子文章中漏写了一个逗号???) 其实,这种写法才是在进行查询的时候通过 sql 语句来强制使用索引。

比较官方的解释是,这样的查询语句会导致二次检索数据表,第一次检索是去索引表中查找符合 cookie_id 为 b80da72c-c6e4-4c79-9fc5-08e7253f6596 的数据,这时候发现 sex 字段并不在索引字段中,会去 user 表中进行第二次扫描,因此只有当用户明确知道符合检索条件的数据较少的时候才适合使用,否则就又会造成全表扫描,对性能影响较大。也就是说,如果第二次检索的数据量过大时,依旧等同于发生了全表扫描,同样会影响查询效率。

5. 主表数据的不同更新方式对索引表数据的影响

按照索引表的工作原理,即使没有 Phoenix 的存在,我们也可以实现自己在 HBase 中维护数据表的二级索引表,只不过,在这种情况下,就需要依靠我们自己来维护索引表的数据更新了。这样的话,我们的维护工作就会成倍增加。而 Phoenix 二级索引表的便利之处也正在此时凸显,随着数据表的数据更新,它在内部自动帮我们维护与之绑定的索引表的数据更新。

那么,不管是怎么样的数据写入场景,数据表的更新都会引起索引表的自动更新吗?开始实验求证之前,我们先来思考这几个问题,数据经 HBase client API 直接 put 写入数据表,会引起索引表的数据更新吗?数据经过 HBase 的 bulkload 方式写入,会引起索引表的数据更新吗?数据经 Phoenix 的 upsert 方式写入,会引起索引表的数据更新吗?

下面开始我们的实验,为了更好的观察测试的结果,我们另行创建一个具有简单表结构和少量数据的 PERSON.USER 表。

create table person.user(
id VARCHAR PRIMARY KEY,
info.name VARCHAR,
info.age VARCHAR,
info.phone_id VARCHAR,
info.create_time VARCHAR
)column_encoded_bytes=0;

create table person.user(
id VARCHAR PRIMARY KEY,
info.name VARCHAR,
info.age VARCHAR,
info.phone_id VARCHAR,
info.create_time VARCHAR
)column_encoded_bytes=0;

为 phone_id 列创建索引(全局索引):

create index user_phone_index on person.user (info.phone_id) ;

 create index user_phone_index on person.user (info.phone_id) ;

此时,数据表和索引表都没有数据:

varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_19

「put 写入数据」在 HBase 表中 put 几条数据,然后观察索引表里的数据变化。

hbase(main):004:0> put 'PERSON:USER','1','INFO:NAME','leo'
Took 0.1438 seconds
hbase(main):005:0> put 'PERSON:USER','1','INFO:AGE','18'
Took 0.0189 seconds
hbase(main):006:0> put 'PERSON:USER','1','INFO:PHONE_ID','18739577988'
Took 0.0118 seconds
hbase(main):007:0> put 'PERSON:USER','1','INFO:CREATE_TIME','2019-01-01 10:23:12
hbase(main):004:0> put 'PERSON:USER','1','INFO:NAME','leo'
Took 0.1438 seconds
hbase(main):005:0> put 'PERSON:USER','1','INFO:AGE','18'
Took 0.0189 seconds
hbase(main):006:0> put 'PERSON:USER','1','INFO:PHONE_ID','18739577988'
Took 0.0118 seconds
hbase(main):007:0> put 'PERSON:USER','1','INFO:CREATE_TIME','2019-01-01 10:23:12

Phoenix 数据表和索引表中数据变化:

varchar 用什么索引类型 varchar建立索引_apache_20

数据表中有数据,索引表中数据为空。

「upsert into 插入数据」

upsert into person.user(ID,NAME,AGE,PHONE_ID,CREATE_TIME) values ('2','leo2','17','18739577932','2019-01-01 10:21:12');

upsert into person.user(ID,NAME,AGE,PHONE_ID,CREATE_TIME) values ('2','leo2','17','18739577932','2019-01-01 10:21:12');

Phoenix 数据表和索引表中数据变化:

varchar 用什么索引类型 varchar建立索引_apache_21

数据表中有数据,索引表中也存在数据。

然后分别查看如下两条 SQL 的执行计划:

select * from person.user;
1 select PHONE_ID from person.user where PHONE_ID = '18739577988';
2 select PHONE_ID from person.user where PHONE_ID = '18739577932';
3 select * from person.user where PHONE_ID = '18739577988';

select * from person.user;
1 select PHONE_ID from person.user where PHONE_ID = '18739577988';
2 select PHONE_ID from person.user where PHONE_ID = '18739577932';
3 select * from person.user where PHONE_ID = '18739577988';

且看查询结果:

varchar 用什么索引类型 varchar建立索引_varchar 索引_22

以索引列 PHONE_ID 作为筛选条件时,查询语句 1 和 2 都会先走索引表,然后再从数据表中拿数据。如果索引表不存在该对应查询条件的索引数据,即使数据表中存在该数据,查询结果也依然是空。语句 3 因为走了全表扫描,所以会查询出结果。

Phoenix 可以帮助我们自动维护索引表数据的更新,但前提是,数据的更新采取的是 Phoenix API 的写入方式,至于以 HBase bulkload 的方式导入的数据,更不会引起索引表数据的更新了,各位看官可以自行尝试。那么,有什么办法来解决这种问题呢?

第一种方式就是重建索引,把索引表清空后重新装配数据,这就意味着我们需要手动来触发索引表数据的更新机制。而重建索引表的方式一般使用如下命令。

1 alter index USER_PHONE_INDEX  on person.USER rebuild;
2 alter index USER_PHONE_INDEX  on person.USER rebuild async;

1 alter index USER_PHONE_INDEX  on person.USER rebuild;
2 alter index USER_PHONE_INDEX  on person.USER rebuild async;

如果你的 Phoenix 表数据量很大,还是老老实实使用语句 2 来进行异步索引重建吧。异步索引重建之后,索引的状态被置为了 BUILDING,此时,还需要运行 IndexTool 命令来加载索引数据。

hbase org.apache.phoenix.mapreduce.index.IndexTool \
--schema PERSON --data-table USER --index-table USER_PHONE_INDEX \
--output-path USER_PHONE_INDEX

hbase org.apache.phoenix.mapreduce.index.IndexTool \
--schema PERSON --data-table USER --index-table USER_PHONE_INDEX \
--output-path USER_PHONE_INDEX

如果没有异步重建索引的那一步,不管你怎么运行这个 MR 任务,貌似都没有任何效果,索引表依旧是空的。

这里说起索引表的状态,不得不提一下索引表的生命周期。我们以异步的方式初次创建或者二次创建的索引表的状态是 BUILDING,即构建状态,此时索引表还不能正常使用,当我们运行完 IndexTool 这个索引表构建工具之后,索引表的状态被修改为 ACTIVE,意味激活状态。此时索引表就可以正常使用了,这里就涉及到了索引表的生命周期。有关生命周期更详细的介绍,还请移步至 Phoenix 的官方文档,或者参考此篇博客

异步重建完索引表,其状态被改变,IndexTool 的命令也执行成功,此时再来查看下我们的索引表数据,一切都恢复了正常。对于大批量数据没有被加载进索引表的,这种方式是可行的。但是对于非 Phoenix 客户端一次次的数据写入,都得以这样的方式进行索引重建,显然是不合理的,所以就有了下面的这种方式。

第二种: 非 Phoenix 客户端写入数据之后,手动更新索引表数据,也即写入两份数据,先来看一下 HBase 中索引表数据的构造。

varchar 用什么索引类型 varchar建立索引_二级索引_23

Phoenix 的索引表会把所有索引字段+ID 拼接起来写进 HBase,做为 RowKey。那我们也仿照这样的格式,插入主表数据的同时,把相应索引字段拼接起来插进索引表。格式为:索引列\x00ID

先在 HBase 数据表中 put 一条数据。

put 'PERSON:USER','3','INFO:NAME','leo3'
put 'PERSON:USER','3','INFO:AGE','16'
put 'PERSON:USER','3','INFO:PHONE_ID','18739577966'
put 'PERSON:USER','3','INFO:CREATE_TIME','2019-11-01 10:23:12
put 'PERSON:USER','3','INFO:NAME','leo3'
put 'PERSON:USER','3','INFO:AGE','16'
put 'PERSON:USER','3','INFO:PHONE_ID','18739577966'
put 'PERSON:USER','3','INFO:CREATE_TIME','2019-11-01 10:23:12


varchar 用什么索引类型 varchar建立索引_二级索引_24

no data

往 HBase 对应的索引表里插入一条我们自己构造的索引数据。

put 'PERSON:USER_PHONE_INDEX',"18739577966\x003",'0:_0','x'

 put 'PERSON:USER_PHONE_INDEX',"18739577966\x003",'0:_0','x'

(注意,这里的 rowkey 是双引号,如果是单引号会把‘\’转译掉,插入的数据就不对了)

最终的效果:

varchar 用什么索引类型 varchar建立索引_varchar 用什么索引类型_25

当我们以非 Phoenix API 的方式写入数据的时候,在数据表中更新数据之后,还得需要在与之对应的索引表中插入拼装后的索引数据。保证索引表数据的完整性很重要,否则,即使不出现查询超时,也可能存在明明表里有数据,可就是查不到数据的尴尬场景。

6. 索引性能调优

一般来说,索引已经很快了,不需要特别的优化。但这里也提供了一些方法,让你在面对特定的环境和负载的时候可以进行一些调优。下面的这些配置需要在 hbase-site.xml 文件中设置,针对所有的 RegionServer 节点,以下配置也是来自栋公子的原文,同样的官网文档里也有很详细的说明。其具体的作用我并没有在生产环境中测试过,写下来只是为了做一个参考。

1. index.builder.threads.max
创建索引时,使用的最大线程数。
默认值: 10。

2. index.builder.threads.keepalivetime
创建索引的创建线程池中线程的存活时间,单位:秒。
默认值: 60

3. index.writer.threads.max
写索引表数据的写线程池的最大线程数。
更新索引表可以用的最大线程数,也就是同时可以更新多少张索引表,数量最好和索引表的数量一致。
默认值: 10

4. index.writer.threads.keepalivetime
索引写线程池中,线程的存活时间,单位:秒。
默认值:60

5. hbase.htable.threads.max
每一张索引表可用于写的线程数。
默认值: 2,147,483,647

6. hbase.htable.threads.keepalivetime
索引表线程池中线程的存活时间,单位:秒。
默认值: 60

7. index.tablefactory.cache.size
允许缓存的索引表的数量。
增加此值,可以在写索引表时不用每次都去重复的创建htable,这个值越大,内存消耗越多。
默认值: 10

8. org.apache.phoenix.regionserver.index.handler.count
处理全局索引写请求时,可以使用的线程数。
默认值: 30

1. index.builder.threads.max
创建索引时,使用的最大线程数。
默认值: 10。

2. index.builder.threads.keepalivetime
创建索引的创建线程池中线程的存活时间,单位:秒。
默认值: 60

3. index.writer.threads.max
写索引表数据的写线程池的最大线程数。
更新索引表可以用的最大线程数,也就是同时可以更新多少张索引表,数量最好和索引表的数量一致。
默认值: 10

4. index.writer.threads.keepalivetime
索引写线程池中,线程的存活时间,单位:秒。
默认值:60

5. hbase.htable.threads.max
每一张索引表可用于写的线程数。
默认值: 2,147,483,647

6. hbase.htable.threads.keepalivetime
索引表线程池中线程的存活时间,单位:秒。
默认值: 60

7. index.tablefactory.cache.size
允许缓存的索引表的数量。
增加此值,可以在写索引表时不用每次都去重复的创建htable,这个值越大,内存消耗越多。
默认值: 10

8. org.apache.phoenix.regionserver.index.handler.count
处理全局索引写请求时,可以使用的线程数。
默认值: 30

7. 最后

Phoenix 的二级索引在某些场景中还是非常有用的,可一旦使用姿势不当,极大可能会造成全表扫描,严重时线上其他的查询服务也会深受其害。其次牢记,不走 Phoenix 的 API 而更新数据的方式,索引表可能不会随之更新,必要时需要手动维护索引表数据,索引表的数据要严格与主表的数据保持一致,否则,会出现遗漏数据的情况。

Phoenix 系列到这里就先告一段落,后续使用过程中如果遇见比较有意思的事,会继续给大家分享。

8. 参考链接

9. 模拟测试数据的脚本

#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
:Description: 模拟用户数据
:Owner: leo_jie
:Create time: 2020/7/14 3:24 下午
"""

import uuid
import time
import random
import struct
import socket


def generate_random_time():
    a1 = (2018, 1, 1, 0, 0, 0, 0, 0, 0)  # 设置开始日期时间元组(1976-01-01 00:00:00)
    a2 = (2019, 12, 31, 23, 59, 59, 0, 0, 0)  # 设置结束日期时间元组(1990-12-31 23:59:59)
    start = time.mktime(a1)  # 生成开始时间戳
    end = time.mktime(a2)  # 生成结束时间戳
    # 随机生成10个日期字符串
    t = random.randint(start, end)  # 在开始和结束时间戳中随机取出一个
    date_tuple = time.localtime(t)  # 将时间戳生成时间元组
    random_date = time.strftime("%Y-%m-%d %H:%M:%S", date_tuple)  # 将时间元组转成格式化字符串
    return random_date


def generate_random_user_id(length=9):
    user_id = str(random.randint(1, 999999999))
    if len(user_id)         append_length = length - len(user_id)
        user_id = '0' * append_length + user_id
    return user_id


def random_str():
    data = "1234567890zxcvbnmlkjhgfdsaqwertyuiop"
    # 用时间来做随机播种
    random.seed(time.time())
    # 随机选取数据
    sa = []
    for i in range(20):
        sa.append(random.choice(data))
    salt = "gp_" + ''.join(sa)
    return salt


def generate_random_url():
    channels = [
        "BTV2", "BTV3", "BTV4", "BTV5", "BTV6", "BTV7", "BTV8", "BTV9",
        "BTVWorld", "CCTV12", "CCTV2", "CCTV3", "CCTV5", "CCTV6", "CCTV6World", "CCTV7",
        "CCTV8", "CCTV9World", "CCTVA", "CCTVChild", "CCTVE", "CCTVEN", "CCTVF", "CCTVFYYY",
        "CCTVHJJC", "CCTVMusic", "CCTVNEWS", "CCTVR", "CETV1", "CETV3", "ChongQingTV", "ChongQingWorld",
        "GZChild", "GZEnglish", "GZFinance", "GZJingSai", "GZMovie", "GanSuTV", "GuangXiTV", "GuiZhouTV",
    ]

    host = '10.160.1.21'
    date = '2014/10/31'
    bitrates = ['800000']
    channel = random.choice(channels)
    bitrate = random.choice(bitrates)
    ts_url = ''.join(
        ['http://', host, '/', 'online01/stream/', bitrate, '/', channel, '/', date, '/', random_str()])
    return ts_url

def generate_random_ip():
    RANDOM_IP_POOL = ['192.168.10.222/0']
    str_ip = RANDOM_IP_POOL[random.randint(0, len(RANDOM_IP_POOL) - 1)]
    str_ip_addr = str_ip.split('/')[0]
    str_ip_mask = str_ip.split('/')[1]
    ip_addr = struct.unpack('>I', socket.inet_aton(str_ip_addr))[0]
    mask = 0x0
    for i in range(31, 31 - int(str_ip_mask), -1):
        mask = mask | (1 <    ip_addr_min = ip_addr & (mask & 0xffffffff)
    ip_addr_max = ip_addr | (~mask & 0xffffffff)
    return socket.inet_ntoa(struct.pack('>I', random.randint(ip_addr_min, ip_addr_max)))

def generate_random_province():
    provinces = ['河北省', '山西省', '辽宁省', '吉林省', '黑龙江省', '江苏省', '浙江省', '安徽省', '福建省',
                 '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '海南省', '四川省', '贵州省',
                 '云南省', '陕西省', '甘肃省', '青海省', '台湾省', '上海市', '北京市', '天津市', '重庆市',
                 '香港', '澳门']
    return provinces[random.randint(0, len(provinces) - 1)]


def generate_shop_id():
    shop_no = ['A', 'B', 'C', 'D', 'F', 'F', 'G']
    return shop_no[random.randint(0, len(shop_no) - 1)] + str(random.randint(1, 9999))


def mock_user_data():
    session_id = uuid.uuid4()
    cookie_id = uuid.uuid4()
    visit_time = generate_random_time()
    user_id = generate_random_user_id()
    age = str(random.randint(10, 80))
    sex = 'F' if random.randint(0, 1) == 1 else 'M'
    visit_url = generate_random_url()
    visit_os = 'Windows OS' if random.randint(0, 1) == 1 else 'Mac OS'
    browser_name = 'Google' if random.randint(0, 1) == 1 else 'IE'
    visit_ip = generate_random_ip()
    province = generate_random_province()
    city = province
    page_id = str(random.randint(1, 99999))
    goods_id = str(random.randint(1, 999999999))
    shop_id = generate_shop_id()
    return "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" \
           % (session_id, cookie_id, visit_time, user_id, age, sex, visit_url,
              visit_os, browser_name, visit_ip, province, city,
              page_id, goods_id, shop_id)


def create_data(rows=5000000):
    for i in range(rows):
        with open('user.csv', 'a')as f:
            f.write(mock_user_data() + "\n")


create_data()

#!/usr/bin/env python
# -*- coding:utf-8 -*-

"""
:Description: 模拟用户数据
:Owner: leo_jie
:Create time: 2020/7/14 3:24 下午
"""

import uuid
import time
import random
import struct
import socket


def generate_random_time():
    a1 = (2018, 1, 1, 0, 0, 0, 0, 0, 0)  # 设置开始日期时间元组(1976-01-01 00:00:00)
    a2 = (2019, 12, 31, 23, 59, 59, 0, 0, 0)  # 设置结束日期时间元组(1990-12-31 23:59:59)
    start = time.mktime(a1)  # 生成开始时间戳
    end = time.mktime(a2)  # 生成结束时间戳
    # 随机生成10个日期字符串
    t = random.randint(start, end)  # 在开始和结束时间戳中随机取出一个
    date_tuple = time.localtime(t)  # 将时间戳生成时间元组
    random_date = time.strftime("%Y-%m-%d %H:%M:%S", date_tuple)  # 将时间元组转成格式化字符串
    return random_date


def generate_random_user_id(length=9):
    user_id = str(random.randint(1, 999999999))
    if len(user_id)         append_length = length - len(user_id)
        user_id = '0' * append_length + user_id
    return user_id


def random_str():
    data = "1234567890zxcvbnmlkjhgfdsaqwertyuiop"
    # 用时间来做随机播种
    random.seed(time.time())
    # 随机选取数据
    sa = []
    for i in range(20):
        sa.append(random.choice(data))
    salt = "gp_" + ''.join(sa)
    return salt


def generate_random_url():
    channels = [
        "BTV2", "BTV3", "BTV4", "BTV5", "BTV6", "BTV7", "BTV8", "BTV9",
        "BTVWorld", "CCTV12", "CCTV2", "CCTV3", "CCTV5", "CCTV6", "CCTV6World", "CCTV7",
        "CCTV8", "CCTV9World", "CCTVA", "CCTVChild", "CCTVE", "CCTVEN", "CCTVF", "CCTVFYYY",
        "CCTVHJJC", "CCTVMusic", "CCTVNEWS", "CCTVR", "CETV1", "CETV3", "ChongQingTV", "ChongQingWorld",
        "GZChild", "GZEnglish", "GZFinance", "GZJingSai", "GZMovie", "GanSuTV", "GuangXiTV", "GuiZhouTV",
    ]

    host = '10.160.1.21'
    date = '2014/10/31'
    bitrates = ['800000']
    channel = random.choice(channels)
    bitrate = random.choice(bitrates)
    ts_url = ''.join(
        ['http://', host, '/', 'online01/stream/', bitrate, '/', channel, '/', date, '/', random_str()])
    return ts_url

def generate_random_ip():
    RANDOM_IP_POOL = ['192.168.10.222/0']
    str_ip = RANDOM_IP_POOL[random.randint(0, len(RANDOM_IP_POOL) - 1)]
    str_ip_addr = str_ip.split('/')[0]
    str_ip_mask = str_ip.split('/')[1]
    ip_addr = struct.unpack('>I', socket.inet_aton(str_ip_addr))[0]
    mask = 0x0
    for i in range(31, 31 - int(str_ip_mask), -1):
        mask = mask | (1 <    ip_addr_min = ip_addr & (mask & 0xffffffff)
    ip_addr_max = ip_addr | (~mask & 0xffffffff)
    return socket.inet_ntoa(struct.pack('>I', random.randint(ip_addr_min, ip_addr_max)))

def generate_random_province():
    provinces = ['河北省', '山西省', '辽宁省', '吉林省', '黑龙江省', '江苏省', '浙江省', '安徽省', '福建省',
                 '江西省', '山东省', '河南省', '湖北省', '湖南省', '广东省', '海南省', '四川省', '贵州省',
                 '云南省', '陕西省', '甘肃省', '青海省', '台湾省', '上海市', '北京市', '天津市', '重庆市',
                 '香港', '澳门']
    return provinces[random.randint(0, len(provinces) - 1)]


def generate_shop_id():
    shop_no = ['A', 'B', 'C', 'D', 'F', 'F', 'G']
    return shop_no[random.randint(0, len(shop_no) - 1)] + str(random.randint(1, 9999))


def mock_user_data():
    session_id = uuid.uuid4()
    cookie_id = uuid.uuid4()
    visit_time = generate_random_time()
    user_id = generate_random_user_id()
    age = str(random.randint(10, 80))
    sex = 'F' if random.randint(0, 1) == 1 else 'M'
    visit_url = generate_random_url()
    visit_os = 'Windows OS' if random.randint(0, 1) == 1 else 'Mac OS'
    browser_name = 'Google' if random.randint(0, 1) == 1 else 'IE'
    visit_ip = generate_random_ip()
    province = generate_random_province()
    city = province
    page_id = str(random.randint(1, 99999))
    goods_id = str(random.randint(1, 999999999))
    shop_id = generate_shop_id()
    return "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" \
           % (session_id, cookie_id, visit_time, user_id, age, sex, visit_url,
              visit_os, browser_name, visit_ip, province, city,
              page_id, goods_id, shop_id)


def create_data(rows=5000000):
    for i in range(rows):
        with open('user.csv', 'a')as f:
            f.write(mock_user_data() + "\n")


create_data()