2020.11.28(spark-sql-源码、sql解析、dataset到rdd)

接上节课:2020.11.23(spark-sql、复杂SQL、函数、自定义函数)
带点理论为下节课讲源码铺路。
在这里插入图片描述

如果想聊sqarkSQL这里面最重要的是SQL,这里面有很多的细活,首先,第一个维度,如果SQL是个字符串,(SQL在之前的认知里就是个字符串)。这个字符串是如何变成逻辑、指令、计算,作用在数据上的,其实它经历了如下的步骤:
既然是个字符串,第一步就是要对字符串的词法分析,这个字符串按关键字分词,其实把SQL写成Java语言等等,只要是语言语法的,其实SQL也是门语言,language,上来第一个就是词法分析,先切词
切完词之后组建token,一个域一个域,select后面跟了个啥,from后跟了个啥,where跟了个啥,先把这个切开,把token找到,把作用域找到,然后语法分析。词法语法分析是Parser环节(为了得到token)该完成的事情。
全都通过了,token也有了,因为有语法,比如父查询,子查询,子查询套子查询等等一系列的出来之后,就可以生成一个东西叫做AST 抽象语法树,树上就会有node节点的概念,现在只聊普通话,不过度深入的东西,然后有了它之后,只是面向曾经source原字符串得到的东西,但这个东西最终作用计算之前,要和一个东西做关联,如果不做关联的话,你的计算,虽然可以转成计算但是算的不知道忙什么,而且根本连优化都无从谈起,所以他要和元数据做一次关联。analyzed这个环节会和元数据做一次关联,等等一些分析的事情,分析树的过程,其实在AST之后已经可以产生逻辑计划了,LogicalPlan,下一步要做的是优化,对这棵树做优化的事情,比如里面有一个where的条件判断还是过滤,查询,分组,会有优化,分为两类:一类基于成本优化,一类基于规则优化。
成本优化的代表:hive
规则优化的代表:spark
Oracle的优化器规则有2000多条,但不是写的SQL这些都触发,可能会触发其中都某些条优化规则,所以dba要熟悉你SQL这么去写改一下就有可能会触发优化器,有一些优化器也不需要人为的去知道,它默认就会去做。
规则优化方面最好是要绑定一些成本和推算,更好一些,纯规则计划太武断了,规则可能不是最优的,而且一条SQL语句的逻辑可能匹配上多条规则都可以优化,选其中哪一种,速度还有差异,spark还没有完全做到它在自己的规则里挑出一个成本最低的,因为它成本这块没有做的特别好,但优化器你要知道干什么事情。优化器是基于什么规则的,当有了优化规则可以选了之后,就把逻辑计划趋向于向物理计划的转换。
下一步就由逻辑计划(只要有规则调优完了逻辑计划)转成物理计划,因为最终逻辑选了哪个规则会有一个物理的函数对应上,这是以函数的维度来描述,1对1,N对N的匹配,这里选了一个最终会匹配上一个,在spark这里转成物理计划之后,往后还有一步优化,为啥,因为物理执行计划,它是RDD,RDD提交之后,基于RDD,源码分析过了,这块最终会触发spark core的DAGScheler,在DAGScheler和TaskScheler这一过程当中,也会有相应的优化,因为它会做一些尽量减少shuffle的产生,这是它本职应该去做的事情,所以RDD还有一个优化,所以整个会看到,如果只有面向RDD编程的话它只有一次优化,如果未来人们在开发的时候尽量的使用DataSet写SQL的方式的话,其实它里面会有一次优化给到你,转成RDD又一次优化,其实这整个代码运行下来的速度会更快,比纯写RDD,要不你写RDD已经登峰造极了,能知道代码每一行分布式每个细节都知道的时候,玩命的照着这个去优化,但有一些可能还是优化不来,但是使用DataSet的时候,会有很大的优化的效果出现。
最终集群提交执行了,这是整个流程的过程,先把这个事情先接受了。在这儿注意看一个特点,思考一个问题,在我们见过的东西里面,SQL的确是个字符串,有一种情况是我可以把一切东西,对象的执行变成SQL字符串的流程,也有可能是跳过SQL的环节,比如我上来就直接用对象就变成了逻辑执行计划,就是起使的时候,它并不是来自于字符串,有可能来自于object对象,其实在spark当中会用DataSet对象的方式来完成这一操作,它是直接得到了逻辑执行计划,而没有再经过词法语法的分析,因为你给它的不是一个字符串,是一个对象,而且这个对象它不需要转成SQL,它直接可以转成逻辑执行计划,但是有一些东西,比如说,可能也是object,只不过这个object要先根据某种配置先转成SQL,由SQL再走后面的流程。是不是有这样的东西,一些ORM的框架,是不是就它给了你object,但是会发现object最终是要转成SQL,然后SQL再去压给一个引擎数据库,数据库再去做语法词法分析,再做后面的执行,但是spark已经是一个引擎了,所以它可以跳过SQL给出基于api的方式,它的api完全可以转成逻辑执行计划。
但是站在人的角度,人更倾向于使用写SQL的方式,谁会去编程呢?这么麻烦的一件事情,最终最终虽然分析出得到字符串解析它也能走,但这个东西词法语法解析它也稍稍的有成本,然后也可以选择直接用api,大家一听就觉得使用api的方式会稍微快一点,但是站在易用度的角度来说的话,谁还会写api呢,人最终是选择写SQL的方式更多一些。
所以其实基于它这种方式可以用的手段就特别多:
spark提供了spark-shell,spark-sql,thriftserver+beeline,jdbc的方式,让人最终只要写SQL就可以了,把这个先支起来,理解了之后得到最简单的道理,就是最大的问号,什么问号?
在你已写的api的开发环境当中,首先第一个大问号是:
DataSet是如何变成的RDD?
如果不明白DataSet是如何变成的RDD就不可能明白这张图,spark-sql为什么给它一个字符串它就能跑起来,所以这个是本质。下节课源码上来之后,我们先以最快的方式找到它的框架当中,你写的api是怎么变成的RDD?
当你把这个过程捋清楚了,剩下的就是些细节了 ,词法语法分析常用的技术是antlr,专门做词法语法分析的,不是spark自己做而是集成了第三方的这个做Paser操作的。

