1.RDD执行原理
RDD是spark框架中用于数据处理的核心模型,spark框架执行数据处理时,需要申请资源后将应用程序的数据处理逻辑拆分为一个个的计算任务,并将其发到已经分配资源的计算节点上,按照指定的计算模型进行数据统计。
1.1 步骤
1)启动yarn环境,resoureManager和nodemanager既意味有资源了
2)spark通过申请资源创建调度节点driver和计算节点executor(两者都是运行在nodemanager上,也就是实际工作的是nodemanager);
3)spark框架会依据需求将计算逻辑根据分区分成不同的任务;driver主要用于调度,当driver中包含多个RDD时,形成一定的关联,最终会分解成一个个task,并放到taskpool中,便于调度;
4)调度节点会在任务池taskpool中将任务根据计算节点状态和首选位置的配置发送给最佳的计算节点executor进行计算;
总结:RDD在流程中主要将逻辑进行封装,生成task,然后由driver将task发送飞不同的executor节点执行计算
2. RDD创建(4种方式)
2.1 从内存中创建(常用)
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDMemory {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
val seq = Seq[Int](1, 2, 3)
val content = sc.makeRDD(seq)
content.foreach(println)
sc.stop()
}
}
2.2 从外部文件创建,读取数据(常用)
从文件中创建RDD,将文件的内容作为待处理的数据源。
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDFile {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
val content: RDD[String] = sc.textFile("data/1.txt")
content.foreach(println)
sc.stop()
}
}
解析:data/1.txt为绝对路径,既根目录下的文件夹的文件,
运行结果:
hello spark
hello world
也可以对文件夹下所有的文件进去读取
val content: RDD[String] = sc.textFile("data")
path路径还可以使用通配符
val content: RDD[String] = sc.textFile("data/1*.txt")
path也可以是分布式存储系统的路径,如HDFS
hdfs://IP:PORT/data/%s/v_%s/*.limf
疑问:当有多个文件时,如何知道数据来源于哪个文件。
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDFile1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//wholeTextFiles:以文件为单位读取数据,结果为元组,第一个元素为文件路径,第二个元素为文件内容
//textFile:以行 为单位读取数据,读取的数据为字符串
val content= sc.wholeTextFiles("data")
content.foreach(println)
sc.stop()
}
}
2.3 从其他RDD创建(不常用)
2.4 直接创建(不常用)
3.RDD并行度和分区
RDD内部是可以进行分区设定,提高效率;
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDMemory_par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//makeRDD方法的第二个参数numSlices为分区数量,也可不传第二个参数,采用默认值并行度defaultParallelism
val content = sc.makeRDD(List(1, 2, 3, 4), 2)
// val content = sc.makeRDD(List(1, 2, 3, 4) )
//将分区数据存到根目录下的文件夹中
content.saveAsTextFile("output")
sc.stop()
}
}
运行结果:
会生成一个output文件夹,并且里面有两个分区文件part_00000(内容为1,2),part_00001(内容为3,4)
当第二个参数numSlices不设置时
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDMemory_par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//makeRDD方法的第二个参数numSlices为分区数量,也可不传第二个参数,采用默认值并行度defaultParallelism
// val content = sc.makeRDD(List(1, 2, 3, 4), 2)
val content = sc.makeRDD(List(1, 2, 3, 4))
//将分区数据存到根目录下的文件夹中
content.saveAsTextFile("output")
sc.stop()
}
}
运行结果:
产生output文件夹,文件夹内有part_00000至part_00015个文件,因为本机是16核
不设置会采用默认的并行度,源码分析如下:
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
默认的并行度 defaultParallelism
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism
}
进入taskScheduler.defaultParallelism方法内显示为抽象方法,
def defaultParallelism(): Int
选定该方法,按快捷键ctrl+H显示实现类,进入实现类中发现
override def defaultParallelism(): Int = backend.defaultParallelism()
进入backend.defaultParallelism()发现还是个抽象方法,def defaultParallelism(): Int
选定该方法,按快捷键ctrl+H显示实现类,进入本地实现类LocalSchedulerBackend,发现源码
override def defaultParallelism(): Int =
scheduler.conf.getInt("spark.default.parallelism", totalCores)
其中进入scheduler.conf发现 val conf = sc.conf
sc 就是SparkConf,回到代码来看就是
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDMemory_par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
conf.set("spark.default.parallelism", "6")
val sc = new SparkContext(conf)
//makeRDD方法的第二个参数numSlices为分区数量,也可不传第二个参数,采用默认值并行度defaultParallelism
val content = sc.makeRDD(List(1, 2, 3, 4))
//将分区数据存到根目录下的文件夹中
content.saveAsTextFile("output")
sc.stop()
}
}
运行结果
产生output文件夹,文件夹内有part_00000至part_0005个文件
4. 数据在分区内的分配
当数据源比较少,甚至个数小于分区数时,数据是怎么分配的?
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDMemory_par1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
val content = sc.makeRDD(List(1, 2, 3, 4, 5), 3)
//将分区数据存到根目录下的文件夹中
content.saveAsTextFile("output")
sc.stop()
}
}
运行结果output文件夹是有3个part_0000到part_0002,分别存的数据是[1] [2,3] [4,5]
为什么数据分区是这样的?源码上分析
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices)
}
1. parallelize方法往下走
def parallelize[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
assertNotStopped()
new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
}
2. new ParallelCollectionRDD对象中,获取数据切分getPartitions
override def getPartitions: Array[Partition] = {
val slices = ParallelCollectionRDD.slice(data, numSlices).toArray
slices.indices.map(i => new ParallelCollectionPartition(id, i, slices(i))).toArray
}
3.slice方法切分
def slice[T: ClassTag](seq: Seq[T], numSlices: Int): Seq[Seq[T]] = {
if (numSlices < 1) {
throw new IllegalArgumentException("Positive number of partitions required")
}
// Sequences need to be sliced at the same set of index positions for operations
// like RDD.zip() to behave as expected
def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
(0 until numSlices).iterator.map { i =>
val start = ((i * length) / numSlices).toInt
val end = (((i + 1) * length) / numSlices).toInt
(start, end)
}
}
seq match {
case r: Range =>
positions(r.length, numSlices).zipWithIndex.map { case ((start, end), index) =>
// If the range is inclusive, use inclusive range for the last slice
if (r.isInclusive && index == numSlices - 1) {
new Range.Inclusive(r.start + start * r.step, r.end, r.step)
}
else {
new Range(r.start + start * r.step, r.start + end * r.step, r.step)
}
}.toSeq.asInstanceOf[Seq[Seq[T]]]
case nr: NumericRange[_] =>
// For ranges of Long, Double, BigInteger, etc
val slices = new ArrayBuffer[Seq[T]](numSlices)
var r = nr
for ((start, end) <- positions(nr.length, numSlices)) {
val sliceSize = end - start
slices += r.take(sliceSize).asInstanceOf[Seq[T]]
r = r.drop(sliceSize)
}
slices
case _ =>
val array = seq.toArray // To prevent O(n^2) operations for List etc
positions(array.length, numSlices).map { case (start, end) =>
array.slice(start, end).toSeq
}.toSeq
}
}
从判断符合条件上看是走 case _ 匹配,然后会根据def positions()函数进行数据分配,
详细可以通过断点查看每一步的变化情况
5. 分区的设定
5.1 如果是文件的读取,又是如何分区?
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDFile_par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//1. textFile是读取数据源,默认分区的参数minPartitions: Int = defaultMinPartitions
//2. def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
val content: RDD[String] = sc.textFile("data/1.txt")
content.saveAsTextFile("output")
sc.stop()
}
}
运行结果验证是有两个分区,因为
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)是取最小的
5.2 是否可以设置分区数量
val content: RDD[String] = sc.textFile("data/1.txt",3)
5.3 spark读取文件的方式在底层来看是采用hadoop的方法
如何理解 minPartitions?
一、新建3.txt
内容写
1
2
3
二、代码分析
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDFile_par {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//1. textFile是读取数据源,默认最小分区的参数minPartitions: Int = defaultMinPartitions
//2. def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
// val content: RDD[String] = sc.textFile("data/1.txt")
val content: RDD[String] = sc.textFile("data/3.txt", 2)
content.saveAsTextFile("output")
sc.stop()
}
}
我们设置了2个分区,但是运行结果是产生了3个分区,也就意味真正的分区数比设定的分区数要大,这是为什么?
三、分区数的计算
3.1 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)
}
3.2 进入TextInputFormat
public class TextInputFormat extends FileInputFormat<LongWritable, Text>
implements JobConfigurable {
3.3 进入FileInputFormat找到方法
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
3.4 内部有个for,作用是计算文件的字节大小totalSize
for (FileStatus file: files) { // check we have valid files
if (file.isDirectory()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}
注意:字节大小并不是文件内容得数量大小,可能包含换行之类的统计
比如3.txt文件是有7个字节长度,因为每一行后尾会有回车CR 换行LF两个字节
1
2
3
3.5 知道totalSize后计算每个分区goalSize应该放多少个字节,numSplits就是我们设置的分区数2
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
7/2 = 3 表示每个分区应该放3个字节,但是我们设置2个分区,共7个字节,还有一个字节怎么办?
此处则应该采用hadoop读取文件的 1.1原则(则超过分区百分之十则应该新建一个分区),
则会产生一个新的分区,共3个分区。
6. 分区数据的分配
package com.byxrs.spark_core.rdd_builder
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object BuilderRDDFile_par1 {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setMaster("local[*]").setAppName("RDD_Builder")
val sc = new SparkContext(conf)
//todo 数据分区的分配
//1. spark读取文件是采用hadoop方法,为一行一行的读取,与字节数无关
//2. 数据读取是以偏移量(从0开始)为单位基准,偏移量不会被重新读取
/**
* 比如3.txt文件 每一行后尾会有回车CR 换行LF两个字节
* 1@@ -》偏移量 012
* 2@@ -》偏移量 345
* 3 -》偏移量 6
*/
//3. 数据分区偏移量的范围计算
//part_0000分区 -》 [0,3] --》读到的数据1,2
//part_0001分区 -》 [3,6] --》因偏移量不会被重新读取,读到的数据3
//part_0002分区 -》 [6,7] --》因偏移量不会被重新读取,读到的数据空
val content: RDD[String] = sc.textFile("data/3.txt", 2)
content.saveAsTextFile("output")
sc.stop()
}
}
如果数据源是多个文件,则计算分区时以文件为单位进行分区。