目录
一、RDD分区的优势
Spark速度快的原因得益于它的RDD的数据处理方式,RDD有弹性、不可变、可分区、里面的元素可并行计算的特性。
而RDD的并行计算是通过分区实现的,可以让计算更快。
分区增加了RDD的容错,数据丢失或出现错误不会读取以整块数据,而只需重新读取出错的分区
RDD的分区是Spark分布式的体现
二、分区原理
RDD为了提高并行计算的能力,提供了分区,把读取到的数据分为很多个区域,每个区域分发给一个单独的Task(任务),再把Task提交给一个单独的Executor来处理数据,所以RDD和分区数和Task、Executor的数量相同。
分区数量默认取给该app分配的核数量
RDD有四种创建方法(两种常用):
从集合(内存)中创建、从外部存储(文件)创建、从其他RDD创建、直接创建(即 new)
-
从内存中创建
//TODO 准备环境
//[*]表示当前系统的最大可用核数,如不写,则表示单核
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(sparkConf)
//TODO 创建RDD
val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
rdd.collect().foreach(println)
代码分析:
Spark为从内存中创建RDD提供了两种函数,分别为parallelize()和makeRDD(),parallelize是并行的意思
makeRDD的源码:
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
可以看出makeRDD的底层其实直接调用了parallelize方法,两个方法几乎相同
makeRDD接收两个参数,第一个是Seq集合
第二个numSlices表示分区的数量,有一个默认值defaultParallelism
defaultParallelism的源码:
override def defaultParallelism(): Int = scheduler.conf.getInt("spark.default.parallelism", totalCores)
/*
查看conf底层可以发现它就是最开始定义的sparkConf
它优先取我最开始定义的local[*]中的可用核数,没有则取totalCores,值为当前环境的最大可用核数
*/
将处理的数据保存为分区文件
rdd.saveAsTextFile(path = "output")
由于我电脑是16核,保存了16个分区文件
发现只有文件3,7,11,15中分别存在数据1,2,3,4
数组分区的源码:
//匹配到的是非range类型时
case _ =>
val array = seq.toArray // Seq集合数组化
//向position传入了数组长度和分区个数
positions(array.length, numSlices).map { case (start, end) =>
array.slice(start, end).toSeq//根据传入的start和end来切分数组
}.toSeq//将数组转换回Seq集合
positions方法的源码:
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
//迭代器迭代0到分区数,转化成map数组
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
}
传入length为4,numSlices为16,得到map集合:
(0,0),(0,0),(0,0),(0,1),(1,1),(1,1),(1,1),(1,2),(2,2),(2,2),(2,2),(2,3),(3,3),(3,3),(3,3),(3,4),(4,4)
然后将(start,end)逐个传入slice函数来切分数组
slice函数的源码:
public static Object slice(IndexedSeqOptimized $this, int from, int until) {
int lo = scala.math.package..MODULE$.max(from, 0);//取from和0的最大值
//先比较until和0,取对大值,再和整个数组的长度比较,取最小值
int hi = scala.math.package..MODULE$.min(scala.math.package..MODULE$.max(until, 0), $this.length());
int elems = scala.math.package..MODULE$.max(hi - lo, 0);
Builder b = $this.newBuilder();//这个函数返回的是自身
b.sizeHint(elems);
//从lo到hi是个闭开区间
for(int i = lo; i < hi; ++i) {
b.$plus$eq($this.apply(i));
}
return b.result();
}
由于是个闭开区间,从0号区间到2号区间的 [0,0) 不符合slice中的循环条件
所以第一个读到数的是3号区间是 [0,1) ,读到第0个数字1,然后是第4号到6区间 [1,1) 不符合
所以 7 = > [1,2) = > 2,11 = > [2,3) => 3, 15 = > [3,4) => 4。
符合结果
-
从外部存储(文件)创建
//TODO 准备环境
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("RDD")
val sc = new SparkContext(sparkConf)
//TODO 创建RDD
val rdd: RDD[String] = sc.textFile(path = "datas/1.txt",3)
rdd.saveAsTextFile("output")
//TODO 关闭环境
sc.stop()
关于path:
path路径可以是绝对路径,也可以是相对路径,可以是文件夹名称(会将文件夹中所有文件所谓数据源),
可以使用通配府*,也可以是分布式存储系统路径HDFS
textFile源码:
//默认分区个数会在2和可用核数之间取最小值
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
没有设置值的话默认会是两个分区
textFile底层源码:
package org.apache.hadoop.mapred;
@Public
@Stable
public class TextInputFormat
extends FileInputFormat<LongWritable, Text>
implements JobConfigurable
{
从导包可以看出,RDD文件读取的部分底层使用的原来是Hadoop的读取方式
TextInputFormat即是Hadoop默认的 切片 机制
所以文件读取采用的分区代码底层是Hadoop的切片
三、Hadoop切片机制
Hadoop是按行读取数据,Hadoop文件读取是以偏移量为单位进行计算
单文件读取的话会根据传入和分区数或默认的分区数和文件的字节数计算得出切片的数量和切片大小
如果是多文件读取会直接根据文件数量来决定切片数量,每个切片大小取决于文件大小
文件内容:
运行结果:
产生了三个分区
前两个分区分别只存储了文件的一行内容
最后一个分区为空
分区的具体源码+注释:
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);//minSplitSize是值为1的常量
// generate splits
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);//创建分区大小的FileSplit类型数组
NetworkTopology clusterMap = new NetworkTopology();
for (FileStatus file: files) {//FileStatus封装了文件系统中文件的各种信息,遍历每个文件
Path path = file.getPath();
long length = file.getLen();//把正在遍历的文件的总长度赋值给length
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();
//如果想切片大小大于块大小,则配置参数,使minSize > blockSize
//如果想切片小于块大小,则配置参数,使maxSize < blockSize
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
long bytesRemaining = length;//把这个文件的总长度赋值给bytesRemaining
//SPLIT_SLOP是值为 1.1 的常量
//如果 文件的总长度除以分区大小的余数大于 1.1 则创建新的分区,循环切片
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;//总长度减去已经切去的部分
}
文件有24个字节
Hadoop文件读取是以偏移量为单位进行计算
原数据 | 偏移量 |
---|---|
Hello [空格符] World [回车符] [换行符] | 0,1,2,3,4,5,6,7,8,9,10,11,12 |
Hello [空格符] Spark | 13,14,15,16,17,18,19,20,21,22,23 |
计算得出每个分区是 24/3=8 字节
划分分区,即:[0,8],[8,16],[16,24]
第一次读取为 ”Hello [空格符] Wo“ 因为会按行读取所以第一行的数据会全部读取
第二次从8下标开始,因为Hadoop不会i读取重复的偏移量,所以读取第二行的内容
为 “Hello [空格符] Spark“
最后得到
Hello World
Hello Spark
和结果相同