大数据SQL调优专题——Hive执行原理

引入

Apache Hive 是基于Hadoop的数据仓库工具,它可以使用SQL来读取、写入和管理存在分布式文件系统中的海量数据。在Hive中,HQL默认转换成MapReduce程序运行到Yarn集群中,大大降低了非Java开发者数据分析的门槛,并且Hive提供命令行工具和JDBC驱动程序,方便用户连接到Hive进行数据分析操作。

严格意义上,Hive并不属于计算引擎,而是建立在Hadoop生态之上的数据仓库管理工具。它将繁杂的MapReduce作业抽象成SQL,使得开发及维护成本大幅降低。得益于HDFS的存储和MapReduce的读写能力,Hive展现出了强大的兼容能力、数据吞吐能力和服务稳定性,时至今日依然是大数据架构中不可或缺的一部分。

Hive的核心特点

  • Hive是基于Hadoop的数仓工具,底层数据存储在HDFS中;

  • Hive提供标准SQL功能,支持SQL语法访问操作数据;

  • Hive适合OLAP数据分析场景,不适合OLTP数据处理场景,所以适合数据仓库构建;

  • HQL默认转换成MapReduce任务执行,也可以配置转换成Apache Spark、Apache Tez任务运行;

  • Hive中支持定义UDF、UDAF、UDTF函数扩展功能。

Hive的架构设计

Hive用户接口

访问Hive可以通过CLI、Beeline、JDBC/ODBC、WebUI几种方式。在Hive早期版本中可以使用Hive CLI来操作Hive,Hive CLI并发性能差、脚本执行能力有限并缺乏JDBC驱动支持,从Hive 4.x版本起废弃了Hive CLI推荐使用Beeline。Beeline是一个基于JDBC的Hive客户端,支持并发环境、复杂脚本执行、JDBC驱动等,在Hive集群内连接Hive可以使用Beeline方式。在Hive集群外,通过代码或者工具连接操作Hive时可以通过JDBC/ODBC方式。通过WebUI方式可以通过浏览器查看到Hive集群的一些信息。

HiveServer2服务

HiveServer2服务提供JDBC/ODBC接口,主要用于代理远程客户端对Hive的访问,是一种基于Thrift协议的服务。例如通过JDBC或者Beeline连接访问Hive时就需要启动HiveServer2服务,就算Beeline访问本机上的Hive服务也需要启动HiveServer2服务。

HiveServer2代理远程客户端对Hive操作时会涉及到操作HDFS数据,就会有操作权限问题,那么操作HDFS中数据的用户是启动HiveServer2的用户还是远程客户端的用户需要通过“hive.server2.enable.doAs” 参数决定,该参数默认为true,表示HiveServer2操作HDFS时的用户为远程客户端用户,如果设置为false表示操作HDFS数据的用户为启动HiveServer2的用户。

MetaStore服务

MetaStore服务负责存储和管理Hive元数据,为HiverServer2提供元数据访问接口。Hive中的元数据包括表的名字,表的列和分区及其属性,表的属性(表拥有者、是否为外部表等),表的数据所在目录等。

Hive MetaStore可以将元数据存储在mysql、derby数据库中。

Hive Driver

Driver中包含解释器(SQL Parser)、编译器(Compiler)、优化器(Optimizer),负责完成HQL查询语句从词法分析、语法分析、编译、优化以及查询计划的生成。生成的查询计划存储在HDFS中,并在随后有执行器(Executor)调用MapReduce执行。

对于Hive有了一个初步认识,我们下面开始梳理Hive的执行原理。

Hive的执行原理

Hive无论采用哪种调用方式,最终都会辗转到org.apache.hadoop.hive.ql.Driver类。SQL语句在Driver类中,通过Antlr框架进行解析编译,将SQL转换成最终执行的MapReduce任务。

如果直接盲目的去看Driver类的代码,会很容易看懵逼,我们需要再往前一点。

SQLOperation

先看org.apache.hive.service.cli.operation.SQLOperation 类,它负责创建Driver对象、编译SQL、异步执行SQL。其中核心的就是 runInternal()方法,主要进行如下两个步骤:

  1. Driver对象创建并编译SQL,将SQL编译成Query Plan执行计划。
  2. 对QueryPaln 进行处理,转换成MR 任务执行。

