Spark性能调优(原理篇)

1.开篇词 | 前言

2020年6月,Spark正式发布了新版本,从2.4直接跨越到了3.0。这次大版本升级的亮点就在于性能优化,它添加了诸如自适应查询执行(AQE)、动态分区裁剪(DPP)、扩展的Join Hints等特性。

Spark已经成为了各大头部互联网公司的标配,在海量数据处理上,扮演着不可获取的关键角色。 比如,字节跳动基于Spark构建的数据仓库去服务几乎所有的产品线,包括抖音、今日头条、西瓜视频、火山视频等。再比如,百度基于Spark推出BigSQL,为海量用户提供次秒级的即席查询。

可以预见的是,这次版本升级带来的新特性,会让Spark在未来5到10年继续雄霸大数据生态圈。

在日增数据量以TB、甚至PB为单位计数的当下,想要在小时级别完成海量数据处理,不做性能调优简直是天方夜谭。

性能导向的应用开发

遵循这套方法论,开发者可以按图索骥地去开展性能调优工作,做到有的放矢、事半功倍
性能导向的spark应用开发

专栏划分

结合方法论,专栏划分成3个部分:原理篇、性能篇和实战篇

原理篇:聚焦Spark底层原理。Spark的原理非常多,我们会聚焦那些和性能调优息息相关的核心概念,包括RDD、DAG、调度系统、存储系统和内存管理

性能篇: 性能篇分为两部分。
一部分讲解性能调优的通用技巧,包括应用开发的基本原则、配置项的设置、Shuffle的优化、以及资源利用率的提升。
另一部分会专注于数据分析领域、借助如Tungsten、AQE这样的Spark内置优化项和数据关联这样的典型场景,来聊聊Spark SQL中的调优方法和技巧。

2.开篇词 | 性能调优的抓手

尽管Spark自身运行高效,但是作为开发者,我们仍需要对应用进行性能调优。

但是性能调优应该从哪里切入呢?面对成百上千行代码,近百个Spark配置项,如何找到优化的抓手。

性能调优的本质

在ETL场景中,我们需要对数据进行各式各样的转换,有的时候,因为业务需求太复杂,我们往往还需要自定义UDF(User Defined Functions)来实现特定的转换逻辑。但是,无论是Databricks的官方博客,还是网上浩如烟海的Spark技术文章,都警告我们尽量不要用自定义UDF来实现业务逻辑,要尽可能的使用Spark内置的SQL Functions。

但是有时候我们花费了大量时间用SQL functions去替代自定义udf重构业务代码,却发现ETL作业端到端的执行性能并没有什么显著的提升。调优的时间没少花,却没啥效果

为什么用SQL Functions重构UDF没有像书本上说的那样奏效呢?

是因为这条建议不对吗?不是的。通过对比查询计划,我们能够明显的看到UDF与SQL Functions的区别。Spark SQL的Catalyst Optimizer能够明确的感知到SQL Functions每一步在做什么,因此有足够的优化空间。相反,UDF里面封装的计算逻辑对于Catalyst Optimizer来说就是个黑盒,除了把UDF塞到闭包里去,也没什么别的工作可做。

那么是因为UDF相比SQL Functions没有性能开销吗?也不是,实际上在单测中对比udf实现和sql functions实现的运行时间,通常情况下,UDF实现相比SQL Functions会慢3%-5%不等,UDF的性能开销还是有的。

原因是因为:UDF很可能不是最短的那块木板。

**根据木桶理论,最短的木板决定了木桶的容量。因此,对于一只有短板的木桶,其他木板调节的再高也无济于事。最短的木板才是木桶容量的瓶颈。**对于ETL应用这只木桶来说,UDF到SQL Functions的调优之所以对执行性能的影响微乎其微,根本原因在于他不是最短的那块木板。换句话说,ETL应用端到端执行性能的瓶颈不是开发者自定义的UDF。

