目录
<1>、当RDD数据来源于内存
一、看local~模式下访问Spark,默认内存分区数
二、那么totalCores是一个什么值呢?
<2>、当RDD数据来源于内存,并指定分区
<3>、当RDD数据来源于文件而非内存
<4>、总结
<1>、当RDD数据来源于内存
首先上IDEA代码,这时makeRDD数据来自于内存
object Spark01_Partition {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Partition")
//构建Spark上下文对象
val sc = new SparkContext(conf)
val arrayRDD = sc.makeRDD(Array(1,2,3,4))
println(arrayRDD.partitions.length)
//释放资源
sc.stop()
}
}
arrayRDD分区数,打印结果
(1)setMaster("local")时,为1
(2)setMaster("local[4]")时,为4
(3)setMaster("local[*]")时,为8(本机是4核8线程;设备处理器8个)
下面通过追源码的方式,来分析当通过local模式访问Spark时,RDD的分区数的影响因素。
一、看local~模式下访问Spark,默认内存分区数
1. 看makeRDD源码
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
defaultParallelism:并行度;
每一个任务可以放在不同的Executor并行执行,defaultParallelism决定有多少个Executor,也就是有多少个分区。
2. 继续追defaultParallelism源码
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
3. 追taskScheduler源码
private[spark] def taskScheduler: TaskScheduler = _taskScheduler
private[spark] def taskScheduler_=(ts: TaskScheduler): Unit = {
_taskScheduler = ts
}
4. Ctrl+h,查找TaskScheduler的实现类;然后Ctrl+f查找defaultParallelism,如下
override def defaultParallelism(): Int = backend.defaultParallelism()
5. backend是系统默认后台,查找defaultParallelism(默认并行度)方法源码
private[spark] trait SchedulerBackend {
private val appId = "spark-application-" + System.currentTimeMillis
6. 发现ScheduleBackend依然是个trait,查找其实现类,发现有三个
分别是:CoarseGrained粗粒度调度后台(用的集群yarn)、Standalone独立模式调度后台、Local本地调度后台
7. 当前模式用的是local,所以打开LocalScheduleBackend实现类,继续搜defaultParallelism
override def defaultParallelism(): Int =
scheduler.conf.getInt("spark.default.parallelism", totalCores)
如果在spark中配置了并行度就用配置的值,否则用默认的totalCores;这里我们程序并没有设置并行度,所以
默认并行度defaultParallelism取值为totalCores,即totalCores决定了默认内存RDD的分区数。
二、那么totalCores是一个什么值呢?
8. 搜SparkContext源码,2496行,createTaskScheduler,创建任务调度器,有几个分区创建几个任务;传入的master就是setMaster中传入的String
private def createTaskScheduler(
sc: SparkContext,
master: String,
deployMode: String): (SchedulerBackend, TaskScheduler) = {
import SparkMasterRegex._
// When running locally, don't try to re-execute tasks on failure.
val MAX_LOCAL_TASK_FAILURES = 1
master match {
case "local" =>
val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
val backend = new LocalSchedulerBackend(sc.getConf, scheduler, 1)
scheduler.initialize(backend)
(backend, scheduler)
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
if (threadCount <= 0) {
throw new SparkException(s"Asked to run locally with $threadCount threads")
}
val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
val backend = new LocalSchedulerBackend(sc.getConf, scheduler, threadCount)
scheduler.initialize(backend)
(backend, scheduler)
分了三种情况:
(1)当master是local时,val backend = new LocalSchedulerBackend(sc.getConf, scheduler, 1),再搜LocalSchedulerBackend源码
private[spark] class LocalSchedulerBackend(
conf: SparkConf,
scheduler: TaskSchedulerImpl,
val totalCores: Int)
发现此时totalCores就是1,
即如果使用local模式访问Spark,默认的内存RDD分区为1。
(2)当master是local[K],threadCount = threads.toInt,totalCores = threads.toInt;通过LOCAL_N_REGEX(threads)可知local[K],threads = K,所以totalCores = K;
即如果使用local[K]访问Spark,默认的内存RDD分区为K。
(3)当master是local[*],threadCount = localCpuCount,即本地设备处理器的核数;LocalScheduleBackend中的totalCores = localCpuCount;
即如果使用local[*]访问Spark,默认的内存RDD分区为当前CPU的最大核数(设备处理器最大核数,即最大线程数)。
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
if (threadCount <= 0) {
throw new SparkException(s"Asked to run locally with $threadCount threads")
}
val scheduler = new TaskSchedulerImpl(sc, MAX_LOCAL_TASK_FAILURES, isLocal = true)
val backend = new LocalSchedulerBackend(sc.getConf, scheduler, threadCount)
scheduler.initialize(backend)
(backend, scheduler)
(4)如果不是local,在下面的源码中也有说明,这里不着重涉及。
<2>、当RDD数据来源于内存,并指定分区
object Spark01_Partition {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Partition")
//构建Spark上下文对象
val sc = new SparkContext(conf)
val arrayRDD = sc.makeRDD(Array(1,2,3,4),2)
println(arrayRDD.partitions.length)
//释放资源
sc.stop()
}
}
这里makeRDD中指定了分区数为2,打印结果发现,无论setMaster后是local、local[K]、local[*],打印结果即RDD分区数都是2
结论:如果makeRDD中指定了分区数,以指定的分区数为准
<3>、当RDD数据来源于文件而非内存
新建两个txt文件,位于E:\\input目录下
1.txt中:是12,2个字节;
2.txt中:是1\n2,4个字节(换行符2个字节)
object Spark01_Partition {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Partition")
//构建Spark上下文对象
val sc = new SparkContext(conf)
// val arrayRDD = sc.makeRDD(Array(1,2,3,4),2)
val fileRDD = sc.textFile("E:\\input")
println(fileRDD.partitions.length)
//释放资源
sc.stop()
}
}
打印结果为3,即1.txt和2.txt分成了3个分区
1. 查看textFile源码
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
2. minPartitions最小分区数,是defaultMinPartitions,点击这个继续追源码
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
3. 发现defaultMinPartitions取值是defaultParallelism和2的较小值,追defaultParallelism源码
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
4. defaultParallelism并行度,setMaster后是local[*],所以按照<1>中的结论,defaultParallelism是8
此时defaultMinPartitions(最小分区数)是2,最小分区数知道了,那么实际的分区数是多少呢?继续往下看
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
5. 发现分区规则与hadoop的分区规则一致,打开TextInputFormat
public class TextInputFormat extends FileInputFormat<LongWritable, Text>
6. 点击父类FileInputFormat
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
totalSize是input目录下的总字节数,6
numSplits是切分数量,取值为minPartitions=2;所以goalSize=6/2=3
7. 下面计算每个分片的大小
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
goalSize=3,minSize=1,blockSize=32M(文件的块大小,本地文件的块大小是32M,HDFS中块大小是128M);接下来计算到底每个分片中大小是多少,点击computeSplitSize
protected long computeSplitSize(long goalSize, long minSize,
long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
8. 最终return值是3;即每个分片要求3个字节
9. 目前1.txt是2字节,2.txt是4字节;所以1.txt分一片,2.txt分两片;4个数分了3个区;那么分区怎么分的呢?
object Spark01_Partition {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("Partition")
//构建Spark上下文对象
val sc = new SparkContext(conf)
// val arrayRDD = sc.makeRDD(Array(1,2,3,4),2)
val fileRDD = sc.textFile("E:\\input")
//取出分区后的文件
fileRDD.saveAsTextFile("E:\\output")
println(fileRDD.partitions.length)
//释放资源
sc.stop()
}
}
执行程序,把分片后的文件取出到e盘下的output目录下
由上而下分别是3字节、4字节、0字节
每一个分区里3个字节,逐行读:
分区1里存放12LF,刚好3字节;
3LF是2个字节,放入分区2,然后分区没满,所以第二行4LF两个字节也放入分区2;
分区3里没有字节;
结论:
内存读取时平均分区,而文件读取分区会涉及行的概念;分区按字节数分,读数据按行来读,这就是文件读取的区别。
分3个区,划分3个task,对应3个executor执行
所以,在当前情况下,任务数与分区有关,分区与setMaster有关,内存和文件又有区别
<4>、总结
1. 当RDD数据来源于内存,看setMaster的设定:
(1)如果使用local模式访问Spark,默认的内存RDD分区为1
(2)如果使用local[K]模式访问Spark,默认的内存RDD分区为K
(3)如果使用local[*]模式访问Spark,默认的内存RDD分区为当前CPU最大核数
2. 当RDD数据来源于内存且在makeRDD中设置了分区数,以设置的为准
3. 当RDD数据来源于文件:
(1)分区数 = 文件总字节数 / 最小分片数(最小分片数由local模型并行度=totalCores;与2的较小值决定)
(2)分区大小(字节数),由goalSize、minSize和blockSize(块大小,HDFS是128M,文件是32M)决定
4. 当RDD数据来源于内存时,分区后平均分配数据
当RDD数据来源于文件时,分区后按照每个文件的字节数与分区大小进行比对,进行分区,不平均
5. 分区数 = 任务数task = 对应的executor数