Hive代码分析报告(二):语义分析的前序步骤分析①

2021SC@SDUSC

目录

Hive编译过程回顾

语义分析框架概述

语义分析模块的前序步骤分析

前序分析①:HQL进入编译器的步骤


Hive编译过程回顾

在上一篇文章中,我们知道,hive的核心是HQL的解析过程,也就是编译的过程,HQL的编译需要经过一个复杂的流程,主要有三大部分内容:

  • 根据HQL语句生成抽象语法树AST:将HQL转换为AST
  • 进行语义分析:对AST进行类型检查、语义分析等工作
  • 执行计划的生成

语义分析框架概述

Hive编译流程中,语义分析框架的主要作用是将AST转换为QB,即编译流程中的第二大步骤。语义分析框架担任着编译流程的中后端驱动的角色。它主要通过SemanticAnalyzerFactory获得语义分析器,在AST上完成类型检查、其它语义分析、优化等工作,最终生成Tasks;

语义分析模块的前序步骤分析

Hive的编译流程是一个线性流水线处理的过程,整个流程完成了HQL到执行计划的转换,其中语义分析框架接收的输入是AST(抽象语法树),为了更清楚语义分析框架的工作原理,我们有必要先搞清楚HQL到AST是如何生成的?AST包含了哪些信息?这些语义分析的前序步骤,有助于我们理解语义分析框架是如何解析、处理这些信息的。

前序分析①:HQL进入编译器的步骤

在Hive的编译流程中,HQL经过命令行接口进入编译器,由HQL经过语法解析变换生成AST,这部分的详细内容由队友负责,我这里先做一个简要的查阅和学习分析,以便于我后续分析的开展。

一条hql语句是在用户接口(或者命令行接口)那里得到的,命令行接口负责接受用户输入的信息,然后准备执行并将执行的结果返回。而真正底层干事情的是驱动器(Driver),它将接收到的命令进行编译。

我们先看位于命令行接口模块的CliDriver.java文件里的processCmd()函数:

先看这个函数的整体结构(省略部分代码)

public int processCmd(String cmd) {


if (cmd_trimmed.toLowerCase().equals("quit") || cmd_trimmed.toLowerCase().equals("exit")) {

       ……

  } else if (tokens[0].equalsIgnoreCase("source")) {

       ……
   
    } else if (cmd_trimmed.startsWith("!")) {

             ……

        }  else { // local mode

              try {

        CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);

        ret = processLocalCmd(cmd, proc, ss);

      } catch (SQLException e) {

        console.printError("Failed processing command " + tokens[0] + " " + e.getLocalizedMessage(),

          org.apache.hadoop.util.StringUtils.stringifyException(e));

        ret = 1;

      }

    }

    ss.resetThreadName();

return ret

processCmd会首先判断,如果命令是`quit`或`exit`,则退出;如果命令是`source`开头,则校验`source`命令后面的文件是否存在,存在则执行`processFile`;如果命令是以感叹号!开头,表明是`shell`命令,这时候直接调用`shell`命令执行器执行。

简言之,就是判断命令是不是退出quit命令,或者source或者“!”,即特殊命令(非SQL)的处理,如果都不是,最后才是sql的处理逻辑,其他sql的主要执行方法为processLocalCmd。

再来看代码的一些细节:

CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);

这句生成了一个CommandProcessor ,那么CommandProcessor 是个什么呢?

其get()函数的代码如下:

public static CommandProcessor get(String[] cmd, HiveConf conf)

      throws SQLException {

    CommandProcessor result = getForHiveCommand(cmd, conf);

    if (result != null) {

      return result;

    }

    if (isBlank(cmd[0])) {

      return null;

    } else {

      if (conf == null) {

        return new Driver();

      }

      Driver drv = mapDrivers.get(conf);

      if (drv == null) {

        drv = new Driver();

        mapDrivers.put(conf, drv);

      } else {

        drv.resetQueryState();

      }

      drv.init();

      return drv;

    }

  }

CommandProcessor是CommandProcessorFactory 根据用户指令生成的tokens和配置文件,返回CommandProcessor的一个具体实现。

其中getForHiveCommand()函数根据tokens的第一个字串,也就是用户输入指令的第一个单词,在HiveCommand这个enum中定义的一些非SQL查询操作集合中进行匹配,确定相应的HiveCommand类型。在依据HiveCommand选择合适的CommandProcessor实现方式。

我们注意到看到此处代码中的这两句:

if (conf == null) {

        return new Driver();
}

返回的是一个Driver,根据函数定义和返回类型,可以判断出来,Driver是CommandProcessor 的下属类型,由此可知get()函数得到一个CommandProcessor对象。

我们再次回到上面的代码中,看这两句:

 try {

        CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);

        ret = processLocalCmd(cmd, proc, ss);

      }

由前面的分析,这里的proc是一个driver,将作为下一句函数调用的一个主要参数,所以我们继续看processLocalCmd函数调用:

