spark的Catalyst到底做了什么

背景

最近又看了一些Catalyst相关文章,对它认识更深了一点,在这里做下总结

Catalyst是什么

我们知道,Hadoop上面有Hive,Hive能把SQL转成MapReduce作业;而由于Shark可以将SQL语句转成RDD执行,因此就可以基于Shark来实现Spark版本的“Hive”,但是Shark依赖Hive的Metastore、解析器等把hql执行变成Spark上的计算,灵活性不足,因此诞生了spark sql。

而spark sql其实就是在Spark Core的执行引擎基础上针对结构化数据处理进行优化和改进:

而我们 要讲到的Catalyst就是spark sql的核心,它能将SQL经过一系列操作,最终转化为Spark系统中执行的RDD。

Catalyst具体是怎么运作的

catalyst具体流程可以如下图描述:

下面我们来具体看看每个步骤都干了什么,以及为什么要这样设计。

一、SQL->Unresolved Logical Plan

这一步,通过catalyst的Parser将SQL/Dataset/DataFrame转化成一棵未经解析(Unresolved)的语法树(BST),得到Unresolved Logical Plan。

之所以这个计划是未解析的,是因为尽管代码在语法上是正确的,但是它引用的表或列可能不存在。 

下面是网上找到的一个例子(侵删),左边是原始的sql,右侧是生成的Unresolved Logical Plan

二、Unresolved Logical Plan->Analyzed/Resolved Logical Plan

这一步是在 BST 中加入元数据信息,通过analyzer分析得到Analyzed/Resolved logical plan

 做这一步主要是为了一些优化, 例如下图中

score.id → id#1#L 为 score.id 生成 id 为 1, 类型是 Long

由于上一步得到的“Unresolved Logical Plan”携带的信息相当有限,它只包含查询语句从 DSL 语法变换成 AST 语法树的信息。例如,在计划的最底层节点“告诉”Catalyst:“你需要扫描一张表,这张表有 n 个字段,分别是 ABCD...,文件格式是 Parquet”。但这些信息对于优化还远远不够,我们还需要知道这张表的 Schema 是啥?字段的类型都是什么?字段名是否真实存在?数据表中的字段名与计划中的字段名是一致的吗?

因此,在这一步中,Catalyst 就是要结合 DataFrame 的 Schema 信息,来确认计划中的表名、字段名、字段类型与实际数据是否一致。完成确认之后,Catalyst 会生成“Analyzed Logical Plan”。

从上图中我们能够看到,逻辑计划已经完成了一致性检查,并且可以识别两张表的字段类型,比如 score.id 的类型是Long  等等。

三、Analyzed Logical Plan->Optimized Logical Plan

对于Analyzed logical plan,如果我们不做任何优化,直接把它转换为物理计划也可以。但是,这种照搬开发者的计算步骤去制定物理计划的方式,它的执行效率往往不是最优的。为什么这么说呢?

例如,在运行时,Spark 会先全量扫描 Parquet 格式的xxx表和yyy表,然后遴选出 n个字段,接着按id进行join后,过滤掉z字段不符合条件的数据。

对于这样的执行计划来说,最开始的全量扫描是一种浪费。一方面,查询实际上只涉及部分字段,并不需要 所有列数据;另一方面,字段 z 上带有过滤条件,我们完全可以利用这些过滤条件减少需要扫描的数据量。

因此,我们需要通过Optimizer对resolved logical plan进行优化,得到optimized logical plan。

Optimizer 是一组启发式规则的集合(其实就是一些经验规则)用于优化逻辑计划,通过谓词下推等方式进行优化,最终输出 optimized logical plan

在 Spark 3.0 版本中,Catalyst 总共有 81 条优化规则(Rules),这 81 条规则会分成 27 组(Batches),其中有些规则会被收纳到多个分组里。因此,如果不考虑规则的重复性,27 组算下来总共会有 129 个优化规则。如果从优化效果的角度出发,这些规则可以归纳到以下 3 个范畴:

谓词下推(Predicate Pushdown)

列剪裁(Column Pruning)

常量替换 (Constant Folding)

(源码在 org.apache.spark.sql.catalyst.optimizer.Optimizer)

谓词下推 Predicate Pushdown

谓词下推主要是围绕着查询中的过滤条件做文章。其中,“谓词”指代的是像用户表上“people.age > 10”这样的过滤条件,“下推”指代的是把这些谓词沿着执行计划向下,推到离数据源最近的地方,从而在源头就减少数据扫描量。换句话说,让这些谓词越接近数据源越好。

不过,在下推之前,Catalyst 还会先对谓词本身做一些优化,比如像 OptimizeIn 规则,它会把“gender in ‘M’”优化成“gender = ‘M’”,也就是把谓词 in 替换成等值谓词。再比如,CombineFilters 规则,它会把“age > 10”和“gender = ‘M’”这两个谓词,捏合成一个谓词:“age != null AND gender != null AND age >10 AND gender = ‘M’”。

完成谓词本身的优化之后,Catalyst 再用 PushDownPredicte 优化规则,把谓词推到逻辑计划树最下面的数据源上。对于 Parquet、ORC 这类存储格式,结合文件注脚(Footer)中的统计信息,下推的谓词能够大幅减少数据扫描量,降低磁盘 I/O 开销。

