今天认真的看了看RDD 的分区,感觉挺麻烦的,就在此记录总结一下,如果有错误,还请各位大神指出!
在我们使用并行化的方式创建sparkRDD的时候,我们可以指定RDD的分区。
我们知道,mapreduce读取文件时允许的最小分区是1,而spark允许的最小分区是2
hdfs读取文件时,默认会根据输入文件数量创建多少个task,生成对应数量的切片(文件小于blocksize).
首先查看一下hdfs上面的文件信息:
可以看到,一共有三个文件,它们的大小分别是6b,6b,684b.都小于一个blocksize(128m)。
接着创建一个RDD:
此时它产生了4个逻辑切片。
我们再创建一次RDD,这次指定分区数量为1:
这时我们看到它产生了3个逻辑切片。
经过查看源码,发现hdfs在进行分区切片的时候定义了新的规则:
首先我们跟踪textFile的源码:
从这里我们可以看出他定义的minPartitions(最小分区数量)是2。然后下面创建了一个hadoopFile,将这个最小的分区数量作为参数。
然后查看hadoopFile,
在这里创建了HadoopRDD,同时将这个最小分区数量传了进去。
在HadoopRDD这个类中,定义了构造器:
创建完了之后,返回这个RDD。
在启动任务之前,会记录一下要启动多少task,所以在创建完HadoopRDD后,决定要启动多少task,然后开始执行(触发action),将task在driver端生成,调度到excutor端执行。
继续跟踪源码,在执行action时,他会去调用一个方法,getPartitions(HadoopRDD上定义的一个抽象方法):
这里val inputSplits = inputFormat.getSplits(jobConf,minPartitions)
将minpartitins传进去了,而minpartitions的值为2.
接着往下执行:
这里totalSize是要读取文件的总长度。计算时会先把文件的总长度读取出来。
numSplits是我们可以自定义传入的分区数,它的默认值是2.
然后它用totalSize除以numSplits得到一个goalSize(目标长度)
下面就开始切分了:
用一个computeSplitSize方法计算怎么切分:
这里可以看出,他计算的方法就是在goalSize和blockSize中取出最小值,再跟minSize(最小是1b)比较,取出最大值当作切分的大小。
切完之后,会判断剩余文件的大小:
拿着剩下文件的大小除以splitSize(刚刚计算出来的切分长度),得到的结果如果大于SPLIT_SLOP,就继续切,如果小于这个值,就不再切分了。SPLIT_SLOP的默认值是1.1
总结:
1.上面就是hdfs计算每个切片大小的规则,但是这值是逻辑上的切分,只是记录了切分的偏移量,并不是真的这样切。
但是,这样处理是有好处的,因为每个文件的大小是不确定的,这样更加的灵活,可以尽量保证每个分区上切片一样大。
2.如果有的文件很小,就像上面的例子,我们不希望他再切分了,我们可以将它的最小分区数量设为1:
val rdd = sc.textFile("hdfs://L1:9000/wc",1)
3.如果要读取的文件个数为1,而这个文件很小(小于blockSize),而且设置最小分区数量为1的时候,那么他最后产生的分区数量就是1,如果指定最小分区数量为2,那么他最后产生的分区数量为2.
也就是说,当读取的文件数为n,而且每个文件都是小于blockSize的,那么当设置最小分区数量为1的时候,他最后产生的分区数量就是n。
但是为什么有多少个文件最后就产生多少个分区呢,是因为hdfs中mapreduce在执行时,有多少个map就产生多少个切片。