Spark2.0-RDD分区原理分析

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zg_hover/article/details/73476265

Spark分区原理分析

介绍

分区是指如何把RDD分布在spark集群的各个节点的操作。以及一个RDD能够分多少个分区。

一个分区是大型分布式数据集的逻辑块。

那么思考一下:分区数如何映射到spark的任务数?如何验证?分区和任务如何对应到本地的数据?

Spark使用分区来管理数据,这些分区有助于并行化分布式数据处理,并以最少的网络流量在executors之间发送数据。

默认情况下,Spark尝试从靠近它的节点读取数据到RDD。由于Spark通常访问分布式分区数据,为了优化transformation(转换)操作,它创建分区来保存数据块。

存在在HDFS或Cassandra中的分区数据是一一对应的(由于相同的原因进行分区)。

默认情况下,每个HDFS的分区文件(默认分区文件块大小是64M)都会创建一个RDD分区。

默认情况下,不需要程序员干预,RDD会自动进行分区。但有时候你需要为你的应用程序,调整分区的大小,或者使用另一种分区方案。

你可以通过方法def getPartitions: Array[Partition]来获取RDD的分区数量。

在spark-shell中执行以下代码:

val v = sc.parallelize(1 to 100)
scala> v.getNumPartitions
res2: Int = 20 //RDD的分区数是20?why? 原因在后面讲解。

一般来说分区数和任务数是相等的。以上代码可以看到分区是20个,再从spark管理界面上看,有20个任务。

可以通过参数指定RDD的分区数:

val v = sc.parallelize(1 to 100, 2)
scala> v.getNumPartitions
res2: Int = 2 //RDD的分区数是2

可以看出,指定了分区数量以后,输出的是指定的分区数。通过界面上看,只有2个任务。

分区大小对Spark性能的影响

分区块越小,分区数量就会越多。分区数据就会分布在更多的worker节点上。但分区越多意味着处理分区的计算任务越多,太大的分区数量(任务数量)可能是导致Spark任务运行效率低下的原因之一。

所以,太大或太小的分区都有可能导致Spark任务执行效率低下。那么,应该如何设置RDD的分区?

Spark只能为RDD的每个分区运行1个并发任务,直到达到Spark集群的CPU数量。
所以,如果你有一个拥有50个CPU的Spark集群,那么你可以让RDD至少有50个分区(或者是CPU数量的2到3倍)。

一个比较好的分区数的值至少是executors的数量。可以通过参数设置RDD的默认分区数,也就是我们所说的并行度:
sc.defaultParallelism
上一节中,当没有设定分区时,在我的Spark集群中默认的分区数是20,是因为在Spark默认配置文件:conf/spark-defaults.conf中我设置了变量:
spark.default.parallelism 20

同样,RDD的action函数产生的输出文件数量,也是由分区的数量来决定的。

分区数量的上限,取决于executor的可用内存大小。

RDD执行的第一个transformation函数的分区数量,决定了在该RDD上执行的后续一系列处理过程的分区数量。例如从hdfs读取数据的函数:
sc.textFile(path, partition)

当使用函数 rdd = SparkContext().textFile("hdfs://…​/file.txt")时,你得到的分区数量可能很少,这将会和HDFS的块的多少相等。但当你的文件中的行比较大时,得到的分区可能更少。

你也可以通过textFile函数的第2个参数指定读取的分区数量,但该分区数量:
sc.textFile("hdfs://host:port/path", 200)
这样读取path的文件后,会生成200个分区。
注意:第2个参数指定的分区数,必须大于等于2。

注意:以上描述只是对非压缩文件适用,对于压缩文件不能在textFile中指定分区数,而是要进行repartition:

rdd = sc.textFile('demo.gz')
rdd = rdd.repartition(100)

一些函数,例如:map,flatMap,filter不会保留分区。会把每个函数应用到每一个分区上。

RDD的Repartition

函数的定义定义如下:

/**
* Return a new RDD that has exactly numPartitions partitions.
* Can increase or decrease the level of parallelism in this RDD. Internally, this uses a shuffle to redistribute data.
* If you are decreasing the number of partitions in this RDD, consider using coalesce, which can avoid performing a shuffle.
*/
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T] = withScope {
coalesce(numPartitions, shuffle = true)
}

从代码上可以看到,repartition是shuffle和numPartitions分区的合并操作。

