RDD是spark中的一个最基本的抽象,代表着一个不可变、可分区、可以并行计算的分布式数据集;
RDD是一个基本的抽象,是对存储在分布式文件系统上的数据操作进行代理。RDD并不存储需要计算数据,而是一个代理,你对RDD进行的操作,他会在Driver端转换成Task,下发到Executor中,计算分散在集群中的数据。RDD是抽象的,并不存储数据,而是封装记录你对数据的操作。
RDD计算是以分区为单位的
RDD算子的操作包括两种类型:Transformation和Action;初始创建都是由SparkContext来负责的
RDD中的所有转换(Transformation)都是延迟加载(Lazy)的,也就是说,它们并不会直接计算结果。相反的,它们只是记住这些应用到基础数据集(例如一个文件)上的转换动作。只有当发生一个要求返回结果给Driver的动作(action)时,这些转换才会真正运行。
RDD支持缓存操作,由cache实现,cache可以对RDD进行持久化操作,可以让RDD保存在磁盘或者内存中,以便后续重复使用;但是没有生成新的RDD,也没有触发任务执行,只会标记该RDD分区对应的数据,在第一次触发Action时放入到内存
检查点(checkpoint)是将 RDD 保存到磁盘上的操作,以便将来对此 RDD 的引用能直接访问磁盘上的那些中间结果,而不需要从其源头重新计算 RDD。 它与缓存类似,只是它不存储在内存中,只存在磁盘上。
什么是有向无环图? 在图论中,如果一个有向图无法从某个顶点出发经过若干条边回到该点,则这个图是一个有向无环图(DAG图)。
Spark使用DAG来反映各RDD之间的依赖或血缘关系。
MapReduce的流程一般包含为map和reduce两个阶段,map/reduce可在不同分区并行执行多个任务,然而map任务对所负责的分块数据进行map处理后,并写入缓冲区,然后进行分区、排序、聚合等操作,最后将数据输出到磁盘上的不同分区。随后的reduce任务在执行时,必须要将map输出到磁盘的数据通过网络拷贝到本地内存,经过一系列归并、排序计算以后输出回文件系统中。
从上面的过程中可以看出,MapReduce的缺陷:
为什么使用Spark?
与MapReduce不同,Spark的计算流程分为两部分:逻辑处理流程、执行阶段和执行任务划分。
Spark首先根据用户代码中的数据操作语义和顺序,转换成逻辑处理流程(数据计算语义=>输入/输出、中间数据的抽象化表示,RDD;执行过程=>DAG有向无环图),然后Spark对逻辑处理流程进行划分(宽窄依赖),生成物理执行计划(执行阶段Stage+这些任务task)。与MapReduce不同的是,一个SparkJob可以包含多个执行阶段(stage),而且每个执行阶段可以包含多种计算任务,而不是单一地将计算任务区分为map或者reduce。另外,Spark的RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。Spark的cache方法允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。
另外,MapReduce部署模式中。会为每一个task启动一个JVM进程运行,而且是在task将要运行时启动JVM,而Spark会预先启动资源容器(Executor JVM),然后当需要执行task时,再在Executor JVM里启动线程运行。
Spark集群由集群管理器(Cluster Manager)、工作节点(Worker)、执行器(Executor)、驱动器(Driver)、应用程序(Application)等部分组成
** Cluster Manager:** Spark的集群管理器,主要负责对整个集群资源的分配与管理。ClusterManager在YARN部署模式下为ResourceManager;在Standalone部署模式下为Master。Cluster Manager分配的资源属于一级分配,它将各个Worker上的内存、CPU等资源分配给Application,但是并不负责对Executor的资源分配。Standalone部署模式下的Master会直接给Application分配内存、CPU及Executor等资源。
Worker:Spark的工作节点。在YARN部署模式下实际由NodeManager替代。Worker节点主要负责以下工作:将自己的内存、CPU等资源通过注册机制告知Cluster Manager;创建Executor;将资源和任务进一步分配给Executor;同步资源信息、Executor状态信息给Cluster Manager等。在Standalone部署模式下,Master将Worker上的内存、CPU及Executor等资源分配给Application后,将命令Worker启动CoarseGrainedExecutorBackend进程
**Executor:**主要负责任务的执行及与Worker、Driver的信息同步。
Driver: Application的驱动程序,Application通过Driver与Cluster Manager、Executor进行通信。 Driver可以运行在Application中,也可以由Application提交给Cluster Manager并由Cluster Manager安排Worker运行。
Application:用户使用Spark提供的API编写的应用程序,Application通过Spark API将进行RDD的转换和DAG的构建,并通过Driver将Application注册到Cluster Manager。 Cluster Manager将会根据Application的资源需求,通过一级分配将Executor、内存、CPU等资源分配给Application。Driver通过二级分配将Executor等资源分配给每一个任务,Application最后通过Driver告诉Executor运行任务。
task内部数据的存储与计算问题
task对于一些流水线式的操作,会在计算时只需要在内存中保留当前被处理的单个record即可,同时将结果保存至内存中,以提高task的执行效率,并减少内存使用率。
task之间的数据传递和计算问题:
stage之间的依赖关系是ShuffleDependency,下游stage中的每个task会从父RDD的每个分区中获取数据。上游的stage预先将输出数据进行划分,按分区存放,分区个数与下游task个数一致,这个过程叫Shuffle write。按照分区存放完了后,下游task将属于自己分区的数据通过网络传输获取,然后将上游不同分区的数据聚合在一起,这个过程叫Shuffle read
yarn-cluster:
yarn-client:
yarn-client和yarn-cluster的选择问题:
Spark On Yarn模式的优点:
1)与其他计算框架共享集群资源(Spark框架与MapReduce框架同时运行,如果不用Yarn进行资源分配,MapReduce分到的内存资源会很少,效率低下);资源按需分配,进而提高集群资源利用等。
2)相较于Spark自带的Standalone模式,Yarn的资源分配更加细致。
3)Application部署简化,例如Spark,Storm等多种框架的应用由客户端提交后,由Yarn负责资源的管理和调度,利用Container作为资源隔离的单位,以它为单位去使用内存,cpu等。
4)Yarn通过队列的方式,管理同时运行在Yarn集群中的多个服务,可根据不同类型的应用程序负载情况,调整对应的资源使用量,实现资源弹性管理。
在提交任务时的几个重要参数
master ——提交模式,local,yarn-cluster…
executor-cores —— 每个 executor 使用的内核数, 默认为 1, 官方建议 2-5 个
num-executors —— 启动 executors 的数量, 默认为 2
executor-memory —— executor 内存大小, 默认 1G
driver-cores —— driver 使用内核数, 默认为 1
driver-memory —— driver 内存大小, 默认 512
1.num-executors 线程数:一般设置在50-100之间,必须设置,不然默认启动的executor非常少,不能充分利用集群资源,运行速度慢
2.executor-memory 线程内存:参考值4g-8g,num-executor乘以executor-memory不能超过队列最大内存,申请的资源最好不要超过最大内存的1/3-1/2
3.executor-cores 线程CPU core数量:core越多,task线程就能快速的分配,参考值2-4,num-executor*executor-cores的1/3-1/2
1.spark-submit spark提交
2.--queue spark 在spark队列
3.--master yarn 在yarn节点提交
4.--deploy-mode client 选择client模型,还是cluster模式;在同一个节点用client,在不同的节点用cluster
5.--executor-memory=4G 线程内存:参考值4g-8g,num-executor乘以executor-memory不能超过队列最大内存,申请的资源最好不要超过最大内存的1/3-1/2
6.--conf spark.dynamicAllocation.enabled=true 是否启动动态资源分配
7.--executor-cores 2 线程CPU core数量:core越多,task线程就能快速的分配,参考值2-4,num-executor*executor-cores的1/3-1/2
8.--conf spark.dynamicAllocation.minExecutors=4 执行器最少数量
9.--conf spark.dynamicAllocation.maxExecutors=10 执行器最大数量
10.--conf spark.dynamicAllocation.initialExecutors=4 若动态分配为true,执行器的初始数量
11.--conf spark.executor.memoryOverhead=2g 堆外内存:处理大数据的时候,这里都会出现问题,导致spark作业反复崩溃,无法运行;此时就去调节这个参数,到至少1G(1024M),甚至说2G、4G)
12.--conf spark.speculation=true 推测执行:在接入kafaka的时候不能使用,需要考虑情景
13.--conf spark.shuffle.service.enabled=true 提升shuffle计算性能
Spark通过宽窄依赖解决RDD和分区之间的数据依赖关系:父RDD的一个分区的数据分发给不同的子RDD,则为宽依赖,要进行Shuffle,反之为窄依赖(即使存在分发给子RDD的可能就是宽依赖)
为什么要划分依赖?
会从触发action操作的那个rdd开始往前倒推,首先会为最后一个rdd创建一个stage,然后往前倒推的时候,如果发现对某个rdd是宽依赖,那么就会将宽依赖的那个rdd创建一个新的stage,那个rdd就是新的stage的最后一个rdd。然后依次类推,继续往前倒推,根据窄依赖或者宽依赖,进行stage的划分。直到所有的rdd全部遍历完了为止。
Shuffle机制即是,运行在不同Stage、不同结点上的task之间进行数据传递的过程。Shuffle机制分为Shuffle Write和Shuffle Read两个阶段,Shuffle Write解决上游Stage输出数据的分区问题,后者解决下游Stage从上游stage获取数据、重新组织、并为后续操作提供数据的问题。
Shuffle Write计算框架的顺序为:map()输出——>数据聚合——>排序——>分区
对于不同算子,Spark对Shuffle Write过程进行了优化
不需要map端聚合和排序,BypassMergeSortShuffleWriter
这种情况最为简单,只需要实现分区即可,但具体细节在按分区写入到磁盘过程中,在内存中为每个分区添加了一个buffer,Spark根据partitionId,将record输出到buffer,而后当buffer填满时,溢写到磁盘中。分配buffer的原因:map端输出record速度很快,需要进行缓冲减少磁盘IO。
该模式的优点就是速度快,但在分区过多的情况下,消耗资源,每个分区都需要一个buffer(大小默认为32kb)。
该模式适用于分区较少的情况(默认分区个数为spark.Shuffle.sort.bypassMergeThreshold=200个),如gruopByKey(100)
不需要聚合,但需要排序,SortShuffleWriter(KeyOrdering=true)
按partitionId+Key对Map进行排序,实现方式为:创建一个Array(PartitionedPairBuffer)存放record,并对PartitionedPairBuffer中的元素的Key按<(PID,K),V>进行存储,最后进行排序,并写入磁盘,通过简历索引来标示每个分区。
值得注意的是:
sortByKey的排序是在Shuffle Read完成的,上述这种方法是为了解决分区个数较多导致Buffer较大的问题。当groupByKey和sortByKey的传参分区大于spark.Shuffle.sort.bypassMergeThreshold时,会选择SortShuffleWriter,小于时选择BypassMergeSortShuffleWriter,通过SortShuffleManager控制两种模式的选择。
SortShuffleManager中的registerShuffle方法,调用SortShuffleWriter.shouldBypassMergeSort判断是否用BypassMergeSort,可以看出,当map端需要Combine和dep.partitioner.numPartitions > bypassMergeThreshold时选择SortShuffleWriter,其他选择BypassMergeSort。
def shouldBypassMergeSort(conf: SparkConf, dep: ShuffleDependency[_, _, _]): Boolean = {
// We cannot bypass sorting if we need to do map-side aggregation.
if (dep.mapSideCombine) {
false
} else {
val bypassMergeThreshold: Int = conf.getInt("spark.shuffle.sort.bypassMergeThreshold", 200)
dep.partitioner.numPartitions <= bypassMergeThreshold
}
}
override def registerShuffle[K, V, C](
shuffleId: Int,
numMaps: Int,
dependency: ShuffleDependency[K, V, C]): ShuffleHandle = {
if (SortShuffleWriter.shouldBypassMergeSort(conf, dependency)) {
// If there are fewer than spark.shuffle.sort.bypassMergeThreshold partitions and we don't
// need map-side aggregation, then write numPartitions files directly and just concatenate
// them at the end. This avoids doing serialization and deserialization twice to merge
// together the spilled files, which would happen with the normal code path. The downside is
// having multiple files open at a time and thus more memory allocated to buffers.
new BypassMergeSortShuffleHandle[K, V](
shuffleId, numMaps, dependency.asInstanceOf[ShuffleDependency[K, V, V]])
} else if (SortShuffleManager.canUseSerializedShuffle(dependency)) {
// Otherwise, try to buffer map outputs in a serialized form, since this is more efficient:
new SerializedShuffleHandle[K, V](
shuffleId, numMaps, dependency.asInstanceOf[ShuffleDependency[K, V, V]])
} else {
// Otherwise, buffer map outputs in a deserialized form:
new BaseShuffleHandle(shuffleId, numMaps, dependency)
}
}
通过HashMap(PartitionedAppendOnlyMap)实现,key为PID+Key,Value则为combine的结果。聚合后采用Array排序,如2.如果不需要对key排序,则只按PID排序。先进行聚合,再排序,最后将排序后的record写入一个分区文件。如果HashMap存不下,则先扩容,最后如果还存储不下,则将HashMap中的record排序后spill到磁盘。
Shuffle Read的技术和数据结构和Shuffle Write过程非常类似,而且不需要分区。Shuffle Read阶段包含三个功能:跨结点数据获取、聚合和排序。过程中也采用数据结构ExternalAppendOnlyMap和PartitionedPairBuffer。
Shuffle Read也分是否需要排序、聚合三种情况,与Shuffle Write过程类似。不同点有两个:
Spark中的task是否需要进行Shuffle read,一个stage中的计算步骤是什么,如何确定计算路径上是否有缓存数据。
参考博客
http://hbasefly.com/tag/spark/
微批次,每个批次计算的数据比较小
准实时,每个批次的产生有时间间隔;Spark会在Driver端定期的生成微批次的job并生成task调度到Executor中,因此task的调度也有延迟
流式计算,这个Application会一直运算,除非认为停止或出现异常;
什么是DStream?
DStream 本质上是一个以时间为键,RDD 为值的哈希表,保存了按时间顺序产生的 RDD,而每个 RDD 封装了批处理时间间隔内获取到的数据。SS 每次将新产生的 RDD 添加到哈希表中,而对于已经不再需要的 RDD 则会从这个哈希表中删除,所以 DStream 也可以简单地理解为以时间为键的 RDD 的动态序列。
早期版本 Spark 不支持反向压力,但从 Spark 1.5 版本开始,Spark Streaming 也引入了反向压力功能,这是不是正说明了反向压力功能对流计算系统的必要性!默认情况下 Spark Streaming 的反向压力功能是关闭的。当要使用反向压力功能时,需要将 spark.streaming.backpressure.enabled 设置为 true。
整体而言,Spark 的反向压力借鉴了工业控制中 PID 控制器的思路,其工作原理如下。
首先,当 Spark 在处理完每批数据时,统计每批数据的处理结束时间、处理时延、等待时延、处理消息数等信息。
然后,根据统计信息估计处理速度,并将这个估计值通知给数据生产者。
最后,数据生产者根据估计出的处理速度,动态调整生产速度,最终使得生产速度与处理速度相匹配。
由于流计算系统是长期运行、数据不断流入的,因此其Spark守护进程(Driver)的可靠性是至关重要的,它决定了Streaming程序能否一直正确地运行下去。
Driver实现HA的解决方案就是将元数据持久化,以便重启后的状态恢复。Driver持久化的元数据包括:Block元数据(Receiver从网络上接收到的数据,组装成Block后产生的Block元数据)和Checkpoint数据(包括配置项、DStream操作、未完成的Batch状态、和生成的RDD数据等)
Driver失败重启后:
参考:博客地址
Receiver模式数据接收流程 :
当执行 SS 的 start 方法后,SS 会标记 StreamingContext 为 Active 状态,并且单独起个线程通过ReceiverTracker 将从ReceiverInputDStreams 中获取的 receivers 以并行集合的方式分发到 worker 节点,并运行他们。worker 节点会启动ReceiverSupervisor。接着按如下步骤处理:
Receiver存在的问题:
worker节点中 exeutor线程里的 receiver接口会一直消费kafka中的数据,如果spark集群定义了每个worker使用的cpu资源不足以消费完了这5秒的数据,那么就会出现数据的丢失,消费不了的那些数据就没了,并且streaming一经启动会一直循环消费拉取资源,如果出现上述问题,分配的cpu不足以消费5秒拉取的数据,那么丢失的数据便会越积越多,这在程序里是严重的bug。
此时则必须要通过Wal方式把日志偏移量存到hdfs上面做备份,防止数据丢失,但是这样会影响性能。
Direct模式相比Receiver模式的优点:
源码分析
A batch-oriented interface for consuming from Kafka.Starting and ending offsets are specified in advance,
so that you can control exactly-once semantics.
从kafka 消费的针对批处理的API,开始和结束 的 offset 都提前设定了,所以我们可以控制exactly-once 的语义。
https://mp.weixin.qq.com/s/urA3S1zdxyGIU-ZyDS1aJA
https://zhuanlan.zhihu.com/p/161963838
spark调优比较复杂,但是大体可以分为三个方面来进行
1)平台层面的调优:防止不必要的jar包分发,提高数据的本地性,选择高效的存储格式如parquet
2)应用程序层面的调优:过滤操作符的优化降低过多小任务,降低单条记录的资源开销,处理数据倾斜,复用RDD进行缓存,作业并行化执行等等
3)JVM层面的调优:设置合适的资源量,设置合理的JVM,启用高效的序列化方法如kyro,增大off head内存等等
Spark中的数据本地性有三种:
1)PROCESS_LOCAL是指读取缓存在本地节点的数据
2)NODE_LOCAL是指读取本地节点硬盘数据
3)ANY是指读取非本地节点数据
通常读取数据PROCESS_LOCAL>NODE_LOCAL>ANY,尽量使数据以PROCESS_LOCAL或NODE_LOCAL方式读取。其中PROCESS_LOCAL还和cache有关,如果RDD经常用的话将该RDD cache到内存中,注意,由于cache是lazy的,所以必须通过一个action的触发,才能真正的将该RDD cache到内存中。
Spark1.6之前的版本采用静态内存管理模型,将内存空间划分为三个分区:
这种内存管理模型的优点是: 各个分区的职责分明,实现简单
缺点:分区中间存在”硬“界限,难以平衡三者的内存消耗
自从1.6版本开始,为了平衡用户代码、Shuffle机制中的中间数据,以及数据缓存的内存空间需求,Spark提出统一内存管理模型,为三者分配一定的内存配额,并在运行时根据三者的实际内存用量,动态调整配额比例。三者当中,Shuffle的中间数据和缓存数据的内存消耗可以被监控,但用户代码的内存很难被监控和估计。所以统一内存管理模型中,优化思想主要是根据监控的内存使用总量,来动态调节Shuffle机制和缓存数据内存空间的,并为每个内存消耗来源设置一个上下界,其内存配额在上下界范围内动态可调。
统一内存管理模型将内存依旧划为三个分区:数据缓存空间、框架执行空间和用户代码空间
与静态内存管理模型不同点:统一内存管理模型使用软边界调整分区的占用比例
数据缓存空间**(Storage Memory)和框架执行空间(Execution Memory)组成一个大的空间,称为Framework memory**
Framework memory 大小固定,为缓存空间和执行空间设置了初始比例,但可以动态调整,如框架执行空间不足时可以借用数据存储空间来存储Shuffle中间数据,同时二者比例也有上下界,避免一方被另一方完全占用。总大小为spark.memory.faction(default 0.6)*(heap-Reserved Memory) 约60%的内存空间,缓存空间和执行空间相互借用内存,均至少要保证二者具有约50%左右的空间
用户代码空间被设为固定大小,原因是难以在运行时回去用户真实内存消耗。默认为40%的内存空间
框架执行空间不足时,会将Shuffle数据spill到磁盘;
数据缓存空间不足时,Spark会进行缓存替换、移除缓存数据等操作。
为了限制每个Task的内存使用,为了解决内存共享和竞争问题,也会对每个task的内存使用进行限额,每个task可使用的内存空间被均分,每个task的空间被控制在**[1/2N,1/N]Execution Memory* N是当前Task数目。堆外内存同理
系统保留空间(Reserved Memory) 除了上述三组空间外,系统保留内存使用较小的空间存储Spark框架产生的内部对象。大小默认为300MB
Framework memory的堆外空间: 为了减少垃圾回收开销,Spark也允许使用堆外内存,该空间不受JVM垃圾回收机制管理。堆外空间主要存储序列化对象数据,而用户代码处理的是普通Java对象,因此堆外内存只用于框架执行空间和数据缓存空间。 Spark仍按照堆内内存使用的spark.memory.storageFaction比例将堆外内存分为框架执行空间和数据缓存空间。
数据缓存空间主要存放: RDD缓存数据、广播数据、task计算结果。并且也可以同时存放于堆内和堆外**,数据缓存空间由多个task共享。**
广播数据: 广播数据存储的位置是数据缓存空间。 Broadcast默认使用TorrentBroadcast , 需要广播的数据一般预先存储在Driver端,Spark在Driver端将要广播的数据划分大小为Spark.Broadcast.blockSize=4MB的数据块,并给予一个blockid为**(id,piece+i),id为block编号,piece表示被划分后的第几个block。之后使用类似BT方式将每个block广播到Executor中,Executor接收到每个block数据后,将其放到堆内的数据缓存空间的ChunkedByteBuffer**里面,缓存模式为MEMORY_AND_DISK_SER。
数据缓存机制的主要目的是加速计算。在应用执行过程中,数据缓存机制对某些需要多次使用的数据进行缓存,当应用需要再次访问这些数据时,可以直接从缓存中读取,避免重复计算。
Spark的缓存操作通过cache算子实现:
缓存数据实质上是一种空间换时间的方法,因此是否缓存数据需要考虑数据的计算代价和存储价值
(1) 会被重复使用的数据:确切的说是被多个job共享使用的数据,被共享的次数越多,缓存该数据的价值越大。
(2) 数据不宜过大。过大的数据会占用大量存储空间,导致内存不足,降低数据计算时可使用的空间
(3) 非重复缓存的数据。重复缓存的意思是如果缓存了某个RDD,那么该RDD通过OneToOneDependency连接的parent RDD就不需要缓存了。
Spark从三个方面考虑存储级别:
举例:
MEMORY_ONLY:存储在内存,不进行序列化
MEMORY_AND_DISK:内存+磁盘,不进行序列化
MEMORY_AND_DISK_SER:内存+磁盘,并且序列化
MEMORY_AND_DISK_SER_2:内存+磁盘,并且序列化,存储在多台机器上
当缓存的内存不够时,我们可以进行缓存替换。如需要缓存reducedRDD时,内存空间不足,可以及时将mappedRDD进行替换,以腾出空间存储reducedRDD。因此在空间有限的情况下,Spark需要缓存替换和回收机制。
Hadoop的MapReduce虽然设计了缓存机制,但不是用来存放job运行中间结果的,而用来缓存job运行所需的文件的,如jar包,一些文本文件等。而且缓存文件存放于每个Worker的本地磁盘,而不是内存。
错误容错机制就是在应用执行失败时,能够自动恢复应用执行,并且执行结果与正常执行得到的结果相同。
错误容忍主要需要考虑两方面内容:
Spark解决上述问题的方法为:重新计算来尝试修复。具体为:
3. 通过重新执行计算任务来容忍错误。当job抛出异常不能继续执行时,重新启动计算任务,再次执行。
4. 通过采用checkpoint机制,对一些重要的输入/输出,中间数据进行持久化,可以一定程度上解决数据丢失问题,而且提高重新计算时的任务执行效率。
重新计算需要满足三个前提条件:
(1) 重新执行失效的task时,是否需要重新执行其上游stage中的task?
为了避免对上游的task的重新计算,Spark采用了**“延迟删除策略”,即将上游stage的Shuffle Write的结果写入本地磁盘,只有在当前job完成后,才删除Shuffle Write写入磁盘的数据**。所以Spark通过Shuffle切分stage既保证了task的独立性,也方便了错误容忍的重新计算。
(2) 一个task一般会连续计算多个RDD,那么每个RDD都需要重新计算?
对于没有缓存的情况,每个RDD都要被重新计算。
(3) 如果缓存数据丢失,那么从哪里开始计算?
重新计算存在一个缺点:如果某个RDD的计算链过长,那么重新计算该数据的代价非常高。因此为了提升重新计算的效率,Spark采用检查点机制(checkpoint)机制,该机制的核心思想是将计算过程中某些重要数据进行持久化,这样在再次执行时可以从检查点执行,从而减少重新计算时的开销。
checkpoint的时机与计算顺序?
checkpoint的读取与写入?
checkpoint数据格式为序列化的RDD,因此读取时需要进行反序列化重新恢复RDD中的record
checkpoint时存放了RDD的分区信息,如使用了什么partitioner,在读取时,不仅恢复了RDD的数据,也可以恢复其分区方法的信息。
checkpoint的写入过程: RDD需要经过[Initialized->CheckPointingInProcess->Checkpointed]三个阶段才能被checkpoint;1. Initialized阶段,当应用程序使用checkpoint算子设置某个RDD需要被checkpoint,Spark为该RDD添加一个checkpointData属性,用来管理该RDD相关的checkpoint信息。2. CheckpointingInProcess阶段:当前job结束,调用该job最后一个RDD的doCheckpoint方法。该方法根据finalRDD的计算链回溯扫描,遇到需要checkpoint的RDD就将其标记为CheckpointInProcess。之后,Spark会调用runJob()再次提交一个job完成checkpoint。3. Checkpointed:再次提交的job对RDD完成checkpoint后,Spark会建立一个newRDD,类型为ReliableCheckPointRDD,用来表示被checkpoint到磁盘的RDD。newRDD会将lineage截断,不再保留父依赖的数据和计算,原因是:RDD已经被持久化到可靠分布式文件系统了,不再需要保留RDD是如何计算得到的了。 并且生成newRDD后,会将RDD和newRDD进行关联。
注意的是:缓存操作不能切断lineage,RDD还保存了其上游依赖的数据和操作,保留lineage的原因是缓存数据不可靠,一旦丢失,还需要根据lineage进行重新计算。
注意情况:在一个job中对多个RDD进行Checkpoint。
对一个RDD进行Checkpoint时,会将其上游依赖关系切断,不再回溯父RDD,这个机制会导致在从后往前的checkpoint搜索过程中不能被访问到,因此前面的checkpoint将无效。
拟采用的解决方法:从前往后扫描,先对parent RDD进行checkpoint。
广播变量是一个只读变量,通过它我们可以将一些共享数据集或者大变量缓存在Spark集群中的各个机器上而不用每个task都需要copy一个副本,后续计算可以重复使用,减少了数据传输时网络带宽的使用,提高效率。相比于Hadoop的分布式缓存,广播的内容可以跨作业共享。广播变量要求广播的数据不可变、不能太大但也不能太小(一般几十M以上)、可被序列化和反序列化、并且必须在driver端声明广播变量,适用于广播多个stage公用的数据,存储级别目前是MEMORY_AND_DISK。广播变量存储目前基于Spark实现的BlockManager分布式存储系统,Spark中的shuffle数据、加载HDFS数据时切分过来的block块都存储在BlockManager中。
Spark两种广播变量对比
HttpBroadcast在Spark后续的版本中已经被废弃,但考虑到部分公司用的Spark版本较低,面试中仍有可能问到两种实现的相关问题,这里简单介绍一下:
总之就是HttpBroadcast导致获取广播变量的请求集中于driver端,容易引起driver端单点故障,网络IO过高影响性能等问题,而TorrentBroadcast获取广播变量的请求服务即可以请求到driver端也可以在executor,避免了上述问题,当然这只是主要的优化点。
主要有两种方案:
Spark数据倾斜的表现:
Spark定位数据倾斜
Spark数据倾斜只会发生在shuffle过程中。
这里给大家罗列一些常用的并且可能会触发shuffle操作的算子:distinct、groupByKey、reduceByKey、aggregateByKey、join、cogroup、repartition等。
Spark数据倾斜的解决方案
适合场景
如果发现导致倾斜的key就少数几个,而且对计算本身的影响并不大的话,那么很适合使用这种方案。比如99%的key就对应10条数据,但是只有一个key对应了100万数据,从而导致了数据倾斜。
适合场景
对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。
适合场景
在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个RDD或表的数据量比较小,比较适用此方案。
适合场景
两个较大的RDD/Hive表进行join时,且一个RDD/Hive表中少数key数据量过大,另一个RDD/Hive表的key分布较均匀(RDD中两者之一有一个更倾斜)。
适合场景
RDD中有大量key导致倾斜。
引用:http://hbasefly.com/2017/03/19/sparksql-basic-join/
SparkSQL支持三种Join算法-shuffle hash join、broadcast hash join以及sort merge join。其中前两者归根到底都属于hash join,只不过在hash join之前需要先shuffle还是先broadcast。
先来看看这样一条SQL语句:select * from order,item where item.id = order.i_id,很简单一个Join节点,参与join的两张表是item和order,join key分别是item.id以及order.i_id。现在假设这个Join采用的是hash join算法,整个过程会经历三步:
**确定Build Table以及Probe Table:**这个概念比较重要,Build Table使用join key构建Hash Table,而Probe Table使用join key进行探测,探测成功就可以join在一起。通常情况下,小表会作为Build Table,大表作为Probe Table。此事例中item为Build Table,order为Probe Table。
**构建Hash Table:**依次读取Build Table(item)的数据,对于每一行数据根据join key(item.id)进行hash,hash到对应的Bucket,生成hash table中的一条记录。数据缓存在内存中,如果内存放不下需要dump到外存。
**探测:**再依次扫描Probe Table(order)的数据,使用相同的hash函数映射Hash Table中的记录,映射成功之后再检查join条件(item.id = order.i_id),如果匹配成功就可以将两者join在一起。
hash join是传统数据库中的单机join算法,在分布式环境下需要经过一定的分布式改造,说到底就是尽可能利用分布式计算资源进行并行化计算,提高总体效率。hash join分布式改造一般有两种经典方案:
broadcast hash join可以分为两步:
broadcast阶段:将小表广播分发到大表所在的所有主机。广播算法可以有很多,最简单的是先发给driver,driver再统一分发给所有executor;要不就是基于bittorrete的p2p思路;
hash join阶段:在每个executor上执行单机版hash join,小表映射,大表试探;
SparkSQL规定broadcast hash join执行的基本条件为被广播小表必须小于参数spark.sql.autoBroadcastJoinThreshold,默认为10M。
在大数据条件下如果一张表很小,执行join操作最优的选择无疑是broadcast hash join,效率最高。但是一旦小表数据量增大,广播所需内存、带宽等资源必然就会太大,broadcast hash join就不再是最优方案。**此时可以按照join key进行分区,根据key相同必然分区相同的原理,就可以将大表join分而治之,划分为很多小表的join,充分利用集群资源并行化。**如下图所示,shuffle hash join也可以分为两步:
shuffle阶段:分别将两个表按照join key进行分区,将相同join key的记录重分布到同一节点,两张表的数据会被重分布到集群中所有节点。这个过程称为shuffle
hash join阶段:每个分区节点上的数据单独执行单机hash join算法。
SparkSQL对两张大表join采用了全新的算法-sort-merge join,如下图所示,整个过程分为三个步骤:
shuffle阶段:将两张大表根据join key进行重新分区,两张表数据会分布到整个集群,以便分布式并行处理
sort阶段:对单个分区节点的两表数据,分别进行排序
merge阶段:对排好序的两张分区表数据执行join操作。join操作很简单,分别遍历两个有序序列,碰到相同join key就merge输出,否则取更小一边,见下图示意:
仔细分析的话会发现,sort-merge join的代价并不比shuffle hash join小,反而是多了很多。那为什么SparkSQL还会在两张大表的场景下选择使用sort-merge join算法呢?这和Spark的shuffle实现有关,目前spark的shuffle实现都适用sort-based shuffle算法,因此在经过shuffle之后partition数据都是按照key排序的。因此理论上可以认为数据经过shuffle之后是不需要sort的,可以直接merge。
参考:
https://blog.csdn.net/qq_39313597/article/details/89947187?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-1&spm=1001.2101.3001.4242