int processLocalCmd(String cmd, CommandProcessor proc, CliSessionState ss) {
        int tryCount = 0;
        int ret = 0;

        boolean needRetry;
        do {
            try {
                needRetry = false;
                if (proc != null) {
                	// 如果CommandProcessor是Driver实例
                    if (proc instanceof Driver) {
                        Driver qp = (Driver)proc;
                        // 获取标准输出流,打印结果信息
                        PrintStream out = ss.out;
                        long start = System.currentTimeMillis();
                        // 输出命令原文
                        if (ss.getIsVerbose()) {
                            out.println(cmd);
                        }
						// 获取运行的命令
                        qp.setTryCount(tryCount);
                        // Driver实例运行用户指令,获取运行结果响应码
                        ret = qp.run(cmd).getResponseCode();
                        // 如果执行失败,直接返回ret
                        if (ret != 0) {
                            qp.close();
                            return ret;
                        }
						// 统计指令的运行时间
                        long end = System.currentTimeMillis();
                        double timeTaken = (double)(end - start) / 1000.0D;
                        ArrayList<String> res = new ArrayList();
                        //  打印查询结果的列名称
                        this.printHeader(qp, out);
                        int counter = 0;
						// 打印查询结果
                        try {
                            if (out instanceof FetchConverter) {
                                ((FetchConverter)out).fetchStarted();
                            }

                            while(qp.getResults(res)) {
                                Iterator var17 = res.iterator();

                                while(var17.hasNext()) {
                                    String r = (String)var17.next();
                                    out.println(r);
                                }

                                counter += res.size();
                                res.clear();
                                if (out.checkError()) {
                                    break;
                                }
                            }
                        } catch (IOException var19) {
                            this.console.printError("Failed with exception " + var19.getClass().getName() + ":" + var19.getMessage(), "\n" + StringUtils.stringifyException(var19));
                            ret = 1;
                        }
						
                        int cret = qp.close();
                        if (ret == 0) {
                            ret = cret;
                        }

                        if (out instanceof FetchConverter) {
                            ((FetchConverter)out).fetchFinished();
                        }

                        this.console.printInfo("Time taken: " + timeTaken + " seconds" + (counter == 0 ? "" : ", Fetched: " + counter + " row(s)"));
                    } else {
                    	//  如果proc不是Driver,也就是用户执行的是非SQL查询操作,直接执行语句,不执行FetchResult的操作
                        String firstToken = this.tokenizeCmd(cmd.trim())[0];
                        String cmd_1 = this.getFirstCmd(cmd.trim(), firstToken.length());
                        if (ss.getIsVerbose()) {
                            ss.out.println(firstToken + " " + cmd_1);
                        }

                        CommandProcessorResponse res = proc.run(cmd_1);
                        if (res.getResponseCode() != 0) {
                            ss.out.println("Query returned non-zero code: " + res.getResponseCode() + ", cause: " + res.getErrorMessage());
                        }

                        if (res.getConsoleMessages() != null) {
                            Iterator var10 = res.getConsoleMessages().iterator();

                            while(var10.hasNext()) {
                                String consoleMsg = (String)var10.next();
                                this.console.printInfo(consoleMsg);
                            }
                        }

                        ret = res.getResponseCode();
                    }
                }
            } catch (CommandNeedRetryException var20) {
            	// 如果执行过程中出现异常,修改needRetry标志,下次循环是retry。
                this.console.printInfo("Retry query with a different approach...");
                ++tryCount;
                needRetry = true;
            }
        } while(needRetry);

        return ret;
    }

解析见程序注释。

另外,这里的ret = qp.run(cmd).getResponseCode();一句中,调用的是Driver的run,此时,程序运行到run()方法:
 

@Override

  public CommandProcessorResponse run(String command)

      throws CommandNeedRetryException {

    return run(command, false);

  }


  public CommandProcessorResponse run(String command, boolean alreadyCompiled)

        throws CommandNeedRetryException {

    CommandProcessorResponse cpr = runInternal(command, alreadyCompiled);


    if(cpr.getResponseCode() == 0) {

      return cpr;

    }

    SessionState ss = SessionState.get();

    if(ss == null) {

      return cpr;

    }

    MetaDataFormatter mdf = MetaDataFormatUtils.getFormatter(ss.getConf());

    if(!(mdf instanceof JsonMetaDataFormatter)) {

      return cpr;

    }

我们重点关注CommandProcessorResponse cpr = runInternal(command, alreadyCompiled);这句,根据字面意思,它的功能实执行sql的编译和返回结果,继续调用,就是runInternal函数

private CommandProcessorResponse runInternal(String command, boolean alreadyCompiled) throws CommandNeedRetryException {
			......
            perfLogger = null;
            PerfLogger perfLogger;
            int ret;
            if (!alreadyCompiled) {
            	// 调用compileInternal方法把SQL编译为QueryPlan
                ret = this.compileInternal(command, true);
                perfLogger = SessionState.getPerfLogger();
                if (ret != 0) {
                    CommandProcessorResponse var8 = this.createProcessorResponse(ret);
                    return var8;
                }
            } else {
                perfLogger = SessionState.getPerfLogger();
                this.plan.setQueryStartTime(perfLogger.getStartTime("Driver.run"));
            }

          	......
				// 调用execute执行QueryPlan中的所有task
                ret = this.execute(true);
                if (ret != 0) {
                    var10 = this.rollback(this.createProcessorResponse(ret));
                    return var10;
                }
			......
    }

这里ret = this.compileInternal(command, true);说明继续调用compileInternal(command, true)

private int compileInternal(String command, boolean deferClose) {

……


    try {

      ret = compile(command, true, deferClose);

    } finally {

      compileLock.unlock();

}

到了这里,compile(command, true, deferClose); 就是hive的编译流程的入口了,接下来,compile()函数会进行将SQL转换为ASTNode,将ASTnode封装等一系列操作,正式进入编译流程。由此可见,经过上述的层层调用,Driver的run方法最终会执行compile()操作,由Compiler作后续的语法解析和语义分析,这就是HQL语句从CLI进入编译器的简单程序逻辑。

通过阅读项目代码,我深刻的体会到优秀的项目代码编的规范,函数的命名非常的精炼又简明,让人能很快的找到并理解各个函数的作用和调用逻辑,非常值得学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值