这节课内容:
要去分析整个 过程的时候有两个入口点,在spark-core的时候入口点是sparkContext,在spark-sql的时候入口点变成了sparkSession,为什么不是继续使用sparkContext呢?因为sparkContext只能给你创建rdd,但是并没有能力创建SQL解析,所以在兼容之前的sparkContext,它用sparkSession包装了sparkContext,在原有基础之上又包装了一些进去。
另外一个维度:在sparkCore的时候,变成模型是rdd,基于rdd之上的一些转换和操作,我们在写SQL的时候,知道了有一个东西叫dataSet,或者dataFrame,是以这个编程模型,而不是使用的rdd的编程模型,也简单说了,DataSet和DataFrame是对SQL方面无论是api级别的,还是字符串解析级的一个支持,扩充rdd的能力,而且内部还有些优化,两个入口点:
所以两个入口点:
1.先去分析sparkSession,因为都是通过sparkSession得到的dataSet和dataFrame,有前后顺序的因果关系
2.再去分析DataSet和DataFrame

在书写的时候,无论哪一版代码都是统一的格式,都是通过sparkSession.builder()构建对象,再继续config()然后getOrCreate()得到了一个所谓的session,核心方法就是这个getOrCreate()
在getOrCreate()方法,先跳过一些复杂的东西,最终它会给我一个session返回

//它最终会new一个SparkSession,只不过会包含了什么?这就是之前说的基于SparkContext它包装了一下
//SparkContext还是要有的,spark如果没有SparkContext的话得不到rdd,就没有灵魂了,未来什么都不能执行了,前面课也白讲了
session = new SparkSession(sparkContext, None, None, extensions)

SparkSession:
SparkContext , SessionState
所以上来的时候肯定是要得到一个SparkSesion,它是非常重要的,构造sparkSession之后,代码才能线性往下走,有了session之后,才能通过session得到一些dataSet,但是现在要聊的是,在它构建的时候还有什么事情发生,基础的对象,逻辑模式展出来,准备着。
new SparkSession()点进去,在Scala里面,构造方法就是默认的裸露的那些代码,

@InterfaceStability.Unstable
@transient
lazy val sessionState: SessionState = {
  parentSessionState
    .map(_.clone(this))
    .getOrElse {
      val state = SparkSession.instantiateSessionState(
        SparkSession.sessionStateClassName(sparkContext.conf),
        self)
      initialSessionOptions.foreach { case (k, v) => state.conf.setConfString(k, v) }
      state
    }
}

定义了一个变量,但这个变量来自于下面代码的执行,但是注意了,在new构造的时候好像是成员属性要赋值,代码要执行,但是这有一个关键字lazy,Java当中也有,真正的懒加载。什么是懒加载,在new的时候只有一个成员属性的占位,会有这么一个成员属性,但是成员属性后面的东西并不会执行,什么时候拿着session.sessionState使用的时候,后面这堆东西才真的去执行,仅此而已。但在这个环节要看一下,sessionState是sparkSession里面非常重要的一个属性。后面很多的环节都会用到他。
对sessionState先做一个介绍:
首先:发现没有创建过的时候,会走.getOrElse方法,会通过自己这个类,实例化出一个东西来,在实例化的时候有两个参数:SparkSession.sessionStateClassName(sparkContext.conf)通过自己类的方法,这个方法ctrl点不过去要复制它,然后搜索回车,它有两个选择:

private def sessionStateClassName(conf: SparkConf): String = {
  conf.get(CATALOG_IMPLEMENTATION) match {
    case "hive" => HIVE_SESSION_STATE_CLASS_NAME
    case "in-memory" => classOf[SessionState].getCanonicalName
  }
}

