Spark原理分析


前言

Spark 原理主要包括:核心组件的运行机制(Master、Worker、SparkContext等)任务调度的原理Shuffle原理内存管理数据倾斜处理Spark优化,熟练掌握 Spark 内核原理,能够帮助我们更好地完成 Spark 应用开发,并能够准确锁定项目运行过程中出现问题的症结所在。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Spark运行

1.1核心组件

Master:集群中的管理节点,管理集群资源,通知Worker启动Executor或Driver。
Worker:集群中的工作节点,负责管理本节点的资源,定期向Master’汇报心跳,接收Master的命令,负责启动Driver或Executor。
Driver:执行Spark应用中的main方法,负责实际代码的执行工作。其主要任务为:

  • 负责向集群申请资源,向master注册信息
  • Executor启动后向 Driver 反向注册
  • 负责作业的解析、生成Stage并调度Task到Executor上
  • 监控Task的执行情况,执行完毕后释放资源
  • 通知Master 注销应用程序
    Executor:是一个 JVM 进程,负责执行具体的Task,负责运行组成 Spark 应用的任务,并将结果返回给 Driver 进程,通过自身的 Block Manage 为应用程序缓存RDD.

1.2运行流程

  • 用户提交应用程序
  • 执行具体的mian()发法,启动Driver进程
  • Driver向集群管理器请求启动Executor所需的资源。
  • 集群管理器代表Driver启动执行器。
  • 基于RDDs上的操作和转换,Driver将工作以任务的形式发送给Excutor。
  • Exexutor处理任务,结果通过集群管理器发送回驱动器。
    在这里插入图片描述

1.3集群部署模式

Spark 支持 3 种集群管理器,分别为:

  • Standalone:独立模式,Spark 原生的简单集群管理器,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统,使用 Standalone 可以很方便地搭建一个集群;
  • Hadoop YARN:统一的资源管理机制,在上面可以运行多套计算框架,如MapReduce、Storm 等,根据 driver 在集群中的位置不同,分为 yarn client和 yarn cluster;
  • Apache Mesos:一个强大的分布式资源管理框架,它允许多种不同的框架部署在其上;

1.4yarn模式运行机制

1.Yarn Cluster模式
在这里插入图片描述

  • Client 向RM提交请求,并上传jar到HDFS上
  • RM在集群中选择一个NM,在其上启动AppMaster,在AppMaster中实例化SparkContext(Driver)
  • AppMaster向RM注册应用程序,注册的目的是申请资源。RM监控App的运行状态直到结束
  • AppMaster申请到资源后,与NM通信,在Container中启动Executor进程
  • Executor向Driver注册,申请任务
  • Driver对应用进行解析,最后将Task发送到Executor上
  • Executor中执行Task,并将执行结果或状态汇报给Driver
  • 应用执行完毕,AppMaster通知RM注销应用,回收资源
    2.Yarn Client模式
    在这里插入图片描述
  • 启动应用程序实例化SparkContext,向RM申请启动AppMaster
  • RM在集群中选择一个NM,在其上启动AppMaster
  • AppMaster向RM注册应用程序,注册的目的是申请资源。RM监控App的运行状态直到结束
  • AppMaster申请到资源后,与NM通信,在Container中启动Executor进程
  • Executor向Driver注册,申请任务
  • Driver对应用进行解析,最后将Task发送到Executor上
  • Executor中执行Task,并将执行结果或状态汇报给Driver
  • 应用执行完毕,AppMaster通知RM注销应用,回收资源

1.5Spark RPC框架

RPC(Remote Procedure Call)远程过程调用。两台服务器A、B,A服务器上的应用,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
如果把分布式系统(Hadoop、Spark等)比作一个人,那么RPC可以认为是人体的血液循环系统。它将系统中各个不同的组件联系了起来。在Spark中,不同组件之间的通信、jar的上传、Shuffle数据的传输、Block数据的复制与备份都是基于RPC来实现的,所以说 RPC 是分布式系统的基石毫不为过。
1、RpcEnv
RpcEnv是RPC的环境对象,管理着整个 RpcEndpoint 的生命周期,其主要功能有:根据name或uri注册endpoints、管理各种消息的处理、停止endpoints。其中RpcEnv只能通过RpcEnvFactory创建得到。
2、RpcEndpoint
RpcEndpoint:表示一个消息通信体,可以接收、发送、处理消息。
3、RpcEndPointRef
RpcEndpointRef 是对远程 RpcEndpoint 的一个引用。当需要向一个具体的RpcEndpoint发送消息时,需要获取到该RpcEndpoint的引用,然后通过该引用发送消息。

二、SparkContext

2.1SparkContext内部组件

