由一条SQL分析SparkSQL执行过程(二)

对于下面一段SQL

SELECT a.uid,b.name,SUM(clk_pv) AS clk_pv 
FROM log  a
JOIN user b ON  a.uid = b.uid
WHERE a.fr = 'android'
GROUP BY a.uid,b.name

在上一部分,我们分析了SparkSQL的建议执行流程图。我们知道一条SQL在Spark执行要经历以下几步:

  1. 用户提交SQL文本
  2. 解析器将SQL文本解析成逻辑计划
  3. 分析器结合Catalog对逻辑计划做进一步分析,验证表是否存在,操作是否支持等
  4. 优化器对分析器分析的逻辑计划做进一步优化,如将过滤逻辑下推到子查询,查询改写,子查询共用等
  5. Planner再将优化后的逻辑计划根据预先设定的映射逻辑转换为物理执行计划
  6. 物理执行计划做RDD计算,最终向用户返回查询数据

那么详细的执行计划是什么样的呢?如下图:


image.png

主要的执行步骤如下:

  1. 用户构建SparkSession,并调用Sql函数,传入Sql脚本
  2. 构建SparkSession时,Spark内部会构造SessionState,SessionState会构造解析器,分析器,Catalog,优化器,Planner还有逻辑计划转化为执行计划的方法
  3. SparkSession调用sessionState的解析器将sql文件解析为逻辑计划
  4. SparkSession调用DataSet 对象(注意不是DataSet类)的ofRow方法,返回DataFrame
  5. DataSet对象调用SparkSession的SessionState的executePlan方法(这是一个函数)返回一个QueryExecution
  6. 构造QueryExecution的过程是分析(Catalog)->优化->转化为逻辑计划的过程
  7. DataSet对象调用QueryExecution的assertAnalyzed()方法,判断是否已分析
  8. DataSet对象通过DataSet类创建一个DataSet实例。同时传入RowEncoder即Schema并返回一个DataFrame即DataSet[Row]类型。

然后就可以在DataFrame上做各种算子。

上述的流程又具体可分为几个关键阶段:

1. 用户构建SparkSession ,调用sql函数

import org.apache.spark.sql.SparkSession

val spark = SparkSession
  .builder()
  .appName("Spark SQL basic example")
  .config("spark.some.config.option", "some-value")
  .getOrCreate()

val sqlContext = "....."
val df = spark.sql(sqlContext)

SparkSession是SparkSQL的统一入口。SparkSession是通过Builder构造的。(则这是一种典型的设计模式:构造者模式,目的是将类的构造和类的表现解耦)。

class SparkSession private(
    @transient val sparkContext: SparkContext,
    @transient private val existingSharedState: Option[SharedState],
    @transient private val parentSessionState: Option[SessionState],
    @transient private[sql] val extensions: SparkSessionExtensions)
  extends Serializable with Closeable with Logging  {
 // Sparksession 的实现
}

如上,SparkSession在构造时,会传入一个SessionState类型的对象。这个对象是SparkSession能完成所有功能的核心。

private[sql] class SessionState(
    sharedState: SharedState,
    val conf: SQLConf,
    val experimentalMethods: ExperimentalMethods,
    val functionRegistry: FunctionRegistry,
    val udfRegistration: UDFRegistration,
    val catalog: SessionCatalog, 
    val sqlParser: ParserInterface,
    val analyzer: Analyzer,
    val optimizer: Optimizer,
    val planner: SparkPlanner,
    val streamingQueryManager: StreamingQueryManager,
    val listenerManager: ExecutionListenerManager,
    val resourceLoader: SessionResourceLoader,
    createQueryExecution: LogicalPlan => QueryExecution,
    createClone: (SparkSession, SessionState) => SessionState) {
//SessionState 实现
}

从上面代码可以看出来,构造SessionState就构造一个catalog,解析器,分析器,优化器和Planner。同时还传入一个函数createQueryExecution,这个函数会将逻辑计划转化为查询计划。

总结
SparkSession在底层封装了SessionState的构建,并对用户透明,用户只需构建SparkSession即可完成相应操作。
问题:如何实现外部数据源?
观察SparkSession构建SessionState的源码:

// 如果父Session有SessionState 则复制一份,否则调用instantiateSessionState构建
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
      }
  }
// 构建SessionState的方法,利用反射和ClassLoader创建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)
    }
  }

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

从上面的代码可知:用户配置CATALOG_IMPLEMENTATION,也就是参数spark.sql.catalogImplementation 参数指明定制的的SessionState。当日这个SessionState有一个Builder,且是BaseSessionStateBuilder的子类。
另外,在Object:StaticSQLConf 中我们发现:

  val CATALOG_IMPLEMENTATION = buildStaticConf("spark.sql.catalogImplementation")
    .internal()
    .stringConf
    .checkValues(Set("hive", "in-memory"))
    .createWithDefault("in-memory")

这段代码的含义是如果用户指明的spark.sql.catalogImplementation参数不是"hive", "in-memory"或者说未指明,则默认为:in-memory。也就是说默认情况,使用的是in-memory 模式。而in-memory 模式实际构造的是Spark内部的SessionStateBuilder 的实例。

