Spark性能调优之数据倾斜调优一站式解决方案原理与实战

来源:互联网 发布:total recall mac 编辑:程序博客网 时间:2024/06/15 07:03

第29章Spark性能调优之数据倾斜调优一站式解决方案原理与实战


29.1为什么说数据倾斜是分布式大数据系统的性能噩梦?

大数据有基本的三个特性:第一个是数据多样化,有着不同类型的数据,其中包括结构化和非结构化数据;第二个就是庞大的数据量;第三就是数据的流动性,从批处理到流处理。一般在处理大数据的时候都会面对这三个特性的问题,而Spark就是基于内存的分布式计算引擎,以处理高效和稳定著称,是目前处理大数据的一个非常好的选择。然而在实际的应用开发过程中,开发者还是会遇到种种问题,其中一大类就是和性能相关的。

在分布式系统中,数据分布在不同的节点上,每一个节点计算一部份数据,如果不对各个节点上独立的部份进行汇聚的话,我们是计算不到最终的结果。这就是因为我们需要利用分布式来发挥它本身并行计算的能力,而后续又需要计算各节点上最终的结果,所以需要把数据汇聚集中,这就会导致 Shuffle,而 Shuffle 又会导致数据倾斜。

数据倾斜杀人利器就是Out-Of-Memory(OOM),一般OOM都是由于数据倾斜所致!如果应用程序在运行时速度变的非常慢,这就有可能出现数据倾斜。它所带来的结果是原本程序可以在10分钟内运行完毕的程序,因为数据倾斜的原因,其中有一个任务要处理的数据特别的多,这个时候,当其他程序都运行完成时,就因为这个数据量特大的任务还在运行,导致这个程序原本可以用10分钟完成,最后用了1个小时。这极大的降低工作的效率!

所有编程高手级别的人无论做什么类型的编程,最终思考的都是硬件方面的问题!最终思考都是在一秒、一毫秒、甚至一纳秒到底是如何运行的,并且基于此进行算法实现和性能调优,最后都是回到了硬件!大数据最怕的就是数据本地性(内存中)和数据倾斜或者叫数据分布不均衡、数据转输的问题,这是所有分布式系统的问题!数据倾斜其实是跟业务紧密相关的。所以调优 Spark 的重点一定是在数据本地性和数据倾斜入手。

分布式计算引擎在调优方面有四个主要关注方向,分别是CPU、内存、网络开销和I/O,其具体调优目标如下:

·        提高CPU利用率。

·        避免OOM。

·        降低网络开销。

·        减少I/O操作。

因为Spark作业运行过程中,最消耗性能的地方就是Shuffle过程。Shuffle过程,简单来说,就是将分布在集群中多个节点上的同一个key,拉取到同一个节点上,进行聚合或join等操作。例如reduceByKey、join等算子,都会触发Shuffle操作。Shuffle过程中,各个节点上的相同key都会先写入本地磁盘文件中,然后其他节点需要通过网络传输拉取各个节点上的磁盘文件中的相同key。而且相同key都拉取到同一个节点进行聚合操作时,还有可能会因为一个节点上处理的key过多,导致内存不够存放,进而溢写到磁盘文件中。因此在Shuffle过程中,可能会发生大量的磁盘文件读写的IO操作,以及数据的网络传输操作。磁盘IO和网络数据传输也是Shuffle性能较差的主要原因。

如果实在是要做Shuffle,就要注意是否有数据倾斜的情况存在。出现数据倾斜的时候,Spark作业看起来会运行得非常缓慢,甚至可能因为某个Task处理的数据量过大导致内存溢出。

