接上节课: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了,但是还没讲它是个什么东西。