Spark应用程序的第一步就是创建并初始化SparkContext,SparkContext的初始化过程包含了内部组件的创建和准备,主要涉及网络通信、分布式、消息、存储、计算、调度、缓存、度量、清理、文件服务和UI等方面。SparkContext 是 Spark 程序主要功能的入口点,链接Spark集群,创建RDD、累加器和广播变量,一个线程只能运行一个SparkContext。SparkContext在应用程序中将外部数据转换成RDD,建立了第一个RDD,也就是说SparkContext建立了RDD血缘关系的根,是DAG的根源。
1.SparkConf
Spark Application 的配置,用来设置 Spark 的 KV 格式的参数。可用通过 new 实例化一个 SparkConf 的对象,这可以把所有的以 spark 开头的属性配置好,使用 SparkConf 进行参数设置的优先级是高于属性文件.
2.SparkEnv
SparkEnv 是Spark的执行环境对象,其中包括与众多Executor执行相关的对象。Executor 有自己的 Spark 的执行环境 SparkEnv。有了SparkEnv,就可以将数据存储在存储体系中;就能利用计算引擎对计算任务进行处理,就可以在节点间进行通信等。
3.DAGScheduler
DAG调度器,调度系统中最重要的组件之一,负责创建job,将DAG的RDD划分为不同的stage,提交stage
4.TaskScheduler
任务调度器,调度系统中最重要的组件之一,按照调度算法对集群管理器已经分配给应用程序的资源进行二次调度后分配任务,TaskScheduler调度的 Task是 DAGScheduler创建的,因此DAGScheduler是TaskScheduler的前置调度器
5.SchedulerBackend
用于对接不同的资源管理系统
6.SparkUI
用户界面,依赖计算引擎、调度系统、存储体系、作业、阶段、存储、执行器等组件的监控数据,以SparkListenerEnvent的形式投递给LiveListener,Spark从SparkListener中读取数据

2.2SparkEnv内部组件

SparkEnv是spark计算层的基石,不管是 Driver 还是 Executor,都需要依赖SparkEnv来进行计算,它是Spark的执行环境对象,其中包括与众多Executor执行相关的对象。Spark 对任务的计算都依托于 Executor 的能力,所有的 Executor 都有自己的 Spark 的执行环境 SparkEnv。有了 SparkEnv,可以将数据存储在存储体系中;利用计算引擎对计算任务进行处理,可以在节点间进行通信等。内部主要组件有:
1.RpcEnv
通过Netty技术来实现对组件之间的通信
2.Serializer
Spark使用的序列化器,默认使用Java的序列化器org.apache.spark.serializer.JavaSerializer
3.MapOutPutTracker
MapOutputTracker 用于跟踪Map阶段任务的输出状态,此状态便于Reduce阶段任务获取地址及中间结果。每个Map任务或者Reduce任务都会有其唯一标识,分别为mapId 和 reduceId。每个Reduce任务的输入可能是多个Map任务的输出,Reduce会到各个Map任务的所在节点上拉取Block。每个Shuffle过程都有唯一的表示shuffleId。
4.ShuffleManager
ShuffleManager负责管理本地及远程的Block数据的shuffle操作。
5.BlockManager
BlockManager负责对Block的管理
6.MemoryManager
MemoryManager 的主要实现有 StaticMemoryManager和 UnifiedMemoryManager(默认)

2.3SparkContext整体启动流程

SparkContext 涉及到的组件多,源码比较庞大。有些边缘性的模块主要起到辅助的功能,暂时省略。
初始化步骤:

  1. 初始设置
  2. 创建 SparkEnv
  3. 创建 SparkUI
  4. Hadoop 相关配置
  5. Executor 环境变量
  6. 注册 HeartbeatReceiver 心跳接收器
  7. 创建 TaskScheduler、SchedulerBackend
  8. 创建和启动 DAGScheduler
  9. 启动TaskScheduler、SchedulerBackend
  10. 启动测量系统 MetricsSystem
  11. 创建事件日志监听器
  12. 创建和启动 ExecutorAllocationManager
  13. ContextCleaner 的创建与启动
  14. 自定义 SparkListener 与启动事件
  15. Spark 环境更新
  16. 投递应用程序启动事件
  17. 测量系统添加Source
  18. 将 SparkContext 标记为激活

2.4 三大组件启动流程

  • DAGScheduler(高层调度器,class):负责将 DAG 拆分成不同Stage的具有依赖关系(包含RDD的依赖关系)的多批任务,然后提交给TaskScheduler进行具体处理
  • TaskScheduler(底层调度器,trait,只有一种实现TaskSchedulerImpl):负责实际每个具体Task的物理调度执行
  • SchedulerBackend(trait):有多种实现,分别对应不同的资源管理器
  • 在这里插入图片描述
    完整流程在这里插入图片描述
    1.创建SchedulerBackend、TaskScheduler
    根据传入的不同参数,启动不同的 SchedulerBackend, TaskScheduler(都是trait)
  • TaskScheduler的实现只有一个,但是不同模式传入的参数不同
  • SchedulerBackend的实现有多个
  • 在Standalone模式下,创建的分别是:StandaloneSchedulerBackend、TaskSchedulerImpl
  • 创建TaskSchedulerImpl后,构建了任务调度池 FIFOSchedulableBuilder /FairSchedulableBuilder
    2.创建 DAGScheduler
    负责将 DAG 拆分成不同Stage的具有依赖关系(包含RDD的依赖关系)的多批任务,然后提交给TaskScheduler进行具体处理
    3.执行 TaskScheduler.start
    4.CoarseGrainedSchedulerBackend.start
  • CoarseGrainedSchedulerBackend.start 启动中最重要的事情是:创建并注册driverEndpoint
  • 在DriverEndpoint.onStart 方法中创建定时调度任务,定时发送 ReviveOffers消息;最终调用 makeOffers() 方法处理该消息
  • DriverEndpoint 代表Driver管理App的计算资源(即Executor)
  • makeOffers方法,将集群的资源以Offer的方式发给上层的TaskSchedulerImpl
    5.创建 StandaloneAppClient
  • 执行start(),在其中创建 ClientEndpoint
  • ClientEndpoint执行onstart方法
  • ClientEndpoint 代表应用程序向 Master 注册【RegisterApplication】
    6.Master处理注册信息

三、作业执行原理

3.1job触发

