一、Spark 简述
Apache Spark 是专为大规模数据处理而设计的快速通用的计算引擎。拥有Hadoop MapReduce所具有的优点;但不同于MapReduce的是——Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好地适用于数据挖掘与机器学习等需要迭代的MapReduce的算法。
二、开始Spark
首先需要引入maven
<!-- spark core -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.12</artifactId>
<version>3.1.1</version>
</dependency>
<!-- spark core(看yarn启动源码用) -->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.12</artifactId>
<version>3.1.1</version>
</dependency>
// 创建和spark的连接
val wordCount: SparkConf = new SparkConf().setMaster("local[*]") // [*]代表当前系统最大可用核数
.setAppName("RDD") // 设置名称
val context: SparkContext = new SparkContext(wordCount)
// 读取文件的数据,进行筛选
val lines: RDD[String] = context.textFile("datas/1.txt")
val words: RDD[String] = lines.flatMap(_.split(" "))
val tuples: Array[(String, Int)] = value.collect()
tuples.foreach(println)
// 关闭连接
context.stop()
三、RDD
RDD是Spark中重要的一部分,是弹性分布式数据,spark的基本数据模型。RDD中不保存数据,如果重复使用RDD就会重新计算。RDD有五个重要的属性:
- 分区列表:执行并行计算,实现分布式的重要属性
- 分区的计算函数
- RDD之间的依赖关系:多个RDD之间的依赖 spark任务的容错机制就是根据这个特性而来
- 对于kv类型的rdd才会有分区函数
- 当计算每个分区会有首选的位置 选择最优的执行节点
1. 在内存中创建RDD
val data = List(1, 2, 3, 4, 5, 6)
1. 方法一
val rdd: RDD[Int] = context.parallelize(data)
2. 方法二
val rdd: RDD[Int] = context.makeRDD(data) // makeRDD底层是parallelize
2. 通过文件创建RDD
Spark底层读取文件实际上使用的是Hadoop的读取方式。
1. 读取文件内容 path可以是具体路径也可以写到目录(就会读取多个文件), 文件创建RDD
2. textFile 以行为单位读取 wholeTextFile 以文件为单位读取 返回结果是个元组(文件路径, 内容)
3. minPartitions 指定最小分区数, 默认是2
val line1: RDD[String] = context.textFile("datas/1.txt", minPartitions = 2)
val line2: RDD[String] = context.wholeTextFile("datas")
3. 分区的设置
// 默认 "spark.default.parallelism"这个配置的 就是去SparkConf读这个参数 没有配置就是总核数
val rdd: RDD[Int] = context.makeRDD(data, 2) // numSlices 代表分区数
//将处理的数据保存成分区文件
rdd.saveAsTextFile("outPut")
当我们传入一个数组时Spark是如何将数据进行分区的,源码解读
context.makeRDD(data, 2) // 进入到makeRDD中
------------------------------------------------
def makeRDD[T: ClassTag](
seq: Seq[T],
numSlices: Int = defaultParallelism): RDD[T] = withScope {
parallelize(seq, numSlices) // 进入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]]())
}
------------------------------------------------
private[spark] class ParallelCollectionRDD[T: ClassTag](
sc: SparkContext,
@transient private val data: Seq[T],
numSlices: Int,
locationPrefs: Map[Int, Seq[String]])
extends RDD[T](sc, Nil) {
override def getPartitions: Array[Partition] = {
val slices = ParallelCollectionRDD.slice(data, numSlices).toArray // 在这里进行分区
slices.indices.map(i => new ParallelCollectionPartition(id, i, slices(i))).toArray
}
}
-------------------------------------------------
private object ParallelCollectionRDD {
def slice[T: ClassTag](seq: Seq[T], numSlices: Int): Seq[Seq[T]] = {
// 在这里根据数组的长度进行的计算,然后通过数组进行切片
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 _ => // 由于是数组格式走这个匹配
val array = seq.toArray
positions(array.length, numSlices).map { case (start, end) =>
array.slice(start, end).toSeq
}.toSeq
}
}
}
读取文件时的分区,Spark是通过所有文件的总字节数 ÷ 最小分区数得到要分多少个区,上源码。
context.textFile("datas/1.txt")
------------------------------------------
def textFile(
path: String,
minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
assertNotStopped()
// 在这里传入TextInputFormat,这里对文件的格式做了区分
hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
minPartitions).map(pair => pair._2.toString).setName(path)
}
------------------------------------------
// 然后看TextInputFormat的父类 FileInputFormat
public InputSplit[] getSplits(JobConf job, int numSplits) throws IOException {
StopWatch sw = (new StopWatch()).start();
FileStatus[] stats = this.listStatus(job);
job.setLong("mapreduce.input.fileinputformat.numinputfiles", (long)stats.length);
long totalSize = 0L;
List<FileStatus> files = new ArrayList(stats.length);
FileStatus[] var9 = stats;
int var10 = stats.length;
// 在这里进行了分区个数的计算
long goalSize = totalSize / (long)(numSplits == 0 ? 1 : numSplits);
}
hadoop在读取的时候,已经读取过的不会在进行读取。
文件:
1
2
3
这样读出来会有三个分区,是因为三个字符加上两个回车换行(/r/n)总共7字节,
7/2 = 3 每个分区读三个字节,所以需要三个分区。
分区:
0 => [0,3] => 1 2 每次左右都是包含
1 => [3,6] => 3 第三个字节被读过不会再次读入
2 => [6,7] =>
4. collect
// 如果最后不去触发collect方法,spark是不会进行作业的,前面的只是在进行一层层的包装
val tuples: Array[(String, Int)] = value.collect()
5. 算子
每个算子就是一个操作,通过算子来转换状态,下面开始介绍RDD相关的算子。
RDD的计算在一个分区内都是一个个执行,前面一个执行完成才会执行下一个,同一个分区内的数据是有序的,分区间的数据是无序的。
※ 转换算子
map:一个个进行操作。
val rdd: RDD[Int] = context.makeRDD(data, 2)
val value: RDD[Int] = rdd.map((number: Int) => {
number * 2
})
mapPartitions: 是把一个分区的数据全部拿到,而不是一个个进行操作, 但是会将整个分区的数据加载内存,处理完的数据不会释放内存。
rdd.mapPartitions(iter =>
{
iter.map(_*2) // iter就是迭代器,每次iter拿到的都是分区的数据
}
)
mapPartitionsWithIndex:将待处理的数据以分区为单位发送到计算节点。
// index代表哪个分区 iter代表当前分区全部数据
val value: RDD[Int] = rdd.mapPartitionsWithIndex((index, iter) => {
if(index == 1){
iter
}else{
Nil.iterator
}
})
flatMap:扁平映射 flatten + map的操作。
val data: Seq[Any] = List(List(1,2), 3,4, List(5,6), List(7,8), List(9))
val rdd: RDD[Any] = context.makeRDD(data) // numSlices 代表分区数
// 传入是个每一个单个的值,返回一定要是个可迭代的元素
val value: RDD[Any] = rdd.flatMap((data) => {
data match {
case list: List[_] => list
case d => List(d).iterator
}
})
glom:将一个分区的数据转换成相同类型的内存数组进行处理,分区不变。
val gloms: RDD[Array[Int]] = rdd.glom()
groupBy:分组,分区默认不变,但是数据会打乱重新组合,一个组的数据在一个分区中。但并不是说一个分区只有一个组,根据返回的key进行分组。
val groups: RDD[(Int, Iterable[Int])] = rdd.groupBy((num: Int) => {
num % 2
})
filter: 符合规则的数据会保留,过滤后分区不变,分区内的数据可能不平衡,会出现数据倾斜的情况。
val groups: RDD[Int] = rdd.filter(num => num % 2 == 0)
sample: 抽取一部分数据,用于验证是否出现数据倾斜的情况。
val groups: RDD[Int] = rdd.sample(false, // 抽取是否有放回
0.4, // 如果不放回就是代表一个基准率,有放回的情况就是代表抽取的次数
1 // 随机种子,不传就是使用的是系统的默认时间
)