class SessionStateBuilder(
    session: SparkSession,
    parentState: Option[SessionState] = None)
  extends BaseSessionStateBuilder(session, parentState) {
  override protected def newBuilder: NewBuilder = new SessionStateBuilder(_, _)
}

而SessionStateBuilder 只是BaseSessionStateBuilder 的简单实现。因此构造自己外部数据源,必须重写自己的SessionState,并继承BaseSessionStateBuilder。

2. 构建SessionState。

在1中我们看到SparkSession实际上就做了一件事:

构建SessionState,以及怎么构建SessionState?

为了构建SessionState,首先从用户配置参数spark.sql.catalogImplementation中读到catalog实现,然后通过模式匹配找到对应的类名,再通过反射方法创建一个BaseSessionStateBuilder类型的实例。(这又是一个构造者设计模式的应用)

所以,SessionState的构造,实际是BaseSessionStateBuilder 的构造。我们知道,SessionState肯定有解析器,catalog,分析器,优化器,Planner(没找到好的中文名字对应),还有一个将逻辑计划转化为QueryExecution的函数。
那么这些组件是如何构造的的呢?

1. 解析器

 protected lazy val sqlParser: ParserInterface = {
    extensions.buildParser(session, new SparkSqlParser(conf))
  }

解析器是通过extensions.buildParser 构造的,extensions 是什么?

protected def extensions: SparkSessionExtensions = session.extensions

extensions是一个SparkSession的一个变量,是SparkSessionExtensions类型,我们在SparkSession对象的Builder类中我们看到:

    /**
     * Inject extensions into the [[SparkSession]]. This allows a user to add Analyzer rules,
     * Optimizer rules, Planning Strategies or a customized parser.
     *
     * @since 2.2.0
     */
    def withExtensions(f: SparkSessionExtensions => Unit): Builder = {
      f(extensions)
      this
    }

这是2.2以后运行用户注入自己的分析的一个扩展。
在SparkSessionExtensions的buildParser

  private[sql] def buildParser(
      session: SparkSession,
      initial: ParserInterface): ParserInterface = {
    parserBuilders.foldLeft(initial) { (parser, builder) =>
      builder(session, parser)
    }
  }

实际上就是将用户扩展和Spark自己的解析器做合并。

** Spark你搞这么绕,合适吗? **

2. Catalog

protected lazy val catalog: SessionCatalog = {
    val catalog = new SessionCatalog(
      session.sharedState.externalCatalog,
      session.sharedState.globalTempViewManager,
      functionRegistry,
      conf,
      SessionState.newHadoopConf(session.sparkContext.hadoopConfiguration, conf),
      sqlParser,
      resourceLoader)
    parentState.foreach(_.catalog.copyStateTo(catalog))
    catalog
  }

如果用户定义自己的数据源,就是要改这个地方,实现自己的CataLog.

3. 分析器

protected def analyzer: Analyzer = new Analyzer(catalog, conf) {
    override val extendedResolutionRules: Seq[Rule[LogicalPlan]] =
      new FindDataSourceTable(session) +:
        new ResolveSQLOnFile(session) +:
        customResolutionRules

    override val postHocResolutionRules: Seq[Rule[LogicalPlan]] =
      PreprocessTableCreation(session) +:
        PreprocessTableInsertion(conf) +:
        DataSourceAnalysis(conf) +:
        customPostHocResolutionRules

    override val extendedCheckRules: Seq[LogicalPlan => Unit] =
      PreWriteCheck +:
        HiveOnlyCheck +:
        customCheckRules
  }

在构造分析器时,重写了三个方法:外部的解析规则,postHoc(??什么鬼)解析规则
和外部的Check规则。这对于DDL命令等不需要解析的文本解析的文本,定义的规则。当然,用户也可以通过重写这个类,或者通过配置加载一些规则进来

4. 优化器

 protected def optimizer: Optimizer = {
    new SparkOptimizer(catalog, conf, experimentalMethods) {
      override def extendedOperatorOptimizationRules: Seq[Rule[LogicalPlan]] =
        super.extendedOperatorOptimizationRules ++ customOperatorOptimizationRules
    }
  }

extendedOperatorOptimizationRules 说明,优化器也可以加载外部规则

5. Planner

 protected def planner: SparkPlanner = {
   new SparkPlanner(session.sparkContext, conf, experimentalMethods) {
     override def extraPlanningStrategies: Seq[Strategy] =
       super.extraPlanningStrategies ++ customPlanningStrategies
   }
 }

extraPlanningStrategies 说明,Planner也可以加载外部规则

6. 构造QueryExecution的函数

 protected def createQueryExecution: LogicalPlan => QueryExecution = { plan =>
    new QueryExecution(session, plan)
  }

实际上就是创建了一个QueryExecution实例

总结
在BaseSessionStateBuilder构造各类组件时,都暴露了外部接口,每一个组件用户都可以去实现定制的规则或策略。

最后,经过上面的分析,我们得到SparkSpark的类的关系图:


image.png

我们回到我们怎么使用SparkSQL

val spark = SparkSession
  .builder()   
  .appName("Spark SQL basic example")
  .config("spark.some.config.option", "some-value")
  .getOrCreate()

val  df = spark.sql(".............")

这也许就是为什么Spark那么受欢迎的原因了,辛苦我自己,幸福千万程序狗!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容