Action 操作后会触发 Job 的计算,并交给 DAGScheduler 来提交。
在这里插入图片描述
1、Action 触发 sc.runJob
2、触发 dagScheduler.runJob
3、dagScheduler.runJob 提交job
作业提交后发生阻塞,等待执行结果, job 是串行执行的。

3.2Stage划分

Spark的任务调度从 DAG 划分开始,由 DAGScheduler 完成

  • DAGScheduler 根据 RDD 的血缘关系构成的 DAG 进行切分,将一个Job划分为若干Stages,具体划分策略是:从最后一个RDD开始,通过回溯依赖判断父依赖是否是宽依赖(即以Shuffle为界),划分Stage;窄依赖的RDD之间被划分到同一个Stage中,可以进行 pipeline 式的计算
  • 在向前搜索的过程中使用深度优先搜索算法
  • 最后一个Stage称为ResultStage,其他的都是ShuffleMapStage
  • 一个Stage是否被提交,需要判断它的父Stage是否执行。只有父Stage执行完毕才能提交当前Stage,如果一个Stage没有父Stage,那么从该Stage开始提交

3.3Task调度

Task 的调度是由 TaskScheduler 来完成(底层调度)。DAGScheduler 将 Stage 打包到 TaskSet 交给TaskScheduler,TaskScheduler 会将TaskSet 封装为 TaskSetManager 加入到调度队列中,TaskSetManager 结构如下图所示:
在这里插入图片描述
TaskSetManager 负责监控管理同一个 Stage 中的 Tasks,TaskScheduler 以TaskSetManager 为单元来调度任务。

TaskScheduler 初始化后会启动 SchedulerBackend。(在 SparkContext 源码中)SchedulerBackend负责跟外界打交道,接收 Executor 的注册,维护 Executor 的状态。 SchedulerBackend 是管“资源”(Executor)的,它在启动后会定期地去“询问”TaskScheduler 有没有任务要运行。TaskScheduler 在 SchedulerBackend “问”它的时候,会从调度队列中按照指定的调度策略选择 TaskSetManager 去调度运行,大致方法调用流程如下图所示:
在这里插入图片描述
将 TaskSetManager 加入 rootPool 调度池中之后,调用 SchedulerBackend 的reviveOffers 方法给driverEndpoint 发送 ReviveOffer 消息;driverEndpoint 收到ReviveOffer 消息后调用 makeOffers 方法,过滤出活跃状态的 Executor(这些Executor都是任务启动时反向注册到 Driver 的 Executor),然后将 Executor 封装成 WorkerOffer 对象;准备好计算资源(WorkerOffer)后, taskScheduler 基于这些资源调用resourceOffer 在 Executor 上分配 task。

3.4调度策略

TaskScheduler会先把 DAGScheduler 给过来的 TaskSet 封装成 TaskSetManager扔到任务队列里,然后再从任务队列里按照一定规则把它们取出来,由SchedulerBackend 发送给Executor运行;
在这里插入图片描述
TaskScheduler 以树的方式来管理任务队列,树中的节点类型为 Schedulable,叶子节点为 TaskSetManager,非叶子节点为Pool。
TaskScheduler 支持两种调度策略:FIFO(默认调度策略)、FAIR。

3.5本地化调度

  • DAGScheduler切割Job,划分Stage。调用submitStage来提交一个Stage对应的tasks,submitStage会调用submitMissingTasks,submitMissingTasks 确定每个需要计算的 task 的 preferred Locations
  • 通过调用 getPreferrdeLocations 得到分区的优先位置,一个partition对应一个task,此分区的优先位置就是task的优先位置
  • 从调度队列中拿到 TaskSetManager 后,那么接下来的工作就是TaskSetManager 按照一定的规则一个个取出 task 给 TaskScheduler,TaskScheduler 再交给 SchedulerBackend 发送到 Executor 上执行
  • 根据每个 task 的优先位置,确定 task 的 Locality 级别,Locality一共有五种,优先级由高到低顺序:
    PROCESS_LOCAL>NODE_LOCA>NO_PREF>RACK_LOCAL >ANY
    在调度执行时,Spark总是会尽量让每个 Task 以最高的本地性级别来启动

四、shuffle详解

在 Spark 或 MapReduce 分布式计算框架中,数据被分成一块一块的分区,分布在集群中各节点上,每个计算任务一次处理一个分区,当需要对具有某种共同特征的一类数据进行计算时,就需要将集群中的这类数据汇聚到同一节点。这个按照一定的规则对数据重新分区的过程就是Shuffle。

4.1Spark Shuffle的两个阶段

对于Spark来讲,一些Transformation或Action算子会让RDD产生宽依赖,即ParentRDD中的每个Partition被child RDD中的多个Partition使用,这时需要进行Shuffle,根据Record的key对parent RDD进行重新分区。以Shuffle为边界,Spark将一个Job划分为不同的Stage。Spark的Shuffle分为Write和Read两个阶段,分属于两个不同的Stage,前者是Parent Stage的最后一步,后者是Child Stage的第一步。在这里插入图片描述

4.2Spark Shuffle技术演进

  • Spark 1.1 以前是Hash Shuffle
  • Spark 1.1 引入了Sort Shuffle
  • Spark 1.6 将Tungsten-sort并入Sort Shuffle(利用对外内存进行排序)
  • Spark 2.0 Hash Shuffle退出历史舞台

4.3Hash Shuffle V1