runInternal() 方法源码内容如下:

  /**
   * 内部运行方法,用于执行SQL操作。
   *
   * @throws HiveSQLException 如果在执行过程中发生Hive SQL异常。
   */
  public void runInternal() throws HiveSQLException {
    // 设置操作状态为PENDING
    setState(OperationState.PENDING);

    // 判断是否应该异步运行
    boolean runAsync = shouldRunAsync();
    // 判断是否应该异步编译
    final boolean asyncPrepare = runAsync
      && HiveConf.getBoolVar(queryState.getConf(),
        HiveConf.ConfVars.HIVE_SERVER2_ASYNC_EXEC_ASYNC_COMPILE);
    // 如果不是异步编译,则同步准备查询
    if (!asyncPrepare) {
      //创建Driver对象,编译SQL
      //Driver经过:SQL -> AST(抽象语法树) -> QueryBlock(查询块) -> Operator(e逻辑执行计划) -> TaskTree(物理执行计划) -> QueryPlan(查询计划)
      prepare(queryState);
    }
    // 如果不是异步运行,则同步运行查询
    if (!runAsync) {
      runQuery();
    } else {
      // 我们将在后台线程中传递ThreadLocals,从前台(处理程序)线程传递。
      // 1) ThreadLocal Hive对象需要在后台线程中设置
      // 2) Hive中的元数据存储客户端与正确的用户相关联。
      // 3) 当前UGI将在元数据存储处于嵌入式模式时被元数据存储使用
      Runnable work = new BackgroundWork(getCurrentUGI(), parentSession.getSessionHive(),
          SessionState.getPerfLogger(), SessionState.get(), asyncPrepare);

      try {
        // 如果没有可用的后台线程来运行此操作,此提交将阻塞
        Future<?> backgroundHandle = getParentSession().submitBackgroundOperation(work);
        // 设置后台操作句柄
        setBackgroundHandle(backgroundHandle);
      } catch (RejectedExecutionException rejected) {
        // 设置操作状态为ERROR
        setState(OperationState.ERROR);
        // 抛出HiveSQLException异常
        throw new HiveSQLException("The background threadpool cannot accept" +
            " new task for execution, please retry the operation", rejected);
      }
    }
  }

1.Driver对象创建并编译SQL,将SQL编译成Query Plan执行计划

其中核心的是prepare()方法,它的源码在2.x和3.x、4.x有一些区别,不过其核心功能是没变的,主要是创建Driver对象,并编译SQL,然后通过Driver将SQL最终转换成Query Plan。

prepare()方法3.x的源码如下:

  /**
   * 准备执行SQL查询的操作。
   * 此方法负责初始化Driver,设置查询超时,编译查询语句,并处理可能的异常。
   *
   * @param queryState 包含查询状态信息的对象。
   * @throws HiveSQLException 如果在准备过程中发生Hive SQL异常。
   */
  public void prepare(QueryState queryState) throws HiveSQLException {
    // 设置操作状态为运行中
    setState(OperationState.RUNNING);
    try {
      // 创建Driver实例,返回的Driver对象是 ReExecDriver
      driver = DriverFactory.newDriver(queryState, getParentSession().getUserName(), queryInfo);

      // 如果查询超时时间大于0,则启动一个定时任务来取消查询
      if (queryTimeout > 0) {
        // 创建一个单线程的定时任务执行器
        timeoutExecutor = new ScheduledThreadPoolExecutor(1);
        // 创建一个定时任务,在查询超时后取消查询
        Runnable timeoutTask = new Runnable() {
          @Override
          public void run() {
            try {
              // 获取查询ID
              String queryId = queryState.getQueryId();
              // 记录日志,查询超时并取消执行
              LOG.info("Query timed out after: " + queryTimeout
                  + " seconds. Cancelling the execution now: " + queryId);
              // 取消查询
              SQLOperation.this.cancel(OperationState.TIMEDOUT);
            } catch (HiveSQLException e) {
              // 记录日志,取消查询时发生错误
              LOG.error("Error cancelling the query after timeout: " + queryTimeout + " seconds", e);
            } finally {
              // 关闭定时任务执行器
              timeoutExecutor.shutdown();
            }
          }
        };
        // 安排定时任务在查询超时后执行
        timeoutExecutor.schedule(timeoutTask, queryTimeout, TimeUnit.SECONDS);
      }

      // 设置查询显示信息
      queryInfo.setQueryDisplay(driver.getQueryDisplay());

      // 设置操作句柄信息,以便Thrift API用户可以使用操作句柄查找Yarn ATS中的查询信息
      String guid64 = Base64.encodeBase64URLSafeString(getHandle().getHandleIdentifier()
          .toTHandleIdentifier().getGuid()).trim();
      driver.setOperationId(guid64);

      // 编译SQL查询并响应 ReExecDriver.compileAndRespond(...) -> Driver.compileAndRespond(...)
      response = driver.compileAndRespond(statement);
      // 如果响应代码不为0,则抛出异常
      if (0 != response.getResponseCode()) {
        throw toSQLException("Error while compiling statement", response);
      }

      // 设置是否有结果集
      setHasResultSet(driver.hasResultSet());
    } catch (HiveSQLException e) {
      // 设置操作状态为错误
      setState(OperationState.ERROR);
      // 抛出异常
      throw e;
    } catch (Throwable e) {
      // 设置操作状态为错误
      setState(OperationState.ERROR);
      // 抛出异常
      throw new HiveSQLException("Error running query: " + e.toString(), e);
    }
  }