如果不开启hive支持,就是"in-memory"的,运行之后可以创建一些视图,有了"hive"之后,可以拿一些外部 的东西过来,这是sessionStateClassName(conf: SparkConf)方法,它的实例方法SparkSession.instantiateSessionState也是ctrl点不过去,所以也要选择,拷贝,搜索:

/**
 * Helper method to create an instance of `SessionState` based on `className` from conf.
 * The result is either `SessionState` or a Hive based `SessionState`.
 */
private def instantiateSessionState(
    className: String,
    sparkSession: SparkSession): SessionState = {
  try {
    // invoke `new [Hive]SessionStateBuilder(SparkSession, Option[SessionState])`
    val clazz = Utils.classForName(className)
    val ctor = clazz.getConstructors.head
    ctor.newInstance(sparkSession, None).asInstanceOf[BaseSessionStateBuilder].build()
  } catch {
    case NonFatal(e) =>
      throw new IllegalArgumentException(s"Error while instantiating '$className':", e)
  }
}

注意:实例完成一个对象,还要asInstanceOf转换一下类型,BaseSessionStateBuilder基于这个类型,调用了build方法:

def build(): SessionState = {
  new SessionState(
    session.sharedState,
    conf,
    experimentalMethods,
    functionRegistry,
    udfRegistration,
    () => catalog,
    sqlParser,
    () => analyzer,
    () => optimizer,
    planner,
    streamingQueryManager,
    listenerManager,
    () => resourceLoader,
    createQueryExecution,
    createClone)
}

它的传参列表很大,有很多的参数要传,这个东西才是最重要的,且new出来的session的引用SessionState,会被sparksession持有,所以现在得出一个非常重要的东西:
因为是Scala语言,传递进去的参数就是它的成员属性,这些成员属性分别是什么:

private[sql] class SessionState(
    sharedState: SharedState,
    val conf: SQLConf,
    val experimentalMethods: ExperimentalMethods,
    val functionRegistry: FunctionRegistry,   //函数注册
    val udfRegistration: UDFRegistration,     //UDF注册
    catalogBuilder: () => SessionCatalog,
    val sqlParser: ParserInterface,           //它是做什么用的,只是解析字符串,字符串按照语法解析之后生成的一颗树,其实并没有实际意义,生成的树最终每一个叶子,每一个节点都要翻转成和我们的方法映射的,带执行操作的对象,这个翻转的过程才有用,而且在翻转的过程中要用到元数据的参考,所以它只是解析字符串的
    analyzerBuilder: () => Analyzer,           //这个才是和元数据相关联的,才开始写SQL里面的user1,是内部表还是外部表,在哪里,基本数所有的基于SQL的都会有这个过程,只不过里面具体的实现不一样,在开始spark的时候,用的是Scala的语法解析,在2.0x的时候就换成了antlr v4这个版本,hive现在用的javacc,这些都不重要
    optimizerBuilder: () => Optimizer,          //优化
    val planner: SparkPlanner,					//逻辑到物理的转换
    val streamingQueryManager: StreamingQueryManager,
    val listenerManager: ExecutionListenerManager,
    resourceLoaderBuilder: () => SessionResourceLoader,
    createQueryExecution: LogicalPlan => QueryExecution,    //LogicalPlan => QueryExecution 签名,传进来什么样子,返回的值什么样子
    createClone: (SparkSession, SessionState) => SessionState) {

SessionState:
sqlParser , Analyzer , Optimizer , planner , createQueryExecution

很多同学会认为SQL是个字符串到最后的执行,在语法解析生成语法树的环节很厉害,其实这个环节是最不值钱的,它其实还是从一个字符串到一堆带数据结构的字符串,仅此而已,后面关联元数据,映射成方法,再做优化,后面还有一堆事情,无论MySQL还是Oracle都有优化的环节,都有逻辑计划到物理计划转换的过程,这里面还牵扯到RBO和CBO,RBO是基于规则的,CBO是基于成本的。这两种优化,一定要说服自己,SQL的语法环节不是很重要的,要听的是更多的东西。如果能把DataSet到Rdd转化完,如果把Rdd换成一个函数库,也是可以理解的,变成本地的MySQL,有一堆的方法是执行不同的option操作,但是这些操作函数是怎么被调起的,其实是根据开始写的SQL和优化转化过程是有关系的。这些是可以类比的。
现在要做元数据转换,转换完之后要变成树,然后要进行优化的过程Optimizer,有两个考虑,一个是基于数据量读的越少,没必要读的东西越不读,分为行级的减少,和列级的减少,比如把Pushdowns(谓词下推),filter过滤,推到最原始读文件的时候,而不是最后的时候才做一下过滤,比如说列的裁剪。可能把select * 作为子查询了,但未来只需要一个name,在真正读的时候,就只读name那个列,但这个能不能生效,取决于文件的格式。如果是文本文件,那是一行一行读的,并不是列是存储,但假如是parquet列式存储。从文件进入IO的时候,那一行就不会真正的读取,这样才能达到最大的优化。
可以把一些字母的可折叠的,可预计算的先计算好,而不发生计算,在数据量和CPU上和IO上要做一些优化。
优化完了之后,是有逻辑到物理的转换,SparkPlanner,肯定需要一个转换的过程。
createQueryExecution非常重要的东西,这是sessionState里面的成员属性,这个属性的名字叫做createQueryExecution,但是它不是一个对象,它是一个函数:

createQueryExecution: LogicalPlan => QueryExecution
//它的方法体长什么样子,未来方法体如果传进来这个东西要做一件什么事情
protected def createQueryExecution: LogicalPlan => QueryExecution = { plan =>   //接收的参数是plan 一个逻辑的计划,拿逻辑计划做什么事情?
  new QueryExecution(session, plan)     //最终最终它是非常重要的,最终这个方法是要给我们创建出一个QueryExecution
}

new QueryExecution(session, plan) 把plan传递进去,看一下这个对象的成员属性:

class QueryExecution(val sparkSession: SparkSession, val logical: LogicalPlan) {

  // TODO: Move the planner an optimizer into here from SessionState.
  protected def planner = sparkSession.sessionState.planner

  def assertAnalyzed(): Unit = analyzed

  def assertSupported(): Unit = {
    if (sparkSession.sessionState.conf.isUnsupportedOperationCheckEnabled) {
      UnsupportedOperationChecker.checkForBatch(analyzed)
    }
  }

  lazy val analyzed: LogicalPlan = {
    SparkSession.setActiveSession(sparkSession)
    sparkSession.sessionState.analyzer.executeAndCheck(logical)  //逻辑的计划最终是要绑定元数据的过程,绑定元数据之后就分析完了,它analyzed再作为参数继续传  是动的sessionState里面的analyzer逻辑计划绑定
  }

  lazy val withCachedData: LogicalPlan = {
    assertAnalyzed()
    assertSupported()
    sparkSession.sharedState.cacheManager.useCachedData(analyzed)   //看有没有做过缓存,做过缓存的化就修改缓存的位置,用它的数据,有可能拿到缓存,也可能没有,没拿到缓存就还是原始的位置,又作为参数传递
  }

  lazy val optimizedPlan: LogicalPlan = sparkSession.sessionState.optimizer.execute(withCachedData)   //优化器优化,得到优化后的计划

  lazy val sparkPlan: SparkPlan = {
    SparkSession.setActiveSession(sparkSession)
    // TODO: We use next(), i.e. take the first plan returned by the planner, here for now,
    //       but we will implement to choose the best plan.
    planner.plan(ReturnAnswer(optimizedPlan)).next()     //接收逻辑到物理的转换的过程
  }

  // executedPlan should not be used to initialize any SparkPlan. It should be
  // only used for execution.
  lazy val executedPlan: SparkPlan = prepareForExecution(sparkPlan)   //sparkPlan又变成了可执行的物理计划

  /** Internal version of the RDD. Avoids copies and has no schema */
  lazy val toRdd: RDD[InternalRow] = executedPlan.execute()     //会得到一个Rdd,拿到Rdd之后就回归到spark-core的核心,  最终是要把spark的dataSet转换成Rdd这里能看到对应关系,但注意他们都是lazy的,DataSet只是new对象的环节根本不会产生这个过程。 关键是execute什么时候执行的,这个时候才会有rdd。得到rdd之后,有没有一个action算子作用在rdd身上,最终会调起DAGScheduler,最终推动任务。

定义了一些属性都是lazy的,逻辑的计划。它这些对象是lazy的,且这些作为参数传递,和前面说的执行流程是相似的。一个逻辑计划,进来之后得到元数据,得到元数据之后再去优化,优化完之后得到rdd,最终再去执行,和上面的图一模一样。
一定要记住了,new QueryExecution(val sparkSession: SparkSession, val logical: LogicalPlan)的时候是要传参数的,一定要传进来一个逻辑执行计划,这个逻辑计划要称为它的成员属性,因为它也是Scala语言,这里依次的拿出来,把它的属性先准备好。

createQueryExecution:
analyzed:sparkSession.sessionState.analyzer.executeAndCheck(logical)
optimizedPlan:sparkSession.sessionState.optimizer.execute(withCachedData)
sparkPlan:planner.plan(ReturnAnswer(optimizedPlan)).next()
executedPlan: SparkPlan = prepareForExecution(sparkPlan)
toRdd: RDD[InternalRow] = executedPlan.execute() 通过它.execute()方法是可以得到Rdd的

在这里插入图片描述

在new sparkSession构造的时候会得到这些东西,当这些有了之后,如果想得到一个DataSet怎么办,有很多种方式,非常多,最简单的,基于文本文件读取得到一个DataSet再往下走流程,DataSet再去转换,DataSet也像Rdd一样有一些转换的算子可以操作,怎么去用?
在sparkSession身上.read().textFile(path),调用了它自己的方法read(),得到一个对象,再.text()传了一个路径,它才能给我们一个DataSet出来,关键这个DataSet是如何产生的。
在sparkSession.read()会返回一个啥?

def read: DataFrameReader = new DataFrameReader(self)

是在这个对象中有很多的方法

def textFile(paths: String*): Dataset[String] = {
  assertNoSpecifiedSchema("textFile")
  text(paths : _*).select("value").as[String](sparkSession.implicits.newStringEncoder)
}
def text(paths: String*): DataFrame = format("text").load(paths : _*)
def format(source: String): DataFrameReader = {
  this.source = source
  this
}

会传入一个source的源的类型是"text",因为spark-sql会读取很多种数据源,包含文本文件类型,parquet类型,scv类型,其他类型,format里面有个成员属性叫source,标识出要读的东西是什么类型,这个source是作用在未来真读的时候,会选择某种的输入格式化类,有一个推断的过程。为什么会有一个DataFrameReader,因为要标识出未来我读哪种类型。
最终它有一个load()

@scala.annotation.varargs
def load(paths: String*): DataFrame = {
  if (source.toLowerCase(Locale.ROOT) == DDLUtils.HIVE_PROVIDER) {
    throw new AnalysisException("Hive data source can only be used with tables, you can not " +
      "read files of Hive data source directly.")
  }
  val cls = DataSource.lookupDataSource(source, sparkSession.sessionState.conf)
  if (classOf[DataSourceV2].isAssignableFrom(cls)) {
    val ds = cls.newInstance()
    val sessionOptions = DataSourceV2Utils.extractSessionConfigs(
      ds = ds.asInstanceOf[DataSourceV2],
      conf = sparkSession.sessionState.conf)
    val options = new DataSourceOptions((sessionOptions ++ extraOptions).asJava)
    // Streaming also uses the data source V2 API. So it may be that the data source implements
    // v2, but has no v2 implementation for batch reads. In that case, we fall back to loading
    // the dataframe as a v1 source.
    val reader = (ds, userSpecifiedSchema) match {
      case (ds: ReadSupportWithSchema, Some(schema)) =>
        ds.createReader(schema, options)
      case (ds: ReadSupport, None) =>
        ds.createReader(options)
      case (ds: ReadSupportWithSchema, None) =>
        throw new AnalysisException(s"A schema needs to be specified when using $ds.")
      case (ds: ReadSupport, Some(schema)) =>
        val reader = ds.createReader(options)
        if (reader.readSchema() != schema) {
          throw new AnalysisException(s"$ds does not allow user-specified schemas.")
        }
        reader
      case _ => null // fall back to v1
    }
    if (reader == null) {
      loadV1Source(paths: _*)
    } else {
      Dataset.ofRows(sparkSession, DataSourceV2Relation(reader))
    }
  } else {
    loadV1Source(paths: _*)
  }
}

里面有两个分支,loadV1Source和DataSourceV2,现在只分析V1的版本的,V2会和流挂钩,但是流还没有讲,最终要把路径传递进来调用方法,loadV1Source()
如果调用了loadV1Source(),返回值类型是一个DataFrame

private def loadV1Source(paths: String*) = {
  // Code path for data source v1.
  sparkSession.baseRelationToDataFrame(
    DataSource.apply(
      sparkSession,
      paths = paths,
      userSpecifiedSchema = userSpecifiedSchema,
      className = source,
      options = extraOptions.toMap).resolveRelation())
}

看似是整体一行,有几段逻辑:
最外面一层是sparkSession.baseRelationToDataFrame()这么一个大的方法,这个方法会返回一个DataFrame

def baseRelationToDataFrame(baseRelation: BaseRelation): DataFrame = {
  Dataset.ofRows(self, LogicalRelation(baseRelation))
}

到这里看到DataSet的影子了,忙这么多就是要知道DataSet是怎么创建出来的。但这里需要一个参数,baseRelation,baseRelation是什么?退回来是由底下的代码来实现的

DataSource.apply(
  sparkSession,
  paths = paths,
  userSpecifiedSchema = userSpecifiedSchema,
  className = source,
  options = extraOptions.toMap).resolveRelation()

resolveRelation()这个方法执行的时候,会根据数据源的类型(DataSource)做一个匹配,匹配的时候会有很多的数据源可以匹配出来,但在这里面只找FileFormat:

// This is a non-streaming file based datasource.
case (format: FileFormat, _) =>
  val allPaths = caseInsensitiveOptions.get("path") ++ paths
  val hadoopConf = sparkSession.sessionState.newHadoopConf()
  val globbedPaths = allPaths.flatMap(
    DataSource.checkAndGlobPathIfNecessary(hadoopConf, _, checkFilesExist)).toArray
  val fileStatusCache = FileStatusCache.getOrCreate(sparkSession)
  val (dataSchema, partitionSchema) = getOrInferFileFormatSchema(format, fileStatusCache)
  val fileCatalog = if (sparkSession.sqlContext.conf.manageFilesourcePartitions &&
      catalogTable.isDefined && catalogTable.get.tracksPartitionsInCatalog) {
    val defaultTableSize = sparkSession.sessionState.conf.defaultSizeInBytes
    new CatalogFileIndex(
      sparkSession,
      catalogTable.get,
      catalogTable.get.stats.map(_.sizeInBytes.toLong).getOrElse(defaultTableSize))
  } else {
    new InMemoryFileIndex(
      sparkSession, globbedPaths, options, Some(partitionSchema), fileStatusCache)
  }
  HadoopFsRelation(
    fileCatalog,
    partitionSchema = partitionSchema,
    dataSchema = dataSchema.asNullable,
    bucketSpec = bucketSpec,
    format,
    caseInsensitiveOptions)(sparkSession)

文件格式的,其实看这么多比较没用,可以想象数据源无非是文件的,jdbc的,其他的,这里直接看文件格式的这一个分支来走,这里有两个FileFormat,取决于是不是流,Streaming的,最终最终是要准备出一个HadoopFsRelation,它里面会传参,一系列关于数据源的参数。而且HadoopFsRelation是一个样例类,可以构造出它。可以想成是对数据源元数据的包装,而且是一个relation关系,得到包装之后,只是一个样例类,带了一些属性,参数。这个东西会返回作为参数传给baseRelationToDataFrame,最终到这个方法。
Dataset.ofRows(self, LogicalRelation(baseRelation))接收的参数,一个是自己SparkSession,另外一个是要经过一个方法,逻辑的relation转成一个计划,里面传的就是数据源的元数据,那个对象/样例类。
baseRelation作为参数传进去之后,最主要的就是LogicalRelation()这个方法

object LogicalRelation {
  def LogicalRelation : Boolean = false): LogicalRelation =
    LogicalRelation(relation, relation.schema.toAttributes, None, isStreaming)

  def apply(relation: BaseRelation, table: CatalogTable): LogicalRelation =
    LogicalRelation(relation, relation.schema.toAttributes, Some(table), false)
}

它是一个object,如果传参传进去之后,它有一个relation,下面方法怎么执行的,又调了LogicalRelation,这个方法名字一样,它是伴生关系,会有一个case class类:

case class LogicalRelation(
    relation: BaseRelation,
    output: Seq[AttributeReference],
    catalogTable: Option[CatalogTable],
    override val isStreaming: Boolean)
  extends LeafNode with MultiInstanceRelation {
}

这个类接收了相应传进来的一些参数,从这儿就看出抽象语法树,那棵树的影子了,从这可以看到一个继承关系,继承了一个LeafNode ,叶子节点。点进去:

abstract class LeafNode extends LogicalPlan {
  override final def children: Seq[LogicalPlan] = Nil
  override def producedAttributes: AttributeSet = outputSet

  /** Leaf nodes that can survive analysis must define their own statistics. */
  def computeStats(): Statistics = throw new UnsupportedOperationException
}

叶子节点继承了LogicalPlan逻辑的Plan,整个LogicalPlan是父类的话,里面有叶子节点LeafNode,单分支节点UnaryNode,多分枝节点BinaryNode,有这么三个节点类型。
逻辑计划的这个树有一个继承关系,来自于QueryPlan,未来有两个东西,一个是逻辑LogicalPlan,一个是物理SparkPlan,看着像个树,只是类型的继承关系,

LogicalPlan:
LeafNode、UnaryNode、BinaryNode
LogicalPlan extends QueryPlan:
LogicalPlan 、SparkPlan
在这里插入图片描述

现在我们得到的是所谓逻辑的,如果未来用这些节点封装出一棵树,这棵树会在优化的时候,向物理翻转,节点会对应有一个翻转的过程。
回忆一遍:
刚才发生什么事情,通过sparkSession里面的方法,最核心的得到了哪些东西?
首先拿到的是一个DataFrameReader,里面有一个非常重要的方法loadV1Source,这个方法最终做什么事情,baseRelationToDataFrame,里面要准备参数DataSource数据源,要把刚才的参数传进去,通过数据源自带的方法resolveRelation会对数据源具体的类型做匹配,匹配的时候有很多种数据源的形式,这里先看文件类型的,文件类型还分流的和非流的,无论是哪种,只要是文件的,最后都是想得到HadoopFsRelation。 baseRelationToDataFrame这个方法会动用dataset.offRows,但是在这之前还做了一个处理,这个参数会变成一个样例类,LogicalRelation,最终给用户返回的DataSet。
看offRows做了哪些事情?

def ofRows(sparkSession: SparkSession, logicalPlan: LogicalPlan): DataFrame = {
  val qe = sparkSession.sessionState.executePlan(logicalPlan)
  qe.assertAnalyzed()
  new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))
}

