目录
1、创建RDD
Spark提供了两种方式创建RDD:
读取外部数据集,如SparkContext.textFile
在驱动器程序中对一个集合进行并行化,如SparkContext.paralleize或makeRDD
2、RDD分区有关操作
2.1、查看分区方式
在scala中可以通过RDD的partitioner属性获取RDD的分区方式,会返回一个scala.Option对象,这是scala中用来存放可能存在对象的容器类,可调用这个Option对象的isDefined来先插是否有值,调用get来获取其中的值。若存在值将会是spark.Partitioner对象
以下为standalone模式,集群中有4个Worker节点
scala> val rdd = sc.parallelize(List(1,2,3,4,5,6,7))
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:24
scala> rdd.partitioner
res0: Option[org.apache.spark.Partitioner] = None
2.2、查看分区数
scala> rdd.partitions.size
res2: Int = 4
2.3、查看不同分区内的数据
rdd.glom.collect
res27: Array[Array[Int]] = Array(Array(1), Array(2, 3), Array(4, 5), Array(6, 7))
2.4、重新分区
重新分区是代价相对比较大的操作,因为它会把数据通过网络进行混洗,并创建出新的RDD
reparation函数可以增多多减少分区数,coalesce函数仅能将减少分数区
scala> rdd.repartition(3).glom.collect
res31: Array[Array[Int]] = Array(Array(5, 6), Array(7, 1, 2), Array(3, 4))
scala> rdd.repartition(5).glom.collect
res34: Array[Array[Int]] = Array(Array(5, 6), Array(1, 7, 2), Array(3), Array(), Array(4))
scala> rdd.coalesce(3).glom.collect
res32: Array[Array[Int]] = Array(Array(1), Array(2, 3), Array(4, 5, 6, 7))
scala> rdd.coalesce(5).glom.collect
res33: Array[Array[Int]] = Array(Array(1), Array(2, 3), Array(4, 5), Array(6, 7)) //仍然是4个分区
2.5、设置分区数
在创建RDD的函数中,可以传入第二个参数指定分区数
scala> val rdd = sc.parallelize(List(1,2,3,4,5,6,7),3)
rdd: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[54] at parallelize at <console>:24
scala> rdd.partitions.size
res36: Int = 3
3、分区数
3.1、并行化集合
Local模式
通过进入spark-shell时指定local[n]可以设置启动线程数,每个线程拥有1个内核,以此模拟分布式。默认为1个线程,即1个分区
$ spark-shell --master local[3]
scala> sc.parallelize(List(1,2,3)).partitions.size
res0: Int = 3
standalone模式
standalone模式下分区数为执行器线程的数量,也就是Worker的数量,但不能少于2个,即max(2,WorkerNum)
例子见上
yarn模式
yarn模式下分区数默认为2,在进入spark shell时可以通过参数--num-executors设置执行器数量
$ spark-shell --master yarn
scala> sc.parallelize(List(1,2,3)).partitions.size
res0: Int = 2
$ spark-shell --master yarn --num-executors 3
scala> sc.parallelize(List(1,2,3)).partitions.size
res0: Int = 3
3.2、外部数据集textFile
通过textFile创建的RDD的分区数与模式无关
若调用textFile函数时不传入分区参数,则默认为2个分区
//读取集群上或本地文件,并返回一个RDD
def textFile(
path: String,
//若不传入指定分区数,则使用默认值defaultMinPartitions
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
//defaultParallelism为默认并行线程数,即内核数。默认最小分区数不会超过2
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
textFile中调用了hadoopFile函数,这里采用的是TextInputFormat(key为LongWritable类型即行号,value为Text类型即行数据),所以说外部数据集的分区底层采用的是MR中Mapper端的文件逻辑切片
def hadoopFile[K, V](
path: String,
inputFormatClass: Class[_ <: InputFormat[K, V]],
keyClass: Class[K],
valueClass: Class[V],
minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
assertNotStopped()
// 强制加载hdfs-site.xml
FileSystem.getLocal(hadoopConfiguration)
// Hadoop配置文件大概有10KB大小,这已经相当大了,所以广播它
val confBroadcast = broadcast(new SerializableConfiguration(hadoopConfiguration))
// 设置输入路径
val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
// 构造RDD
new HadoopRDD(
this,
confBroadcast,
Some(setInputPathsFunc),
inputFormatClass,
keyClass,
valueClass,
minPartitions).setName(path)
}
在hadoopRDD.scala中可见大多采用的是MR中老版API:org.apache.hadoop.mapred.*
hadoopRDD中的分区函数
//RDD的分区中调用了FileInputFormat.getSplits()
override def getPartitions: Array[Partition] = {
val jobConf = getJobConf()
SparkHadoopUtil.get.addCredentials(jobConf)
val inputFormat = getInputFormat(jobConf)
//获取文件所有分片
val inputSplits = inputFormat.getSplits(jobConf, minPartitions)
val array = new Array[Partition](inputSplits.size)
//将每个分片中数据写入到RDD分区中
for (i <- 0 until inputSplits.size) {
array(i) = new HadoopPartition(id, i, inputSplits(i))
}
array
}
由于textFile文件采用的是老api中的FileInputFormat
//注:以下RDD分区即为文件分片,省略了部分日志、异常有关代码
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
FileStatus[] files = listStatus(job);
long totalSize = 0;
for (FileStatus file: files) {
//获取文件总长度
totalSize += file.getLen();
}
//计算每个分区(分片)中目标数据大小,numsplit为指定分区数
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
//最小分区内数据大小为1字节
long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
// 生成分区
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
for (FileStatus file: files) {
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
FileSystem fs = path.getFileSystem(job);
//若文件可分区
if (isSplitable(fs, path)) {
//获取默认数据块大小,128M
long blockSize = file.getBlockSize();
//计算分区大小,computeSplitSize方法获取了目标分区中大小、最小分区大小(1B)、块大小(128M)中的中间值
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
//最初待分区数据大小即文件大小
long bytesRemaining = length;
//SPLIT_SLOP分区槽为1.1,也就是说最后一个分区大小为1.1倍splitSize之内
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
String[] splitHosts = getSplitHosts(blkLocations,
length-bytesRemaining, splitSize, clusterMap);
//增加分区,并将一个splitSize写入到分区内
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
splitHosts));
//剩余未分区数据减去一个分区大小
bytesRemaining -= splitSize;
}
//将剩余未达到1.1倍分区大小的数据作为最后一个分区
if (bytesRemaining != 0) {
String[] splitHosts = getSplitHosts(blkLocations, length
- bytesRemaining, bytesRemaining, clusterMap);
splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
splitHosts));
}
} else {
//若不可分区,将文件内容全写入一个分区中
String[] splitHosts = getSplitHosts(blkLocations,0,length,clusterMap);
splits.add(makeSplit(path, 0, length, splitHosts));
}
} else {
//文件为空,则创建一个空的分区
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
return splits.toArray(new FileSplit[splits.size()]);
}
举个例子
文件大小为100B,通过sc.textFile("...", 8)设置目标分区为8个则:
totalSize = 100
numSplits = 8
goalSize = totalSize / numSplits = 100/8 = 12
minSize = 1
blockSize = 128M
splitSize = max(minSize, min(goalSize,blockSize)) = 12
splitsNum = (totalSize / splitSize).ceil = 9
4、新版API中FileInputFormat的分片
由于老版的FileInputFormat中是可以传入目标分片数的,这与spark中外部文件RDD自定义分区数的需求比较类似。新Api中分片大小默认为128M,这可能对于1个文件在一个节点中的内存占用过多,而更改分片大小比较麻烦。而老API可以很方便地设置小分区,所以spark采用来老API进行分区
最后回顾一下新API中的FileInputFormat.getSplits(),大部分与老API类似,只有计算分片大小部分不同
//默认最小分片大小为1字节
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
//默认最大分片大小为Long最大值
long maxSize = getMaxSplitSize(job);
//块大小,128M
long blockSize = file.getBlockSize();
//分片大小默认为以上3个值的中间值,即128M
//一般blockSize不会改变,为了获取较小的分片,需要改变maxSize小于blockSize,spark索性采用老API
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
原文链接:https://blog.csdn.net/qq_39192827/article/details/97494565