前言:
關於源碼的文章,我自己其實也一直在有道雲上有總結一些,猶豫平日裏上班的緣故,着實沒有太多的精力來寫體系的寫這些東西,但是,卻着實覺得這些東西其實還是很重要的,特別是隨着工作時間的漸長,越發覺得源碼這個東西還是必須要看的,能帶來很多的啓發,我個人的體會是,每個工作階段去解讀都會有不一樣的感受。
我也不敢說去解讀或者說讓你徹底搞個明白,自己確實沒有那個水平。我寫博客一方面是爲了自己日後回顧方便,另一方面也是希望能夠以此會友,以後希望自己可以堅持做下去,這篇且作爲我的源碼第一篇,目前沒有太明確的寫作的規劃,就想到哪裏就寫到哪裏吧。
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
這個具體的還是挺長的,我只挑我關心的重點來看:
/** Splits files returned by {@link #listStatus(JobConf)} when
* they're too big.*/
//註釋裏說明了它的作用是來切分過大的file的
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);
==//計算總的大小,這裏是一個初始值是0==
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();
}
// goalsize理想的分割尺寸,這裏默認的numSplits就是2 如果說我們設置了值的花那麼就是我們設置的值
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
//這裏有個SPLIT_MINSIZE 最小的切分尺寸。
/* public static final String SPLIT_MINSIZE =
"mapreduce.input.fileinputformat.split.minsize";
minSplitSize 的默認值是1,這個是我們自己可以設置的
*/
//我們沒有去設置minSplitSize所以說那就是1,因爲他兩個的默認值都是1
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)) {
//這個在生產上看你是如何設置的了,windows本地測試話這個值是32m,我們生產上是128m
long blockSize = file.getBlockSize();
//計算切分的尺寸
/*
return Math.max(minSize, Math.min(goalSize, blockSize));
從goalSize和blockSize中取出小的那個,然後和minSize取最大值
*/
//這個操作最終其實就是拿到 這三個裏的 中間的那個值就對了,在生產上很多時候goalsize是大於blocksize 的,所以說一般來說都是按照blockSize來切分的,讓我們造成了一個錯覺,以爲文件的個數決定了我們的並行度,其實真的不是,當文件的數量尺寸小的時候,這個時候中間的那個值就變成了goalSize
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
long bytesRemaining = length;
//這裏又是一處重點:判斷文件的尺寸和splitSize的1.1倍的關係,但是切分的時候是按照splitSize的大小來切分的
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;
}
// 當剩下的文件的大小無法滿足上面的while循環的時候,就會進入這個判斷,剩下的那個小部分數據就直接被添加到了splits數組裏了。這個文件有可能就被切分成了多個split,也就意味着會有不止一個任務來處理這個文件,所以在都是小文件很多的時候你會發現,明明是7個文件,但是有一個文件的尺寸比別的都明顯大的時候,就會出現這種情況,並行度並不是文件的個數,而是會多出來一些分區數,就是這個原因。
if (bytesRemaining != 0) {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
- bytesRemaining, bytesRemaining, clusterMap);
splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
splitHosts[0], splitHosts[1]));
}
}
//這裏如果說不可以被切分的話那麼就是直接成爲一個split
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()]);
}
我們繼續回到:
def getPartitions: Array[Partition]
最終返回的是一個Array[Partition]
我通常看rdd的分區數用到的一個api是:getNumPartitions
這個是rdd裏的一個方法,我們來看下他的實現:
def getNumPartitions: Int = partitions.length
partitions----》def partitions: Array[Partition]//其實就是這個數組的長度
2: 說了這麼多該做一個總結的時候了
其實來說我們的分區數量的決定如果說文件時可分割的話,那麼他就是取決於你的split的最終的個數,如果不可分割的話那就是一個走起。
分割的時候最關鍵的就是要確定切分的尺寸,這個是最終的一個決定的因素,有一個點我沒有搞太懂:
((double) bytesRemaining)/splitSize > SPLIT_SLOP
這裏的 SPLIT_SLOP 是什麼意思,我看與源碼裏給的默認值是1.1 請指教
,先寫到這裏,都是純理論的東西。
我已經知道了 SPLIT_SLOP 是什麼意思了,這是一個很關鍵的操作,這個值是1.1,也就是說只有當剩餘的文件大小/splitsize的1.1倍的時候,才繼續進行切分,這樣一個最大的好處就是防止小文件的出現。我們來舉個例子:如果說剩餘的大小是31 切分的尺寸是30 那麼如果說不是1.1倍的話那麼就會滿足條件,做切分,那麼剩下的1咋辦,是不是就成了一個很小的文件了。