相对于传统的 MapReduce,Spark 假定大多数情况下 Shuffle 的数据不需要排序,强制排序反而会降低性能。因此不在 Shuffle Read 时做 Merge Sort,如果需要合并的操作的话,则会使用聚合(agggregator)。
在 Map Task 过程按照 Hash 的方式重组 Partition 的数据,不进行排序。每个 MapTask 为每个 Reduce Task 生成一个文件,通常会产生大量的文件(即对应为 M*R个中间文件,其中 M 表示 Map Task 个数,R 表示 Reduce Task 个数),伴随大量的随机磁盘 I/O 操作与大量的内存开销。
在这里插入图片描述
Hash Shuffle V1的两个严重问题:

  • 生成大量文件,占用文件描述符,同时引入 DiskObjectWriter 带来的 WriterHandler 的缓存也非常消耗内存
  • 如果在 Reduce Task 时需要合并操作的话,会把数据放在一个 HashMap 中进行合并,如果数据量较大,很容易引发 OOM

4.4 Hash Shuffle V2 – File Consolidation

针对上面的第一个问题,Spark 做了改进,引入了 File Consolidation 机制。一个 Executor 上所有的 Map Task 生成的分区文件只有一份,即将所有的 MapTask 相同的分区文件合并,这样每个 Executor 上最多只生成 N 个分区文件。
这样减少了文件数,但是假如下游 Stage 的分区数 N 很大,还是会在每个 Executor上生成 N 个文件,同样,如果一个 Executor 上有 K 个 Core,还是会开 K*N 个Writer Handler,这里仍然容易导致OOM。

4.5Sort Shuffle V1

为了更好地解决上面的问题,Spark 参考了 MapReduce 中 Shuffle 的处理方式,引入基于排序的 Shuffle 写操作机制。每个 Task 不会为后续的每个 Task 创建单独的文件,而是将所有对结果写入同一个文件。该文件中的记录首先是按照 Partition Id 排序,每个 Partition 内部再按照Key 进行排序,Map Task 运行期间会顺序写每个 Partition 的数据,同时生成一个索引文件记录每个 Partition 的大小和偏移量。在这里插入图片描述
在 Reduce 阶段,Reduce Task 拉取数据做 Combine 时不再采用 HashMap,而是采用ExternalAppendOnlyMap,该数据结构在做 Combine 时,如果内存不足,会刷写磁盘,避免大数据情况下的 OOM。总体上看来 Sort Shuffle 解决了 Hash Shuffle 的所有弊端,但是因为需要其 Shuffle过程需要对记录进行排序,所以在性能上有所损失。
Tungsten-Sort Based Shuffle / Unsafe Shuffle
从 Spark 1.5.0 开始,Spark 开始了钨丝计划(Tungsten),目的是优化内存和CPU的使用,进一步提升Spark的性能。由于使用了堆外内存,而它基于 JDK SunUnsafe API,故 Tungsten-Sort Based Shuffle 也被称为 Unsafe Shuffle。它的做法是将数据记录用二进制的方式存储,直接在序列化的二进制数据上 Sort 而不是在 Java 对象上,这样一方面可以减少内存的使用和 GC 的开销,另一方面避免Shuffle 过程中频繁的序列化以及反序列化。
但是使用 Tungsten-Sort Based Shuffle 有几个限制,Shuffle 阶段不能有aggregate 操作,分区数不能超过一定大小(2^24-1,这是可编码的最大 ParitionId)

4.6 Sort Shuffle V2

从 Spark1.6.0 开始,把 Sort Shuffle 和 Tungsten-Sort Based Shuffle 全部统一到Sort Shuffle 中,如果检测到满足 Tungsten-Sort Based Shuffle 条件会自动采用Tungsten-Sort Based Shuffle,否则采用 Sort Shuffle。从Spark2.0 开始,Spark 把 Hash Shuffle 移除, Spark2.x 中只有一种 Shuffle,即为 Sort Shuffle。

4.7Shuffle Writer

ShuffleWriter(抽象类),有3个具体的实现:

  • SortShuffleWriter。sortShulleWriter 需要在 Map 排序
  • UnsafeShuffleWriter。使用 Java Unsafe 直接操作内存,避免Java对象多余的开销和GC 延迟,效率高
  • BypassMergeSortShuffleWriter。和Hash Shuffle的实现基本相同,区别在于map task输出汇总一个文件,同时还会产生一个index file

4.8Shuffle Writer 流程

1.数据先写入一个内存数据结构中
不同的shuffle算子,可能选用不同的数据结构如果是 reduceByKey 聚合类的算子,选用 Map 数据结构,一边通过 Map进行聚合,一边写入内存如果是 join 类的 shuffle 算子,那么选用 Array 数据结构,直接写入内存。
2.检查是否达到内存阈值
每写一条数据进入内存数据结构之后,就会判断一下,是否达到了某个临界阈值。如果达到临界阈值的话,那么就会将内存数据结构中的数据溢写到磁盘,并清空内存数据结构
3.数据写入缓冲区
写入磁盘文件是通过Java的 BufferedOutputStream 实现的。BufferedOutputStream 是Java的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘IO次数,提升性能
4.数据排序
在溢写到磁盘文件之前,会先根据key对内存数据结构中已有的数据进行排序。排序过后,会分批将数据写入磁盘文件。默认的batch数量是10000条,也就是说,排序好的数据,会以每批1万条数据的形式分批写入磁盘文件
5.重复写多个临时文件
一个 Task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作,会产生多个临时文件
6.临时文件合并
最后将所有的临时磁盘文件进行合并,这就是merge过程。此时会将之前所有临时磁盘文件中的数据读取出来,然后依次写入最终的磁盘文件之中
7.写索引文件
由于一个 Task 就只对应一个磁盘文件,也就意味着该task为下游stage的task准备的数据都在这一个文件中,因此还会单独写一份索引文件,其中标识了下游各个 Task 的数据在文件中的 start offset 与 end offset

