Spark是一种基于内存的快速、通用、可扩展的大数据分析计算引擎。(不做存储,对比Hadoop来看,Hadoop是海量数据的存储和计算工具,主要是能够用来存储,计算的话Hadoop中的MapReduce框架比较简单,计算速度慢,基于硬盘。)
但是,正是由于Spark基于内存,所以在实际的生产环境中,由于内存的限制,可能会由于内存资源不够导致 Job 执行失败,此时,MapReduce 其实是一个更好的选择,所以 Spark 并不能完全替代 MR。
所谓的 Local 模式,就是不需要其他任何节点资源就可以在本地执行 Spark 代码的环境,一般用于教学,调试,演示等,之前在 IDEA 中运行代码的环境我们称之为开发环境,不太一样。
这个模式也就是指,并没有运行在集群环境上,而是直接在服务器本机上运行。
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master local[2] \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
本地模式是用来进行练习演示的,真实工作中还是要将应用提交到对应的集群中去执行,这里我们来看看只使用 Spark 自身节点运行的集群模式,也就是我们所谓的独立部署(Standalone)模式。Spark 的 Standalone 模式体现了经典的 master-slave 模式。
集群规划:
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://linux1:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
任务提交时参数设置:
所谓的高可用是因为当前集群中的 Master 节点只有一个,类似于Hadoop集群会存在单点故障问题。所以为了解决单点故障问题,需要在集群中配置多个 Master 节点,一旦处于活动状态的 Master 发生故障时,由备用 Master 提供服务,保证作业可以继续执行。这里的高可用和Hadoop集群一样采用 Zookeeper 进行设置。Zookeeper中运行着一个检查程序,用于判断当前处于active的master节点是否运行正常,如果不正常则将另一台处于standby的master节点置为active状态。具体运行原理查看Hadoop学习笔记中的第六章NameNode的HA高可用。
在配置高可用之后,我们在提交任务时也会有所变化。
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master spark://linux1:7077,linux2:7077 \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
独立部署(Standalone)模式由 Spark 自身提供计算资源,无需其他框架提供资源。这种方式降低了和其他第三方资源框架的耦合性,独立性非常强。但是你也要记住,Spark 主要是计算框架,而不是资源调度框架,所以本身提供的资源调度并不是它的强项,所以还是和其他专业的资源调度框架集成会更靠谱一些。所以接下来我们来学习在强大的 Yarn 环境下 Spark 是如何工作的(其实是因为在国内工作中,Yarn 使用的非常多)。
bin/spark-submit \
--class org.apache.spark.examples.SparkPi \
--master yarn \
--deploy-mode client \
./examples/jars/spark-examples_2.12-3.0.0.jar \
10
Mesos 是 Apache 下的开源分布式资源管理框架,它被称为是分布式系统的内核,在Twitter 得到广泛使用,管理着 Twitter 超过 30,0000 台服务器上的应用部署,但是在国内,依然使用着传统的 Hadoop 大数据框架,所以国内使用 Mesos 框架的并不多,但是原理其实都差不多,这里我们就不做过多讲解了。
容器化部署是目前业界很流行的一项技术,基于 Docker 镜像运行能够让用户更加方便地对应用进行管理和运维。容器管理工具中最为流行的就是 Kubernetes(k8s),而 Spark 也在最近的版本中支持了 k8s 部署模式。
Spark 提供了可以在 windows 系统下启动本地集群的方式,这样,在不使用虚拟机的情况下,也能学习 Spark 的基本使用。
Spark 框架的核心是一个计算引擎,整体来说,它采用了标准 master-slave 的结构。
如下图所示,它展示了一个 Spark 执行时的基本结构。图形中的 Driver 表示 master,负责管理整个集群中的作业任务调度。图形中的 Executor 则是 slave,负责实际执行任务。
由上图可以看出,对于 Spark 框架有两个核心组件:
(下面这部分可以结合Hadoop学习笔记中的第四章Yarn调度器的调度机制进行学习。)
Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。Driver 在 Spark 作业执行时主要负责:
Spark Executor 是集群中工作节点(Worker)中的一个 JVM 进程,负责在 Spark 作业中运行具体任务(Task),任务彼此之间相互独立。Spark Application 启动时,Executor 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 Executor 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。
Executor 有两个核心功能:
Spark 集群的独立部署环境中,不需要依赖其他的资源调度框架,自身就实现了资源调度的功能,所以环境中还有其他两个核心组件:Master 和 Worker,这里的 Master 是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责,类似于 Yarn 环境中的 RM,而Worker 也是进程,一个 Worker 运行在集群中的一台服务器上,由 Master 分配资源对数据进行并行的处理和计算,类似于 Yarn 环境中 NM。
Hadoop 用户向 YARN 集群提交应用程序时,提交程序中应该包含 ApplicationMaster,用于向资源调度器申请执行任务的资源容器 Container,运行用户自己的程序任务 job,监控整个任务的执行,跟踪整个任务的状态,处理任务失败等异常情况。说的简单点就是,ResourceManager(资源)和 Driver(计算)之间的解耦合靠的就是 ApplicationMaster。
RDD(Resilient Distributed Dataset)叫做弹性分布式数据集(RDD本身不保存数据,其中保存的是流经这个RDD的数据所要做的逻辑),是 Spark 中最基本的数据处理模型。代码中是一个抽象类,它代表一个弹性的、不可变、可分区、里面的元素可并行计算的集合。RDD的数据处理方式类似于IO流,也叫装饰者设计模式。但是RDD是不保存数据,没有缓存,只进行操作,而实际的IO过程有时会使用Buffer暂存一些数据。
RDD数据在调用collect()方法时,会将所有分布式的节点上的数据拉取到Driver所在的节点,然后将数据变成数组类型,这样不利于数据的分布式计算,所以在实际的集群模式中设计spark程序时,推荐不使用collect()方法。
从计算的角度来讲,数据处理过程中需要计算资源(内存 & CPU)和计算模型(逻辑)。执行时,需要将计算资源和计算模型进行协调和整合。
Spark 框架在执行时,先申请资源,然后将应用程序的数据处理逻辑分解成一个一个的计算任务。然后将任务发到已经分配资源的计算节点上, 然后各个节点根据数据分区拉取自己需要处理的数据,按照指定的计算模型进行数据计算,最后得到计算结果。
一个Spark任务的执行过程如下:(以YARN环境为例)
在YARN中,RM是整个集群的老大,管理着所有的节点资源(可能也包括自己)。
一个Spark任务被提交后,RM会给其分配一个NM或者说在某一个NM(具体位置应该也会涉及到机架感知、首选位置等因素的影响)中开启一个Executor,来运行这个任务或者叫任务的Driver,Driver在运行的过程中就会知道,自己的数据要怎么进行分区,分成多少个区,每个区需要什么样的数据(但是这个过程还没有具体地进行数据的处理),并根据分区的数目进行子任务的划分,然后将任务放到任务池中,等待本任务调度节点的调度。
Driver会申请分区数量+1个Executor来进行数据处理。分区数量对应计算节点,加的1代表调度节点。(此处的节点和物理的节点可以是一一对应的,也可以不是,因为一个物理节点如果计算性能比较强,可以同时开启多个Exector,Executor才是真正进行代码运算的“节点”。)
调度节点在调度任务时,会考虑节点的工作状态以及这个任务所需的实际数据所在物理存储位置(机架感知\首选位置),尽可能的避免大量数据在网络上的传输,减少任务的处理时间。
1.如果采取的是makeRDD()方法创建的RDD,则分区方式是下面这种:
如果spark运行的环境是local的话,分区数是通过获取本地 CPU 核数来确定的(即使是集群环境也是通过获取 executor 的核数来确定的):
// local
def defaultParallelism: Int = {
assertNotStopped()
taskScheduler.defaultParallelism // 这个数值就是totalCores
}
// distributed
override def defaultParallelism(): Int = {
conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}
与Hadoop的默认的进行数据分区的方法不同,Hadoop使用数据的Hash值进行取余运算的到的不同的分区号。而Spark则是首先对要处理的数据的类型进行判断:
seq match {
case r: Range => ...... \\ seq的数据类型是Range范围?
case nr: NumericRange[_] => ...... \\ seq的数据类型是NumericRange?
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
}
然后通过下面 positions() 函数对数据进行的切分:
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)
}
}
2.如果是从文件中加载数据使用的是textFile()方法创建RDD,则其分区的方法与Hadoop中MapTask阶段分片的方法比较类似。
// 这是默认的分区的数目,取的是 defaultParallelism(代表totalCores当前spark运行环境中可以使用的核数) 和 2 中较小的一个。
def defaultMinPartitions: Int = math.min(defaultParallelism, 2)
而通过上面的方法spark获取到的是 minPartitions ,实际的分区数有可能比这个数值要大。原因是在源码中spark对文件的读取与分片方式与 Hadoop 中 MapTask 阶段的分片方式是一样的,有个 1.1 倍的概念,也就是说,当前剩余等待切片的数据如果大于分片大小的 1.1 倍时才会进行分片,否则不会在进行分片处理,而是将剩余的 < 1.1 倍切片大小的数据划分为一块。
(参考Hadoop中MapTask的并行度决定方式中的切片原则)
比如说,我们读取到的文件的大小为 7B ,我们获取到的 minPartitions = 2 ,也就是最小分区数为2。所以应当说,我们将文件分成 2 个分区,每一个分区为 3.5B 大小,但是不能有小数,所以实际结果是分区大小为 3B ,7 - 3 = 4 。经过次一分区的划分后还余下 4B。而这一部分的数据由于 4B / 3B = 133.3% > 110%,所以根据Hadoop的分片原则,需要再进行一次分区 4 - 3 = 1B。现在 1B / 3B = 33.3% > 10% 所以,不再进行分区,剩余的数据单独算做一片。在spark中也就是对应单独的一个分区。所以最终的结果是:7B 大小的文件,在minPartitions = 2 的设置下最终分成了 3 个分区。
也可以这样理解,读到的文件大小为7B,minPartitions = 2,所以分区大小应为 7 / 2 = 3.5B,向下取整为 3B,7B大小的文件进行两次分区操作后剩余了 1B 大小的数据,又由于 1B / 3B = 33.3% > 10%,所以它不能与上一个分区进行合并,必须自己换分成单独的一各分区。假设,剩下的数据为 0.1B / 3B = 3.33% < 10% ,这时剩余的这些数据就可以和上一个分区合拼成一个分区了。
RDD算子的叫法由来:在认知心理学中认为问题的解决其实就将问题从初始状态进行改变直到问题变成完成状态。而中间经过的这个“操作”就叫做算子(Operator)。
问题(初始) => 经过某个操作(算子) => 问题(审核中) => 经过某个操作(算子) => 问题(完成)
RDD根据数据处理方式的不同将算子整体上分为单 Value 类型、双 Value类型和 Key-Value 类型。
def map[U:ClassTag](f: T => U): RDD[U]
def mapPartitions[U: ClassTag](
f: Iterator[T] =>
Iterator[U], preservesPartitioning: Boolean = false): RDD[U]
map 和 mapPartitions 的区别?
➢ 数据处理角度
map 算子是分区内一个数据一个数据的执行,类似于串行操作。而 mapPartitions 算子是以分区为单位进行批处理操作。
➢ 功能的角度
map 算子主要目的将数据源中的数据进行转换和改变。但是不会减少或增多数据。mapPartitions 算子需要传递一个迭代器,返回一个迭代器,没有要求的元素的个数保持不变,所以可以增加或减少数据。
➢ 性能的角度
map 算子因为类似于串行操作,所以性能比较低,而是 mapPartitions 算子类似于批处理,所以性能较高。但是 mapPartitions 算子会长时间占用内存,那么这样会导致内存可能不够用,出现内存溢出的错误。所以在内存有限的情况下,不推荐使用。使用 map 操作。
def mapPartitionsWithIndex[U: ClassTag](
f: (Int, Iterator[T]) =>
Iterator[U], preservesPartitioning: Boolean = false): RDD[U]
def flatMap[U: ClassTag](
f: T => TraversableOnce[U]): RDD[U]
def glom(): RDD[Array[T]]
def groupBy[K](f: T => K)(implicit kt: ClassTag[K]): RDD[(K, Iterable[T])]
def filter(f: T => Boolean): RDD[T]
def sample(
withReplacement: Boolean,
fraction: Double,
seed: Long = Utils.random.nextLong): RDD[T]
def distinct()(implicit ord: Ordering[T] = null): RDD[T]
def distinct(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
def coalesce(
numPartitions: Int,
shuffle: Boolean = false,
partitionCoalescer: Option[PartitionCoalescer] = Option.empty)
(implicit ord: Ordering[T] = null): RDD[T]
def repartition(numPartitions: Int)(implicit ord: Ordering[T] = null): RDD[T]
def sortBy[K](
f: (T) => K,
ascending: Boolean = true,
numPartitions: Int = this.partitions.length)
(implicit ord: Ordering[K], ctag: ClassTag[K]): RDD[T]
def intersection(other: RDD[T]): RDD[T]
def union(other: RDD[T]): RDD[T]
def subtract(other: RDD[T]): RDD[T]
def zip[U: ClassTag](other: RDD[U]): RDD[(T, U)]
def partitionBy(partitioner: Partitioner): RDD[(K, V)]
def reduceByKey(func: (V, V) => V): RDD[(K, V)]
def reduceByKey(func: (V, V) => V, numPartitions: Int): RDD[(K, V)]
def groupByKey(): RDD[(K, Iterable[V])]
def groupByKey(numPartitions: Int): RDD[(K, Iterable[V])]
def groupByKey(partitioner: Partitioner): RDD[(K, Iterable[V])]
1.groupByKey和reduceByKey的区别?
答:
从 shuffle 的角度:在 spark 中 shuffle 操作必须要进行落盘处理(溢写到磁盘),不能在内存中进行等待,否则可能会导致内存溢出。针对于聚合操作,reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
从功能的角度:reduceByKey 其实包含分组和聚合的功能。GroupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用 groupByKey
2.groupBy和groupByKey的区别?
答:从两者对数据处理完后的结果来看:
如果数据如下(K1,V1), (K1,V2), (K1,V3), (K2,V1), (K2,V2), (K2,V3),.
groupBy(K)的结果为:
(K1, CompactBuffer((K1, V1), (K,1 V2), ……))
(K2, CompactBuffer((K2, V1), (K2, V2), ……))
gruopByKey()的结果为:
(K1, CompactBuffer(V1, V2, ……))
(K2, CompactBuffer(V1, V2, ……))
可以看出groupByKey的结果相比groupBy更加精简一点,没有重复的分组依据出现。
如果想要把groupBy的结果中重复的数据去除可以配合使用mapValues方法,例如:
package indi.lency.Spark.TransfromOperator
import org.apache.spark.{SparkConf, SparkContext}
object test {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("test")
val sc = new SparkContext(sparkConf)
val a = List(Tuple2('a', 1), Tuple2('a', 1), Tuple2('b', 1))
val rdd = sc.makeRDD(a)
val rdd2 = rdd
.groupBy(_._1)
.mapValues(iter =>
iter.map(x => x._2))
.foreach(println)
val rdd3 = rdd
.groupByKey()
.foreach(println)
}
}
运行结果:
不重复数据的去除处理的结果:
使用mapValues方法进行去除重复数据:
def aggregateByKey[U: ClassTag](zeroValue: U)(
seqOp: (U, V) => U,
combOp: (U, U) => U): RDD[(K, U)] // 函数柯里化
补充:柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
参考
def foldByKey(zeroValue: V)(func: (V, V) => V): RDD[(K, V)]
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C): RDD[(K, C)]
def combineByKey[C](
createCombiner: V => C,
mergeValue: (C, V) => C,
mergeCombiners: (C, C) => C): RDD[(K, C)]
def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))]
join的使用会使的数据量扩大很多,谨慎使用
def leftOuterJoin[W](other: RDD[(K, W)]): RDD[(K, (V, Option[W]))]
连接?左连接?右连接?笛卡尔积?都是什么运算?
答:参考
def cogroup[W](other: RDD[(K, W)]): RDD[(K, (Iterable[V], Iterable[W]))]
思路以及伪代码如下:
// 数据处理:缺什么补什么,多什么删什么
// TODO:
// 1.数据分割
// 2.先按照省份进行分组
// 3.在省份的分组内按照广告的类型进行分组
// 4.统计每个广告的出现的次数
// 5.数据的格式是什么样的呢?城市,top3
/**
* agent.log:时间戳,省份,城市,用户,广告,中间字段使用空格分隔 map =>
* ((省份, 广告), 1) reduceByKey =>
* ((省份, 广告), 总点击量) map =>
* (省份, (广告, 总点击量)) groupByKey =>
* (省份, CompactBuffer((广告, 总点击量), (广告, 总点击量), ……)) mapValues 只处理value的map方法 =>
* Iterator(广告, 总点击量).toList.sortBy(_._2).reverse.take(3)
* (省份, List((广告, 第一点击量), (广告, 第二点击量), (广告, 第三点击量)))
*/
行动算子就是能够触发此算子之上的整个行动执行的算子,例如:collect算子。
def reduce(f: (T, T) => T): T
def collect(): Array[T]
def count(): Long
def first(): T
def take(num: Int): Array[T]
def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T]
def aggregate[U: ClassTag](zeroValue: U)(
seqOp: (U, T) => U,
combOp: (U, U) => U): U
def fold(zeroValue: T)(op: (T, T) => T): T
def countByKey(): Map[K, Long]
def saveAsTextFile(path: String): Unit
def saveAsObjectFile(path: String): Unit
def saveAsSequenceFile(
path: String,
codec: Option[Class[_ <: CompressionCodec]] = None): Unit
def foreach(f: T => Unit): Unit = withScope {
val cleanF = sc.clean(f)
sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}
从计算的角度, 算子以外的代码都是在 Driver 端执行, 算子里面的代码都是在 Executor端执行。那么在 scala 的函数式编程中,就会导致算子内经常会用到算子外的数据,这样就形成了闭包的效果,如果使用的算子外的数据无法序列化,就意味着无法传值给 Executor端执行,就会发生错误,所以需要在执行任务计算前,检测闭包内的对象是否可以进行序列化,这个操作我们称之为闭包检测。Scala2.12 版本后闭包编译方式发生了改变。
Java 的序列化能够序列化任何的类。但是比较重(字节多),序列化后,对象的提交也比较大。Spark 出于性能的考虑,Spark2.0 开始支持另外一种 Kryo 序列化机制。Kryo 速度是 Serializable 的 10 倍。当 RDD 在 Shuffle 数据的时候,简单数据类型、数组和字符串类型已经在 Spark 内部使用 Kryo 来序列化。
注意:即使使用 Kryo 序列化,也要继承 Serializable 接口。
try {
// New stage creation may throw an exception if, for example, jobs are run on a
// HadoopRDD whose underlying HDFS files have been deleted.
// 首先无论如何都会县创建一个 resultStage 作为 finalStage
finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
} catch {
case e: Exception =>
logWarning("Creating new stage failed due to exception - job: " + jobId, e)
listener.jobFailed(e)
return
}
// ……
private def createResultStage( // 创建resultStage
rdd: RDD[_],
func: (TaskContext, Iterator[_]) => _,
partitions: Array[Int],
jobId: Int,
callSite: CallSite): ResultStage = {
val parents = getOrCreateParentStages(rdd, jobId) // 获取当前 rdd 的前依赖 rdd,判断是否需要进行分阶段
val id = nextStageId.getAndIncrement()
val stage =
new ResultStage(id, rdd, func, partitions, parents, jobId, callSite) // 创建新阶段
stageIdToStage(id) = stage
updateJobIdStageIdMaps(jobId, stage)
stage // 返回stage
}
// ……
private def getOrCreateParentStages( // 获取当前 rdd 的前依赖 rdd
rdd: RDD[_], firstJobId: Int): List[Stage] = {
getShuffleDependencies(rdd) // 判断是否进行了分阶段,如果前面是shuffle依赖则分了阶段
.map { shuffleDep =>
getOrCreateShuffleMapStage(shuffleDep, firstJobId) // 对每一个 shuffleDep 创建阶段
}
.toList
}
// ……
private[scheduler] def getShuffleDependencies(
rdd: RDD[_]): HashSet[ShuffleDependency[_, _, _]] = {
val parents = new HashSet[ShuffleDependency[_, _, _]] // 存放 shuffleDep 的 Map
val visited = new HashSet[RDD[_]]
val waitingForVisit = new Stack[RDD[_]]
waitingForVisit.push(rdd) // 将当前的 rdd 放入待访问栈
while (waitingForVisit.nonEmpty) {
val toVisit = waitingForVisit.pop()
if (!visited(toVisit)) {
visited += toVisit
toVisit.dependencies.foreach { // 判断当前 rdd 的每一个依赖
case shuffleDep: ShuffleDependency[_, _, _] =>
parents += shuffleDep // 如果是 shuffleDep 就放到 parents 中
case dependency =>
waitingForVisit.push(dependency.rdd) // 否则递归的处理 rdd 的所有前依赖
}
}
}
parents // 返回shuffleDep
}
一个 Application 中所划分的阶段的数量 = 这个 Application 中所进行的有关 shuffle 的算子的次数 + 1(必然创建的一个resultStage)
7) RDD 任务划分
RDD 任务切分中间分为:Application应用、Job作业、Stage阶段 和 Task任务
⚫ Application:初始化一个 SparkContext 即生成一个 Application;
⚫ Job:一个 Action 算子就会生成一个 Job;
⚫ Stage:Stage数 等于宽依赖(ShuffleDependency)的个数加 1;
⚫ Task:一个 Stage 阶段中,最后一个 RDD 的分区个数就是 Task 的个数,而将一个 Job 中的所有 Stage 的 Task 数目添加到一起,就是整个Job的Task总数。
注意:Application -> Job -> Stage -> Task 每一层都是 1 对 n 的关系。
8)RDD 任务划分源码
val tasks: Seq[Task[_]] = try {
stage match {
case stage: ShuffleMapStage =>
partitionsToCompute.map { id =>
val locs = taskIdToLocations(id)
val part = stage.rdd.partitions(id)
new ShuffleMapTask(stage.id, stage.latestInfo.attemptId, taskBinary, part, locs, stage.latestInfo.taskMetrics, properties, Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
}
case stage: ResultStage =>
partitionsToCompute.map { id =>
val p: Int = stage.partitions(id)
val part = stage.rdd.partitions(p)
val locs = taskIdToLocations(id)
new ResultTask(stage.id, stage.latestInfo.attemptId, taskBinary, part, locs, id, properties, stage.latestInfo.taskMetrics, Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
}
}
……
val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()
……
override def findMissingPartitions(): Seq[Int] = {
mapOutputTrackerMaster
.findMissingPartitions(shuffleDep.shuffleId)
.getOrElse(0 until numPartitions)
}
Spark 目前支持 Hash 分区和 Range 分区,和用户自定义分区。Hash 分区为当前的默认分区。分区器直接决定了 RDD 中分区的个数、RDD 中每条数据经过 Shuffle 后进入哪个分区,进而决定了 Reduce 的个数。
➢ 只有 Key-Value 类型的 RDD 才有分区器,非 Key-Value 类型的 RDD 分区的值是 None
➢ 每个 RDD 的分区 ID 范围:0 ~ (numPartitions - 1),决定这个值是属于那个分区的。
分布式共享只写变量。
一些需要shuffle的工作,可以用累加器实现,这样就减少了节点之间数据的传输。
累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行 merge。
class LongAccumulator extends AccumulatorV2[jl.Long, jl.Long]{...}
class DoubleAccumulator extends AccumulatorV2[jl.Double, jl.Double] {...}
class CollectionAccumulator[T] extends AccumulatorV2[T, java.util.List[T]] {...}
以上三个是系统中已经写好了的累加器。
我们可以自定义累加器,来完成我们想要的功能:
/**
* 1.自定义累加器类型
* 2.向Spark进行注册
*/
val wcAcc = new MyAccumulator // 自定义累加器类型
sc.register(wcAcc, "WC")
/**
* 创建累加器对象:
* 1.继承AccumulatorV2,定义泛型
* IN:累加器输入的数据类型 => String
* OUT:累加器输出的数据类型 => mutalble.Map[String, Long]
*
* 2.实现对应的方法
*/
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Long]] {
private var wcMap = mutable.Map[String, Long]()
// 判断当前累加器变量是不是处于初始状态
override def isZero: Boolean = {
wcMap.isEmpty
}
// 复制一个新的累加器
override def copy(): AccumulatorV2[String, mutable.Map[String, Long]] = {
new MyAccumulator
}
// 重置当前累加器
override def reset(): Unit = {
wcMap.clear()
}
// 获取累加器需要计算的值
override def add(v: String): Unit = {
wcMap.update(v, wcMap.getOrElse(v, 0L) + 1)
}
// 返回到Driver端时进行合并各个累加器值的操作
override def merge(other: AccumulatorV2[String, mutable.Map[String, Long]]): Unit = {
val map1 = this.wcMap
val map2 = other.value
map2.foreach {
case (word, count) => { // 这里如果不加case的话表示,map中前后两个元素进行操作
map1.update(word, map1.getOrElse(word, 0L) + count)
}
}
}
// 获取累加器的值
override def value: mutable.Map[String, Long] = {
wcMap
}
}
分布式共享只读变量。
闭包数据都是以Task为单位发送的,每个Task中都会包含任务运行所需要的数据,但是当Executor的数量小于Task的数量时,这样就会导致一个Executor中会包含大量的重复数据,并且占用大量的内存。
Spark中的广播变量可以将闭包的数据保存到Executor的内存中。
其实每一个Executor就是一个JVM进程,在其启动时就会分配内存,所以我们可以将每个Task中都需要的数据分发到每一个Executor的内存中,已达到在这个Executor中共享的目的。(类似全局变量)
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,广播变量用起来都很顺手。
电商网站的实际数据及需求:
上面的数据图是从数据文件中截取的一部分内容,表示为电商网站的用户行为数据,主要包含用户的 4 种行为:搜索,点击,下单,支付,用户的操作在同一个动作中智能包含这四种动作的一种,不会同时包含两种及以上。数据规则如下:
➢ 数据文件中每行数据采用下划线分隔数据
➢ 每一行数据表示用户的一次行为,这个行为只能是 4 种行为的一种
➢ 如果搜索关键字为 null,表示数据不是搜索数据
➢ 如果点击的品类 ID 和产品 ID 为-1,表示数据不是点击数据
➢ 针对于下单行为,一次可以下单多个商品,所以品类 ID 和产品 ID 可以是多个,id 之
间采用逗号分隔,如果本次不是下单行为,则数据采用 null 表示
➢ 支付行为和下单行为类似
本案例的需求优化为:先按照点击数排名,靠前的就排名高;如果点击数相同,再比较下单数;下单数再相同,就比较支付数。
controller控制层:主要用来做任务调度。
service服务层:完成业务逻辑。
dao持久化层:读取文件或者数据库数据。
application:所有应用的入口。
bean:实体类。
common:指的是在大多数类中抽取出来的部分。比如说,多个类中有一些共同的代码或者方法,我们把他们抽取出来,放到这里。(类似于抽象类,这不过在Scala中叫做特质)
controller:控制层,用来做调度
dao:持久化层,用来做数据交互
service:完成业务执行的逻辑。
util:工具类,所有地方都可以使用,都可能使用的工具类。比如说判断非空等。
➢ RpcEnv:RPC 上下文环境,每个 RPC 终端运行时依赖的上下文环境称为 RpcEnv;当前 Spark 版本中使用的 NettyRpcEnv
➢ RpcEndpoint:RPC 通信终端。Spark 针对每个节点(Client/Master/Worker)都称之为一个 RPC 终端,且都实现 RpcEndpoint 接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用 Dispatcher。在 Spark 中,所有的终端都存在生命周期:
⚫ Constructor
⚫ onStart
⚫ receive*
⚫ onStop
➢ Dispatcher:消息调度(分发)器,针对于 RPC 终端需要发送远程消息或者从远程 RPC接收到的消息,分发至对应的指令发件箱(收件箱)。如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱;
➢ Inbox:指令消息收件箱。一个本地 RpcEndpoint 对应一个收件箱,Dispatcher 在每次向Inbox 存入消息时,都将对应 EndpointData 加入内部 ReceiverQueue 中,另外 Dispatcher 创建时会启动一个单独线程进行轮询 ReceiverQueue,进行收件箱消息消费;
➢ RpcEndpointRef 是对远程 RpcEndpoint 的一个引用。当我们需要向一个具体的 RpcEndpoint 发送消息时,一般我们需要获取到该 RpcEndpoint 的引用,然后通过该引用发送消息。
➢ OutBox:指令消息发件箱。对于当前 RpcEndpoint 来说,一个目标 RpcEndpoint 对应一个发件箱,如果向多个目标 RpcEndpoint 发送信息,则有多个OutBox。当消息放入Outbox 后,紧接着通过 TransportClient 将消息发送出去。消息放入发件箱以及发送过程是在同一个线程中进行;
➢ RpcAddress:表示远程的 RpcEndpointRef 的地址,Host + Port。
➢ TransportClient:Netty通信客户端,一个 OutBox 对应一个TransportClient,TransportClient 不断轮询 OutBox,根据 OutBox 消息的 receiver 信息,请求对应的远程 TransportServer;
➢ TransportServer:Netty 通信服务端,一个 RpcEndpoint 对应一个 TransportServer,接受远程消息后调用 Dispatcher 分发消息至对应收发件箱;
Job 是以 Action 方法为界,遇到一个 Action 方法则触发一个 Job;
Stage 是 Job 的子集,以 RDD 宽依赖(即 Shuffle)为界,遇到 Shuffle 做一次划分;
Task 是 Stage 的子集,以并行度(分区数)来衡量,分区数是多少,则有多少个 task。
Spark 的任务调度总体来说分两路进行,一路是 Stage 级的调度,一路是 Task 级的调度,总体调度流程如下图所示:
Spark RDD 通过其 Transactions 操作,形成了 RDD 血缘(依赖)关系图,即 DAG,最后通过 Action 的调用,触发 Job 并调度执行,执行过程中会创建两个调度器:DAGScheduler和 TaskScheduler。
➢ DAGScheduler 负责 Stage 级的调度,主要是将 job 切分成若干 Stages,并将每个 Stage 打包成 TaskSet 交给 TaskScheduler 调度。
➢ TaskScheduler 负责 Task 级的调度,将 DAGScheduler 给过来的 TaskSet 按照指定的调度
策略分发到 Executor 上执行,调度过程中 SchedulerBackend 负责提供可用资源,其中 SchedulerBackend 有多种实现,分别对接不同的资源管理系统。
作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对 JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。堆内内存受到 JVM 统一管理,堆外内存是直接向操作系统进行内存的申请和释放。
凭借统一内存管理机制,Spark 在一定程度上提高了堆内和堆外内存资源的利用率,降低了开发者维护 Spark 内存的难度,但并不意味着开发者可以高枕无忧。如果存储内存的空间太大或者说缓存的数据过多,反而会导致频繁的全量垃圾回收,降低任务执行时的性能,因为缓存的 RDD 数据通常都是长期驻留内存的。所以要想充分发挥 Spark 的性能,需要开发者进一步了解存储内存和执行内存各自的管理方式和实现原理。
在 Spark 中,DataFrame 是一种以 RDD 为基础的分布式数据集,类似于传统数据库中的二维表格。DataFrame 与 RDD 的主要区别在于,前者带有 schema 元信息,即 DataFrame所表示的二维表数据集的每一列都带有名称和类型。这使得 Spark SQL 得以洞察更多的结构
信息,从而对藏于 DataFrame 背后的数据源以及作用于 DataFrame 之上的变换进行了针对性的优化,最终达到大幅提升运行时效率的目标。反观 RDD,由于无从得知所存数据元素的具体内部结构,Spark Core 只能在 stage 层面进行简单、通用的流水线优化。同时,与 Hive 类似,DataFrame 也支持嵌套数据类型(struct、array 和 map)。从 API易用性的角度上看,DataFrame API 提供的是一套高层的关系操作,比函数式的 RDD API 要更加友好,门槛更低。
左侧的 RDD[Person]虽然以 Person对象为类型参数,但 Spark 框架本身不了解 Person 类的内部结构。所以我们在对Person中某个属性进行获取或操作时,总是要对 Person 进行获取和操作。
而右侧的 DataFrame 却提供了详细的结构信息,使得 Spark SQL 可以清楚地知道该数据集中包含哪些列,每列的名称和类型各是什么。DataFrame 是为数据提供了 Schema 的视图。可以把它当做数据库中的一张表来对待DataFrame 也是懒执行的,但性能上比 RDD 要高,主要原因:优化的执行计划,即查询计划通过 Spark catalyst optimiser 进行优化。比如下面一个例子:
为了说明查询优化,我们来看上图展示的人口数据分析的示例。图中构造了两个DataFrame,将它们 join 之后又做了一次 filter 操作。如果原封不动地执行这个执行计划,最终的执行效率是不高的。因为 join 是一个代价较大的操作,也可能会产生一个较大的数据集。如果我们能将 filter 下推到 join 下方,先对 DataFrame 进行过滤,再 join 过滤后的较小的结果集,便可以有效缩短执行时间。而 Spark SQL 的查询优化器正是这样做的。简而言之,逻辑查询计划优化就是一个利用基于关系代数的等价变换,将高成本的操作替换为低成本操作的过程。
DataSet 是分布式数据集合。DataSet 是 Spark 1.6 中添加的一个新抽象,是 DataFrame的一个扩展。它提供了 RDD 的优势(强类型,使用强大的 lambda 函数的能力)以及 SparkSQL 优化执行引擎的优点。DataSet 也可以使用功能性的转换(操作 map,flatMap,filter
等等)。
➢ DataSet 是 DataFrame API 的一个扩展,是 SparkSQL 最新的数据抽象
➢ 用户友好的 API 风格,既具有类型安全检查也具有 DataFrame 的查询优化特性;
➢ 用样例类来对 DataSet 中定义数据的结构信息,样例类中每个属性的名称直接映射到 DataSet 中的字段名称;
➢ DataSet 是强类型的。比如可以有 DataSet[Car],DataSet[Person]。
➢ DataFrame 是 DataSet 的特列,DataFrame=DataSet[Row] ,所以可以通过 as 方法将DataFrame 转换为 DataSet。Row 是一个类型,跟 Car、Person 这些的类型一样,所有的表结构信息都用 Row 来表示,获取数据时需要按照 Row 指定的顺序进行数据获取。
在 SparkSQL 中 Spark 为我们提供了两个新的抽象,分别是 DataFrame 和 DataSet。他们和 RDD 有什么区别呢?首先从版本的产生上来看:
➢ Spark1.0 => RDD
➢ Spark1.3 => DataFrame
➢ Spark1.6 => Dataset
如果同样的数据都给到这三个数据结构,他们分别计算之后,都会给出相同的结果。不同是的他们的执行效率和执行方式。在后期的 Spark 版本中,DataSet 有可能会逐步取代 RDD和 DataFrame 成为唯一的 API 接口。
➢ RDD、DataFrame、DataSet 全都是 spark 平台下的分布式弹性数据集,为处理超大型数据提供便利;
➢ 三者都有惰性机制,在进行创建、转换,如 map 方法时,不会立即执行,只有在遇到 ActionOperator 如 foreach 时,三者才会开始遍历运算;
➢ 三者有许多共同的函数,如 filter,排序等;
➢ 在对 DataFrame 和 Dataset 进行操作许多操作都需要这个包: import spark.implicits._(在创建好 SparkSession 对象后尽量直接导入)
➢ 三者都会根据 Spark 的内存情况自动缓存运算,这样即使数据量很大,也不用担心会内存溢出
➢ 三者都有 partition 的概念
➢ DataFrame 和 DataSet 均可使用模式匹配获取各个字段的值和类型
SparkStreaming是一个准实时(秒、分)、微批次(每隔一段时间,当做一批)的数据处理框架。
答:Application->Job->Stage->Task 每一层都是 1 对 n 的关系。
RDD 任务切分中间分为:Application、Job、Stage 和 Task
⚫ Application:初始化一个 SparkContext 即生成一个 Application;
⚫ Job:一个 Action 算子就会生成一个 Job;
⚫ Stage:Stage 等于宽依赖(ShuffleDependency)的个数加 1;
⚫ Task:一个 Stage 阶段中,最后一个 RDD 的分区个数就是 Task 的个数。
首先数据文件存储在HDFS上,每个File都包含了很多块,称为Block。当Spark读取这些文件作为输入时,会根据具体数据格式对应的InputFormat进行解析,一般都是将若干个Block合并成一个输入分片(当然有的输入格式是Block中的每个file划分成一个InputSplit,但是可以选择性的进行combiner),称为InputSplit,注意InputSplit不能跨越文件。
随后将为这些输入分片生成具体的Task。InputSplit与Task是一一对应的关系。随后这些具体的Task每个都会被分配到集群上的某个节点的某个Executor去执行。
注意: 这里的core是虚拟的core而不是机器的物理CPU核,可以理解为就是Executor的一个工作线程。而 Task被执行的并发度 = Executor数目 * 每个Executor核数。
至于partition的数目:
RDD在计算的时候,每个分区都会起一个task,所以rdd的分区数目决定了总的的task数目。申请的计算节点(Executor)数目和每个计算节点虚拟核数,决定了你同一时刻可以并行执行的task。
比如的RDD有100个分区,那么计算的时候就会生成100个task,你的资源配置为10个计算节点,每个两2个核,同一时刻可以并行的task数目为20,计算这个RDD就需要5个轮次。如果计算资源不变,你有101个task的话,就需要6个轮次,在最后一轮中,只有一个task在执行,其余核都在空转。如果资源不变,你的RDD只有2个分区,那么同一时刻只有2个task运行,其余18个核空转,造成资源浪费。这就是在spark调优中,增大RDD分区数目,增大任务并行度的做法。
(参考:https://blog.csdn.net/weixin_41590998/article/details/115710162)
是这样的,但是也不全是。
不管是什么技术,都是基于内存进行计算的。因为我们都需要将数据从磁盘中读取出来,然后加载到内存进行操作。
但是Spark的关键在于:在涉及到多个任务的交接时是不需要进行数据落盘的,这样就避免了与磁盘的 IO ;而 MapReduce 每个任务最终都是要先落盘才能进行下一个任务。所以 Spark 相比 MapReduce 比较快。
但是,Spark 中有的算子一样会涉及到磁盘 IO —— Shuffle,所以我们在进行数据处理的过程中尽量要避免使用涉及到Shuffle的算子。
一般来说,Spark比MapReduce运行速度快的原因主要有以下几点:
发生的原因:根本原因是数据不均衡,引起数据不均衡的原因有:
1.可能原本的数据就是分布不均衡的
2.还有就是可能进行了不当的合并分区操作,比如说使用了coalesce(),虽然第二个参数是shuffle可以避免数据倾斜,但是会将所有的数据进行收集然后重新分区,发生大量的数据传输。
解决办法:
1.首先通过任务运行的监测后端,查看是哪一个节点发生的数据倾斜。然后得到的它的数据分区(怎么得到呢?),然后通过smaple算子判断其中那些数据比较多,然后再对对应的数据进行更细致的划分,比如说对其进行转换成其他字符、对数量较多的数据在在其他标准进行数据分区。
答:
从 shuffle 的角度:在 spark 中 shuffle 操作必须要进行落盘处理(溢写到磁盘),不能在内存中进行等待,否则可能会导致内存溢出。针对于聚合操作,reduceByKey 和 groupByKey 都存在 shuffle 的操作,但是 reduceByKey可以在 shuffle 前对分区内相同 key 的数据进行预聚合(combine)功能,这样会减少落盘的数据量,而 groupByKey 只是进行分组,不存在数据量减少的问题,reduceByKey 性能比较高。
从功能的角度:reduceByKey 其实包含分组和聚合的功能。GroupByKey 只能分组,不能聚合,所以在分组聚合的场合下,推荐使用 reduceByKey,如果仅仅是分组而不需要聚合。那么还是只能使用 groupByKey
答:从两者对数据处理完后的结果来看:
如果数据如下(K1,V1), (K1,V2), (K1,V3), (K2,V1), (K2,V2), (K2,V3),.
groupBy(K)的结果为:
(K1, CompactBuffer((K1, V1), (K,1 V2), ……))
(K2, CompactBuffer((K2, V1), (K2, V2), ……))
gruopByKey()的结果为:
(K1, CompactBuffer(V1, V2, ……))
(K2, CompactBuffer(V1, V2, ……))
可以看出groupByKey的结果相比groupBy更加精简一点,没有重复的分组依据出现。
如果想要把groupBy的结果中重复的数据去除可以配合使用mapValues方法,例如:
package indi.lency.Spark.TransfromOperator
import org.apache.spark.{SparkConf, SparkContext}
object test {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setMaster("local[*]").setAppName("test")
val sc = new SparkContext(sparkConf)
val a = List(Tuple2('a', 1), Tuple2('a', 1), Tuple2('b', 1))
val rdd = sc.makeRDD(a)
val rdd2 = rdd
.groupBy(_._1)
.mapValues(iter =>
iter.map(x => x._2))
.foreach(println)
val rdd3 = rdd
.groupByKey()
.foreach(println)
}
}
运行结果:
不重复数据的去除处理的结果:
使用mapValues方法进行去除重复数据:
1)Cache 缓存只是将数据保存起来,不切断血缘依赖。Checkpoint 检查点切断血缘依赖,相当于改变了原始数据的来源。
2)Cache 缓存、persist 的数据通常存储在磁盘、内存等地方,可靠性低,而且是临时存储,当本次的Job运行结束时这些数据都会被删除,如果下次再用时还需要重新计算。Checkpoint 的数据通常存储在 HDFS 等容错、高可用的文件系统,可靠性高,Job运行结束后也不会消失,可以多次使用,相当于改变了Job的原始数据来源。
3)建议对使用了 checkpoint() 的 RDD 使用 Cache 缓存,这样 checkpoint 的 job 只需从 Cache 缓存中读取数据即可,否则需要再从头计算一次 RDD。