传参有SparkSession,有LogicalPlan,会有qe = sparkSession.sessionState.executePlan(logicalPlan)这个操作非常重要,在这个方法里面会有一步先将逻辑计划转换为queryExecution,而且在这步的时候要回忆起,在queryExecution里面所有的属性都是lazy的,所以那些属性在这一步的时候并不会被执行得到queryExecution。
且new Dataset[Row](sparkSession, qe, RowEncoder(qe.analyzed.schema))在这里会new出真正的对象,一个独立的Dataset,走的是Dataset类名的构造列表,且queryExecution是作为DataSet的成员属性。
最终得出结论,dataSet中必须持有的东西是queryExecution,在未来的某一时刻,这些懒惰的东西一定会被执行起来,但是现在要知道queryExecution是最重要的,它涉及到SQL解析的所有流程,和上面图一样,上面就是推导出dataSet是怎么诞生的,而且这是api级别的,不是SQL字符串级别的,api级别它不涉及sql解析的过程,只是对象级别。

站在使用者的角度,我有sparkSession得到了DataSet了,当我得到一个对象之后,期望的下一步是通过DataSet继续编程,继续编程的时候,在DataSet身上可以点出来一些方法,有基于集合的map,基于SQL的where 对象方法,这时候拿where,更贴合SQL的逻辑,where肯定是基于SQL才会有的,那么where是个啥,