29.1.1什么是数据倾斜?

         何谓数据倾斜?数据倾斜是指并行处理数据集的某一部分(如Spark或Kafka的一个Partition)的数据显著多于其它部分,从而使得该部分的处理速度成为整个数据集处理的瓶颈。数据倾斜的基本特征:个别任务处理大量数据,20%,80%,基本上都存在业务热点问题,这是现实问题!

         举个例子:在Spark中,同一个Stage不同的Partition可以并行处理,而具体依赖关系不同Stage之间是串行处理的。假设某个Spark Job分为Stage 0和Stage 1两个Stage,且Stage 1依赖于Stage 0,那Stage 0完全处理结束之前不会处理Stage 1。而Stage 0可能包含N个Task,这N个Task可以并行进行。如果其中N-1个Task都在10秒内完成,而另外一个Task却耗时1分钟,那该Stage的总时间至少为1分钟。换句话说,一个Stage所耗费的时间,主要由最慢的那个Task决定,由于同一个Stage内的所有Task执行相同的计算,在排除不同计算节点计算能力差异的前提下,不同Task之间耗时的差异主要由该Task所处理的数据量决定。

 

         什么原因导致数据倾斜,原因很简单,数据分布给不同的 Task,一般就是 Shuffle 的过程,在 Shuffle 的过程中,同样一个 Key 一般都会交给一个 Task 去处理,可能有时候运气很不好,同样一个 Key 的 Value 太多了。假设上图的例子,有5个 Key,分别是 K1, K2, K3, K4, K5;同样的一个Key 会分成一个Task,现在 K3 中的 Value 特别多,这会导致很多数据集中在 K3 这个 Task 中。

数据倾斜的原理很简单:在进行Shuffle的时候,必须将各个节点上相同的key拉取到某个节点上的一个Task来进行处理,例如按照key进行聚合或join等操作。此时如果某个key对应的数据量特别大的话,就会发生数据倾斜。例如大部分key对应10条数据,但是个别key却对应了100万条数据,那么大部分Task可能就只会分配到10条数据,然后1秒钟就运行完了;但是个别Task可能分配到了100万数据,要运行一两个小时。因此,整个Spark作业的运行进度是由运行时间最长的那个Task决定的。

29.1.2 数据倾斜的对性能的巨大影响

下图是数据倾斜示意图:hello这个key,在三个节点上对应了总共7条数据,这些数据都会被拉取到同一个Task中进行处理;而world和you这两个key分别才对应1条数据,所以另外两个Task只要分别处理1条数据即可。此时第一个Task的运行时间可能是另外两个Task的7倍,而整个Stage的运行速度也由运行最慢的那个Task所决定。

 

29.1.3如何判断Spark程序运行中出现了数据倾斜?

绝大多数Task执行得都非常快,但个别Task执行极慢。例如,总共有1000个Task,997个Task都在1分钟之内执行完了,但是剩余两三个Task却要一两个小时。这种情况很常见。原本能够正常执行的Spark作业,某天突然报出OOM(内存溢出)异常,观察异常栈,是我们写的业务代码造成的。这种情况比较少见。

我们都可以在Spark Web UI上深入看一下当前这个Stage各个Task分配的数据量,从而进一步确定是不是Task分配的数据不均匀导致了数据倾斜。例如下图中,倒数第三列显示了每个Task的运行时间。明显可以看到,有的Task运行特别快,只需要几秒钟就可以运行完;而有的Task运行特别慢,需要几分钟才能运行完,此时单从运行时间上看就已经能够确定发生数据倾斜了。此外,倒数第一列显示了每个Task处理的数据量,明显可以看到,运行时间特别短的Task只需要处理几百KB的数据即可,而运行时间特别长的Task需要处理几千KB的数据,处理的数据量差了10倍。此时更加能够确定是发生了数据倾斜。

 

 知道数据倾斜发生在哪一个Stage之后,接着我们就需要根据Stage划分原理,推算出来发生倾斜的那个Stage对应代码中的哪一部分,这部分代码中肯定会有一个Shuffle类算子。精准推算Stage与代码的对应关系,需要对Spark的源码有深入的理解,这里我们可以介绍一个相对简单实用的推算方法:只要看到Spark代码中出现了一个Shuffle类算子或者是Spark SQL的SQL语句中出现了会导致Shuffle的语句(例如group by语句),那么就可以判定,以那个地方为界限划分出了前后两个Stage。

