Hive直接读取Hbase及MySQL数据
0.概述
Hive对外提供了StorageHandler接口,提供了访问各种存储组件中的数据的能力。Hbase提供了HbaseStorageHandler,使得hive可以通过建立外部映射表访问hbase中的数据。但是,公司CDH集群的版本比较低,不支持新版hive原生的JdbcStorageHandler。因而要访问JDBC数据源中的数据,只能通过添加第三方类库实现。
1.Hive 访问Hbase
use ods_sdb;
create external table if not exists ods_sdb.$v_table(
ajbs string comment '标识',
hytcyqdrq string comment '合议庭成员确定日期',
splcbgkyy string comment '审判流程不公开原因',
ajgyxx_stm string comment '实体码',
bygksplc string comment '不宜公开审判流程',
jbfy string comment '经办法院',
labmbs string comment '立案部门标识',
splcygk string comment '审判流程已公开',
ajgyxx_ajbs string comment '案件标识',
ajmc string comment '案件名称',
stm string comment '实体码',
cbbmbs string comment '承办部门标识'
) comment '概要信息'
stored by 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
with serdeproperties (
'hbase.columns.mapping' = ':key,f:anjiangaiyaoxinxi.heyitingchengyuanquedingriqi,f:anjiangaiyaoxinxi.shenpanliuchengbugongkaiyuanyin,f:anjiangaiyaoxinxi.shitima,f:anjiangaiyaoxinxi.buyigongkaishenpanliucheng,f:anjiangaiyaoxinxi.jingbanfayuan,f:anjiangaiyaoxinxi.lianbumenbiaozhi,f:anjiangaiyaoxinxi.shenpanliuchengyigongkai,f:anjiangaiyaoxinxi.anjianbiaozhi,f:anjiangaiyaoxinxi.anjianmingcheng,f:shitima,f:anjiangaiyaoxinxi.chengbanbumenbiaozhi'
) tblproperties ( 'hbase.table.name' = 'aj_15_baseinfo')
stored by 'org.apache.hadoop.hive.hbase.HBaseStorageHandler':底层数据在hbase中存储的话,需要指定该类进行处理。
with serdeproperties 中指定hbase中的字段与hive外部表的对应映射关系,其中::key为hbase的rowkey,其他字段按照外部表的定义顺序,依次以列族:字段名的顺序排列,用半角逗号分隔。
tblproperties 中指定对应的hbase表名。
需要注意的是:不要对该表进行复杂的条件查询,where中最好只使用rowkey对应的字段进行条件判断。
建议:只对该表进行数据的导出操作,即从hbase中把数据导出到hive实体表中。
2.Hive 访问MySQL
如同前面的描述,hive从 HIVE-1555 才开始支持自带的JdbcStorageHandler,在低版本的hive中要直接访问jdbc中的数据,只能通过第三方的JdbcStorageHandler实现。
第三方源码:https://github.com/qubole/Hive-JDBC-Storage-Handler
使用方式:
- git clone https://github.com/qubole/Hive-JDBC-storage-Handler.git
- mvn clean install -Phadoop-1
- add jar 注意需要加入mysql jdbc驱动
add jar /home/csc/20190729/qubole-hive-JDBC.jar;
add jar /home/csc/20190729/udf-1.0.jar;
use ods_sdb;
create external table if not exists ods_sdb.$v_table(
id string comment 'id',
fdm string comment '案件标识',
cBh string comment '当事人主键',
cCxm string comment '案件查询码',
nBgrpxh string comment '被告人排序号',
nFzje string comment '犯罪金额',
nSf string comment '特殊身份',
cSf string comment '特殊身份中文',
nZy string comment '职业',
cZy string comment '职业中文',
create_time string comment '创建时间'
) comment '当事人情况'
stored by 'org.apache.hadoop.hive.jdbc.storagehandler.JdbcStorageHandler'
tblproperties (
'mapred.jdbc.driver.class'='com.mysql.jdbc.Driver',
'mapred.jdbc.url'='jdbc:mysql://ip:port/fb_data?characterEncoding=utf8',
'mapred.jdbc.username'='username',
'mapred.jdbc.input.table.name'='fb_15_dsr',
'mapred.jdbc.password'='password',
'mapred.jdbc.hive.lazy.split'= 'false'
);
tblproperties中的各项配置,可以参考git上的描述。
问题定位解决:
(涉及到具体机器资源、环境,非重复复现的问题,修改方案,也只是临时性解决)
在实际使用过程中,处理当事人数据时,反复出现jdbc链接超时的情况,导致数据导出任务失败。
问题定位过程如下:
1.非标任务首次执行比较耗时,执行成功后,重跑速度相对较快;
2.show processlist;发现,在执行非标数据导出任务时,会优先执行一个count()的sql,比较耗时。分析非标数据在mysql中的存储,首先量级已达千万,其次,存储引擎采用的是InnoDB,对count()的执行需要遍历全表;
3.真正执行数据导出任务时,分为两个mapper执行select xxx的操作,时间消耗较少。
结合mysql的运行日志及数据导出任务的日志,基本定位到为count(*)导致的会话超时。
问题解决过程如下:
1.首先阅读JdbcStorageHandler的源码,定位count(*)的来源:
/*
* Copyright 2013-2015 Qubole
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hive.wrapper;
import java.io.IOException;
import org.apache.hadoop.mapred.RecordReader;
import org.apache.hadoop.mapred.InputSplit;
import org.apache.hadoop.mapred.FileSplit;
import org.apache.hadoop.mapred.JobConf;
import org.apache.hadoop.mapred.Reporter;
import org.apache.hadoop.mapreduce.lib.db.DBConfiguration;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.TaskAttemptID;
import org.apache.hadoop.hive.shims.ShimLoader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.sql.*;
import org.apache.hadoop.hive.jdbc.storagehandler.Constants;
import org.apache.hadoop.hive.jdbc.storagehandler.JdbcDBInputSplit;
public class RecordReaderWrapper<K, V> implements RecordReader<K, V> {
private static final Log LOG = LogFactory.getLog(RecordReaderWrapper.class);
private org.apache.hadoop.mapreduce.RecordReader<K, V> realReader;
private long splitLen; // for getPos()
// expect readReader return same Key & Value objects (common case)
// this avoids extra serialization & deserialazion of these objects
private K keyObj = null;
protected V valueObj = null;
private boolean firstRecord = false;
private boolean eof = false;
private Connection conn = null;
private String tblname = null;
private DBConfiguration delegate = null;
private long taskIdMapper = 0;
private boolean lazySplitActive = false;
private long count = 0;
private int chunks = 0;
public RecordReaderWrapper(InputFormat<K, V> newInputFormat,
InputSplit oldSplit, JobConf oldJobConf, Reporter reporter)
throws IOException {
TaskAttemptID taskAttemptID = TaskAttemptID.forName(oldJobConf
.get("mapred.task.id"));
if (taskAttemptID !=null) {
LOG.info("Task attempt id is >> " + taskAttemptID.toString());
}
if(oldJobConf.get(Constants.LAZY_SPLIT) != null &&
(oldJobConf.get(Constants.LAZY_SPLIT)).toUpperCase().equals("TRUE")){
lazySplitActive = true;
ResultSet results = null;
Statement statement = null;
delegate = new DBConfiguration(oldJobConf);
try{
conn = delegate.getConnection();
statement = conn.createStatement();
results = statement.executeQuery("Select Count(*) from " + oldJobConf.get("mapred.jdbc.input.table.name"));
results.next();
count = results.getLong(1);
chunks = oldJobConf.getInt("mapred.map.tasks", 1);
LOG.info("Total numer of records: " + count + ". Total number of mappers: " + chunks );
splitLen = count/chunks;
if((count%chunks) != 0)
splitLen++;
LOG.info("Split Length is "+ splitLen);
results.close();
statement.close();
}
catch(Exception e){
// ignore Exception
}
}
org.apache.hadoop.mapreduce.InputSplit split;
if(lazySplitActive){
((JdbcDBInputSplit)(((InputSplitWrapper)oldSplit).realSplit)).setStart(splitLen);
((JdbcDBInputSplit)(((InputSplitWrapper)oldSplit).realSplit)).setEnd(splitLen);
}
if (oldSplit.getClass() == FileSplit.class) {
split = new org.apache.hadoop.mapreduce.lib.input.FileSplit(
((FileSplit) oldSplit).getPath(),
((FileSplit) oldSplit).getStart(),
((FileSplit) oldSplit).getLength(), oldSplit.getLocations());
} else {
split = ((InputSplitWrapper) oldSplit).realSplit;
}
// create a MapContext to pass reporter to record reader (for counters)
TaskAttemptContext taskContext = ShimLoader.getHadoopShims()
.newTaskAttemptContext(oldJobConf,
new ReporterWrapper(reporter));
try {
realReader = newInputFormat.createRecordReader(split, taskContext);
realReader.initialize(split, taskContext);
// read once to gain access to key and value objects
if (realReader.nextKeyValue()) {
firstRecord = true;
keyObj = realReader.getCurrentKey();
valueObj = realReader.getCurrentValue();
} else {
eof = true;
}
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
results = statement.executeQuery("Select Count(*) from " + oldJobConf.get("mapred.jdbc.input.table.name"));
可以看到在执行数据导出任务前,首先会获取该表的总行数,用于进行任务的分割。但是,这里的触发条件是
'mapred.jdbc.hive.lazy.split'= 'true'
但是,该操作配置为false的情况下,仍然会默认执行count(*)的操作。
- 添加自定义表量级的阈值定义:
count = oldJobConf.getInt("mapred.jdbc.input.table.count", 20000000);
// results = statement.executeQuery("Select Count("+ (key==null?"*":key) + ") from " + oldJobConf.get("mapred.jdbc.input.table.name"));
// results.next();
// count = results.getLong(1);
3.外部表定义修改:
添加 'mapred.jdbc.input.table.count'='3000000'
4.重新打包,并上传;
5.问题搞定!