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;
}
}
我们重点注意到看到此处代码中的这两句:
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进入编译器的简单程序逻辑。
通过阅读项目代码,我深刻的体会到优秀的项目代码编的规范,函数的命名非常的精炼又简明,让人能很快的找到并理解各个函数的作用和调用逻辑,非常值得学习。
|