2.x与3.x源码最核心的区别就是在创建Driver,其对应源码是:

driver = new Driver(queryState, getParentSession().getUserName());

而4.x与3.x源码最核心的区别如下:

  1. 利用 Java 8 的 Lambda 表达式特性,简化代码逻辑,提高代码的可读性和可维护性。
  2. 通过将 queryTimeout 的类型改为 long,支持了更大的超时值,避免了溢出问题。
  3. 在资源管理方面,对调度器的生命周期管理也进行了优化,不需要显式的关闭操作。

4.x对应源码是:

if (queryTimeout > 0L) {
  timeoutExecutor = Executors.newSingleThreadScheduledExecutor();
  timeoutExecutor.schedule(() -> {
    try {
      final String queryId = queryState.getQueryId();
      log.info("Query timed out after: {} seconds. Cancelling the execution now: {}", queryTimeout, queryId);
      SQLOperation.this.cancel(OperationState.TIMEDOUT);
    } catch (HiveSQLException e) {
      log.error("Error cancelling the query after timeout: {} seconds", queryTimeout, e);
    }
    return null;
  }, queryTimeout, TimeUnit.SECONDS);
}

DriverFactory.newDriver()方法中返回 ReExecDriver对象,该对象表示执行过程失败可重试的Driver对象,然后调用 Driver.compileAndRespond() 方法进行编译SQL。

2.对QueryPaln 进行处理,转换成MR 任务执行

BackgroundWork是一个线程,负责异步处理QueryPlan,通过submitBackgroundOperation(work)提交运行,执行到SQLOperator.BackgroundOperation.run()方法,最终调用到Driver.run() 方法。

Driver

下面我们再来Driver类,它在不同版本中也有一些差别,比如2.x版本是直接 implements CommandProcessor,而3.x和4.x版本则是implements IDriver,而IDriver 则是 extends CommandProcessor。本质是为了更好的解耦和扩展性,使得代码更加模块化,易于维护和扩展。同时,通过继承 CommandProcessor 接口,也保持了与旧版本的兼容性,确保了功能的连续性。不过其核心功能是没变的,主要包含编译、优化及执行。

执行步骤

为了方便理解,我们先梳理整个执行步骤如下:

  1. 通过Antlr解析SQL语法规则和语法解析,将SQL语法转换成AST(抽象语法树)

  2. 遍历AST(抽象语法树) 将其转化成Query Block(查询块,可以看成查询基本执行单元)

  3. 将Query Block(查询块) 转换成OperatorTree(逻辑执行计划),并进行优化。

  4. OperatorTree(逻辑执行计划)转换成TaskTree(物理执行计划,每个Task对应一个MR Job任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黄雪超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值