4.9Shuffle MapOutputTracker

Spark的shuffle过程分为Writer和Reader:

  • Writer负责生成中间数据
  • Reader负责整合中间数据
    而中间数据的元信息,则由MapOutputTracker负责管理。它负责Writer和Reader的沟通。Shuffle Writer会将中间数据保存到Block里面,然后将数据的位置发送给MapOutputTracker。
    Shuffle Reader通过向 MapOutputTracker 获取中间数据的位置之后,才能读取到数据。Shuffle Reader 需要提供 shuffleId、mapId、reduceId 才能确定一个中间数据:
  • shuffleId,表示此次shuffle的唯一id
  • mapId,表示map端 rdd 的分区索引,表示由哪个父分区产生的数据
  • reduceId,表示reduce端的分区索引,表示属于子分区的那部分数据

4.10Shuffle Reader

  • Map Task 执行完毕后会将文件位置、计算状态等信息封装到 MapStatus 对象中,再由本进程中的 MapOutPutTrackerWorker 对象
  • 将其发送给Driver进程的MapOutPutTrackerMaster对象Reduce Task开始执行之前会先让本进程中的 MapOutputTrackerWorker 向Driver 进程中的 MapOutputTrackerMaster 发动请求,获取磁盘文件位置等信息
  • 当所有的Map Task执行完毕后,Driver进程中的 MapOutputTrackerMaster 就掌握了所有的Shuffle文件的信息。此时MapOutPutTrackerMaster会告诉MapOutPutTrackerWorker磁盘小文件的位置信息
  • 完成之前的操作之后,由 BlockTransforService 去 Executor 所在的节点拉数据,默认会启动五个子线程。每次拉取的数据量不能超过48M

4.11 Hadoop Shuffle 与 Spark Shuffle 的区别

共同点:二者从功能上看是相似的;从High Level来看,没有本质区别,实现(细节)上有区别
实现上的区别

  • Hadoop中有一个Map完成,Reduce便可以去fetch数据了,不必等到所有Map任务完成;而Spark的必须等到父stage完成,也就是父stage的 map 操作全部完成才能去fetch数据。这是因为spark必须等到父stage执行完,才能执行子stage,主要是为了迎合stage规则
  • Hadoop的Shuffle是sort-base的,那么不管是Map的输出,还是Reduce的输出,都是partition内有序的,而spark不要求这一点
  • Hadoop的Reduce要等到fetch完全部数据,才将数据传入reduce函数进行聚合,而 Spark是一边fetch一边聚合

五、内存管理

在执行 Spark 的应用程序时,Spark 集群会启动 Driver 和 Executor 两种 JVM 进程:

  • Driver为主控进程,负责创建 Spark 上下文,提交 Spark 作业,将作业转化为Task,并在各个 Executor 进程间协调任务的调度
  • Executor负责在工作节点上执行具体的计算任务,并将结果返回给 Driver,同时为需要持久化的 RDD 提供存储功能
    Driver 的内存管理(缺省值 1G)相对来说较为简单,这里主要针对 Executor 的内存管理进行分析,下文中提到的 Spark 内存均特指 Executor 的内存。

5.1堆内内存与堆外内存

作为一个 JVM 进程,Executor 的内存管理建立在 JVM 的内存管理之上,Spark 对JVM 的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用。堆内内存受到 JVM 统一管理,堆外内存是直接向操作系统进行内存的申请和释放。在这里插入图片描述
1.堆内内存
堆内内存的大小,由 Spark 应用程序启动时的 executor-memory 或spark.executor.memory 参数配置。Executor 内运行的并发任务共享 JVM 堆内内存。

  • 缓存 RDD 数据和广播变量占用的内存被规划为存储内存
  • 执行 Shuffle 时占用的内存被规划为执行内存
  • Spark 内部的对象实例,或者用户定义的 Spark 应用程序中的对象实例,均占用剩余的空间
    Spark 通过对存储内存和执行内存各自独立的规划管理,可以决定是否要在存储内存里缓存新的 RDD,以及是否为新的任务分配执行内存,在一定程度上可以提升内存的利用率,减少异常的出现。、
    2.堆外内存
    为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,存储经过序列化的二进制数据。
    利用 JDK Unsafe API,Spark 可以直接操作系统堆外内存,减少了不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。堆外内存可以被精确地申请和释放(堆外内存之所以能够被精确的申请和释放,是由于内存的申请和释放不再通过JVM 机制,而是直接向操作系统申请,JVM 对于内存的清理是无法准确指定时间点的,因此无法实现精确的释放),而且序列化的数据占用的空间可以被精确计算,所以相比堆内内存来说降低了管理的难度,也降低了误差。
    在默认情况下堆外内存并不启用,可通过配置 spark.memory.offHeap.enabled 参数启用,并由 spark.memory.offHeap.size 参数设定堆外空间的大小。除了没有other 空间,堆外内存与堆内内存的划分方式相同,所有运行中的并发任务共享存储内存和执行内存

5.2静态内存管理

Spark 2.0 以前版本采用静态内存管理机制。存储内存、执行内存和其他内存的大小在 Spark 应用程序运行期间均为固定的,但用户可以应用程序启动前进行配置,堆内内存的分配如下图所示:在这里插入图片描述

