Spark SQL CLI 实现分析

背景

本文主要介绍了Spark SQL里眼下的CLI实现,代码之后肯定会有不少变动,所以我关注的是比較核心的逻辑。主要是对照了Hive CLI的实现方式,比較Spark SQL在哪块地方做了改动,哪些地方与Hive CLI是保持一致的。可以先看下总结一节里的内容。


Spark SQL的hive-thriftserver项目里是其CLI实现代码。以下先说明Hive CLI的主要实现类和关系,再说明Spark SQL CLI的做法。


Hive CLI

核心启动类是org.apache.hive.service.server.HiveServer2,启动方式:

    try {
      ServerOptionsProcessor oproc = new ServerOptionsProcessor("hiveserver2");
      if (!oproc.process(args)) {
        LOG.fatal("Error starting HiveServer2 with given arguments");
        System.exit(-1);
      }
      HiveConf hiveConf = new HiveConf();
      HiveServer2 server = new HiveServer2();
      server.init(hiveConf);
      server.start();
    } catch (Throwable t) {
      LOG.fatal("Error starting HiveServer2", t);
      System.exit(-1);
    }

HiveServer2继承CompositeService类,CompositeService类内部维护一个serviceList。可以增加、删除、启动、停止不同的服务。

HiveServer2在init(hiveConf)的时候,会增加CLIService和ThriftCLIService两个Service。依据传输模式,假设是http或https的话。就使用ThriftHttpCLIService,否则使用ThriftBinaryCLIService。不管是哪个ThriftCLIService。都传入了CLIService的引用,thrift仅仅是一个封装。

增加了这些服务后,把服务都启动起来。


CLIService也继承自CompositeService,CLIService 在init的时候会增加SessionManager服务,而且依据hiveConf,从 hadoop shims里得到UGI里的serverUsername。

SessionManager管理hive连接的开启、关闭等管理功能,已有的连接会维护在一个HashMap里,value为HiveSession类,里面大致是username、password、hive配置等info。

所以CLIService里差点儿全部的事情都是托付给SessionManager做的。

 

SessionManager内主要是OperationManager这个服务,是最重要的和运行逻辑有关的类,以下会详细说。

 

另外,关于ThriftCLIService,有两个实现子类,子类仅仅复写了run()方法。设置thrift server相关的网络连接,其它对CLIService的调用逻辑都在父类ThriftCLIService本身里面。


实际上。ThriftCLIService里非常多事情也是托付给CLIService做的。

 

那么上面大致是Hive CLI、Thrift server启动的流程,以及几个主要类的相互关系。


Spark SQL CLI

依据上面Hive CLI的逻辑,看看Spark SQL的CLI是怎么做的。

Spark里的HiveThriftServer2(这个类名看起来有点奇怪)继承了Hive的HiveServer2。而且复写了init方法,其初始化的时候增加的是SparkSQLCLIService和ThriftBinaryCLIService两个服务。

前者继承了Hive的CLIService,有一些不同的逻辑。后者直接使用的是Hive的类。但传入的是SparkSQLCLIService的引用。


SparkSQLCLIService内部,相似Hive的CLIService。有一个SparkSQLSessionManager,继承自Hive的SessionManager。

也有得到serverUsername的逻辑,代码和CLIService是一样的。

 

SparkSQLSessionManager复写了init这种方法,里面有Spark自己的SparkSQLOperationManager服务,继承自Hive的OperationManager类。

 

可能上面这几个类有点看晕了,本质上都是一些封装而已,没什么大的差别。

真正重要的是SparkSQLOperationManager这个类里面,定义了怎样使用Spark SQL来处理query操作。


SparkSQLOperationManager关键逻辑

Hive的CLI Operation父类有例如以下的子类继承体系,代表hive cli会处理的不同操作类型:

上半部分ExecuteStatementOperation子类体系是实际和查询相关的操作,下半部分是一些元数据读取操作。SparkSQLOperationManager实际改写的就是ExecuteStatementOperation子类的运行逻辑,而元数据相关的操作还是沿用hive本来的处理逻辑。

 

