Apache Hive中JdbcStorageHandler的入门和源码分析

一、JdbcStorageHandler入门

1.为什么要有StorageHandler

引入StorageHandler,Hive用户使用SQL可读写外部数据源,具体请移步这篇文章
Hive Storage Handler入门和实战

2.JdbcStorageHandler介绍

JdbcStorageHandlerStorageHandler的一个扩展,使得Hive能够读取 JDBC 数据源,具体介绍请移步Apache Hive 联邦查询

下面我简单介绍自己使用的一个案例。

3.开发步骤
(1)环境搭建

本地已经搭建好了Hadoop3.3.0版本的分布式集群,并安装了Apache Hive 2.3.8版本,机器和对应的节点信息如下

CNSZ22PL0272CNSZ22PL0273CNSZ22PL0274
HDFSNameNodeDataNodeDataNode
YARNNodeManagerResourceManagerNodeManager
HiveHive
(2)建表语法

我们有一张mysql类型的表endpoint_tmp,结构如下

CREATE TABLE `endpoint_tmp` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `ENDPOINT` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `ts` int(11) DEFAULT NULL,
  `t_create` datetime NOT NULL COMMENT 'create time',
  `t_modify` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'last modify time',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_endpoint` (`ENDPOINT`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
 

其所在连接信息为

jdbc:mysql://ip:3306/graph

通过如下语法,可以在Hive中创建一张关联上述endpoint_tmp的表,表名为endpoint

CREATE EXTERNAL TABLE endpoint
(
 id  INT,
 endpoint STRING,
 ts INT,
 t_create TIMESTAMP,
 t_modify TIMESTAMP
)
STORED BY 'org.apache.hive.storage.jdbc.JdbcStorageHandler'
TBLPROPERTIES (
    "hive.sql.database.type" = "MYSQL","hive.sql.jdbc.driver" = "com.mysql.jdbc.Driver","hive.sql.jdbc.url" = "jdbc:mysql://100.80.170.91:3306/graph","hive.sql.dbcp.username" = "falcon","hive.sql.dbcp.password" = "+nlmG2zq+nlmG2zq","hive.sql.query"="select id,endpoint,ts,t_create,t_modify from endpoint_tmp","hive.sql.dbcp.maxActive" = "1"
);

其中endpoint表字段名和字段类型都要与endpoint_tmp表中的对应;

STORED BY必须指定使用'org.apache.hive.storage.jdbc.JdbcStorageHandler',表明这是一张与jdbc相关的表;

TBLPROPERTIES中每个字段的具体含义,在前文Apache Hive 联邦查询中可以找到详细的介绍。

需要注意的是,TBLPROPERTIEShive.sql.query书写的字段顺序,必须和CREATE EXTERNAL TABLE中两个小括号里的字段顺序一致,否则在Hive中查询后,无法将字段名和值正确对应。

(3)创建外部表

现在我们进入Hive命令行界面,通过上述语法创建表
在这里插入图片描述
建表成功!

(4)查询数据

endpoint_tmp表中已存在一条如下数据
在这里插入图片描述

我们在Hive中查询,结果也正确展示了出来。
在这里插入图片描述

上面我们演示的是JdbcStorageHandler的基本使用方式。

下面我们从源码的角度分析JdbcStorageHandler在Hive中的实现原理,以便于后续我们对其进行改造时做到了然于胸。

二、JdbcStorageHandler源码分析

上述开发步骤使用的是Apache Hive 2.3.8版本,所以我们需要下载对应的源码包Hive2.3.8源码包下载地址,下载好之后使用IDEA打开并下载项目所需依赖。

最终整体代码结构如下,其中用红色箭头指向的包就是与JdbcStorageHandler有关的核心代码
在这里插入图片描述
JdbcStorageHandler相关的逻辑并不复杂,核心逻辑是先根据数据源划分切片,然后根据划分好的切片去数据源查询数据。下面我带着大家一步步分析(最好是按照Hive远程debug步骤,提前在本地搭建好远程debug的环境)。

1.划分切片

当我们在Hive中输入一条sql时,Hive首先会将其变为一个语法树数据结构,然后根据语法树结构转化为逻辑执行计划,最后对其进行优化并生成物理执行计划,也就是MR任务,最终将MR任务提交到YARN执行。
这个过程是Hive逻辑的基本框架,不是本文要分析的重点。

我们要分析的是:

在MR任务提交到YARN执行之前
1.如何根据jdbc数据源划分MapTask数量?
2.每个MapTask查询多少数据?

(1)JdbcStorageHandler类

我们先看JdbcStorageHandler类,也就是创建表是我们指定的'org.apache.hive.storage.jdbc.JdbcStorageHandler'
它实现了HiveStorageHandler接口并重写了getInputFormatClass方法,方法返回JdbcInputFormat
在这里插入图片描述

(2)JdbcInputFormat类

JdbcInputFormat类继承了HiveInputFormat类。
在这里插入图片描述
写过MR程序的都知道InputFormat作用是用来做分片的,那么里面必然有个getSplits方法返回划分好的切片数,我们查看JdbcInputFormat中该方法逻辑,具体看注释

  public InputSplit[] getSplits(JobConf job, int numSplits) throws IOException {
    try {
      //numSplits即切片数
      if (numSplits <= 0) {
        numSplits = 1;
      }
      LOGGER.debug("Creating {} input splits", numSplits);
      if (dbAccessor == null) {
        //实际获取的是MySqlDatabaseAccessor类,后面会介绍
        dbAccessor = DatabaseAccessorFactory.getAccessor(job);
      }

      //根据MySqlDatabaseAccessor类,查询数据库的记录总数
      int numRecords = dbAccessor.getTotalNumberOfRecords(job);
      // 每个切片需要查询的条数=记录总数/切片数量
      int numRecordsPerSplit = numRecords / numSplits;
      int numSplitsWithExtraRecords = numRecords % numSplits;

      LOGGER.debug("Num records = {}", numRecords);
      //创建InputSplit数组,大小为numSplits
      InputSplit[] splits = new InputSplit[numSplits];
      Path[] tablePaths = FileInputFormat.getInputPaths(job);

      int offset = 0;
      for (int i = 0; i < numSplits; i++) {
        int numRecordsInThisSplit = numRecordsPerSplit;
        if (i < numSplitsWithExtraRecords) {
          numRecordsInThisSplit++;
        }
		//创建JdbcInputSplit
		//其中numRecordsInThisSplit变量未来会被设置为sql语句limit关键字的第一个参数
		//offset对应limit的第二个参数
		//比如 select * from table limit 10,0
        splits[i] = new JdbcInputSplit(numRecordsInThisSplit, offset, tablePaths[0]);
        offset += numRecordsInThisSplit;
      }
      //返回切片
      return splits;
    }
    catch (Exception e) {
      LOGGER.error("Error while splitting input data.", e);
      throw new IOException(e);
    }
  }

其中numSplits参数很关键,它是通过上游HiveInputFormat类的getSplits方法传入的。在Hive中可以通过设置如下参数,来改变numSplits参数的值。

 set mapred.map.tasks=数字

该参数决定着每个表将来生成的MapTask数量。这就回答了上面提到的第一个问题

是如何根据jdbc数据源划分MapTask数量的?

第二个很关键的地方是getTotalNumberOfRecords方法——查询数据库的记录总数。

Hive会根据记录总数/切片数量,计算每个MapTask需要从数据库查询的数据量。

这就回答了上面提到的第二个问题

每个MapTask查询多少数据?

此外,DatabaseAccessorFactory.getAccessor(job)返回的具体实现以及getTotalNumberOfRecords方法是如何查询总数据量的,在后面会有介绍。

总结一下,getSplits方法主要做了以下事情:

1.通过MySqlDatabaseAccessor类的getTotalNumberOfRecords方法获取数据源的总记录
2.根据记录总数和切片数量,计算每个切片对应的limit和offset
3.返回切片数组

(3)DatabaseAccessor接口

与数据源有交互的工作,都是这个接口完成的,包括如下方法:
在这里插入图片描述
具体实现类如下

在这里插入图片描述
上面说到getSplits方法里会通过DatabaseAccessorFactory.getAccessor(job)获取具体的实现类,具体逻辑如下

  public static DatabaseAccessor getAccessor(DatabaseType dbType) {

    DatabaseAccessor accessor = null;
    switch (dbType) {
    case MYSQL:
      //如果建表时hive.sql.database.type指定的MYSQL,则返回MySqlDatabaseAccessor
      accessor = new MySqlDatabaseAccessor();
      break;

    default:
      //反之返回GenericJdbcDatabaseAccessor
      accessor = new GenericJdbcDatabaseAccessor();
      break;
    }

    return accessor;
  }

如果建表时hive.sql.database.type指定MYSQL,这个方法就会返回MySqlDatabaseAccessor,反之返回GenericJdbcDatabaseAccessor

其中GenericJdbcDatabaseAccessor类使用模板设计模式,定义了通用的模板方法。

下面我们看看接口中每个方法的作用。

- getColumnNames方法

通过JDBC规范连接数据库,获取表的字段名信息。主要场景是hive解析sql生成语法树时,给语法树设置元数据信息。
但这个方法不是我们关注的重点。

- getTotalNumberOfRecords方法

该方法就是上述getSplits方法里用到的逻辑,代码如下,具体看注释

  @Override
  public int getTotalNumberOfRecords(Configuration conf) throws HiveJdbcDatabaseAccessException {
    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
      //使用JDBC规范加载驱动、获取数据源
      initializeDatabaseConnection(conf);
      //获取hive.sql.query属性的值,即select id,endpoint,ts,t_create,t_modify from endpoint_tmp
      String sql = JdbcStorageConfigManager.getQueryToExecute(conf);
      //查询总记录数的sql
      //SELECT COUNT(*) FROM (select id,endpoint,ts,t_create,t_modify from endpoint_tmp) tmptable
      String countQuery = "SELECT COUNT(*) FROM (" + sql + ") tmptable";
      LOGGER.debug("Query to execute is [{}]", countQuery);
      //获取连接
      conn = dbcpDataSource.getConnection();
      ps = conn.prepareStatement(countQuery);
      rs = ps.executeQuery();
      //返回记录总数
      if (rs.next()) {
        return rs.getInt(1);
      }
      else {
        LOGGER.warn("The count query did not return any results.", countQuery);
        throw new HiveJdbcDatabaseAccessException("Count query did not return any results.");
      }
    }
    catch (HiveJdbcDatabaseAccessException he) {
      throw he;
    }
    catch (Exception e) {
      LOGGER.error("Caught exception while trying to get the number of records", e);
      throw new HiveJdbcDatabaseAccessException(e);
    }
    finally {
      cleanupResources(conn, ps, rs);
    }
  }

initializeDatabaseConnection方法会根据建表时hive.sql.jdbc.url属性的值,使用JDBC规范加载驱动、获取数据源。

JdbcStorageConfigManager.getQueryToExecute获取hive.sql.query属性的值,也就是我们建表时指定的sql,然后它被拼接成查询总记录数的sql,再交给JDBC数据源查询,并返回总记录数。

- getRecordIterator方法

查询分片数据并返回JdbcRecordIterator对象,后面详细介绍。

2.查询分片

通过上面分析可知,我们通过JdbcInputFormat类的getSplits方法获取了切片信息。之后hive将MR任务提交至YARN,MapTask任务便开始运行。

众所周知,每个MapTask运行时,在map阶段都会需要根据当前分片信息,使用RecordReader类去存储端读取实际的数据。
如果我们查询的是HDFS的数据,则使用FileInputFormat 中的LineRecordReader读取文件每一行。
而我们现在用的是JdbcInputFormat这里,hive会怎么读取呢?

根据分片信息,生成的带有limit和offset的sql语句,去jdbc数据源查询数据,然后使用JdbcRecordReader遍历每行数据。

(1)JdbcRecordReader类

因此,我们需要分析JdbcRecordReader的读取数据的逻辑,代码如下

  //map阶段会在循环里调用next方法
  @Override
  public boolean next(LongWritable key, MapWritable value) throws IOException {
    try {
      LOGGER.debug("JdbcRecordReader.next called");
      if (dbAccessor == null) {
        dbAccessor = DatabaseAccessorFactory.getAccessor(conf);
        //根据limit和offset查询当前分片数据,并返回JdbcRecordIterator
        iterator = dbAccessor.getRecordIterator(conf, split.getLimit(), split.getOffset());
      }
	
	 //如果有下一行
      if (iterator.hasNext()) {
        LOGGER.debug("JdbcRecordReader has more records to read.");
        key.set(pos);
        pos++;
        //通过JdbcRecordIterator获取当前行
        Map<String, String> record = iterator.next();
        if ((record != null) && (!record.isEmpty())) {
         //设置当前行的所有字段名和字段值
          for (Entry<String, String> entry : record.entrySet()) { 
            value.put(new Text(entry.getKey()), new Text(entry.getValue()));
          }
          return true;
        }
        else {
          LOGGER.debug("JdbcRecordReader got null record.");
          return false;
        }
      }
      else {
        LOGGER.debug("JdbcRecordReader has no more records to read.");
        return false;
      }
    }
    catch (Exception e) {
      LOGGER.error("An error occurred while reading the next record from DB.", e);
      return false;
    }
  }

map阶段会在循环里调用上面的next方法,每次遍历一行,会设置当前行的所有字段名和字段值并返回。
而我们数据是从哪里来的呢?通过dbAccessor.getRecordIterator(conf, split.getLimit(), split.getOffset())方法获取——根据limit和offset查询当前分片数据,并返回JdbcRecordIterator

(2) JdbcRecordIterator类

我们重点分析dbAccessor.getRecordIterator(conf, split.getLimit(), split.getOffset())这段代码。

 @Override
  public JdbcRecordIterator
    getRecordIterator(Configuration conf, int limit, int offset) throws HiveJdbcDatabaseAccessException {

    Connection conn = null;
    PreparedStatement ps = null;
    ResultSet rs = null;

    try {
       //使用JDBC规范加载驱动、获取数据源
      initializeDatabaseConnection(conf);
      //获取hive.sql.query属性的值,此处是select id,endpoint,ts,t_create,t_modify from endpoint_tmp
      String sql = JdbcStorageConfigManager.getQueryToExecute(conf);
      //拼接limit和offset
      //select id,endpoint,ts,t_create,t_modify from endpoint_tmp limit 10,0
      String limitQuery = addLimitAndOffsetToQuery(sql, limit, offset);
      LOGGER.debug("Query to execute is [{}]", limitQuery);
	 //从数据源获取连接
      conn = dbcpDataSource.getConnection();
      ps = conn.prepareStatement(limitQuery, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
      ps.setFetchSize(getFetchSize(conf));
      //查询数据
      rs = ps.executeQuery();
	 //返回JdbcRecordIterator,持有当前分片的数据
      return new JdbcRecordIterator(conn, ps, rs);
    }
    catch (Exception e) {
      LOGGER.error("Caught exception while trying to execute query", e);
      cleanupResources(conn, ps, rs);
      throw new HiveJdbcDatabaseAccessException("Caught exception while trying to execute query", e);
    }
  }

其中limitQuery 就是生成的带有limit和offset的sql语句。

总结一下,getRecordIterator方法主要做了以下事情:

1.用JDBC规范加载驱动、获取数据源
2.生成带有limit和offset的sql语句
3.根据sql查询当前分片数据,并返回JdbcRecordIterator

三、总结

本文介绍了JdbcStorageHandler的基本使用方式,以及简单的源码分析。
通过JdbcStorageHandler,我们可以使用标准的JDBC方式读取数据。
但是,由于JdbcStorageHandler本身不支持写入数据到JDBC,所以目前仅限于查询数据的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值