这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
前言
最近项目用到Hbase, 之前没怎么接触过,在搭建项目的过程中遇到的一些问题,记录一下。
项目是用Spring-boot搭建的,看着正好有Spring官方依赖,spring-data-hadoop-hbase,省的自己重新造轮子了。我们测试环境Hbase版本,HBase 1.2.0,但是用HbaseTemplete发现一个问题每次查询都很慢?
1. HbaseTemplete 读写Hbase api
Spring封装的框架用起来其实很简单,虽然支持的API比较少,但是对于我的场景来说已经够用。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-hadoop-hbase</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
Hbase Configuration 配置信息zk地址和端口号
@Configuration
@EnableConfigurationProperties
public class HbaseConfig {
@Value("${hbase.zookeeper.quorum}")
private String zkHosts;
@Value("${hbase.zookeeper.property.clientPort}")
private String zkPort;
@Bean
public HbaseTemplate hbaseTemplate(){
org.apache.hadoop.conf.Configuration config = new org.apache.hadoop.conf.Configuration();
config.set("hbase.zookeeper.quorum", zkHosts);
config.set("hbase.zookeeper.property.clientPort", zkPort);
return new HbaseTemplate(config);
}
}
直接查询获取对象
public <T> T get(String rowKey, Class<T> clazz) {
String tableName = clazz.getSimpleName();
T obj = hbaseTemplate.execute(tableName, table->{
Get get = new Get(Bytes.toBytes(rowKey));
Result result = table.get(get);
T bean = clazz.newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor t : pds) {
String pName = t.getName();
if ("class".equals(pName)) {
continue;
}
Class pClazz = t.getPropertyType();
Object inObj = pClazz.newInstance();
parseColumn(pName, inObj, result);
PropertyUtils.setProperty(bean, pName, inObj);
}
return bean;
});
return obj;
}
但是发现一个问题每次查询耗时的时间都很长,然后DEBUG进去发现hbaseTemplate 每次都会去创建Connection.
2. HbaseTemplete 源码分析
我们从new HbaseTemplate(config) 开始分析继承关系很简单,
public HbaseTemplate(Configuration configuration) {
// Configuration赋值
setConfiguration(configuration);
// 初始化bean之后属性校验
afterPropertiesSet();
}
我们可以看到从Bean Configuration 初始化创建到创建HbaseTemplate对象都是没有链接信息的。 继续到hbaseTemplate.execute(...)方法 创建链接的核心方法:HTableInterface table = getTable(tableName);
但是最后都会调用Htable.close() 函数关闭链接。 我们其实已经找到查询耗时的原因,因为这个Connection的时间是非常长的。
我们在看下Hbase connection 源码
3. Hbase Client 原理分析
分析到这里我们其实就很好奇Hbase Connection是如何管理的呢?和我们关系型数据库链接有没有区别,是不是需要创建一个连接池?
Hbase Client三个不同的角色
- ZooKeeper:主要用于获得meta-region位置,集群Id、master等信息。
- HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。
- HBase RegionServer:用于读、写数据。
当HBase-client第一次请求读写的时候,需要三步走:
1)HBase-client从zk中获取保存meta table的位置信息,知道meta table保存在了哪个region server,然后缓存这个位置信息;
2)HBase-client会查询这个保存meta table的特定的region server,查询meta table信息,在table中获取自己想要访问的row key所在的region在哪个region server上。
3)客户端直接访问目标region server,获取对应的row
4. 封装Hbase Connection单例
public class HbaseConnectionFactory {
/**
* Logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(HbaseConnectionFactory.class);
private HbaseConnectionFactory() {
}
/**
* Hbase链接
*/
private volatile static Connection connection = null;
/**
* DCL 模式保证hbase链接成功
*
* @param configuration
* @return Connection
*/
public static Connection getConnection(Configuration configuration) {
if (connection == null) {
synchronized (HbaseConnectionFactory.class) {
if (connection == null) {
try {
connection = ConnectionFactory.createConnection(configuration);
LOGGER.info("HbaseConnectionFactory create connection success!");
} catch (IOException e) {
LOGGER.error("HbaseConnectionFactory create connection failed!", e);
// todo 对外抛异常
}
}
}
}
return connection;
}
}
public class HbaseOperationUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(HbaseOperationUtil.class);
private Connection connection = null;
public HbaseOperationUtil(Configuration configuration) {
this.connection = HbaseConnectionFactory.getConnection(configuration);
}
/**
* 查询namespace中所有的表名
*
* @param namespace
* @return List<String>
*/
public List<String> listTables(String namespace) {
List<String> tableNameList = new ArrayList<>();
// 获取namespace中所有的表名
TableName[] tableNames = new TableName[0];
try {
tableNames = connection.getAdmin().listTableNamesByNamespace(namespace);
LOGGER.info("HbaseOperationUtil listTables tableNames:{}", JSON.toJSONString(tableNames));
} catch (IOException e) {
LOGGER.error("HbaseOperationUtil listTables error.", e);
}
for (TableName tableName : tableNames) {
tableNameList.add(tableName.toString());
}
return tableNameList;
}
}
最后就可以直接操作hbaseclient api了
@Test
public void test2() {
String rowKey = StringUtils.substring(MD5Hash.getMD5AsHex("urOs1H".getBytes()), 0, 3);
Result result = hbaseOperationUtil.get("mk:testTable", rowKey);
for(Cell cell:result.rawCells()){
System.out.print("行健: "+new String(CellUtil.cloneRow(cell)));
System.out.print("列簇: "+new String(CellUtil.cloneFamily(cell)));
System.out.print(" 列: "+new String(CellUtil.cloneQualifier(cell)));
System.out.print(" 值: "+new String(CellUtil.cloneValue(cell)));
System.out.println("时间戳: "+cell.getTimestamp());
}
}
5. 各种依赖问题
图上面是我遇到的一些依赖问题,比如第一个guava版本的问题,swagger UI依赖的版本比较高20几了,但是hbase-client 客户端版本依赖比较低,这就很尴尬向上不兼容,向下也不兼容。这种问题解决方法,就是将自己所有依赖的包都添加进来打成一个shaded包。类似C或者C++中静态编译和动态编译。
6. Java访问带有Kerberos认证的Hbase库
本地连接测试需要配置3个选项
-
- 本地hosts文件
-
- krb5.conf kerberos的相关配置信息,KDC对应的IP,默认的realm等
-
- hbase.keytab 存储密码相关的信息
代码这里就不贴了,就是在configuration初始化加载bean的时候设置,对应的参数值
详细原理可以参考下面:kerberos认证原理
参考文档
HBase基本架构及原理
HBase连接池
spring-data管理HbaseTemplate connection 坑之源码分析
HBase Connection的使用
Java访问带有Kerberos认证的HBase
kerberos认证原理