原本hive的ExecuteStatementOperation处理逻辑是这种:

  public static ExecuteStatementOperation newExecuteStatementOperation(
      HiveSession parentSession, String statement, Map<String, String> confOverlay, boolean runAsync) {
    String[] tokens = statement.trim().split("\\s+");
    String command = tokens[0].toLowerCase();

    if ("set".equals(command)) {
      return new SetOperation(parentSession, statement, confOverlay);
    } else if ("dfs".equals(command)) {
      return new DfsOperation(parentSession, statement, confOverlay);
    } else if ("add".equals(command)) {
      return new AddResourceOperation(parentSession, statement, confOverlay);
    } else if ("delete".equals(command)) {
      return new DeleteResourceOperation(parentSession, statement, confOverlay);
    } else {
      return new SQLOperation(parentSession, statement, confOverlay, runAsync);
    }
  }

ExecuteStatementOperation也分两部分。HiveCommandOperation和SQLOperation。

不同的ExecuteStatementOperation子类终于由相应的CommandProcessor子类来完毕操作请求。


那Spark是怎样改写ExecuteStatementOperation的运行逻辑的呢?

最核心的逻辑例如以下:

      def run(): Unit = {
        logInfo(s"Running query '$statement'")
        setState(OperationState.RUNNING)
        try {
          result = hiveContext.sql(statement)
          logDebug(result.queryExecution.toString())
          val groupId = round(random * 1000000).toString
          hiveContext.sparkContext.setJobGroup(groupId, statement)
          iter = result.queryExecution.toRdd.toLocalIterator
          dataTypes = result.queryExecution.analyzed.output.map(_.dataType).toArray
          setHasResultSet(true)
        } catch {
          // Actually do need to catch Throwable as some failures don't inherit from Exception and
          // HiveServer will silently swallow them.
          case e: Throwable =>
            logError("Error executing query:",e)
            throw new HiveSQLException(e.toString)
        }
        setState(OperationState.FINISHED)
      }

statement是一个String。即query本身。调用HiveContext的sql()方法,返回的是一个SchemaRDD。HiveContext的这段逻辑例如以下:

  override def sql(sqlText: String): SchemaRDD = {
    // TODO: Create a framework for registering parsers instead of just hardcoding if statements.
    if (dialect == "sql") {
      super.sql(sqlText)
    } else if (dialect == "hiveql") {
      new SchemaRDD(this, HiveQl.parseSql(sqlText))
    }  else {
      sys.error(s"Unsupported SQL dialect: $dialect.  Try 'sql' or 'hiveql'")
    }
  }

调完sql()后返回的是一个带被解析过了的基础逻辑计划的SchemaRDD。兴许。

logDebug(result.queryExecution.toString())

这一步触发了逻辑运行计划的进一步分析、优化和变成物理运行计划的几个过程。之后,

result.queryExecution.toRdd

toRdd这步是触发计算并返回结果。这几个逻辑在之前Spark SQL源代码分析的文章里都提到过。

除了上面这部分,另一些schema转化、数据类型转化的逻辑,是由于Catalyst这边。有自己的数据行表示方法,也有自己的dataType。而且schema这块呢。在生成SchemaRDD的时候也转化过一次。所以在返回运行结果的时候,须要有转换回Hive的TableSchema、FieldSchema的逻辑。

 

以上说明了Spark SQL是怎样把query的运行转换到Spark SQL里的。


总结

基本上Spark SQL在CLI这块的实现非常靠近Hive Service项目里的CLI模块,主要类继承体系、运行逻辑差点儿相同都一样。Spark SQL改动的关键逻辑在CLIService内的SessionManager内的OperationManager里,将非元数据查询操作的query丢给了Spark SQL的Hiveproject里的HiveContext.sql()来完毕。通过返回的SchemaRDD,来进一步得到结果数据、得到中间运行计划的Schema信息。



全文完 :)




posted on 2017-05-27 10:36  yjbjingcha  阅读(186)  评论(0编辑  收藏  举报

导航