草庐IT

袋鼠云数栈基于CBO在Spark SQL优化上的探索

袋鼠云数栈 2023-03-28 原文

原文链接:袋鼠云数栈基于CBO在Spark SQL优化上的探索

一、Spark SQL CBO选型背景

Spark SQL的优化器有两种优化方式:一种是基于规则的优化方式(Rule-Based Optimizer,简称为RBO);另一种是基于代价的优化方式(Cost-Based Optimizer,简称为CBO)。

1、RBO是传统的SQL优化技术

RBO是发展比较早且比较成熟的一项SQL优化技术,它按照制定好的一系列优化规则对SQL语法表达式进行转换,最终生成一个最优的执行计划。RBO属于一种经验式的优化方法,严格按照既定的规则顺序进行匹配,所以不同的SQL写法直接决定执行效率不同。且RBO对数据不敏感,在表大小固定的情况下,无论中间结果数据怎么变化,只要SQL保持不变,生成的执行计划就都是固定的。

2、CBO是RBO改进演化的优化方式

CBO是对RBO改进演化的优化方式,它能根据优化规则对关系表达式进行转换,生成多个执行计划,在根据统计信息(Statistics)和代价模型(Cost Model)计算得出代价最小的物理执行计划。

3、 CBO与RBO优势对比

● RBO优化例子

下面我们来看一个例子:计算t1表(大小为:2G)和t2表(大小为:1.8G)join后的行数


file

上图是:

SELECT COUNT(t1.id) FROM t1 JOIN t2 ON t1.id = t2.id WHERE t1.age > 24

基于RBO优化后生成的物理执行计划图。在图中我们可以看出,执行计划最后是选用SortMergeJoin ⑴ 进行两个表join的。

在Spark中,join的实现有三种:

1.Broadcast Join

2.ShuffleHash Join

3.SortMerge Join

ShuffleHash Join和SortMerge Join都需要shuffle,相对Broadcast Join来说代价要大很多,如果选用Broadcast Join则需要满足有一张表的大小是小于等于

spark.sql.autoBroadcastJoinThreshold 的大小(默认为10M)。

而我们再看,上图的执行计划t1表,原表大小2G过滤后10M,t2表原表大小1.8G过滤后1.5G。这说明RBO优化器不关心中间数据的变化,仅根据原表大小进行join的选择了SortMergeJoin作为最终的join,显然这得到的执行计划不是最优的。

● CBO优化例子

而使用CBO优化器得到的执行计划图如下:


file

我们不难看出,CBO优化器充分考虑到中间结果,感知到中间结果的变化满足能Broadcast Join的条件,所以生成的最终执行计划会选择Broadcast Join来进行两个表join。

● 其他优势

其实除了刻板的执行导致不能得到最优解的问题,RBO还有学习成本高的问题:开发人员需要熟悉大部分优化规则,否则写出来的SQL性能可能会很差。

● CBO是数栈Spark SQL 优化的更佳选择

相对于RBO,CBO无疑是更好的选择,它使Spark SQL的性能提升上了一个新台阶,Spark作为数栈平台底层非常重要的组件之一,承载着离线开发平台上大部分任务,做好Spark的优化也将推动着数栈在使用上更加高效易用。所以数栈选择CBO做研究探索,由此进一步提高数栈产品性能。

二、Spark SQL CBO实现原理

Spark SQL中实现CBO的步骤分为两大部分,第一部分是统计信息收集,第二部分是成本估算:

1、统计信息收集

统计信息收集分为两个部分:第一部分是原始表信息统计、第二部分是中间算子的信息统计。

1)原始表信息统计

Spark中,通过增加新的SQL语法ANALYZE TABLE来用于统计原始表信息。原始表统计信息分为表级别和列级别两大类,具体的执行如下所示:

● 表级别统计信息

通过执行 ANALYZE TABLE table_name COMPUTE STATISTICS 语句来收集,统计指标包括estimatedSize解压后数据的大小、rowCount数据总条数等。

● 列级别统计信息