def where(condition: Column): Dataset[T] = filter(condition)

它包装了一下,依然是一个filter,filter是属于集合的,在rdd中也有filter,但并不是同一个,只是有这个语义,看这个filter也是一个方法:

def filter(condition: Column): Dataset[T] = withTypedPlan {
  Filter(condition.expr, planWithBarrier)
}

这个方法名称filter的方法体是什么东西,指向了另外一个方法withTypedPlan:

/** A convenient function to wrap a logical plan and produce a Dataset. */
@inline private def withTypedPlan[U : Encoder](logicalPlan: LogicalPlan): Dataset[U] = {
  Dataset(sparkSession, logicalPlan)
}

要给它一个传参,参数是一个逻辑执行计划,先不看withTypedPlan调起做什么事情,先看里面的东西是个啥:Filter(condition.expr, planWithBarrier)

case class Filter(condition: Expression, child: LogicalPlan)
  extends UnaryNode with PredicateHelper {
  override def output: Seq[Attribute] = child.output

  override def maxRows: Option[Long] = child.maxRows

  override protected def validConstraints: Set[Expression] = {
    val predicates = splitConjunctivePredicates(condition)
      .filterNot(SubqueryExpression.hasCorrelatedSubquery)
    child.constraints.union(predicates.toSet)
  }
}

