文章目录
零、本讲学习目标
- 学会如何指定分区数量
- 会定义与使用自定义分区器
一、RRD分区
(一)RDD分区概念
- RDD是一个大的数据集合,该集合被划分成多个子集合分布到了不同的节点上,而每一个子集合就称为分区(Partition)。因此,也可以说,RDD是由若干个分区组成的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/778959cf04b9420b98fa594dd741ad90.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
(二)RDD分区作用
- 在分布式程序中,网络通信的开销是很大的,因此控制数据分布以获得最少的网络传输可以极大的提升程序的整体性能,Spark程序可以通过控制RDD分区方式来减少通信开销。Spark中所有的RDD都可以进行分区,系统会根据一个针对键的函数对元素进行分区。虽然Spark不能控制每个键具体划分到哪个节点上,但是可以确保相同的键出现在同一个分区上。
二、RDD分区数量
(一)RDD分区原则
- RDD各个分区中的数据可以并行计算,因此分区的数量决定了并行计算的粒度。Spark会给每一个分区分配一个单独的Task任务对其进行计算,因此并行Task的数量是由分区的数量决定的。RDD分区的一个分区原则是使得分区的数量尽量等于集群中CPU核心数量。
(二)影响分区的因素
- RDD的创建有两种方式:一种是使用parallelize()方法从对象集合创建;另一种是使用textFile()方法从外部存储系统创建。而RDD分区的数量与RDD的创建方式以及Spark集群的运行模式有关。
(三)使用parallelize()方法创建RDD时的分区数量
1、指定分区数量
- 使用parallelize()方法创建RDD时,可以传入第二个参数,指定分区数量。
![](https://img-blog.csdnimg.cn/c02fa4d932b6426188edc3054b2ee060.png)
- 分区的数量应尽量等于集群中所有CPU的核心总数,以便可以最大程度发挥CPU的性能。
- 利用
mapPartitionsWithIndex()
函数实现带分区索引的映射![](https://img-blog.csdnimg.cn/eb70c9526ec947f28329797300f0756e.png)
- 第1个分区完成了3个元素的映射,第2个分区完成了3个元素的映射,第3个分区完成了4个元素的映射
2、默认分区数量
- 若不指定分区数量,则默认分区数量为Spark配置文件
spark-defaults.conf
中的参数spark.default.parallelism
的值。若没有配置该参数,则Spark会根据集群的运行模式自动确定分区数量。 - 如果是本地模式,默认分区数量就等于本机CPU核心总数,这样每个CPU核心处理一个分区的计算任务,可以最大程度发挥CPU的性能。
- 如果是Spark Standalone或Spark On YARN模式,默认分区数量就取集群中所有CPU的核心总数与2中的较大值,即最少分区数为2。
![在这里插入图片描述](https://img-blog.csdnimg.cn/86cd6e4680434d25b94e72ffff23ae5f.png)
3、分区源码分析
parallelize()
方法是在SparkContext
类定义的![在这里插入图片描述](https://img-blog.csdnimg.cn/a8e0cac350944028a048e90d9c857e53.png)
numSlices
参数为指定的分区数量,该参数有一个默认值defaultParallelism
,是一个无参函数
![在这里插入图片描述](https://img-blog.csdnimg.cn/1179a8d633984406aa9d034b0feb690a.png)
- 上述代码中的
taskScheduler
的类型为特质TaskScheduler,通过调用该特质的defaultParallelism方法取得默认分区数量,而类TaskSchedulerImpl继承了特质TaskScheduler并实现了defaultParallelism方法。
![在这里插入图片描述](https://img-blog.csdnimg.cn/ef90a59db4b644aaaf0758c02990db43.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/81e94b13ba7e4d3cbb8b4878871d7fe8.png)
- 上述代码中的
backend
的类型为特质SchedulerBackend,通过调用该特质的defaultParallelism()方法取得默认分区数量,特质SchedulerBackend主要用于申请资源和对Task任务的执行和管理;而类LocalSchedulerBackend和类CoarseGrainedSchedulerBackend则继承了特质SchedulerBackend并分别实现了其中的defaultParallelism()方法。
![在这里插入图片描述](https://img-blog.csdnimg.cn/2171f3523fe443b6b827fc7ae38e6ccd.png)
- 类LocalSchedulerBackend用于Spark的本地运行模式(Executor和Master等在同一个JVM中运行),其调用顺序在TaskSchedulerImpl类之后;类CoarseGrainedSchedulerBackend则用于Spark的集群运行模式。
- 类
LocalSchedulerBackend
中的defaultParallelism()方法
![在这里插入图片描述](https://img-blog.csdnimg.cn/f6d8922517044044a3f1695dbb913aa1.png)
- 上述代码中的字符串spark.default.parallelism为Spark配置文件spark-defaults.conf中的参数spark.default.parallelism;totalCores为本机CPU核心总数。
- 类
CoarseGrainedSchedulerBackend
中的defaultParallelism()方法
![在这里插入图片描述](https://img-blog.csdnimg.cn/5e3fe74fedd34bc0aa7cb644a9030f1c.png)
- 上述代码中,
math.max(totalCoreCount.get(), 2)
表示取集群中所有CPU核心总数与2两者中的较大值。
(四)RDD分区方式
- Spark框架为RDD提供了两种分区方式,分别是哈希分区(HashPartitioner)和范围分区(RangePartitioner)。其中,哈希分区是根据哈希值进行分区;范围分区是将一定范围的数据映射到一个分区中。这两种分区方式已经可以满足大多数应用场景的需求。与此同时,Spark也支持自定义分区方式,即通过一个自定义的Partitioner对象来控制RDD的分区,从而进一步减少通信开销。
(五)使用textFile()方法创建RDD时的分区数量
- textFile()方法通常用于读取HDFS中的文本文件,使用该方法创建RDD时,Spark会对文件进行分片操作(类似于MapReduce的分片,实际上调用的是MapReduce的分片接口),分片操作完成后,每个分区将存储一个分片的数据,因此
分区的数量等于分片的数量
。
![在这里插入图片描述](https://img-blog.csdnimg.cn/6341f2cf9fd7486f8880f9456492b6c0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
1、指定最小分区数量
- 使用textFile()方法创建RDD时可以传入第二个参数指定最小分区数量。最小分区数量只是
期望的数量
,Spark会根据实际文件大小、文件块(Block)大小等情况确定最终分区数量
。
![在这里插入图片描述](https://img-blog.csdnimg.cn/89c767e69ae14d83bfabb6c8272a3510.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 在HDFS中有一个文件
/park/test.txt
,读取该文件,并指定最小分区数量为10
,但是实际分区数量是11
。
2、默认最小分区数量
- 若不指定最小分区数量,则Spark将采用默认规则计算默认最小分区数量。
- 查看textFile()源码
![在这里插入图片描述](https://img-blog.csdnimg.cn/1453f79a550d4da8820aef887096ac5b.png)
- 上述代码中的minPartitions参数为期望的最小分区数量,该参数有一个默认值defaultMinPartitions,这是一个无参函数,我们来查看其源码。
![在这里插入图片描述](https://img-blog.csdnimg.cn/fcb8e63b76704d4aacb6ddb095527a38.png)
- 从上述代码中可以看出,默认最小分区数取默认并行度与2中的较小值;而默认并行度则是parallelize()方法的默认分区数。
3、默认实际分区数量
- 最小分区数量确定后,Spark接下来将计算实际分区数量。查看textFile()方法的源码可知,textFile()方法最后调用了一个hadoopFile()方法,并对该方法的结果执行了map()算子。
![在这里插入图片描述](https://img-blog.csdnimg.cn/2bbba21fe63a4b02a09465c6602a4a1a.png)
- 查看hadoopFile()方法的源码
![在这里插入图片描述](https://img-blog.csdnimg.cn/5186b888bbcd41ed8dba4e280e955ba0.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 从上述代码可以看出,最终返回一个HadoopRDD对象。
- 查看HadoopRDD类的部分源码
![在这里插入图片描述](https://img-blog.csdnimg.cn/8bc526f85e7f427ca60077c2420ee30d.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- HadoopRDD类中的
getPartitions()
方法的功能是获取实际分区数量。通过调用getInputFormat()
方法得到InputFormat的实例,然后调用该实例的getSplits()
方法获得输入数据的所有分片,getSplits()
方法是决定最终分区数量的关键方法,该方法的第二个参数即为RDD的最小分区数量。 - 查看
InputFormt
接口getSplits()
抽象方法
![在这里插入图片描述](https://img-blog.csdnimg.cn/c632c83c7a244d2a80f996bb8b5ddc71.png)
InputFormat
有个实现类FileInputFormat
,它实现了getSplits()
方法
![在这里插入图片描述](https://img-blog.csdnimg.cn/9ad14c86e5524d6db616599155a4c1b1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_11,color_FFFFFF,t_70,g_se,x_16)
![在这里插入图片描述](https://img-blog.csdnimg.cn/ca8072f37c5c4846885337433180294b.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 根据期望分片数量(numSplits,即最小分区数量)计算期望分片大小(goalSize)。计算实际分片大小(splitSize)。splitSize最终决定了分片的数量。
- splitSize由3个因素决定:最小分片大小(minSize)、期望分片大小(goalSize)、分块大小(blockSize)。
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
StopWatch sw = new StopWatch().start();
FileStatus[] files = listStatus(job);
// Save the number of input files for metrics/loadgen
job.setLong(NUM_INPUT_FILES, files.length);
long totalSize = 0; // compute total size
for (FileStatus file: files) { // check we have valid files
if (file.isDirectory()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
// generate splits
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
NetworkTopology clusterMap = new NetworkTopology();
for (FileStatus file: files) {
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
FileSystem fs = path.getFileSystem(job);
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(fs, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
length-bytesRemaining, splitSize, clusterMap);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
splitHosts[0], splitHosts[1]));
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
- bytesRemaining, bytesRemaining, clusterMap);
splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
splitHosts[0], splitHosts[1]));
}
} else {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits.toArray(new FileSplit[splits.size()]);
}
![](https://img-blog.csdnimg.cn/img_convert/8ee62faf413aeb914dc8d3c412eb2f93.png)
- 结论:在MapReduce中,每个分片对应一个Map任务,多个Map任务以完全并行的方式处理;而在Spark中,每个分片对应一个分区,每个分区对应一个Task任务,多个Task任务以完全并行的方式处理。
三、Spark分区器
(一)分区器 - Partitioner抽象类
- Spark RDD的Shuffle过程与MapReduce类似,涉及数据重组和重新分区,且要求RDD的元素必须是(key, value)形式的。分区规则是由分区器(Partitioner)控制的,Spark的主要分区器是HashPartitioner和RangePartitioner,都继承了抽象类Partitioner。
![在这里插入图片描述](https://img-blog.csdnimg.cn/9185339217d546329ee2310dd0212e78.png)
- 抽象类Partitioner中有两个方法,分别用于指定分区数量和设置分区规则
![在这里插入图片描述](https://img-blog.csdnimg.cn/6b32e7bba42443ac973c909f6cdca8a4.png)
(二)哈希分区器 - HashPartitioner类
- HashPartitioner是Spark使用的默认分区器,其分区规则为:取(key,value)对中key的hashCode值,然后除以分区数量后取余数。若余数小于0(一般余数都大于等于0),则用余数与分区数量的和作为分区ID,否则将余数作为分区ID。分区ID一致的(key,value)对则会被分配到同一个分区。因此,默认情况下,key值相同的(key,value)对一定属于同一个分区,但是同一个分区中可能有多个key值不同的(key,value)对。该分区器还支持key值为null的情况,当key值等于null时,将直接返回0作为分区ID。
- HashPartitioner分区器中,对key取hashCode值实际上调用的是Java类Object中的hashCode()方法。由于Java数组的hashCode值基于的是数组标识,而不是数组内容,因此具有相同内容的数组的hashCode值不同。如果将数组作为RDD的key,就可能导致内容相同的key不能分配到同一个分区中。这个时候可以将数组转为集合,或者使用自定义分区器,根据数组内容进行分区。
![在这里插入图片描述](https://img-blog.csdnimg.cn/b8bb3d4adfdd4fab849b3ac708b461bd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
四、自定义分区器
(一)提出问题
- 在有些情况下,使用Spark自带的分区器满足不了特定的需求。
- 例如,某学生有以下3科成绩数据:
科目 | 成绩 |
---|
chinese | 98 |
math | 88 |
english | 96 |
- 现需要将每一科成绩单独分配到一个分区中,然后将3科成绩输出到HDFS的指定目录(每个分区对应一个结果文件),此时就需要对数据进行自定义分区。
(二)解决问题
1、新建自定义分区器
- 创建
MyPartitioner
类
![在这里插入图片描述](https://img-blog.csdnimg.cn/d1dbc865d5544aaa9c78752b772a3bdb.png)
package net.huawei.partition
import org.apache.spark.Partitioner
/**
* 功能:自定义分区器
* 作者:华卫
* 日期:2022年03月30日
*/
class MyPartitioner(partitions: Int) extends Partitioner {
/**
* 取得分区数量
*
* @return 分区数量
*/
override def numPartitions: Int = partitions
/**
* 根据key取得分区ID
*
* @param key
* @return 分区ID
*/
override def getPartition(key: Any): Int = {
val partitionId = key.toString match {
case "chinese" => 0
case "math" => 1
case "english" => 2
}
partitionId
}
}
![](https://img-blog.csdnimg.cn/img_convert/0e81ccf1ab6d5536a145dd31b4836ace.png)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
2、使用自定义分区器
- 调用RDD的partitionBy()方法传入自定义分区器类MyPartitioner的实例,可以对RDD按照自定义规则进行重新分区。
- 创建
TestMyPartitioner
单例对象
![在这里插入图片描述](https://img-blog.csdnimg.cn/b11da77411cf440385e5303d3aafbd1a.png)
package net.huawei.partition
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/**
* 功能:测试自定义分区器
* 作者:华卫
* 日期:2022年03月30日
*/
object TestMyPartitioner {
def main(args: Array[String]): Unit = {
// 创建Spark配置对象
val conf = new SparkConf()
.setAppName("TestMyPartitioner")
.setMaster("spark://master:7077")
// 基于Spark配置创建Spark上下文
val sc = new SparkContext(conf)
// 构建模拟数据
val arr = Array(
"chinese,94",
"math,88",
"english,91"
)
// 将模拟数据转成RDD,再转成键值对形式的元组
val data: RDD[(String, Int)] = sc.makeRDD(arr).map(line => {
(line.split(",")(0), line.split(",")(1).toInt)
})
// 将数据重新分区并保存到HDFS的/output目录
data.partitionBy(new MyPartitioner(3))
.saveAsTextFile("hdfs://master:9000/output")
}
}
![](https://img-blog.csdnimg.cn/img_convert/eca630acb95193ece334d96edf4e9260.png)
3、项目打包上传服务器
- 利用IDEA将项目打包 -
MyPartitioner.jar
![在这里插入图片描述](https://img-blog.csdnimg.cn/17d216d2b14a4c1994687ab26f2be03f.gif)
![在这里插入图片描述](https://img-blog.csdnimg.cn/50ee78e5564441ec89d5bcc3a2d6de1a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_9,color_FFFFFF,t_70,g_se,x_16)
- 上传到Spark集群master节点的
/opt
目录
![在这里插入图片描述](https://img-blog.csdnimg.cn/22502b55bf4c41e4a17677b753fed37c.png)
4、提交程序运行
- 执行命令:
spark-submit --master spark://master:7077 --class net.huawei.partition.TestMyPartitioner /opt/MyPartitioner.jar
![在这里插入图片描述](https://img-blog.csdnimg.cn/1d5c9fbcdf6340c08c5747985cdb7d3a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 查看输出目录
/output
![在这里插入图片描述](https://img-blog.csdnimg.cn/52fa37e47b8040a3a9c292a9c61f8742.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)
- 查看三个分区的结果文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/538fe6ff88414a74b7c1d84509e5855f.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAaG93YXJkMjAwNQ==,size_20,color_FFFFFF,t_70,g_se,x_16)