数据倾斜只会发生在Shuffle过程中。一些常用的并且可能会触发Shuffle操作的算子:distinct、groupByKey、reduceByKey、AggregateByKey、join、cogroup、rePartition等。出现数据倾斜时,可能就是业务代码中使用了这些算子中的某一个所导致的。

29.1.4如何定位数据倾斜? 

我们如何定位数据倾斜:

l  Spark WebUI页面,可以清晰的看见Task 运行的数据量大小。

l  Log日志:Log的一个好处可以清晰的显示哪一行出现OOM问题,同时可以清晰的看到在具体哪个Stage 出现数据倾斜(数据倾斜一般是在Shuffle 过程中产生的),从而定位具体Shuffle的代码, 也可能发现绝大多数Task 非常快,但是个别Task 非常慢。

l  代码走读,重点看 join、groupByKey、reduceByKey等的关键代码。

l  对数据特征分布进行分析。

 

总结一下数据倾斜有以下几个方案:

1.      复用 RDD,最小化每个 Job 的工作。

2.      把 Reducer 的操作放在 Mapper 端,可以采用aggregrateByKey和combinedByKey

3.      当两个数据需要 Join 的话,需要把小数据的那一边进行 Broadcast,如果 Broadcast 的数据还是很大的话,也可能会出现 OOM 的情况

4.      可以把倾斜的 Key 通过采样的方式提取出来,RDD1和 RDD2 进行 Join 操作,其中我们采用采样的方式发现RDD1 中有严重的数据斜倾的 Key:

5.      把倾斜的 Key 加上随机数

6.      集群进行扩容

7.      并行度的改变

 

Spark数据倾斜的场景:

(1)      某个Task执行特别慢的情况:

首先要看的,就是数据倾斜发生在第几个Stage中。如果是用YARN-Client模式提交,那么本地是直接可以看到Log的,可以在Log中找到当前运行到了第几个Stage;如果是用YARN-Cluster模式提交,则可以通过Spark Web UI来查看当前运行到了第几个Stage。此外,无论是使用YARN-Client模式还是YARN-Cluster模式,我们都可以在Spark Web UI上深入看一下当前这个Stage各个Task分配的数据量,从而进一步确定是不是Task分配的数据不均匀导致了数据倾斜。

  在Spark WebUI的Task监控页面中,倒数第三列显示了每个Task的运行时间,如果有的Task运行特别快,只需要几秒钟就可以运行完;而有的Task运行特别慢,需要几分钟才能运行完,此时单从运行时间上看就已经能够确定发生数据倾斜了。此外,倒数第一列显示了每个Task处理的数据量,如果运行时间特别短的Task只需要处理几百KB的数据,而运行时间特别长的Task需要处理几千KB的数据,处理的数据量差了10倍。此时更加能够确定是发生了数据倾斜。

知道数据倾斜发生在哪一个Stage之后,接着我们就需要根据Stage划分原理,推算出来发生倾斜的那个Stage对应代码中的哪一部分,这部分代码中肯定会有一个Shuffle类算子。精准推算Stage与代码的对应关系,需要对Spark的源码有深入的理解,这里我们可以介绍一个相对简单实用的推算方法:只要看到Spark代码中出现了一个Shuffle类算子或者是Spark SQL的SQL语句中出现了会导致Shuffle的语句(例如group by语句),那么就可以判定,以那个地方为界限划分出了前后两个Stage。

这里我们就以Spark最基础的入门程序——单词计数来举例,如何用最简单的方法大致推算出一个Stage对应的代码。

WordCount.scala代码如下:

1.         val conf = new SparkConf()

2.         val sc = new SparkContext(conf)

3.         val lines =sc.textFile("hdfs://...")

4.         val words =lines.flatMap(_.split(" "))

5.         val pairs = words.map((_, 1))

6.         val wordCounts =pairs.reduceByKey(_ + _)

7.         wordCounts.collect().foreach(println(_))

 

