Spark SQL运行原理

文章内容摘自<<图解Spark核心技术与案例实战>>

详细内容请参考书中原文

一、简介

    介绍Spark SQL,就不得不提Hive和Shark。Hive是Shark的前身,Shark是Spark SQL的前身。
    Shark是由伯克利实验室技术团队开发的Spark生态环境组件之一,它扩展了Hive并修改了Hive架构中的内存管理、物理计划和执行3个模块,使之可以运行在Spark引擎上。大大加快了在内存和磁盘上的查询。Shark直接建立在Apache Hive代码库上,所以它支持几乎所有Hive特点。它支持现有的Hive SQL语言、Hive数据格式(SerDes)和用户自定义函数(UDF),调用外部脚本查询,并采用Hive解析器、查询优化器等。但也造成了Shark的整体设计架构对Hive的依赖性太强,难以支持其长远发展。
    相比于Shark对Hive的过渡依赖,Spark SQL在Hive兼容层面仅依赖HQL Parser、Hive Metastore和Hive SerDes。也就是说,从HQL被解析成抽象语法树(AST)起,就全部由Spark SQL接管了,执行计划生成和优化都由Catalyst负责。借助Scala的模式匹配等函数式语言特性,利用Catalyst开发执行计划优化策略比Hive要简洁得多。此外,除了兼容HQL、加速现有Hive数据的查询分析外, Spark SQL还支持直接对原生RDD对象进行关系查询。同时,除了HQL以外,Spark SQL还内建了一个精简的SQL Parser以及一套Scala DSL。也就是说,如果只是使用Spark SQL内建的SQL方言或Scala DSL对原生RDD对象进行关系查询,用户在开发Spark应用时完全不需要依赖Hive的任何东西。当然,Spark SQL还吸取了Shark的一些优点,如内存列存储(In-Memory Columnar Storage)等。这些优点使得Spark SQL无论在数据兼容性、性能优化、组件扩展等方面都得到了极大的方便。

二、Spark SQL运行原理

2.1 通用SQL执行原理

  1. 词法和语法解析Parse:生成逻辑计划
  2. 绑定Bind:生成可执行计划
  3. 优化Optimize:生成最优执行计划
  4. 执行Execute:返回实际数据

2.2 SparkSQL运行架构

    SparkSQL对SQL语句的处理和关系型数据库采用了类似的方法, SparkSQL会先将SQL语句进行解析Parse形成一个Tree,然后使用Rule对Tree进行绑定、优化等处理过程,通过模式匹配对不同类型的节点采用不同的操作。而SparkSQL的查询优化器是Catalyst,它负责处理查询语句的解析、绑定、优化和生成物理计划等过程,Catalyst是SparkSQL最核心的部分,其性能优劣将决定整体的性能。
    SparkSQL由4个部分构成
  1. Core:负责处理数据的输入/输出,从不同的数据源获取数据(如RDD、Parquet文件),然后将查询结果输出成DataFrame
  2. Catalyst:负责处理查询语句的整个过程,包括解析、绑定、优化、物理计划等
  3. Hive:负责对Hive数据的处理
  4. Hive-thriftserver:提供CLI和JDBC/ODBC接口等

2.2.1 Tree

    Tree是Catalyst执行计划表示的数据结构。LogicalPlans,Expressions和Pysical Operators都可以使用Tree来表示。Tree具备一些Scala Collection的操作能力和树遍历能力。
    Tree提供三种特质:
  1. UnaryNode:一元节点,即只有一个子节点
  2. BinaryNode:二元节点,即有左右子节点的二叉节点
  3. LeafNode:叶子节点,没有子节点的节点
    针对不同的节点,Tree提供了不同的操作方法。对Tree的遍历,主要是通过迭代将Rule应用到该节点以及子节点。
     Tree有两个子类继承体系,即QueryPlan和Expression
  1. QueryPlan下面的两个子类分别是LogicalPlan(逻辑执行计划)和SparkPlan(物理执行计划)。QueryPlan内部带有output:Seq[Attribute]、transformExpressionDown和transformExpressionUp等方法,它的主要子体系是LogicalPlan,即逻辑执行计划表示,它在Catalyst优化器里有详细实现。LogicalPlan内部带一个reference:Set[Attribute],主要方法为resolve(name:String): Option[NamedExpression],用于分析生成对应的NamedExpression。对于SparkPlan,即物理执行计划表示,需要用户在系统中自己实现。LogicalPlan本身也有很多具体子类,也分为UnaryNode,BinaryNode和LeafNode三类。
  2. Expression是表达式体系,是指不需要执行引擎计算,而可以直接计算或处理的节点,包括Cast操作、Porjection操作、四则运算和逻辑操作符运算等等。