若分区策略不符合你的应用场景,你可以编写自己的Partitioner。

coalesce 转换

改函数的代码如下:

/**
* Return a new RDD that is reduced into numPartitions partitions.
*
* This results in a narrow dependency, e.g. if you go from 1000 partitions
* to 100 partitions, there will not be a shuffle, instead each of the 100
* new partitions will claim 10 of the current partitions.
*
* However, if you’re doing a drastic coalesce, e.g. to numPartitions = 1,
* this may result in your computation taking place on fewer nodes than
* you like (e.g. one node in the case of numPartitions = 1). To avoid this,
* you can pass shuffle = true. This will add a shuffle step, but means the
* current upstream partitions will be executed in parallel (per whatever
* the current partitioning is).
*
* Note: With shuffle = true, you can actually coalesce to a larger number
* of partitions. This is useful if you have a small number of partitions,
* say 100, potentially with a few partitions being abnormally large. Calling
* coalesce(1000, shuffle = true) will result in 1000 partitions with the
* data distributed using a hash partitioner.
*/
def coalesce(numPartitions: Int, shuffle: Boolean = false,
partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
(implicit ord: Ordering[T] = null)
: RDD[T] = withScope {
… …
}

coalesce转换用于更改分区数。它可以根据shuffle标志触发RDD shuffle(默认情况下禁用shuffle,即为false)

从以上代码注释可以看出:该函数是一个合并分区的操作,一般该函数用来进行narrow转换。为了让该函数并行执行,通常把shuffle的值设置成true。

coalesce使用举例


scala> val rdd = sc.parallelize(0 to 10, 8)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at :24

scala> rdd.partitions.size
res0: Int = 8

scala> rdd.coalesce(numPartitions=8, shuffle=false) (1)
res1: org.apache.spark.rdd.RDD[Int] = CoalescedRDD[1] at coalesce at :27

scala> res1.toDebugString
res2: String =
(8) CoalescedRDD[1] at coalesce at :27 []
| ParallelCollectionRDD[0] at parallelize at :24 []

scala> rdd.coalesce(numPartitions=8, shuffle=true)
res3: org.apache.spark.rdd.RDD[Int] = MapPartitionsRDD[5] at coalesce at :27

scala> res3.toDebugString
res4: String =
(8) MapPartitionsRDD[5] at coalesce at :27 []
| CoalescedRDD[4] at coalesce at :27 []
| ShuffledRDD[3] at coalesce at :27 []
+-(8) MapPartitionsRDD[2] at coalesce at :27 []
| ParallelCollectionRDD[0] at parallelize at :24 []

注意:

  • 默认情况下coalesce是不会进行shuffle。
  • 另外,分区数和源RDD的分区数保持一致。

分区相关参数

spark.default.parallelism

设置要用于HashPartitioner的分区数。它对应于调度程序后端的默认并行度。
它也和以下几个数量对应:

  • LocalSchedulerBackend是spark本地执行的调度器,此时,该参数的数量是,本地JVM的线程数。

本地模式的默认并行度的设置源码如下:

case LOCAL_N_REGEX(threads) =>
def localCpuCount: Int = Runtime.getRuntime.availableProcessors()
// local[*] estimates the number of cores on the machine; local[N] uses exactly N threads.
val threadCount = if (threads == "*") localCpuCount else threads.toInt

  • Spark on Mesos的CPU数量,默认是8.
  • 总CPU数:totalCoreCount,在CoarseGrainedSchedulerBackend 是2。

如何查看RDD的分区

通过UI查看使用分区的任务执行

启动spark-shell执行以下命令:

scala> val someRDD = sc.parallelize(1 to 100, 4)
someRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[6] at parallelize at :27

scala> someRDD.map(x => x).collect
17/06/20 07:37:54 INFO spark.SparkContext: Starting job: collect at console:30
… …

再通过spark管理界面查看任务执行情况:
这里写图片描述

通过UI查看Partition Caching

在终端的spark-shell下执行以下命令:

scala> someRDD.setName("toy").cache
scala> someRDD.map(x => x).collect

再通过spark UI查看cache的情况:
这里写图片描述

通过函数调用获取分区数量

  • RDD.getNumPartitions
  • rdd.partitions.size

参考文档: How Many Partitions Does An RDD Have

展开阅读全文

没有更多推荐了,返回首页