堆外内存分配较为简单,只有存储内存和执行内存。可用的执行内存和存储内存占用的空间大小直接由参数 spark.memory.storageFraction 决定。由于堆外内存占用的空间可以被精确计算,无需再设定保险区域。
静态内存管理机制实现起来较为简单,但如果用户不熟悉 Spark 的存储机制,或没有根据具体的数据规模和计算任务或做相应的配置,很容易造成”一半海水,一半火焰”的局面,即存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容。由于新的内存管理机制的出现,这种方式目前已经很少有开发者使用,出于兼容旧版本的应用程序的目的,Spark 仍然保留了它的实现。

5.3统一内存管理

Spark 2.0 之后引入统一内存管理机制,与静态内存管理的区别在于存储内存和执行内存共享同一块空间,可以动态占用对方的空闲区域,统一内存管理的堆内内存结构如下图所示:
在这里插入图片描述
其中最重要的优化在于动态占用机制,其规则如下:

  • 设定基本的存储内存和执行内存区域(spark.storage.storageFraction 参数),该设定确定了双方各自拥有的空间的范围
  • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间;(存储空间不足是指不足以放下一个完整的 Block)
  • 执行内存的空间被对方占用后,可让对方将占用的部分转存到硬盘,然后”归还”借用的空间
  • 存储内存的空间被对方占用后,无法让对方”归还”,因为需要考虑 Shuffle 过程中的很多因素,实现起来较为复杂
  • 在这里插入图片描述

5.4存储内存管理

1.RDD持久化机制
RDD 的持久化由 Spark 的 Storage【BlockManager】模块负责,实现了 RDD 与物理存储的解耦合。Storage 模块负责管理 Spark 在计算过程中产生的数据,将那些在内存或磁盘、在本地或远程存取数据的功能封装了起来。在具体实现时Driver 端和Executor 端的 Storage 模块构成了主从式架构,即 Driver 端的 BlockManager 为Master,Executor 端的 BlockManager 为 Slave。

Storage 模块在逻辑上以 Block 为基本存储单位,RDD 的每个Partition 经过处理后唯一对应一个Block。Driver 端的 Master 负责整个 Spark 应用程序的 Block 的元数据信息的管理和维护,而 Executor 端的 Slave 需要将 Block 的更新等状态上报到Master,同时接收Master 的命令,如新增或删除一个 RDD。
2.RDD缓存过程
RDD 在缓存到存储内存之前,Partition 中的数据一般以迭代器(Iterator)的数据结构来访问,这是 Scala 语言中一种遍历数据集合的方法。通过 Iterator 可以获取分区中每一条序列化或者非序列化的数据项(Record),这些 Record 的对象实例在逻辑上占用了 JVM 堆内内存的other 部分的空间,同一 Partition 的不同 Record 的存储空间并不连续。
Block 有序列化和非序列化两种存储格式,具体以哪种方式取决于该 RDD 的存储级别:

  • 非序列化的 Block 以 DeserializedMemoryEntry 的数据结构定义,用一个数组存储所有的对象
  • 实例序列化的 Block 以 SerializedMemoryEntry 的数据结构定义,用字节缓冲区(ByteBuffer)存储二进制数据

3.淘汰与落盘
由于同一个 Executor 的所有的计算任务共享有限的存储内存空间,当有新的 Block需要缓存但是剩余空间不足且无法动态占用时,就要对 LinkedHashMap 中的旧Block 进行淘汰(Eviction),而被淘汰的 Block 如果其存储级别中同时包含存储到磁盘的要求,则要对其进行落盘(Drop),否则直接删除该 Block。
存储内存的淘汰规则为

  • 被淘汰的旧 Block 要与新 Block 的 MemoryMode 相同,即同属于堆外或堆内内存
  • 新旧 Block 不能属于同一个 RDD,避免循环淘汰
  • 旧 Block 所属 RDD 不能处于被读状态,避免引发一致性问题
  • 遍历 LinkedHashMap 中 Block,按照最近最少使用(LRU)的顺序淘汰,直到满足新 Block 所需的空间。其中 LRU 是 LinkedHashMap 的特性

5.5执行内存管理

执行内存主要用来存储任务在执行 Shuffle 时占用的内存,Shuffle 是按照一定规则对 RDD 数据重新分区的过程, Shuffle 的 Write 和 Read 两阶段对执行内存的使用:
Shuffle Write

  • 在 map 端会采用 ExternalSorter 进行外排,在内存中存储数据时主要占用堆内执行空间。
    Shuffle Read
  • 在对 reduce 端的数据进行聚合时,要将数据交给 Aggregator 处理,在内存中存储数据时占用堆内执行空间
  • 如果需要进行最终结果排序,则要将再次将数据交给 ExternalSorter 处理,占用堆内执行空间
    Spark 的存储内存和执行内存有着截然不同的管理方式
  • 对存储内存来说,Spark 用一个LinkedHashMap来集中管理所有的 Block,Block 由需要缓存的 RDD 的 Partition 转化而成;
  • 对执行内存来说,Spark 用AppendOnlyMap来存储 Shuffle 过程中的数据,在 Tungsten 排序中甚至抽象成为页式内存管理,开辟了全新的 JVM 内存管理机制。

六、BlockManager