通过执行 ANALYZE TABLE table_name COMPUTE STATISTICS FOR COLUMNS column-name1, column-name2, …. 语句来收集。

列级别的信息又分为基本列信息和直方图,基本列信息包括列类型、Max、Min、number of nulls, number of distinct values, max column length, average column length等,直方图描述了数据的分布。Spark默认没有开启直方图统计,需要额外设置参数:spark.sql.statistics.histogram.enabled = true。

原始表的信息统计相对简单,推算中间节点的统计信息相对就复杂一些,并且不同的算子会有不同的推算规则,在Spark中算子有很多,有兴趣的同学可以看Spark SQL CBO设计文档:

https://issues.apache.org/jira/secure/attachment/12823839/Spark_CBO_Design_Spec.pdf

2)中间算子的信息统计

我们这里以常见的filter算子为例,看看推算算子统计信息的过程。基于上一节的SQL SELECT COUNT(t1.id) FROM t1 JOIN t2 ON t1.id = t2.id WHERE t1.age > 24生成的语法树来看下t1表中包含大于运算符 filter节点的统计信息。
图片

在这里需要分三种情况考虑:

第一种

过滤条件常数值大于max(t1.age),返回结果为0;

第二种

过滤条件常数值小于min(t1.age),则全部返回;

第三种

过滤条件常数介于min(t1.age)和max(t1.age)之间,当没有开启直方图时过滤后统计信息的公式为after_filter = (max(t1.age) - 过滤条件常数24)/(max(t1.age) – min(t1.age)) * before_filter,没有开启直方图则默认任务数据分布是均匀的;当开启直方图时过滤后统计信息公式为after_filter = height(>24) / height(All) * before_filter。然后将该节点min(t1.age)等于过滤条件常数24。

2、成本估算

介绍完如何统计原始表的统计信息和如何计算中间算子的统计信息,有了这些信息后就可以计算每个节点的代价成本了。

在介绍如何计算节点成本之前我们先介绍一些成本参数的含义,如下:

Hr: 从 HDFS 读取 1 个字节的成本
    
    Hw: 从 HDFS 写1 个字节的成本

NEt: 在 Spark 集群中通过网络从任何节点传输 1 个字节到 目标节点的平均成本

Tr: 数据总条数

Tsz: 数据平均大小

CPUc: CPU 成本

计算节点成本会从IO和CPU两个维度考虑,每个算子成本的计算规则不一样,我们通过join算子来举例说明如何计算算子的成本:

假设join是Broadcast Join,大表分布在n个节点上,那么CPU代价和IO代价计算公式分别如下:

CPU Cost=小表构建Hash Table的成本 + 大表探测的成本 = Tr(Rsmall) * CPUc + (Tr(R1) + Tr(R2) + … + Tr(Rn)) * n * CPUc

IO Cost =读取小表的成本 + 小表广播的成本 + 读取大表的成本 = Tr(Rsmall) * Tsz(Rsmall) * Hr + n * Tr(Rsmall) * Tsz(Rsmall) * NEt + (Tr(R1)* Tsz(R1) + … + Tr(Rn) * Tsz(Rn)) * Hr

但是无论哪种算子,成本计算都和参与的数据总条数、数据平均大小等因素直接相关,这也是为什么在这之前要先介绍如何统计原表信息和推算中间算子的统计信息。

每个算子根据定义的规则计算出成本,每个算子成本相加便是整个执行计划的总成本,在这里我们可以考虑一个问题,最优执行计划是列举每个执行计划一个个算出每个的总成本得出来的吗?显然不是的,如果每个执行计划都计算一次总代价,那估计黄花菜都要凉了,Spark巧妙的使用了动态规划的思想,快速得出了最优的执行计划。

三、数栈在Spark SQL CBO上的探索

了解完Spark SQL CBO的实现原理之后,我们来思考一下第一个问题:大数据平台想要实现支持Spark SQL CBO优化的话,需要做些什么?

在前文实现原理中我们提到,Spark SQL CBO的实现分为两步,第一步是统计信息收集,第二步是成本估算。而统计信息收集又分为两步:第一步的原始表信息统计、第二步中间算子的信息统计。到这里我们找到了第一个问题的答案:平台中需要先有原始表信息统计的功能。

