目录
零、本讲学习目标
一、RRD分区
(一)RDD分区概念
(二)RDD分区作用
二、RDD分区数量
(一)RDD分区原则
(二)影响分区的因素
(三)使用parallelize()方法创建RDD时的分区数量
1、指定分区数量
2、默认分区数量
3、分区源码分析
(四)使用textFile()方法创建RDD时的分区数量
1、指定最小分区数量
2、默认最小分区数量
3、默认实际分区数量
(五)RDD分区方式
三、Spark分区器
(一)分区器 - Partitioner抽象类
(二)哈希分区器 - HashPartitioner类
四、自定义分区器
(一)提出问题
(二)解决问题
1、准备数据文件
2、新建科目分区器
3、测试科目分区器
mapPartitionsWithIndex()
函数实现带分区索引的映射spark-defaults.conf
中的参数spark.default.parallelism
的值。若没有配置该参数,则Spark会根据集群的运行模式自动确定分区数量。spark-shell
本地模式启动master
的CPU核数为2。8
。为什么是8
呢?集群两个工作节点(slave1和slave2)的CPU核数总和是4 + 4 = 4
parallelize()
方法是在SparkContext
类定义的numSlices
参数为指定的分区数量,该参数有一个默认值defaultParallelism
,是一个无参函数taskScheduler
的类型为特质TaskScheduler,通过调用该特质的defaultParallelism方法取得默认分区数量,而类TaskSchedulerImpl继承了特质TaskScheduler并实现了defaultParallelism方法。backend
的类型为特质SchedulerBackend,通过调用该特质的defaultParallelism()方法取得默认分区数量,特质SchedulerBackend主要用于申请资源和对Task任务的执行和管理;而类LocalSchedulerBackend和类CoarseGrainedSchedulerBackend则继承了特质SchedulerBackend并分别实现了其中的defaultParallelism()方法。LocalSchedulerBackend
中的defaultParallelism()方法CoarseGrainedSchedulerBackend
中的defaultParallelism()方法math.max(totalCoreCount.get(), 2)
表示取集群中所有CPU核心总数与2两者中的较大值。textFile()
方法通常用于读取HDFS中的文本文件,使用该方法创建RDD时,Spark会对文件进行分片操作(类似于MapReduce的分片,实际上调用的是MapReduce的分片接口),分片操作完成后,每个分区将存储一个分片的数据,因此分区的数量等于分片的数量
。使用textFile()方法创建RDD时可以传入第二个参数指定最小分区数量。最小分区数量只是期望的数量
,Spark会根据实际文件大小、文件块(Block)大小等情况确定最终分区数量
。
针对/park/test.txt
文件,实际分区数比指定分区数大1,但是换个文件,情况就未必如此。
若不指定最小分区数量,则Spark将采用默认规则计算默认最小分区数量。
上述代码中的minPartitions参数为期望的最小分区数量,该参数有一个默认值defaultMinPartitions,这是一个无参函数,我们来查看其源码。
从上述代码中可以看出,默认最小分区数取默认并行度与2中的较小值;而默认并行度则是parallelize()
方法的默认分区数。
getPartitions()
方法的功能是获取实际分区数量。通过调用getInputFormat()
方法得到InputFormat的实例,然后调用该实例的getSplits()
方法获得输入数据的所有分片,getSplits()
方法是决定最终分区数量的关键方法,该方法的第二个参数即为RDD的最小分区数量。InputFormt
接口getSplits()
抽象方法InputFormat
有个实现类FileInputFormat
,它实现了getSplits()
方法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 splits = new ArrayList(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()]);
}
Shuffle
过程与MapReduce
类似,涉及数据重组和重新分区,且要求RDD的元素必须是(key, value)
形式的。分区规则是由分区器(Partitioner)控制的,Spark的主要分区器是HashPartitioner
和RangePartitioner
,都继承了Partitioner
抽象类。科目 | 成绩 |
---|---|
chinese | 98 |
math | 88 |
english | 96 |
chinese | 89 |
math | 96 |
english | 67 |
chinese | 88 |
math | 78 |
english | 89 |
package net.huawei.rdd.day04
import org.apache.spark.Partitioner
class SubjectPartitioner(partitions: Int) extends Partitioner {
/**
* @return 分区数量
*/
override def numPartitions: Int = partitions
/**
* @param key(科目)
* @return 分区索引
*/
override def getPartition(key: Any): Int = {
val partitionIndex = key.toString match {
case "chinese" => 0
case "math" => 1
case "english" => 2
}
partitionIndex
}
}
partitionBy()
方法传入科目分区器类SubjectPartitioner
的实例,可以对RDD按照自定义规则进行重新分区。net.huawei.rdd.day04
包里创建TestSubjectPartitioner
单例对象package net.huawei.rdd.day04
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object TestSubjectPartitioner {
def main(args: Array[String]): Unit = {
// 创建Spark配置对象
val conf = new SparkConf()
.setAppName("TestSubjectPartitioner") // 设置应用名称
.setMaster("local[*]") // 设置主节点位置(本地调试)
// 基于Spark配置对象创建Spark容器
val sc = new SparkContext(conf)
// 读取HDFS文件,生成RDD
val lines = sc.textFile("hdfs://master:9000/partition/input/marks.txt")
// 将每行数据映射成(科目,成绩)二元组
val data: RDD[(String, Int)] = lines.map(line => {
val fields = line.split(" ")
(fields(0), fields(1).toInt) // (科目,成绩)
})
// 将数据按科目分区器重新分区
val partitionData = data.partitionBy(new SubjectPartitioner(3))
// 在控制台输出分区数据
partitionData.collect.foreach(println)
// 保存分区数据到HDFS指定目录
partitionData.saveAsTextFile("hdfs://master:9000/partition/output")
}
}