列值裁剪 Column Pruning

列剪裁就是扫描数据源的时候,只读取那些与查询相关的字段。

显然,行存格式无法使用这个规则来优化。

常量替换

它的逻辑比较简单。假设我们在年龄上加的过滤条件是“age < 12 + 18”,Catalyst 会使用 ConstantFolding 规则,自动帮我们把条件变成“age < 30”。再比如,我们在 select 语句中,掺杂了一些常量表达式,Catalyst 也会自动地用表达式的结果进行替换。

Cache Manager 优化

从“Analyzed Logical Plan”到“Optimized Logical Plan”的转换,Catalyst 除了使用启发式的规则以外,还会利用 Cache Manager 做进一步的优化。

Cache manager用来追踪记录对于当前查询计划哪些计算已经被缓存了。当缓存函数被调用的时候(例如调用 DataFrame 的.cache 或.persist,或是在 SQL 语句中使用“cache table”关键字),Cache Manager被直接调用,并且把被调用的dataFrame从逻辑计划中脱离出来,从而储存在名为cachedData的顺序结构中。

这样,当我们触发一个query的时候,查询计划将会被处理以及转化,在处理到cachemanager阶段(在优化阶段之前)的时候,spark将会检查分析计划的每个子计划是否存储在cachedData里。如果存在,则说明同样的计划已经被执行过了,所以可以重新使用该缓存,并且使用InMemoryRelation操作来标示该缓存的计划。InMemoryRelation将在物理计划阶段转换为InMemoryTableScan。

例如下面一个例子:

df = spark.table("users").filer(col(col_name) > x).cache  
df.count() # now check the query plan in Spark UI

 

四、Optimized Logical Plan->Physical Plan

逻辑优化的每一步仅仅是从逻辑上表明 Spark SQL 需要“做什么”,并没有从执行层面说明具体该“怎么做”,即上面过程生成的 OLP 其实最终还没办法直接运行。比如说,在逻辑计划的根节点,出现了“Join Inner”字样,Catalyst 优化器明确了这一步需要做内关联。但是,怎么做内关联,使用哪种 Join 策略来进行关联,Catalyst 并没有交代清楚。因此,逻辑计划本身不具备可操作性。

因此,为了把逻辑计划交付执行,Catalyst 还需要把 Optimized Logical Plan 转换为物理计划。物理计划比逻辑计划更具体,它明确交代了 Spark SQL 的每一步具体该怎么执行。

Catalyst 的物理优化阶段可以分为两个环节: 

  1. Catalyst 基于既定的优化策略(Strategies),把逻辑计划中的关系操作符一一映射成物理操作符,生成 Spark Plan。

    所有优化策略在转换方式上都大同小异,都是使用基于模式匹配的偏函数(Partial Functions),把逻辑计划中的操作符平行映射为 Spark Plan 中的物理算子。比如,BasicOperators 策略直接把 Project、Filter、Sort 等逻辑操作符平行地映射为物理操作符。其他策略的优化过程也类似

  2. 在生成 Physical Plan 过程中,Catalyst 再基于事先定义的 Preparation Rules,对 Spark Plan 做进一步的完善、生成可执行的 Physical Plan。
Preparation Rules 有 6 组规则,这些规则作用到 Spark Plan 之上就会生成 Physical Plan,而 Physical Plan 最终会由 Tungsten 转化为用于计算 RDD 的分布式任务。

其他问题

一、怎么查看逻辑计划和物理计划

使用 queryExecution 方法查看逻辑执行计划, 使用 explain 方法查看物理执行计划。

在这里也可以复习下spark-shell的使用:

首先进入spark shell:

/usr/share/spark-3.1/bin/spark-shell --master yarn --queue rechatbot-prod --driver-cores 1 --driver-memory 1g --num-executors 1 --executor-cores 1 --executor-memory 1g

进入scala命令行页面后,可以按以下步骤执行:

import org.apache.spark.sql.SparkSession

#创建一个SparkSession对象。SparkSession是与Spark集群进行交互的入口

val spark = SparkSession.builder().appName("Write to Hive table").enableHiveSupport().getOrCreate()

#创建2个DataFrame对象

val data = Seq(("John", 1), ("Jane", 2), ("Tom", 3))

val df = spark.createDataFrame(data).toDF("name", "id")

val data2 = Seq(( 99.5,1), (60.5, 2), (22.5, 3))

val df2 = spark.createDataFrame(data2).toDF("math_score", "id")

#将DataFrame注册为一个临时表

df.createOrReplaceTempView("people")

df2.createOrReplaceTempView("score")

#查看逻辑计划和物理计划

spark.sql("select p.name,s.math_score from people p,score s where p.id=s.id and p.id>1").queryExecution

spark.sql("select p.name,s.math_score from people p,score s where p.id=s.id and p.id>1").explain

 二、既然Catalyst有了这么多优化规则,我们开发时是否就可以随心所欲了?

Catalyst的规则其实是经验规则,不一定适合所有场景,更不一定必然适合你的场景。我们应该严格要求自己,自觉进行优化,并且根据业务特点进行改进,不要依赖框架。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值