结合上面的分析,性能调优的本质可以归纳为4点:

  • 性能调优不是一锤子买卖,补齐一个短板,其他板子可能会成为新的短板。因此,它是一个动态的、持续不断的过程。
  • 性能调优的手段和方法是否高效,取决于它针对的是木桶的长板还是瓶颈。针对瓶颈,事半功倍;针对长板,事倍功半。
  • 性能调优的方法和技巧,没有一定之规,也不是一成不变,随着木桶短板的此消彼长需要相应的动态切换。
  • 性能调优的过程收敛于一种所有木板齐平、没有瓶颈的状态。

所以我们就能解释,为什么一些网上的调优方法对于我们自己的任务效果不大,很有可能是这些优化方式没有触及到我们的瓶颈。

定位性能瓶颈的途径有哪些

我们可以通过运行时诊断来定位性能瓶颈。

运行时诊断的方法很多,比如:对于任务的执行情况,Spark UI提供了丰富的可视化面板,来展示DAG、Stages划分、执行计划、Executor负载均衡情况、GC时间、内存缓存消耗等等详尽的运行时状态数据;
对于硬件资源消耗,开发者可以利用Ganglia或者系统级监控工具,如top、vmstat、iostat、iftop等等来实时监测硬件的资源利用率;
特别的,针对GC开销,开发者可以将GC log导入到JVM可视化工具,从而一览任务执行过程中GC的频率和幅度。

性能调优的方法与手段

Spark的性能调优可以从应用代码和Spark配置项这2个层面展开

应用代码是指从代码开发的角度,如何以性能为导向进行应用开发。哪怕两份完全一致的代码,在性能上也会有很大的差异。因此我们需要知道,开发阶段有哪些常规操作、常见误区、从而尽量避免在代码中留下性能隐患。

Spark配置项,Spark官网上罗列了近百个配置项,看的人眼花缭乱。但并不是所有的配置项都和性能调优息息相关,因此我们需要对它们进行甄别、归类。

性能调优的终结

性能调优的本质告诉我们:性能调优是一个动态、持续不断的过程,在这个过程中,调优的手段需要随着瓶颈的此消彼长而相应的切换。那么问题就是,性能调优到底什么时候收敛?

性能调优的最终目的,是在所有参与计算的硬件资源之间寻求协同与平衡,让硬件资源达到一种平衡、无瓶颈的状态。
以大数据服务公司Qubole的案例为例,他们在Spark上集成机器学习框架XGBoost来进行模型训练,在相同的硬件资源、相同的数据源、相同的计算任务中对比不同配置下的执行性能。
从下表可以看出,执行性能最好的训练任务并不是把CPU利用率压榨到100%,以及把内存设置到最大的配置组合。而是哪些硬件资源配置最均衡的计算任务。
性能调优的终结

3.RDD

RDD为何如此重要

RDD作为Spark对于分布式数据模型的抽象,是构建Spark分布式内存计算引擎的基石。很多Spark核心概念与核心组件,如DAG和调度系统都衍生自RDD。因此,深入理解RDD有利于我们更全面、系统的学习Spark的工作原理。

深入理解RDD

从薯片生产过程理解 RDD

RDD,全称Resilient Distributed Datassets,翻译过来就是弹性数据集。本质上,它是对于数据模型的抽象,用于囊括所有内存中和磁盘中的分布式数据实体。
rdd举例
以上图,土豆生产过程为例。刚从地里挖出来的土豆、清洗后的土豆、生薯片、烤熟的薯片等,就像是Spark中RDD对于不同数据集合的抽象。
沿着流水线的方向,每一种食材形态都是在前一种食材之上用相同的加工方法进行处理的到的。每种食材形态都依赖于前一种食材,就像是RDD中dependencies属性记录的依赖关系(记录上一个RDD),而不同环节的加工方法,对应的刚好就是RDD的compute属性(作用在RDD上的计算函数,具体操作细节由用户传入)。