这个Filter是一个样例类,除了人为写的表达式那个参数以外,还带了一个参数planWithBarrier

// Wraps analyzed logical plans with an analysis barrier so we won't traverse/resolve it again.
@transient private[sql] val planWithBarrier = AnalysisBarrier(logicalPlan)

来自于logicalPlan

@transient private[sql] val logicalPlan: LogicalPlan = {
  // For various commands (like DDL) and queries with side effects, we force query execution
  // to happen right away to let these side effects take place eagerly.
  queryExecution.analyzed match {
    case c: Command =>
      LocalRelation(c.output, withAction("command", queryExecution)(_.executeCollect()))
    case u @ Union(children) if children.forall(_.isInstanceOf[Command]) =>
      LocalRelation(u.output, withAction("command", queryExecution)(_.executeCollect()))
    case _ =>
      queryExecution.analyzed
  }
}

是我们传进来的queryExecution,往上推就是那个qe,刚才辛辛苦苦得到一个queryExecution,这个queryExecution有一系列的小动作,会分析一下,从元数据那里得到一个逻辑的计划,得到逻辑计划之后,再去封装成planWithBarrier,最终会作为未来转换算子的参数,进入到Filter这个样例类里,这个参数还会和我们写where条件的表达式有一个加工的过程,这两个结合之后的结果,会得到一个过滤之后的结果。
Filter只是一个样例类,继承了UnaryNode单枝节点类型,一个东西经过filter一定能得到一个东西,所有它是单枝的。再回到:withTypedPlan

/** A convenient function to wrap a logical plan and produce a Dataset. */
@inline private def withTypedPlan[U : Encoder](logicalPlan: LogicalPlan): Dataset[U] = {
  Dataset(sparkSession, logicalPlan)
}

由这个方法又会调用到Dataset,它收到了sparkSession和logicalPlan,依然做了一件事情,new了一个新的DataSet

def apply[T: Encoder](sparkSession: SparkSession, logicalPlan: LogicalPlan): Dataset[T] = {
  val dataset = new Dataset(sparkSession, logicalPlan, implicitly[Encoder[T]])
  // Eagerly bind the encoder so we verify that the encoder matches the underlying
  // schema. The user will get an error if this is not the case.
  dataset.deserializer
  dataset
}