第一个问题解决后,我们需要思考第二个问题:什么时候进行表信息统计比较合适?针对这个问题,我们初步设想了三种解决信息统计的方案:

● 在每次SQL查询前,先进行一次表信息统计

这种方式得到的统计信息比较准确,经过CBO优化后得出的执行计划也是最优的,但是信息统计的代价最大。

● 定期刷新表统计信息

每次SQL查询前不需要进行表信息统计,因为业务数据更新的不确定性,所以这种方式进行SQL查询时得到的表统计信息可能不是最新的,那么CBO优化后得到的执行计划有可能不是最优的。

● 在变更数据的业务方执行信息统计

这种方式对于信息统计的代价是最小的,也能保证CBO优化得到的执行计划是最优的,但是对于业务代码的侵入性是最大的。

不难看出三种方案各有利弊,所以进行表信息统计的具体方案取决于平台本身的架构设计。

基于数栈平台建设数仓的结构图如下图所示:


file
首先通过ChunJun将业务数据库数据采集到Hive ODS层

然后通过Hive或者Spark进行数据处理

最后通过ChunJun将Hive库的数据写入到业务数据库用于业务处理

从结构图可看出数栈有用到Hive、Spark和ChunJun三个组件,并且这三个组件都会读写Hive, 数栈多个子产品(如离线平台和实时平台)也都有可能对Hive进行读写,所以如果基于方案3来做成本是非常高的。

方案1本身代价就已经较大,每次查询前都进行一次信息统计,信息统计的时间是要算在本次查询耗时中的,如果表数据量比较大增加的时间可能是十几分钟甚至更久。

综合考虑,我们选用了更灵活合理的方案2来进行表信息统计。虽然Spark SQL运行时得到的统计信息可能不是最新的,但是总体相比较RBO来说还是有很大的性能提升。

接下来就为大家分享,数栈是如何如何统计收集原表信息统计:

我们在离线平台项目管理页面上添加了表信息统计功能,保证了每个项目可以根据项目本身情况配置不同的触发策略。触发策略可配置按天或者按小时触发,按天触发支持配置到从当天的某一时刻触发,从而避开业务高峰期。配置完毕后,到了触发的时刻离线平台就会自动以项目为单位提交一个Spark任务来统计项目表信息。

在数栈没有实现CBO支持之前,Spark SQL的优化只能通过调整Spark本身的参数实现。这种调优方式很高的准入门槛,需要使用者比较熟悉Spark的原理。数栈CBO的引入大大降低了使用者的学习门槛,用户只需要在Spark Conf中开启

CBO-spark.sql.cbo.enabled=true

然后在对应项目中配置好表信息统计就可以做到SQL优化了。

四、未来展望

在CBO优化方面持续投入研究后,Spark SQL CBO整体相比较RBO而言已经有了很大的性能提升。但这并不说明整个操作系统就没有优化的空间了,已经拿到的进步只会鼓舞我们继续进行更深层次的探索,努力往前再迈一步。

完成对CBO的初步支持探索后,数栈把目光看向了Spark 3.0 版本引入的新特性——AQE(Adaptive Query Execution)。

AQE是动态CBO的优化方式,是在CBO基础上对SQL优化技术又一次的性能提升。如前文所说,CBO目前的计算对前置的原始表信息统计是仍有依赖的,而且信息统计过时的情况会给CBO带来不小的影响。

如果在运行时动态的优化 SQL 执行计划,就不再需要像CBO那样需要提前做表信息统计。数栈正在针对这一个新特性进行,相信不久的将来我们就能引入AQE,让数栈在易用性高性能方面更上一层楼。希望小伙伴们保持关注,数栈愿和大家一起成长。

原文来源:VX公众号“数栈研习社”
袋鼠云开源框架钉钉技术交流群(30537511),欢迎对大数据开源项目有兴趣的同学加入交流最新技术信息,开源项目库地址:https://github.com/DTStack