BlockManager是一个嵌入在 Spark 中的key-value型分布式存储系统,也是Master-Slave 结构的,RDD-cache、 shuffle-output、broadcast 等的实现都是基于BlockManager来实现的:

  • shuffle 的过程中使用 BlockManager 作为数据的中转站
  • 将广播变量发送到 Executor 时, broadcast 底层使用的数据存储层
  • spark streaming 一个 ReceiverInputDStream 接收到的数据,先放在BlockManager 中,然后封装为一个 BlockRdd 进行下一步运算
  • 如果对一个 RDD 进行了cache,CacheManager 也是把数据放在了BlockManager 中,后续 Task 运行的时候可以直接从 CacheManager 中获取到缓存的数据,不用再从头计算

在Driver和所有Executor上都会有BlockManager。每个节点上存储的block信息都会汇报给Driver端的BlockManagerMaster作统一管理,BlockManager对外提供get和set数据接口,可将数据存储在Memory、Disk、Off-heap。
在这里插入图片描述

Driver的组件为Master BlockManager ,负责:

  • 各节点上BlockManager内部管理数据的元数据进行维护,如 block 的增、删、改、查等操作
  • 只要 BlockManager 执行了数据增、删、改操作,那么必须将 Block 的BlockStatus 上报到BlockManager Master,BlockManager Master会对元数据进行维护
    Executor的组件为Slaves BlockManager ,负责:
    每个节点都有一个 BlockManager,每个 BlockManager 创建之后,第一件事就是去向 BlockManager Master 进行注册,此时 BlockManager Master 会为其创建对应的 BlockManagerInfo。
    BlockManager中有3个非常重要的组件
  • DiskStore:负责对磁盘数据进行读写
  • MemoryStore:负责对内存数据进行读写BlockTransferService:负责建立到远程其他节点BlockManager的连接,负责对远程其他节点的BlockManager的数据进行读写

七、数据倾斜

7.1什么是数据倾斜

Task之间数据分配的非常不均匀
在这里插入图片描述
1.数据倾斜有哪些现象

  • Executor lost、OOM、Shuffle过程出错、程序执行慢
  • 单个Executor执行时间特别久,整体任务卡在某个阶段不能结束
  • 正常运行的任务突然失败大多数 Task 运行正常,个别Task运行缓慢或发生OOM
    2.数据倾斜造成的危害有哪些
  • 个别任务耗时远高于其它任务,轻则造成系统资源的浪费,使整体应用耗时过大,不能充分发挥分布式系统并行计算的优势
  • 个别Task发生OOM,导致整体作业运行失败
    3.为什么会发生数据倾斜
    数据异常:参与计算的 key 有大量空值(null),这些空值被分配到同一分区
    Map Task数据倾斜,主要是数据源导致的数据倾斜:数据文件压缩格式(压缩格式不可切分),Kafka数据分区不均匀
    Reduce task数据倾斜(重灾区,最常见):Shuffle (外因)。Shuffle操作涉及到大量的磁盘、网络IO,对作业性能影响极大,Key分布不均(内因)
    4.如何定义数据倾斜
    Web UI,找到对应的Stage;再找到对应的 Shuffle 算子

7.2数据倾斜处理

1.做好数据预处理

  • 过滤key中的空值
  • 消除数据源带来的数据倾斜(文件采用可切分的压缩方式)
    数据倾斜产生的主要原因:Shuffle + key分布不均
    2.处理数据倾斜的基本思路
    避免shuffle
    Map端的join是典型的解决方案可以完全消除Shuffle,进而解决数据倾斜有很强的适用场景(大表和小表关联),典型的大表与小表的join,其他场景不合适
    减少 Shuffle 过程中传输的数据
    使用高性能算子,避免使用groupByKey,用reduceByKey或aggregateByKey替代没有从根本上解决数据分配不均的问题,收效有限,使用场景有限
    选择新的可用于聚合或join的Key
    从业务出发,使用新的key去做聚合或join。如当前key是【省城市日期】,在业务允许的情况下选择新的key【省城市区日期】,有可能解决或改善数据倾斜但,这样的key不好找;或者找到了新的key也不能解决问题
    改变Reduce的并行度
    变更 reduce 的并行度。理论上分区数从 N 变为 N-1 有可能解决或改善数据倾斜一般情况下这个方法不管用,数据倾斜可能是由很多key造成的,但是建议试试因为这个方法非常简单,成本极低。但适用性不广,可能只是解决了这一次的数据倾斜问题,非长远之计。
    加盐强行打散Key
    两阶段聚合加盐打散key。给每个key都加一个随机数,如10以内的随机数。此时key就被打散了局部聚合。对打上随机数的数据,执行一次聚合操作,得到结果全局聚合。将各个key的前缀去掉,再进行一次聚合操作,得到最终结果在这里插入图片描述
    两阶段聚合的优缺点
    对于聚合类的shuffle操作导致的数据倾斜,效果不错。通常都可以解决掉数据倾斜,至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案
    采样倾斜key并拆分join操作
    业务场景:两个RDD/两张表进行 join 的时候,数据量都比较大。
    处理步骤
    1、对包含少数几个数据量过大的key的那个RDD,通过sample算子采样出一份样本来,然后统计一下每个key的数量,计算出数据量最大的是哪几个key;
    2、将这几个key对应的数据从原来的RDD中拆分出来,形成一个单独的RDD,并给每个key都打上n以内的随机数作为前缀,而不会导致倾斜的大部分key形成另外一个RDD;
    3、将需要join的另一个RDD,也过滤出来那几个倾斜key对应的数据并形成一个单独的RDD,将每条数据膨胀成n条数据,这n条数据都按顺序附加一个0~n的前缀,不会导致倾斜的大部分key也形成另外一个RDD;
    4、再将附加了随机前缀的独立RDD与另一个膨胀n倍的独立RDD进行join,此时就可以将原先相同的key打散成n份,分散到多个task中去进行join了;
    5、另外两个普通的RDD就照常join即可;6、最后将两次join的结果使用union算子合并起来即可,就是最终的join结果。
    使用随机前缀和扩容再进行join
    业务场景:如果在进行join操作时,RDD中有大量的key导致数据倾斜,进行分拆key没什么意义,此时就只能使用最后一种方案来解决问题了。在这里插入图片描述
    处理步骤
    1、选一个RDD,将每条数据都打上一个n以内的随机前缀(打散)
    2、对另外一个RDD进行扩容,将每条数据都扩容成n条数据,扩容出来的每条数据都依次打上一个0~n的前缀
    3、将两个处理后的RDD进行join即可
    优缺点:如果两个RDD都很大,那么将RDD进行N倍的扩容显然行不通使用扩容的方式通常能缓解数据倾斜,不能彻底解决数据倾斜问题

