HBase Phoenix
[TOC]
简介
Phoenix作为应用层和HBASE之间的中间件,以下特性使它在大数据量的简单查询场景有着独有的优势
- 二级索引支持(global index + local index)
- 编译SQL成为原生HBASE的可并行执行的scan在数据层完成计算,server端的coprocessor执行聚合下推where过滤条件到server端的scan filter上利用统计信息优化、选择查询计划(5.x版本将支持CBO)
- skip scan功能提高扫描速度
一般可以使用以下三种方式访问Phoenix
- JDBC API
- 使用Python编写的命令行工具(sqlline, sqlline-thin和psql等)
- SQuirrel
安装
Ambari其实已经自带了Phoenix,所以安装比较简单,勾起来直接点击安装就好了。
不过在ambari2.4.2中,Phoenix版本4.7.0与HBase1.1.2中,使用sqlline.py连接的时候会报错
Class org.apache.phoenix.coprocessor.MetaDataEndpointImpl cannot be loaded Set hbase.table.sanity.checks to false at conf or table descriptor if you want to bypass sanity checks
按照提示去增加配置,重启HBase就好了。
新版本ambari2.7.3没有这个问题。Phoenix 5.0.0 HBase2.0.0
本次使用4.7.0版本作为示例,并使用phoenix-sqlline ${zookeeper}演示。
数据类型
- INTEGER Type
- UNSIGNED_INT Type
- BIGINT Type
- UNSIGNED_LONG Type
- TINYINT Type
- UNSIGNED_TINYINT Type
- SMALLINT Type
- UNSIGNED_SMALLINT Type
- FLOAT Type
- UNSIGNED_FLOAT Type
- DOUBLE Type
- UNSIGNED_DOUBLE Type
- DECIMAL Type
- BOOLEAN Type
- TIME Type
- DATE Type
- TIMESTAMP Type
- UNSIGNED_TIME Type
- UNSIGNED_DATE Type
- UNSIGNED_TIMESTAMP Type
- VARCHAR Type
- CHAR Type
- BINARY Type
- VARBINARY Type
- ARRAY
数据类型 详细参看官网
CRUD操作
详细查看官网Phoenix命令大全
以下只有部分演示。
创建表
在Phoenix创建表
CREATE TABLE IF NOT EXISTS ljktest (ID VARCHAR PRIMARY KEY,NAME VARCHAR,AGE TINYINT)
CREATE TABLE IF NOT EXISTS "ljktest" (ID VARCHAR PRIMARY KEY,NAME VARCHAR,AGE TINYINT)
CREATE TABLE LJKTEST2 (ID INTEGER NOT NULL,AGE TINYINT NOT NULL,NAME VARCHAR,CONSTRAINT PK PRIMARY KEY(ID, AGE)) TTL = 86400;
在hbase-shell里面查看下是否有表生成,可以看到默认会在HBase这边表是大写的,要区分大小写,加入引号即可。并且当中加入了很多协处理器。不指定列簇的话,默认列簇用0来表示。
hbase(main):008:0> desc 'LJKTEST'
Table LJKTEST is ENABLED
LJKTEST, {TABLE_ATTRIBUTES => {coprocessor$1 => '|org.apache.phoenix.coprocessor.ScanRegionObser
ver|805306366|', coprocessor$2 => '|org.apache.phoenix.coprocessor.UngroupedAggregateRegionObser
ver|805306366|', coprocessor$3 => '|org.apache.phoenix.coprocessor.GroupedAggregateRegionObserve
r|805306366|', coprocessor$4 => '|org.apache.phoenix.coprocessor.ServerCachingEndpointImpl|80530
6366|', coprocessor$5 => '|org.apache.phoenix.hbase.index.Indexer|805306366|index.builder=org.ap
ache.phoenix.index.PhoenixIndexBuilder,org.apache.hadoop.hbase.index.codec.class=org.apache.phoe
nix.index.PhoenixIndexCodec'}
COLUMN FAMILIES DESCRIPTION
{NAME => '0', VERSIONS => '1', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'false'
, KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'FAST_DI
FF', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'NONE', CAC
HE_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_B
LOCKS_ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}
1 row(s)
Took 0.1066 seconds
映射已经存在HBase中的表
- 当Phoenix映射HBase中的表,只是作为读操作时,强烈建议是用视图表建立。
CREATE VIEW LJK_TEST(ROWKEY VARCHAR PRIMARY KEY,"mycf"."name" VARCHAR);
- 映射一张HBase的表,记住映射的字段一定要写正确了。
`CREATE TABLE LJK_TEST (ROWKEY VARCHAR PRIMARY KEY,
"mycf"."name" VARCHAR) COLUMN_ENCODED_BYTES=0;`
插入数据
UPSERT VALUES
-
简单插入
UPSERT INTO LJKTEST VALUES('0001','LINJIKAI',18);
同步在hbase-shell看到的结果是
hbase(main):010:0> scan 'LJKTEST' ROW COLUMN+CELL 0001 column=0:\x00\x00\x00\x00, timestamp=1557719617275, value=x 0001 column=0:\x80\x0B, timestamp=1557719617275, value=LINJIKAI 0001 column=0:\x80\x0C, timestamp=1557719617275, value=\x92 1 row(s) Took 0.0079 seconds
- 插入特定字段
UPSERT INTO LJKTEST(ID,NAME) VALUES('0002','张三');
-
重复KEY插入策略
更新字段
UPSERT INTO LJKTEST VALUES('0003','李四',19) ON DUPLICATE KEY UPDATE AGE = AGE + 1;
效果就是如果碰到value相同的情况则年龄会大一岁,展示结果如下,李四最后的年龄变成了20岁
0: jdbc:phoenix:> UPSERT INTO LJKTEST VALUES('0003','李四',19) ON DUPLICATE KEY UPDATE AGE = AGE + 1; 1 row affected (0.011 seconds) 0: jdbc:phoenix:> SELECT * FROM LJKTEST; +-------+-----------+-------+ | ID | NAME | AGE | +-------+-----------+-------+ | 0001 | LINJIKAI | 18 | | 0002 | 张三 | null | | 0003 | 李四 | 20 | +-------+-----------+-------+ 3 rows selected (0.026 seconds)
不更新
UPSERT INTO LJKTEST VALUES('0003','李四',19) ON DUPLICATE KEY IGNORE;
这种写法不会改变李四的年龄,因为这条数据的key已经存在了。
二级索引
phoenix二级索引
覆盖索引
Phoenix特别强大,因为我们提供了覆盖索引。一旦找到索引条目,我们就不需要返回主表了。相反,我们将我们关心的数据捆绑在索引行中,从而节省了读取时间开销
建一张表来测试二级索引机制,以下其他功能索引也会使用这张表来建对应二级索引
CREATE TABLE LJKTEST (ID CHAR(4) NOT NULL PRIMARY KEY,AGE UNSIGNED_TINYINT,NAME VARCHAR,COMPANY VARCHAR,SCHOOL VARCHAR)
并插入一些数据
UPSERT INTO LJKTEST VALUES('0001',18,'张三','张三公司','张三学校');
UPSERT INTO LJKTEST VALUES('0002',19,'李四','李四公司','李四学校');
UPSERT INTO LJKTEST VALUES('0003',20,'王五','王五公司','王五学校');
UPSERT INTO LJKTEST VALUES('0004',21,'赵六','赵六公司','赵六学校');
创建多字段覆盖索引 CREATE INDEX COVER_LJK_INDEX ON LJKTEST(COMPANY,SCHOOL) INCLUDE (NAME);
查看索引逻辑
可以看到使用school字段去查询的话,不会走索引,进入了全表扫描。
0: jdbc:phoenix:> EXPLAIN SELECT NAME FROM LJKTEST WHERE SCHOOL ='张三学校';
+---------------------------------------------------------------------------+------------------+
| PLAN | EST_BYTES_READ |
+---------------------------------------------------------------------------+------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER COVER_LJK_INDEX | null |
| SERVER FILTER BY "SCHOOL" = '张三学校' | null |
+---------------------------------------------------------------------------+------------------+
但如果使用company字段去查询则会走前缀索引,进入了range scan.
0: jdbc:phoenix:> EXPLAIN SELECT NAME FROM LJKTEST WHERE COMPANY ='张三公司';
+-------------------------------------------------------------------------------------+--------+
| PLAN | EST_BY |
+-------------------------------------------------------------------------------------+--------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER COVER_LJK_INDEX ['张三公司'] | null |
+-------------------------------------------------------------------------------------+--------+
接下来创建一个单字段覆盖索引看看
CREATE INDEX COVER_LJK_INDEX_COMPANY ON LJKTEST(COMPANY) INCLUDE (NAME);
0: jdbc:phoenix:> EXPLAIN SELECT NAME FROM LJKTEST WHERE COMPANY ='张三公司';
+----------------------------------------------------------------------------------------------+
| PLAN |
+----------------------------------------------------------------------------------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER COVER_LJK_INDEX_COMPANY ['张三公司'] |
+----------------------------------------------------------------------------------------------+
1 row selected (0.028 seconds)
当使用*号作为字段去检索时,走的FULL SCAN。
0: jdbc:phoenix:> EXPLAIN SELECT /*+ INDEX(LJKTEST COVER_LJK_INDEX_COMPANY)*/ * FROM LJKTEST WHERE COMPANY ='张三公司';
+----------------------------------------------------------------------------------------------+
| PLAN |
+----------------------------------------------------------------------------------------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER LJKTEST |
| SKIP-SCAN-JOIN TABLE 0 |
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER COVER_LJK_INDEX_COMPANY_ON |
| SERVER FILTER BY FIRST KEY ONLY |
| DYNAMIC SERVER FILTER BY "LJKTEST.ID" IN ($34.$36) |
+----------------------------------------------------------------------------------------------+
这个时候你需要使用hint来指定索引
0: jdbc:phoenix:> EXPLAIN SELECT /*+INDEX(LJKTEST COVER_LJK_INDEX_COMPANY)*/* FROM LJKTEST WHERE COMPANY='张三公司';
+-----------------------------------------------------------------------------------------------------+-----------------+----------------+--------------+
| PLAN | EST_BYTES_READ | EST_ROWS_READ | EST_INFO_TS |
+-----------------------------------------------------------------------------------------------------+-----------------+----------------+--------------+
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN FULL SCAN OVER LJKTEST | null | null | null |
| SKIP-SCAN-JOIN TABLE 0 | null | null | null |
| CLIENT 1-CHUNK PARALLEL 1-WAY ROUND ROBIN RANGE SCAN OVER COVER_LJK_INDEX_COMPANY ['张三公司'] | null | null | null |
| SERVER FILTER BY FIRST KEY ONLY | null | null | null |
| DYNAMIC SERVER FILTER BY "LJKTEST.ID" IN ($10.$12) | null | null | null |
+-----------------------------------------------------------------------------------------------------+-----------------+----------------+--------------+
5 rows selected (0.046 seconds)
功能索引
CREATE INDEX SCHOOL_WITH_COMPANY ON LJKTEST(COMPANY||' '||SCHOOL)
本地索引与全局索引
本地索引需要增加LOCAL关键字。上面都是全局索引。
CREATE LOCAL INDEX COVER_LJK_INDEX_COMPANY ON LJKTEST(COMPANY) INCLUDE (NAME);
- 本地索引
适合写多读少的情况,数据会存储在原表中,侵入性强。
- 全局索引
适合读多写少的情况,数据会存储上单独的一张表中。
索引异步创建
默认情况下,创建索引时,会在CREATE INDEX调用期间同步填充索引。但是数据表的当前大小,这可能是不可行的。从4.5开始,通过在索引创建DDL语句中包含ASYNC关键字,可以异步完成索引的填充
CREATE INDEX INDEX_LJKTEST_AGE_ASYNC ON LJKTEST(AGE) INCLUDE(SCHOOL) ASYNC;
这只是第一步,你还必须通过HBase命令行单独启动填充索引表的map reduce作业
hbase org.apache.phoenix.mapreduce.index.IndexTool --data-table LJKTEST --index-table INDEX_LJKTEST_AGE_ASYNC --output-path ASYNC_IDX_HFILES
只有当map reduce作业完成时,才会激活索引并开始在查询中使用。
output-path选项用于指定用于写入HFile的HDFS目录
不可变索引
对于其中数据仅写入一次且从未就地更新的表,可以进行某些优化以减少增量维护的写入时间开销。这对于时间序列数据(例如日志或事件数据)很常见,一旦写入一行,就永远不会更新。要利用这些优化,请通过向DDL语句添加IMMUTABLE_ROWS = true属性将表声明为不可变
CREATE TABLE LJKTEST_IMMU (ROWKEY VARCHAR PRIMARY KEY, NAME VARCHAR,AGE VARCHAR) IMMUTABLE_ROWS=true;
CREATE INDEX INDEX_LJKTEST_IMMU ON LJKTEST_IMMU(NAME) INCLUDE(AGE);
测试了下不变索引的特性
即使rowkey相同的数据,做了更新动作,在phoenix中则会做追加动作
UPSERT INTO LJKTEST_IMMU VALUES('1','LILEI','18');
UPSERT INTO LJKTEST_IMMU VALUES('1','HANGMEIMEI','18');
0: jdbc:phoenix:dn1> select * from LJKTEST_IMMU;
+---------+-------------+------+
| ROWKEY | NAME | AGE |
+---------+-------------+------+
| 1 | HANGMEIMEI | 18 |
| 1 | LILEI | 18 |
+---------+-------------+------+
2 rows selected (0.078 seconds)
0: jdbc:phoenix:dn1> SELECT * FROM INDEX_LJKTEST_IMMU;
+-------------+----------+--------+
| 0:NAME | :ROWKEY | 0:AGE |
+-------------+----------+--------+
| HANGMEIMEI | 1 | 18 |
| LILEI | 1 | 18 |
+-------------+----------+--------+
2 rows selected (0.021 seconds)
0: jdbc:phoenix:dn1> SELECT * FROM LJKTEST_IMMU WHERE ROWKEY='1';
+---------+-------------+------+
| ROWKEY | NAME | AGE |
+---------+-------------+------+
| 1 | HANGMEIMEI | 18 |
+---------+-------------+------+
1 row selected (0.017 seconds)
0: jdbc:phoenix:dn1> SELECT * FROM LJKTEST_IMMU WHERE NAME='LILEI';
+---------+--------+------+
| ROWKEY | NAME | AGE |
+---------+--------+------+
| 1 | LILEI | 18 |
+---------+--------+------+
1 row selected (0.024 seconds)
0: jdbc:phoenix:dn1> SELECT * FROM LJKTEST_IMMU WHERE AGE='18';
+---------+-------------+------+
| ROWKEY | NAME | AGE |
+---------+-------------+------+
| 1 | HANGMEIMEI | 18 |
| 1 | LILEI | 18 |
+---------+-------------+------+
2 rows selected (0.027 seconds)
在hbase表中,则是根据原来的规则,rowkey相同替换对应字段
hbase(main):002:0> scan 'LJKTEST_IMMU'
ROW COLUMN+CELL
1 column=0:AGE, timestamp=1563241706845, value=18
1 column=0:NAME, timestamp=1563241706845, value=HANGMEIMEI
1 column=0:_0, timestamp=1563241706845, value=x
1 row(s) in 0.0590 seconds
索引检查工具
使用Phoenix 4.12,现在有一个工具可以运行MapReduce作业来验证索引表是否对其数据表有效。在任一表中查找孤立行的唯一方法是扫描表中的所有行,并在另一个表中查找相应行。因此,该工具可以使用数据或索引表作为“源”表运行,另一个作为“目标”表运行。该工具将找到的所有无效行写入文件或输出表PHOENIX_INDEX_SCRUTINY。无效行是源行,它在目标表中没有对应的行,或者在目标表中具有不正确的值(即覆盖的列值)。
hbase org.apache.phoenix.mapreduce.index.IndexScrutinyTool -dt my_table -it my_index -o
或者
HADOOP_CLASSPATH=$(hbase mapredcp) hadoop jar phoenix-
索引优化参数
开箱即用,索引非常快。但是,要针对特定环境和工作负载进行优化,可以调整几个属性。
属性名 | 描述 | 默认值 |
---|---|---|
index.builder.threads.max | 用于从主表更新构建索引更新的线程数 | 10 |
index.builder.threads.keepalivetime | 我们使构建器线程池中的线程到期后的时间量(以秒为单位)。 | 60 |
index.writer.threads.max | 写入目标索引表时要使用的线程数。 | 10 |
index.writer.threads.keepalivetime | 我们使写入器线程池中的线程到期后的时间量(以秒为单位)。 | 60 |
hbase.htable.threads.max | 每个索引HTable可用于写入的线程数。 | 2,147,483,647 |
hbase.htable.threads.keepalivetime | 我们使HTable的线程池中的线程到期后的时间量(以秒为单位) | 60 |
index.tablefactory.cache.size | 我们应该在缓存中保留的索引HTable的数量。 | 10 |
org.apache.phoenix.regionserver.index.priority.min | 指定索引优先级所在范围的底部(包括)的值。 | 1000 |
org.apache.phoenix.regionserver.index.priority.max | 指定索引优先级所在范围的顶部(不包括)的值。 | 1050 |
org.apache.phoenix.regionserver.index.handler.count | 为全局索引维护提供索引写入请求时要使用的线程数。 | 30 |
报错整理
- Unable find parent table
19/05/14 11:23:40 WARN iterate.BaseResultIterators: Unable to find parent table "LJKTEST" of table "COVER_LJK_INDEX_COMPANY_ONLY" to determine USE_STATS_FOR_PARALLELIZATION
org.apache.phoenix.schema.TableNotFoundException: ERROR 1012 (42M03): Table undefined. tableName=LJKTEST
- 创建索引失败
其中如果创建索引表报错,根据错误信息去修改hbase-site,重启hbase就好了。
Error: ERROR 1029 (42Y88): Mutable secondary indexes must have the hbase.regionserver.wal.codec property set to org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec in the hbase-sites.xml of every region server. tableName=COVER_LJK_INDEX (state=42Y88,code=1029)
附录
- Phoenix从入门到精通 阿里云
- Phoenix官网
- Phoenix博客介绍
- 阿里云 专辑
- Phoenix ambari view表
- Phoenix 本地索引与全局索引测试与区别
- Phoenix简书学习笔记