在WordCount整个代码中,只有一个reduceByKey是会发生Shuffle的算子,因此就可以认为,以这个算子为界限,会划分出前后两个Stage。

  1、Stage0,主要是执行从textFile到map操作,以及执行Shuffle write操作。Shuffle write操作,我们可以简单理解为对pairs RDD中的数据进行分区操作,每个Task处理的数据中相同的key会写入同一个磁盘文件内。

  2、Stage1,主要是执行从reduceByKey到collect操作,Stage1的各个Task一开始运行,就会首先执行Shuffle read操作。执行Shuffle read操作的Task,会从Stage0的各个Task所在节点拉取属于自己处理的那些key,然后对同一个key进行全局性的聚合或join等操作,在这里就是对key的value值进行累加。Stage1在执行完reduceByKey算子之后,就计算出了最终的wordCounts RDD,然后会执行collect算子,将所有数据拉取到Driver上,供我们遍历和打印输出。

通过对单词计数程序的分析,我们可以了解最基本的Stage划分的原理,以及Stage划分后Shuffle操作是如何在两个Stage的边界处执行的,然后我们就知道如何快速定位出发生数据倾斜的Stage对应代码的哪一个部分了。例如我们在Spark Web UI或者本地Log中发现,Stage1的某几个Task执行得特别慢,判定Stage1出现了数据倾斜,那么就可以回到代码中定位出Stage1主要包括了reduceByKey这个Shuffle类算子,此时基本就可以确定是由reduceByKey算子导致的数据倾斜问题。例如某个单词出现了100万次,其他单词才出现10次,那么Stage1的某个Task就要处理100万数据,整个Stage的速度就会被这个Task拖慢。

 

(2) 某个Task莫名其妙内存溢出的情况:

  这种情况下定位出问题的代码比较容易。我们建议直接看YARN-Client模式下本地Log的异常栈,或者是通过YARN查看YARN-Cluster模式下的Log中的异常栈。一般来说,通过异常栈信息就可以定位到你的代码中哪一行发生了内存溢出。然后在那行代码附近找找,一般也会有Shuffle类算子,此时很可能就是这个算子导致了数据倾斜。

  但是要注意的是,不能单纯靠偶然的内存溢出就判定发生了数据倾斜。因为自己编写的代码的bug,以及偶然出现的数据异常,也可能会导致内存溢出。因此还是要按照上面所讲的方法,通过Spark Web UI查看报错的那个Stage的各个Task的运行时间以及分配的数据量,才能确定是否是由于数据倾斜才导致了这次内存溢出。

查看导致数据倾斜的key的数据分布情况

 

  知道了数据倾斜发生在哪里之后,通常需要分析一下那个执行了Shuffle操作并且导致了数据倾斜的RDD/Hive表,查看一下其中key的分布情况。这主要是为之后选择哪一种技术方案提供依据。针对不同的key分布与不同的Shuffle算子组合起来的各种情况,可能需要选择不同的技术方案来解决。

  此时根据执行操作的情况不同,可以有很多种查看key分布的方式:

  1、如果是Spark SQL中的group by、join语句导致的数据倾斜,那么就查询一下SQL中使用的表的key分布情况。

  2、如果是对Spark RDD执行Shuffle算子导致的数据倾斜,那么可以在Spark作业中加入查看key分布的代码,例如RDD.countByKey()。然后对统计出来的各个key出现的次数,collect/take到客户端打印一下,就可以看到key的分布情况。

 

  举例来说,对于上面所说的单词计数程序,如果确定了是Stage1的reduceByKey算子导致了数据倾斜,那么就应该看看进行reduceByKey操作的RDD中的key分布情况,在这个例子中指的就是pairs RDD。如下示例,我们可以先对pairs采样10%的样本数据,然后使用countByKey算子统计出每个key出现的次数,最后在客户端遍历和打印样本数据中各个key的出现次数。

1.         val sampledPairs =pairs.sample(false, 0.1)

2.         val sampledWordCounts =sampledPairs.countByKey()

3.         sampledWordCounts.foreach(println(_))

原创粉丝点击