八、Spark优化

8.1编码优化

1.RDD复用
避免创建重复的RDD。在开发过程中要注意:对于同一份数据,只应该创建一个RDD,不要创建多个RDD来代表同一份数据。
2. RDD持久化
对多次使用的RDD进行持久化,通过持久化将公共RDD的数据缓存到内存/磁盘中,之后对于公共RDD的计算都会从内存/磁盘中直接获取RDD数据RDD的持久化是可以进行序列化的,当内存无法将RDD的数据完整的进行存放的时候,可以考虑使用序列化的方式减小数据体积,将数据完整存储在内存中
3.巧用 filter
尽可能早的执行filter操作,过滤无用数据在filter过滤掉较多数据后,使用 coalesce 对数据进行重分区
4.选择高性能算子
1、避免使用groupByKey,根据场景选择使用高性能的聚合算子 reduceByKey、aggregateByKey
2、coalesce、repartition,选择没有shuffle的操作
3、foreachPartition 优化输出操作
4、map、mapPartitions,选择合理的选择算子mapPartitions性能更好,但数据量大时容易导致OOM
5、用 repartitionAndSortWithinPartitions 替代 repartition + sort 操作
6、合理使用 cache、persist、checkpoint,选择合理的数据存储级别
5.设置合并的并行度
Spark作业中的并行度指各个stage的task的数量设置合理的并行度,让并行度与资源相匹配
6.广播大变量
默认情况下,task中的算子中如果使用了外部变量,每个task都会获取一份变量的复本,这会造多余的网络传输和内存消耗
使用广播变量,只会在每个Executor保存一个副本,Executor的所有task共用此广播变量,这样就节约了网络及内存资源
7.Kryo序列化
默认情况下,Spark使用Java的序列化机制。Java的序列化机制使用方便,不需要额外的配置。但Java序列化机制效率不高,序列化速度慢而且序列化后的数据占用的空间大Kryo序列化机制比Java序列化机制性能提高10倍左右。Spark之所以没有默认使用Kryo作为序列化类库,是它不支持所有对象的序列化,同时Kryo需要用户在使用前注册需要序列化的类型,不够方便。
8.多使用Spark SQL
Spark SQL 编码更容易,开发更简单Spark的优化器对SQL语句做了大量的优化,一般情况下实现同样的功能,SparkSQL更容易也更高效
9.优化数据结构
Spark中有三种类型比较消耗内存:
对象:每个Java对象都有对象头、引用等额外的信息,占用了额外的内存空间
字符串:每个字符串内部都有一个字符数组以及长度等额外信息集合类型:如HashMap、LinkedList等,集合类型内部通常会使用一些内部类来封装集合元素
10.使用高性能库
fastutil是扩展了Java标准集合框架 (Map、List、Set;HashMap、ArrayList、HashSet) 的类库,提供了特殊类型的map、set、list和queue;fastutil能够提供更小的内存占用、更快的存取速度。

8.2参数优化

1.Shuffle调优
2.内存调优
3.资源分配
为 Spark 应用程序分配合理的计算资源。Spark的资源参数,基本都可以在 spark-submit 命令中作为参数设置。
4.动态资源分配
Spark提供了一种机制,使它可以根据工作负载动态调整应用程序占用的资源。这意味着,不使用的资源,应用程序会将资源返回给集群,并在稍后需要时再次请求资源。如果多个应用程序共享Spark集群中的资源,该特性尤为有用动态的资源分配是 executor 级默认情况下禁用此功能,并在所有粗粒度集群管理器上可用(CDH发行版中默认为true)
5.调节本地等待时长
park总是倾向于让所有任务都具有最佳的数据本地性。遵循移动计算不移动数据的思想,Spark希望task能够运行在它要计算的数据所在的节点上,这样可以避免数据的网络传输
如果对应节点资源用尽,Spark会等待一段时间(默认3s)。如果等待指定时间后仍无法在该节点运行,那么自动降级,尝试将task分配到比较差的本地化级别所对应的节点上;
6.调节连接等待时长
在Spark作业运行过程中,Executor优先从自己本地关联的BlockManager中获取某份数据,如果本地BlockManager没有的话,会通过 TransferService 远程连接其他节点上Executor的BlockManager来获取数据;
生产环境下,有时会遇到file not found、file lost这类错误。这些错误很有可能是Executor 的 BlockManager 在拉取数据的时候,无法建立连接,然后超过默认的连接等待时长后,宣告数据拉取失败。此时,可以考虑调节连接的超时时长。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值