接下来我们从上到下的看,每一颗土豆就类似RDD的一个数据分片,3颗土豆一起对应的就是RDD的partitions属性。
带泥土豆经过清洗、切片和烘培之后,按照大小个被分发到下游的3条流水线上,这三条流水线上承载的RDD假设记为shuffledBackedChipsRDD。很明显,这个RDD对于partitions的划分是有讲究的,根据尺寸的不同,即食薯片会被划分到不同的数据分片中。像这种数据分片的划分规则,对应的就是RDD中的partitioner属性。
在分布式运行环境中,partitioner属性定义了RDD所封装的分布式数据集如何划分成数据分片。

总的来说,可以发现,薯片生产的流程和Spark分布式计算是一一对应的,可以总结为6点:

  • 土豆工坊的每条流水线就像是分布式环境中的计算节点(这里有3个)
  • 不同的食材形态,如带泥土豆、土豆切片、烘培的土豆片等等,对应的就是不同形态的RDD
  • 每一种食材形态都会依赖上一种形态,如烤熟的土豆片依赖上一个步骤的生土豆切片。这种依赖关系对应的就是RDD中的dependencies属性。
  • 不同环节的加工方法对应RDD的compute属性。
  • 同一种食材形态在不同流水线上的具体实物,就是RDD的partitions属性。
  • 食材按照什么规则配分配到哪条流水线,对应的就是RDD的partitioner属性。

接下来,我们来一本正经的聊聊RDD。

RDD的核心特征和属性

通过上面的例子,可以知道RDD具有4大属性,分别是partitions、partitioner、dependencies和compute属性。正因为有了这4大属性的存在,才让RDD具有分布式和容错性这两大最突出的特性。

partitions、partitioner属性(横向)

在分布式运行环境中,RDD封装的数据在物理上散落在不同计算节点的内存或者磁盘中。这些散落的数据被称为数据分片,RDD的分区规则决定了哪些数据分片应该散落到哪些节点中去。RDD只是这些实际数据的一个抽象集合,也就是说,RDD并不实际存储数据,它只是实际数据的抽象。RDD的partitions属性对应着RDD分布式数据实体中所有的数据分片,而partitioner属性则定义了划分数据分片的分区规则(默认是hashPartitioner,可选rangePartitioner,或者自己实现自定义partitioner),如按哈希取模或是按区间划分等。

partitions和partitioner属性刻画的是RDD在跨节点方向上的横向扩展,所以可以把他们看作是RDD的"横向属性"。

dependencies、compute属性(纵向)

在Spark中,任何一个RDD都不是凭空产生的,每个RDD都是基于一种计算逻辑从数据源or父RDD中转换而来的。RDD的dependencies属性记录了生成这个RDD父依赖(或父RDD),compute方法则封装了作用在当前RDD上的计算逻辑(具体的计算逻辑由用户实现)。

基于数据源和转换逻辑,无论RDD有什么差池(如节点宕机造成部分数据分片丢失),都可以通过该RDD的dependencies属性定位到其父RDD,然后通过在对其父RDD执行compute封装的计算逻辑再次得到当前的RDD。(注意RDD不保存数据,恢复数据也只能从checkpoint恢复,一般宽依赖会自动制作一个checkpoint)。
dependencies&compute
由dependencies和compute属性提供的容错能力,为Spark分布式内存计算的稳定性打下了坚实的基础。这也正是RDD命名中Resilient的由来。观察上图可以发现,不同的RDD通过dependencies和compute属性链接到一起,逐渐向纵深延展,构建了一张越来越深的有向无环图,这就是DAG。

由此可见,dependencies属性和compute属性负责RDD在纵深方向上的延展,因此可以把他们视为纵向属性。

总的来说,RDD的4大属性可以划分为两类:横向属性和纵向属性。其中,横向属性锚定数据分片实体,并规定了数据分片在分布式集群中如何分布;纵向属性用于在纵深方向构建DAG,通过提供重构RDD的容错能力保证内存计算的稳定性。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值