2.2.2 Rule

    Rule[TreeType <: TreeNode[_]]是一个抽象类,子类需要复写apply(plan: TreeType)方法来指定处理逻辑。对 于Rule的具体实现是通过RuleExecutor完成的,凡是需要处理执行计划树进行实施规则匹配和节点处理的,都需要继承RuleExecutor[TreeType]抽象类。
    在RuleExecutor的实现子类(如Analyzer和Optimizer)中会定义Batch,Once和FixedPoint。其中每个Batch代表着一套规则,这样可以简便地、模块化地对Tree进行Transform操作。Once和FixedPoint是配备策略。RuleExecutor内部有一个Seq[Batch]属性,定义的是该RuleExecutor的处理逻辑,具体的处理逻辑由具体的Rule子类实现。RuleExecutor中的apply方法会按照Batch顺序和Batch内的Rules顺序,对传入的节点进行迭代操作。
    在Analyzer过程中处理由解析器(SqlParser)生成的未绑定逻辑计划Tree时,就定义了多种Rules应用到该Unresolved逻辑计划Tree上。
    Analyzer过程中使用了自身定义的多个Batch,如MultiInstanceRelations,Resolution,CheckAnalysis和AnalysisOperators:每个Batch又由不同的Rules构成,每个Rule又有自己相对应的处理函数。注意,不同Rule的使用次数不同(Once FixedPoint)。

2.2.3 Spark SQL运行流程

  1.     将SQL语句通过词法和语法解析生成未绑定的逻辑执行计划(Unresolved LogicalPlan),包含Unresolved Relation、Unresolved Function和Unresolved Attribute,然后在后续步骤中使用不同的Rule应用到该逻辑计划上
  2. Analyzer使用Analysis Rules,配合元数据(如SessionCatalog 或是 Hive Metastore等)完善未绑定的逻辑计划的属性而转换成绑定的逻辑计划。具体流程是县实例化一个Simple Analyzer,然后遍历预定义好的Batch,通过父类Rule Executor的执行方法运行Batch里的Rules,每个Rule会对未绑定的逻辑计划进行处理,有些可以通过一次解析处理,有些需要多次迭代,迭代直到达到FixedPoint次数或前后两次的树结构没变化才停止操作。
  3. Optimizer使用Optimization Rules,将绑定的逻辑计划进行合并、列裁剪和过滤器下推等优化工作后生成优化的逻辑计划。
  4. Planner使用Planning Strategies,对优化的逻辑计划进行转换(Transform)生成可以执行的物理计划。根据过去的性能统计数据,选择最佳的物理执行计划CostModel,最后生成可以执行的物理执行计划树,得到SparkPlan。
  5. 在最终真正执行物理执行计划之前,还要进行preparations规则处理,最后调用SparkPlan的execute执行计算RDD。

三、SparkContext运行原理分析

    前面我们队Spark SQL运行架构进行分析,知道从输入SQL语句到生成Dataset分为5个步骤,但实际运行过程中在输入SQL语句之前,Spark还有加载SessionCatalog步骤。

3.1 使用SessionCatalog保存元数据

    在解析SQL语句之前需要初始化SQLContext,它定义了Spark SQL执行的上下文,并把元数据保存在SessionCatalog中,这些元数据包括表名称、表字段名称和字段类型等。
    SessionCatalog中保存的是表名和逻辑执行计划对应的哈希列表,这些数据将在解析未绑定的逻辑计划上使用。( SessionCatalog中的表名对应的逻辑执行计划是什么?是这个Dataset对应的逻辑执行计划)

