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

文章目录

    • 一、JdbcStorageHandler入门
        • 1.为什么要有StorageHandler
        • 2.JdbcStorageHandler介绍
        • 3.开发步骤
            • (1)环境搭建
            • (2)建表语法
            • (3)创建外部表
            • (4)查询数据
    • 二、JdbcStorageHandler源码分析
        • 1.划分切片
            • (1)JdbcStorageHandler类
            • (2)JdbcInputFormat类
            • (3)DatabaseAccessor接口
        • 2.查询分片
            • (1)JdbcRecordReader类
            • (2) JdbcRecordIterator类
    • 三、总结

一、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版本,机器和对应的节点信息如下

CNSZ22PL0272 CNSZ22PL0273 CNSZ22PL0274
HDFS NameNode DataNode DataNode
YARN NodeManager ResourceManager NodeManager
Hive Hive
(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有关的核心代码
Apache Hive中JdbcStorageHandler的入门和源码分析_第1张图片
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
Apache Hive中JdbcStorageHandler的入门和源码分析_第2张图片

(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接口

与数据源有交互的工作,都是这个接口完成的,包括如下方法:
Apache Hive中JdbcStorageHandler的入门和源码分析_第3张图片
具体实现类如下

在这里插入图片描述
上面说到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,所以目前仅限于查询数据的场景。

你可能感兴趣的:(入门案例,源码剖析,apache,hive,hadoop)