所以得出一个结论:withTypedPlan会new出来一个新的Dataset,的确where或filter是一个转换算子,由一个对象转换成一个新的对象。且这个对象像Rdd一样会一直传递下去,只不过在这个过程中,比如刚才调用filter,需要两个东西,一个是表达式,一个是queryExecution,最终会转成一个样例类,是一个节点。

def this(sparkSession: SparkSession, logicalPlan: LogicalPlan, encoder: Encoder[T]) = {
  this(sparkSession, sparkSession.sessionState.executePlan(logicalPlan), encoder)
}

这次new DataSet的过程是把queryExecution,能读到原来的queryExecution加上过滤条件之后得到的结果,会发现无论where() map()只要它是一个转换算子,这些算子都不算数,最终都会调用一个withTypedPlan,转换算子只不过能力加工的东西不一样。他们都会绑定自己DataSet持有的queryExecution

@Experimental
@InterfaceStability.Evolving
def map[U : Encoder](func: T => U): Dataset[U] = withTypedPlan {
  MapElements[T, U](func, planWithBarrier)
}

这只是转换,最终还有一个执行过程,是怎么变成Rdd的。
执行算子,比如用做多的show()

def show(): Unit = show(20)

当使用show()这个执行算子的时候,默认是拿回20行

def show(numRows: Int): Unit = show(numRows, truncate = true)
在点进来:
def show(numRows: Int, truncate: Boolean): Unit = if (truncate) {
  println(showString(numRows, truncate = 20))
} else {
  println(showString(numRows, truncate = 0))
}

会调用showString无非是区分是否有分区的概念:

private[sql] def showString(
    _numRows: Int, truncate: Int = 20, vertical: Boolean = false): String = {
  val numRows = _numRows.max(0).min(Int.MaxValue - 1)
  val newDf = toDF()   //要先把自己转成DF的类型,还是它自己这个对象
  val castCols = newDf.logicalPlan.output.map { col =>
    // Since binary types in top-level schema fields have a specific format to print,
    // so we do not cast them to strings here.
    if (col.dataType == BinaryType) {
      Column(col)
    } else {
      Column(col).cast(StringType)
    }
  }
  val takeResult = newDf.select(castCols: _*).take(numRows + 1)   //拿回结果,在自己这个DF上.select()选择所有的列,take()取回多少行   其实select也是一种转换算子,但是take()
  val hasMoreData = takeResult.length > numRows
  val data = takeResult.take(numRows)

take方法:

def take(n: Int): Array[T] = head(n)
head方法:
def head(n: Int): Array[T] = withAction("head", limit(n).queryExecution)(collectFromPlan)

head方法的方法体是什么样子的withAction,是个柯里化的函数,
什么是action算子?
action算子有很多,但是最终会触发withAction,这个是我们的执行算子。
但是这个执行算子是柯里化的,会有两个参数
第一个参数:“head”, limit(n).queryExecution 执行的方法是啥,控制显示行数
第二个参数:collectFromPlan 来自于计划的回收

private def collectFromPlan(plan: SparkPlan): Array[T] = {
  // This projection writes output to a `InternalRow`, which means applying this projection is not
  // thread-safe. Here we create the projection inside this method to make `Dataset` thread-safe.
  val objProj = GenerateSafeProjection.generate(deserializer :: Nil)
  plan.executeCollect().map { row =>
    // The row returned by SafeProjection is `SpecificInternalRow`, which ignore the data type
    // parameter of its `get` method, so it's safe to use null here.
    objProj(row).get(0, null).asInstanceOf[T]
  }
}

是一个方法,未来这个函数会作为参数传给withAction函数的参数列表

private def withAction[U](name: String, qe: QueryExecution)(action: SparkPlan => U) = {
  try {
    qe.executedPlan.foreach { plan =>
      plan.resetMetrics()
    }
    val start = System.nanoTime()
    val result = SQLExecution.withNewExecutionId(sparkSession, qe) {
      action(qe.executedPlan)
    }
    val end = System.nanoTime()
    sparkSession.listenerManager.onSuccess(name, qe, end - start)
    result
  } catch {
    case e: Exception =>
      sparkSession.listenerManager.onFailure(name, qe, e)
      throw e
  }
}

第一个参数列表接收的是name: String, qe: QueryExecution,第二个参数接收的是一个函数action: SparkPlan => U。这个函数def collectFromPlan(plan: SparkPlan)还得接收参数SparkPlan,这个参数哪来的,qe.executedPlan物理计划传进去就开始执行了。其实还是围绕的QueryExecution,是spark-sql的灵魂对象。最终这个方法collectFromPlan可以返回array,scala本地对象。但这里面还是没看到rdd,先看这个方法plan.executeCollect()

def executeCollect(): Array[InternalRow] = {
  val byteArrayRdd = getByteArrayRdd()
  val results = ArrayBuffer[InternalRow]()
  byteArrayRdd.collect().foreach { countAndBytes =>
    decodeUnsafeRows(countAndBytes._2).foreach(results.+=)
  }
  results.toArray
}

到了sparkPlan了,但是还没讲它是个什么东西。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值