3.2 使用Antlr生成未绑定的逻辑计划

    Spark 2.0版本起使用Antlr进行词法和语法解析。使用Antlr生成未绑定的逻辑计划分为两个阶段:第一阶段的过程为词法分析(Lexical Analysis),负责将符号(Token)分组成符号类(Token class or Token type),第二阶段就是真正的Parser,默认Antlr会构建出一颗分析树(Parser Tree)或者叫语法树(Syntax Tree)。
    SQLContext类中定义了SQL的解析方法parseSql。具体的SQL解析在AbastrctSqlParser抽象类中的parse方法进行,解析完毕后生成语法树,语法树会根据系统初始化的AstBuilder解析生成表达式、逻辑计划或表标识对象。
    在AbstractSqlParse的parse方法中,先实例化词法解析器SqlBaseLexer和语法解析器SqlBaseParser,然后尝试用Antlr较快的解析模式SLL,如果解析失败,则会再尝试使用普通解析模型LL,解析完毕后返回解析结果。

3.3 使用Analyzer绑定逻辑执行计划

    该阶段Analyzer使用Analysis Rules,结合SessionCatalog元数据,对未绑定的逻辑计划进行解析,生成了已绑定的逻辑计划。在该处理过程中,先实例化一个Analyzer,在Analyzer中定义了FiexedPoint和Seq[Batch]两个变量,其中FixedPoint为迭代次数的上线,而Seq[Batch]为所定义需要执行批处理的序列,每个批处理由一系列Rule和策略所组成,策略一般分为Once和FixedPoint(可理解为迭代次数)

3.4 使用Optimizer优化逻辑计划

    Optimizer的实现和处理方式同Analyzer类似,在该类中定义一系列Rule并同样继承于RuleExecutor。利用这些Rule对逻辑计划和Expression进行迭代处理,从而达到对树的节点进行合并和优化。其中主要的优化策略总结起来是合并、列裁剪和过滤器下推等几大类。
    Optimizer的优化策略不仅对已绑定的逻辑计划进行优化,而且对逻辑计划中的Expression也进行了优化,其原理就是遍历树,然后应用优化Rule,但是注意一点,对逻辑计划处理时先序遍历,而对Expression的处理是后续遍历 (根据树节点类型来判断是逻辑计划还是Expression吗?)

3.5 使用SparkPlanner生成可执行物理计划

    SparkPlanner使用Planning Strategies对优化的逻辑计划进行转换(Transform),生成可以执行的物理计划。在QueryExecution类代码中,调用SparkPlanner.plan方法对优化的逻辑计划进行处理,而SparkPlanner并未定义plan方法,实际是调用SparkPlanner的祖父类QueyrPlanner的plan方法,然后会返回一个Iterator[Physicalplan]。
    SparkPlanner继承于SparkStrategies,而SparkStrategies继承了QueryPlanner。其中SparkStrategies包含了一系列特定的Strategies,这些Strategies是继承自QueryPlanner中定义的GenericStrategy。在SparkPlanner通过改写祖父类QueryPlanner中strategies策略变量,在该变量中定义了转换成物理计划所执行的策略。

3.6 使用QueryExecution执行物理计划

    在该步骤中先调用SparkPlanner.preparations对物理计划进行准备工作,规范返回数据行的格式等,然后调用SparkPlan.execute执行物理计划,从数据库中查询数据并生成RDD。
    SparkPlan的preparations其实是一个RuleExecutor[SparkPlan],它会调用RuleExecutor的execut方法对前面生成的物理计划应用Rule进行匹配,最终生成一个SparkPlan。
    SparkPlan继承于QueryPlan,SparkPlan中定义了SQL语句执行的execute方法,执行完execute方法返回的是一个RDD,之后可以运行Spark作业对该RDD进行操作。

你可能感兴趣的:(spark学习)