前言:
关于源码的文章,我自己其实也一直在有道云上有总结一些,但由于平日里上班的缘故,着实没有太多的精力来写体系的写这些东西,但是,确实觉得这些东西其实还是很重要的,特别是随着工作时间的渐长,越发觉得源码这个东西还是必须要看的,能带来很多的启发,我个人的体会是,每个工作阶段去解读都会有不一样的感受。
我也不敢说去解读或者说让你彻底搞个明白,自己确实没有那个水平。我写博客一方面是为了自己日后回顾方便,另一方面也是希望能够以此会友,以后希望自己可以坚持做下去,这篇且作为我的源码第一篇,目前没有太明确的写作的规划,就想到哪里就写到哪里吧。
updated on :2020-05-05
搞清楚这个原理其实对于很多地方都是有好处的,比如你的生产上hive调优里的如何调整map的数量,是很有帮助的。这里的原理和mr的基本上都是一致的。
直接写到最开端吧,这样时间比较紧张的朋友们可以直接看关键点,不必太拘泥于细节,不过笔者建议还是自己多去看几遍源码,多思考,多总结,最好是可以debug几次,这样会更加深刻的理解。
这里有几个关键点强调一下:
- 这里有三个关键的值:goalsize,minsize,blockSize
- goalsize 是理想的切分的尺寸,它是和我们指定的切分的数量,以及文件的总的长度有关,也就是文件的字节数,我们可以调节的是 numSplits 这个参数。默认值是 2
- minsize 这个参数也是可以设置的 通过 mapreduce.input.fileinputformat.split.minsize 来设置,默认是1,和minSplitSize 取max,但是minSplitSize的默认值也是1,所以说如果你不设置mapreduce.input.fileinputformat.split.minsize的话,那么minsize 对应的值就是 1
- blockSize 这个参数现在都是128m默认的,也有比较豪横的256m的,根据自己的集群实际情况来看,笔者公司的是128m
他们三个擦出的火花在这里: long splitSize = computeSplitSize(goalSize, minSize, blockSize); 这个方法其实就是求出了三个中的那个中间值。一般来说blocksize是已经定了,那就是去看 goalSize,minsize 你的设定了。【顺便多说一句,blocksize 在生产上一般不要动,(且hive里也不允许你去调整。)】,假如说你有个300m的文件,blocksize是128m,且minsize 你没有设定,让它默认的是1,那么splitsize就是128m了,也就是正好等于blockSize。
2. 进行切分的时候 获取的是每个文件的大小,而不是所有的文件的大小,这个请注意了,否则有可能你做实验的时候会比较疑惑,这点在一种情形下会有影响,就是假如说你的目录下有10个文件,其中有一个文件的大小比其他的9个都要显著的大,这个时候你会发现切分之后得到的partitions的数量和你预期的有出入,会比你预期的要多一个分区。所以这点笔者在这里想强调一下
3. 就是关于SPLIT_SLOP 这个很关键的点了,这个不仅和小文件有关,也会决定了你的文件切分的个数,因为从源码来看,如果说当你的文件的大小,小于splitsize * 1.1 的时候,这个时候就不会再被切分,而是直接被当作一个inputsplit
spark 读取textfile是如何决定分区数的???
之所以写这个是因为当时去面试的时候,被面试官给问到了,当时只回答了一个大概,对很多的细节其实并不清楚,所以就决定分析一下。
我尝试了量两种方式,使用的是spark的2.0的版本 下面的分析是针对如下的情况来分析的。
spark.sparkContext.textFile("")
1:跟进源码我们发现注释是这样子的:
/**
* Read a text file from HDFS, a local file system (available on all nodes), or any
* Hadoop-supported file system URI, and return it as an RDD of Strings.
*/
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)
}
我们可以看到这里的注释写的很清楚,读取的是hdfs上的或者是本地文件系统的,或者是任意的hadoop支持的文件系统,返回的是一个rdd
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
可以看到返回的是一个hadoopfile经过map操作之后的结果。
我们继续往下走:
def hadoopFile[K, V](
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
=new HadoopRDD(
minPartitions).setName(path)
这里我只展示出来重点的代码。我们可以看到这里是new HadoopRDD
我们跟踪进入到 HadoopRDD 去看下这个分区数到到底是如何确定的。 锁定了获取分区的方法:
override def getPartitions: Array[Partition] = {
val jobConf = getJobConf()
// add the credentials here as this can be called before SparkContext initialized
SparkHadoopUtil.get.addCredentials(jobConf)
val inputFormat = getInputFormat(jobConf)
//这个是关键,是如何获取inputSplit的,这个方法里传入了minPartitions,这个值的默认值是 2
== val inputSplits = inputFormat.getSplits(jobConf, minPartitions)==
val array = new Array[Partition](inputSplits.size)
for (i <- 0 until inputSplits.size) {
array(i) = new HadoopPartition(id, i, inputSplits(i))
}
array
}
我们来重点的看下这个getSplits的源码:
/**
* Logically split the set of input files for the job.
*
*
* <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the
* input files are not physically split into chunks. For e.g. a split could
* be <i><input-file-path, start, offset></i> tuple.
*
* @param job job configuration.
* @param numSplits the desired number of splits, a hint.
* @return an array of {@link InputSplit}s for the job.
*/
InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
==通过注释我们注意到:这个是一个逻辑上的划分,而并不是物理物理上的切分。 ==
split of the inputs and the input files are not physically split into chunks
这个就是这个inputsplit的本质。是一个逻辑上的概念
我们来看一个他的具体的实现:
他的是实现有很多我们拿出来一个我们很熟悉的来说:
FileInputFormat 这个类的
这个具体的还是挺长的,我只挑我关心的重点来看:
我们继续回到:
def getPartitions: Array[Partition]
最终返回的是一个Array[Partition]
我通常看rdd的分区数用到的一个api是:getNumPartitions
这个是rdd里的一个方法,我们来看下他的实现:
def getNumPartitions: Int = partitions.length
partitions----》def partitions: Array[Partition]//其实就是这个数组的长度
/** Splits files returned by {@link #listStatus(JobConf)} when
* they're too big.*/
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()]);
}
protected long computeSplitSize(long goalSize, long minSize,
long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
2: 说了这么多该做一个总结的时候了
其实来说我们的分区数量的决定如果说文件时可分割的话,那么他就是取决于你的split的最终的个数,如果不可分割的话那就是一个走起。
分割的时候最关键的就是要确定切分的尺寸,这个是最终的一个决定的因素,有一个点我没有搞太懂:
((double) bytesRemaining)/splitSize > SPLIT_SLOP
这里的 SPLIT_SLOP 是什么意思,我看与源码里给的默认值是1.1 请指教
,先写到这里,都是纯理论的东西。
我已经知道了 SPLIT_SLOP 是什么意思了,这是一个很关键的操作,这个值是1.1,也就是说只有当剩余的文件大小/splitsize的1.1倍的时候,才继续进行切分,这样一个最大的好处就是防止小文件的出现。我们来举个例子:如果说剩余的大小是31 切分的尺寸是30 那么如果说不是1.1倍的话那么就会满足条件,做切分,那么剩下的1咋办,是不是就成了一个很小的文件了。