有关袋鼠云数栈基于CBO在Spark SQL优化上的探索的更多相关文章

  1. ruby-on-rails - date_field_tag,如何设置默认日期? [ rails 上的 ruby ] - 2

    我想设置一个默认日期,例如实际日期,我该如何设置?还有如何在组合框中设置默认值顺便问一下,date_field_tag和date_field之间有什么区别? 最佳答案 试试这个:将默认日期作为第二个参数传递。youcorrectlysetthedefaultvalueofcomboboxasshowninyourquestion. 关于ruby-on-rails-date_field_tag,如何设置默认日期?[rails上的ruby],我们在StackOverflow上找到一个类似的问

  2. ruby-on-rails - openshift 上的 rails 控制台 - 2

    我将我的Rails应用程序部署到OpenShift,它运行良好,但我无法在生产服务器上运行“Rails控制台”。它给了我这个错误。我该如何解决这个问题?我尝试更新ruby​​gems,但它也给出了权限被拒绝的错误,我也无法做到。railsc错误:Warning:You'reusingRubygems1.8.24withSpring.UpgradetoatleastRubygems2.1.0andrun`gempristine--all`forbetterstartupperformance./opt/rh/ruby193/root/usr/share/rubygems/rubygems

  3. ruby-on-rails - 相关表上的范围为 "WHERE ... LIKE" - 2

    我正在尝试从Postgresql表(table1)中获取数据,该表由另一个相关表(property)的字段(table2)过滤。在纯SQL中,我会这样编写查询:SELECT*FROMtable1JOINtable2USING(table2_id)WHEREtable2.propertyLIKE'query%'这工作正常:scope:my_scope,->(query){includes(:table2).where("table2.property":query)}但我真正需要的是使用LIKE运算符进行过滤,而不是严格相等。然而,这是行不通的:scope:my_scope,->(que

  4. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  5. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  6. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  7. ruby-on-rails - Ruby - 如何从 ruby​​ 上的 .pfx 文件中提取公钥、rsa 私钥和 CA key - 2

    我有一个.pfx格式的证书,我需要使用ruby​​提取公共(public)、私有(private)和CA证书。使用shell我可以这样做:#ExtractPublicKey(askforpassword)opensslpkcs12-infile.pfx-outfile_public.pem-clcerts-nokeys#ExtractCertificateAuthorityKey(askforpassword)opensslpkcs12-infile.pfx-outfile_ca.pem-cacerts-nokeys#ExtractPrivateKey(askforpassword)o

  8. 带有 attr_accessor 的类上的 Ruby instance_eval - 2

    我了解instance_eval和class_eval之间的基本区别。我在玩弄时发现的是一些涉及attr_accessor的奇怪东西。这是一个例子:A=Class.newA.class_eval{attr_accessor:x}a=A.newa.x="x"a.x=>"x"#...expectedA.instance_eval{attr_accessor:y}A.y="y"=>NoMethodError:undefinedmethod`y='forA:Classa.y="y"=>"y"#WHATTT?这是怎么回事:instance_eval没有访问我们的A类(对象)然后它实际上将它添加到

  9. ruby-on-rails - rails 上的 ruby : radio buttons for collection select - 2

    我有一个集合选择:此方法的单选按钮是什么?谢谢 最佳答案 Rails3中没有这样的助手。在Rails4中,它是collection_radio_buttons. 关于ruby-on-rails-rails上的ruby:radiobuttonsforcollectionselect,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/18525986/

  10. ruby - 将命令行上的变量传递给 Cucumber 测试 - 2

    我正在尝试将cucumber项目的用户名和密码置于版本控制之外。有没有办法在命令行上手动将用户名和密码等变量传递给Cucumber脚本?我的备份计划是将它们放在一个YML文件中,然后将该文件添加到gitignore,这样它们就不会被置于版本控制中。 最佳答案 所以,我看到了您对铁皮人的评论,答案是肯定的。cucumberPASSWORD=my_passwordPASSWORD被设置为环境变量,您可以通过将其引用为ENV['PASSWORD']来使用它的值。例如,browser.text_field(:id=>'pwd').setEN

随机推荐