最近工作中用到HBase。但HBase的原生客户端使用起来相当繁琐,spring-data-hadoop又年久失修,和最新的HBase集成起来各种异常。因此找到了 Apache Phoenix —— 它支持使用标准SQL和 JDBC接口来操作HBase。
本文记录一下使用phoenix的常见问题(主要来自 phoenix faq),最后是笔者项目中用到的配置。
Phoenix 常见问题
Phoenix JDBC URL 语法格式
Thick Driver
thick driver格式如下,请注意中括号里边的内容是可选的:jdbc:phoenix:[ZooKeeper地址(多个请用逗号分隔) [:port[:hbase root znode[:kerberos_域名[:kerberos keytab地址]]]]
最简单的例子为:jdbc:phoenix:localhost
复杂的例子为:jdbc:phoenix:zookeeper1.domain,zookeeper2.domain,zookeeper3.domain:2181:/hbase-1:phoenix@EXAMPLE.com:/etc/security/keytab/phoenix.keytab
值得注意的是每个可选的内容要求前置所有内容完整。
实际上笔者的driver url为: jdbc:phoenix:zookeeper1.domain,zookeeper2.domain,zookeeper3.comain:2181
Thin Driver
Phoenix Thin Driver必须和Phoenix Query Server配合使用。格式如下: jdbc:phoenix:thin:[key=value[;key=value ...]]
Phoenix暴露了很多Key供客户端使用,常见的如 url 以及 serialization,其中url是用来指定Phoenix服务端地址的。
最简单的URL格式为:jdbc:phoenix:thin:url=http://localhost:8765
复杂的URL格式为: jdbc:phoenix:thin:url=http://queryserver.domain:8765;serialization=PROTOBUF;authentication=SPENGO;principal=phoenix@EXAMPLE.COM;keytab=/etc/security/keytabs/phoenix.keytab 。更多配置项请看这里。
如何在Phoenix中批量加载数据
Map Reduce
请看这里
CSV
CSV可以通过内置的psql工具加载,速度一般为2w-5w行每秒(基于每行数据量)。 下面是一个小例子:
// create table
$ psql.py [zookeeper] ../examples/web_stat.sql
// 批量插入数据
$ psql.py [zookeeper] ../examples/web_stat.csv
使用Phoenix映射已存在的HBase表
你可以使用 CREATE TABLE/ CREATE VIEW 来映射已存在的HBase表结构。我们不会对HBase的元数据做任何改动。对于CREATE TABLE来说,如果已存在相同的表名,我们不会创建任何东西。我们将会为所有的行加上一个空的键值,来避免scan操作时扫描所有列。
这里必须注意我们必须遵守Phoenix的序列化方式。对于VARCHAR、CHAR以及 UNSIGNED_* 类型来说我们使用了HBase自身的Bytes类的方法。CHAR表示一个字符,UNSIGNED 类型表示大于等于0的值。对于有符号的类型来说,Phoenix会翻转第一个比特,因此负数会排在正数前边。因为HBase按照字典顺序对Row key排序,负数的第一个bit是1,而正数是0,所以如果我们不翻转,负数将会“大于”正数。所以 如果你使用HBase原生API写入数据,并且希望用Phoenix做查询,请确保所有的数据类型都是 UNSIGNED 类型!
我们的复合ROW key由简单值拼接而来,并在可变的长度类型之后插入一个0byte以作分隔。
如果你创建了如下的HBase表: create 't1', {NAME => 'f1', VERSIONS => 5}
那么你的HBase表名就叫 t1,里边有一个名为 f1 的列族。请记住,在HBase中你不会对row key的结构建模,甚至不用指定列族的键值对。但是你需要在Phoenix中指定这些信息:
CREATE VIEW 't1' (pk VARCHAR PRIMARY KEY, "f1".val VARCHAR)
“pk” 声明表示row key是 VARCHAR 格式。“f1”.val 表示你有一个列族f1,其中有一个键值对,其 column qualifier 为“VAL”,并且它的值是VARCHAR类型。
如果你希望所有列名都是大写的,那么你可以省略掉双引号。
如果你要创建新的 HBase表,请让Phoenix为你创建它~
Phoenix 的优化
- **Salting** 可以通过将数据划分到多个region来显著提升读写性能
举例来说我们有这样一个表:CREATE TABLE TEST (HOST VARCHAR NOT NULL PRIMARY KEY, DESCRIPTION VARCHAR) SALT_BUCKETS=16
注意:对于具有四核cpu的16个region server集群,选择32-64的salt bucket值可以获得最佳性能
- 预先拆分 Salting会自动拆分表,但如果希望在不添加额外字节或更改row key顺序的情况下精确控制拆分,则可以预先拆分一个表。
下面是一个预先拆分的例子:
CREATE TABLE TEST (HOST VARCHAR NOT NULL PRIMARY KEY, DESCRIPTION VARCHAR) SPLIT ON ('CS', 'EU', 'NA')
- 使用多列族
不同的列族将数据存放在独立的HFile中。将查询中用到的列分组到一个列族中可以提高性能。
下面是一个创建双列族表的例子:
CREATE TABLE TEST (MYKEY VARCHAR NOT NULL PRIMARY KEY, A.COL1 VARCHAR, A.COL2 VARCHAR, B.COL3 VARCHAR)
- 对大表使用压缩以提升性能
CREATE TABLE TEST (HOST VARCHAR NOT NULL PRIMARY KEY, DESCRIPTION VARCHAR) COMPRESSION='GZ'
此外还包括:
如何创建二级索引
从Phoenix 2.1版本开始,Phoenix支持索引可变或者不可变数据。而在2.0版本之前只支持索引不可变数据。不可变表的索引写性能要比可变表快一些。
- 创建表
创建不可变表:create table test (mykey varchar primary key, col1 varchar, col2 varchar) IMMUTABLE_ROWS=true;
创建可变表: create table test (mykey varchar primary key, col1 varchar, col2 varchar); - 在col2上创建索引
create index idx on tst (col2) - 在 col1 上创建索引,并在 col2 上创建覆盖索引
create index idx on test (col1) include (col2)
Phoenix查询优化器会在查询时选取合适的索引。你可以在使用了索引的表上查看Phoenix的执行计划。
为何二级索引不起作用
除非查询中使用的所有列都在二级索引中(作为索引或覆盖列),否则不会使用二级索引。构成数据表主键的所有列将自动包括在索引中。
建表:create table usertable (id varchar primary key, firstname varchar, lastname varchar); create index idx_name on usertable (firstname);
查询:select id, firstname, lastname from usertable where firstname = 'foo';
因为lastname不在索引中,故上面的查询不会用到索引。
为啥我的查询没有走RANGE SCAN
表结构:CREATE TABLE TEST (pk1 char(1) not null, pk2 char(1) not null, pk3 char(1) not null, non-pk varchar CONSTRAINT PK PRIMARY KEY(pk1, pk2, pk3));
RANGE SCAN意味着只检索表中行的子集。在查询中使用了主键约束的引导列时,就会发生RANGE SCAN。如 select * from test where pk2='x' and pk3='y'; 没有走引导列,则进行了全表搜索(full scan)。而 select * from test where pk1='x' and pk2='y' 就会走RANGE SCAN。当然如果你在pk2和pk3上创建了二级索引,也会发生 RANGE SCAN。
退化SCAN意味着你的查询根本无法返回任何数据。Phoenix会在编译器发现它进而终止运行。
FULL SCAN 意味着全表扫描
SKIP SCAN 意味着可能全表扫描,也可能扫描子集。但是它会根据你的filter跳过大段的数据。如果查询中没有使用主键的引导列,我们不会进行SKIP SCAN,除非你通过 /+ SKIP_SCAN/来强制指定。在主键引导列的基数较低时,SKIP SCAN比FULL SCAN更加有效。
是否需要 Phoenix JDBC 连接池
你不需要缓存 Phoenix JDBC 连接池。
由于HBase的特殊性,Phoenix 连接对象有别于其他常规的JDBC连接。Phoenix连接被设计为 thin 对象,创建它的代价很小。如果使用连接池来重用HBase连接,可能会发生前一个用户没有正常退出而导致的连接出于非正常状态。更好的选择是每次创建一个新的连接。
如果需要使用连接池,可以对Phoenix连接做简单的代理,每次从池中获取连接的时候给它初始化一个就好,然后再连接归还到连接池之后就将其关闭。
为什么Phoenix在upsert时会增加一个空的KeyValue值
这个限定符为 ‘_0’ 的空键值被用来确保给定列对所有行可用。
数据在HBase中以键值对的形式存储,这意味着为每个列值存储完整的行键。也意味着除非至少存储了一个列,否则根本不存储行键。
现在考虑这种情况,它有一个整数主键,其余的几个列都是空的。为了能够存储主键,需要存储一个KeyValue来显示行是否存在,这个列由上述空列表示。即执行 “SELECT * FROM TABLE” 能够查询到那些非pk列全为空的记录。
因此在Phoenix上的扫描将包括空列,以确保那些只包含主键的行被包含在扫描结果中。
配置记录
HBase版本
Version 2.1.0-cdh6.3.0
依赖版本
<dependencies>
<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-core</artifactId>
<version>5.0.0-HBase-2.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.9.2</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
自定义连接池
@Bean
public DataSource dataSource() throws ClassNotFoundException {
Class clazz = Class.forName("org.apache.phoenix.jdbc.PhoenixDriver");
return new SimpleDriverDataSource((Driver)BeanUtils.instantiateClass(clazz), phoenixUrl);
}
来源:oschina
链接:https://my.oschina.net/landas/blog/3191186