Spark-内核原理了解

Spark内核

Spark内核概述

Spark 内核泛指 Spark 的核心运行机制,包括 Spark 核心组件的运行机制、Spark 任务调度机制、Spark 内存管理机制、Spark 核心功能的运行原理等,熟练掌握 Spark 内核原理,能够帮助我们更好地完成 Spark 代码设计,并能够帮助我们准确锁定项目运行过程中出现的问题的症结所在。

Spark核心组件

Driver

Spark 驱动器节点,用于执行 Spark 任务中的 main 方法,负责实际代码的执行工作。Driver 在 Spark 作业执行时主要负责:

  1. 将用户程序转化为作业(Job);

  2. 在 Executor 之间调度任务(Task);

  3. 跟踪 Executor 的执行情况;

  4. 通过 UI 展示查询运行情况;

Executor

Spark Executor 对象是负责在 Spark 作业中运行具体任务,任务彼此之间相互独立。 Spark应用启动时,ExecutorBackend 节点被同时启动,并且始终伴随着整个 Spark 应用的生命周期而存在。如果有 ExecutorBackend 节点发生了故障或崩溃,Spark 应用也可以继续执行,会将出错节点上的任务调度到其他 Executor 节点上继续运行。

Executor 有两个核心功能:

  1. 负责运行组成 Spark 应用的任务,并将结果返回给驱动器(Driver);

  2. 它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在 Executor 进程内的,因此任务可以在运行时充分利用缓存数据加速运算。

Spark 通用运行流程

在这里插入图片描述

上图为 Spark 通用运行流程图,体现了基本的 Spark 应用程序在部署中的基本提交流程。这个流程是按照如下的核心步骤进行工作的:

  1. 任务提交后,都会先启动 Driver 程序;

  2. 随后 Driver 向集群管理器注册应用程序;

  3. 之后集群管理器根据此任务的配置文件分配 Executor 并启动;

  4. Driver 开始执行 main 函数,Spark 查询为懒执行,当执行到 Action 算子时开始反向推算,根据宽依赖进行 Stage 的划分,随后每一个 Stage 对应一个 Taskset,Taskset 中有多个 Task,查找可用资源 Executor 进行调度;

5)根据本地化原则,Task 会被分发到指定的 Executor 去执行,在任务执行的过程中,Executor 也会不断与 Driver 进行通信,报告任务运行情况。

Spark部署模式

Spark 支持多种集群管理器(Cluster Manager),分别为:
1)Standalone:独立模式,Spark 原生的简单集群管理器,自带完整的服务,可单独部署到一个集群中,无需依赖任何其他资源管理系统,使用 Standalone 可以很方便地搭建一个集群;
2)Hadoop YARN:统一的资源管理机制,在上面可以运行多套计算框架,如 MR、Storm等。根据 Driver 在集群中的位置不同,分为 yarn client(集群外)和 yarn cluster(集群内部)
3**)Apache Mesos**:一个强大的分布式资源管理框架,它允许多种不同的框架部署在其上,包括 Yarn。
4**)K8S : 容器式部署环境**。

实际上,除了上述这些通用的集群管理器外,Spark 内部也提供了方便用户测试和学习的本地集群部署模式和 Windows 环境。由于在实际工厂环境下使用的绝大多数的集群管理器是 Hadoop YARN,因此我们关注的重点是 Hadoop YARN 模式下的 Spark 集群部署。

YARN模式运行机制

YARN-Cluster模式

在这里插入图片描述

在这里插入图片描述

  1. 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;

  2. SparkSubmit 类中的 main 方法反射调用 YarnClusterApplication 的 main 方法;

  3. YarnClusterApplication 创建 Yarn 客户端,然后向 Yarn 服务器发送执行指令:bin/java ApplicationMaster;

  4. Yarn 框架收到指令后会在指定的 NM 中启动 ApplicationMaster;

  5. ApplicationMaster 启动 Driver 线程,执行用户的作业;

  6. AM 向 RM 注册,申请资源;

  7. 获取资源后 AM 向 NM 发送指令:bin/java YarnCoarseGrainedExecutorBackend;

  8. CoarseGrainedExecutorBackend 进程会接收消息,跟 Driver 通信,注册已经启动的Executor(反向注册);然后启动计算对象 Executor 等待接收任务

9)Driver 线程继续执行完成作业的调度和任务的执行。

  1. Driver 分配任务并监控任务的执行。

注意:SparkSubmit、 ApplicationMaster 和 CoarseGrainedExecutorBackend 是独立的进程;Driver是独立的线程;Executor 和 YarnClusterApplication 是对象。

YARN-Client模式

在这里插入图片描述

  1. 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;

  2. SparkSubmit 类中的 main 方法反射调用用户代码的 main 方法;

  3. 启动 Driver 线程,执行用户的作业,并创建 ScheduleBackend;

  4. YarnClientSchedulerBackend 向 RM 发送指令:bin/java ExecutorLauncher;

  5. Yarn 框架收到指令后会在指定的 NM 中启动 ExecutorLauncher(实际上还是调用ApplicationMaster 的 main 方法);

object ExecutorLauncher {
    def main(args: Array[String]): Unit = {
    	ApplicationMaster.main(args)
    }
}
  1. AM 向 RM 注册,申请资源;

  2. 获取资源后 AM 向 NM 发送指令:bin/java CoarseGrainedExecutorBackend;

  3. CoarseGrainedExecutorBackend 进程会接收消息,跟 Driver 通信,注册已经启动的Executor;然后启动计算对象 Executor 等待接收任务

9)Driver 分配任务并监控任务的执行。

注意:SparkSubmit、ApplicationMaster 和 YarnCoarseGrainedExecutorBackend 是独立的进程;Executor 和 Driver 是对象。

Standalone模式运行机制

Standalone 集群有 2 个重要组成部分,分别是:

  1. Master(RM):是一个进程,主要负责资源的调度和分配,并进行集群的监控等职责;

  2. Worker(NM):是一个进程,一个 Worker 运行在集群中的一台服务器上,主要负责两个职责,一个是用自己的内存存储 RDD 的某个或某些 partition;另一个是启动其他进程和线程(Executor),对 RDD 上的 partition 进行并行的处理和计算。

Standalone-Cluster模式

在这里插入图片描述

在 Standalone Cluster 模式下,任务提交后,Master 会找到一个 Worker 启动 Driver

Driver 启动后向 Master 注册应用程序,Master 根据 submit 脚本的资源需求找到内部资源至少可以启动一个 Executor 的所有 Worker,然后在这些 Worker 之间分配 Executor,Worker 上的 Executor 启动后会向 Driver 反向注册,所有的 Executor 注册完成后, Driver 开始执行 main函数,之后执行到 Action 算子时,开始划分 Stage,每个 Stage 生成对应的 taskSet,之后将Task 分发到各个 Executor 上执行。

Standalone-Client模式

在这里插入图片描述

在 Standalone Client 模式下,Driver 在任务提交的本地机器上运行。

Driver 启动后向Master 注册应用程序,Master 根据 submit 脚本的资源需求找到内部资源至少可以启动一个Executor 的所有 Worker,然后在这些 Worker 之间分配 Executor,Worker 上的 Executor 启动后会向 Driver 反向注册,所有的 Executor 注册完成后,Driver 开始执行 main 函数,之后执行到 Action 算子时,开始划分 Stage,每个 Stage 生成对应的 TaskSet,之后将 Task 分发到各个 Executor 上执行

Spark通讯架构

Spark 中通信框架的发展:

Spark 早期版本中采用 Akka 作为内部通信部件。

Spark1.3 中引入 Netty 通信框架,为了解决 Shuffle 的大数据传输问题使用

Spark1.6 中 Akka 和 Netty 可以配置使用。Netty 完全实现了 Akka 在 Spark 中的功能。 Spark2 系列中,Spark 抛弃 Akka,使用 Netty。

Spark2.x 版本使用 Netty 通讯框架(AIO,其他的IO有BIO和NIO,Linux对AIO支持不够好,Windows支持好,因此Linux采用Epoll方式来模拟AIO)作为内部通讯组件。Spark 基于 Netty 新的 RPC 框架借鉴了 Akka 的中的设计,它是基于 Actor 模型,如下图所示
在这里插入图片描述

Spark 通讯框架中各个组件(Client/Master/Worker)可以认为是一个个独立的实体,各个实体之间通过消息来进行通信。具体各个组件之间的关系图如下:

在这里插入图片描述

Endpoint (Client/Master/Worker)有 1 个 InBox 和 N 个 OutBox ( N>=1, N 取决于当前 Endpoint与多少其他的 Endpoint 进行通信,一个与其他通讯的 Endpoint 对应一个 OutBox),Endpoint接收到的消息被写入 InBox,发送出去的消息写入 OutBox 并被发送到其他 Endpoint 的 InBox中。

Spark 通信终端

// Driver:
class DriverEndpoint extends IsolatedRpcEndpoint

// Executor
class CoarseGrainedExecutorBackend extends IsolatedRpcEndpoint

架构原理

在这里插入图片描述
在这里插入图片描述

RpcEndpoint:RPC 通信终端。Spark 针对每个节点(Client/Master/Worker)都称之为一个 RPC 终端,且都实现 RpcEndpoint 接口,内部根据不同端点的需求,设计不同的消息和不同的业务处理,如果需要发送(询问)则调用 Dispatcher。

在 Spark 中,所有的终端都存在生命周期:Constructor、onStart、receive*、onStop

RpcEnv:RPC 上下文环境,每个 RPC 终端运行时依赖的上下文环境称为 RpcEnv;在把当前 Spark 版本中使用的 NettyRpcEnv

Dispatcher:消息调度(分发)器,针对于 RPC 终端需要发送远程消息或者从远程 RPC接收到的消息,分发至对应的指令收件箱(发件箱)。如果指令接收方是自己则存入收件箱,如果指令接收方不是自己,则放入发件箱;

Inbox:指令消息收件箱。一个本地 RpcEndpoint 对应一个收件箱,Dispatcher 在每次向Inbox 存入消息时,都将对应 EndpointData 加入内部 ReceiverQueue 中,另外 Dispatcher创建时会启动一个单独线程进行轮询 ReceiverQueue,进行收件箱消息消费;

RpcEndpointRef: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 分发消息至对应收发件箱;

Spark任务调度机制

在生产环境下, Spark 集群的部署方式一般为 YARN-Cluster 模式,之后的内核分析内容中我们默认集群的部署方式为 YARN-Cluster 模式。Driver 线程主要是初始化 SparkContext 对 象 , 准备运行所需的上下文 , 然后一方面保持与ApplicationMaster 的 RPC 连接,通过 ApplicationMaster 申请资源,另一方面根据用户业务逻辑开始调度任务,将任务下发到已有的空闲 Executor 上。
当 ResourceManager 向 ApplicationMaster 返回 Container 资源时, ApplicationMaster 就尝试在对应的 Container 上启动 Executor 进程,Executor 进程起来后,会向 Driver 反向注册,注册成功后保持与 Driver 的心跳,同时等待 Driver 分发任务,当分发的任务执行完毕后,将任务状态上报给 Driver。

Spark任务调度概述

当 Driver 起来后,Driver 则会根据用户程序逻辑准备任务,并根据 Executor 资源情况逐步分发任务。在详细阐述任务调度前,首先说明下 Spark 里的几个概念。一个 Spark 应用程序包括 Job、Stage 以及 Task 三个概念:

  1. Job 是以 Action 方法为界,遇到一个 Action 方法则触发一个 Job;

  2. Stage 是 Job 的子集,以 RDD 宽依赖(即 Shuffle)为界,遇到 Shuffle 做一次划分;

  3. 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 有多种实现,分别对接不同的资源管理系统。

在这里插入图片描述

Driver 初始化 SparkContext 过程中,会分别初始化 DAGScheduler(阶段调度器,主要用于阶段的划分及任务的切分)、TaskScheduler(任务调度器,主要用于任务的调度)、SchedulerBackend(通信后台,主要用于和Executor之间进行通信) 以及 HeartbeatReceiver,并启动 SchedulerBackend 以及 HeartbeatReceiver。

SchedulerBackend 通过 ApplicationMaster 申请资源,并不断从 TaskScheduler 中拿到合适的Task 分发到 Executor 执行。HeartbeatReceiver 负责接收 Executor 的心跳信息,监控 Executor的存活状况,并通知到 TaskScheduler。

RDD的依赖

在这里插入图片描述

Spark-Stage级调度

Spark 的任务调度是从 DAG 切割开始,主要是由 DAGScheduler 来完成。当遇到一个Action 操作后就会触发一个 Job 的计算,并交给 DAGScheduler 来提交,下图是涉及到 Job提交的相关方法调用流程图。

在这里插入图片描述

  1. Job 由最终的 RDD 和 Action 方法封装而成;

  2. SparkContext 将 Job 交给 DAGScheduler 提交,它会根据 RDD 的血缘关系构成的 DAG进行切分,将一个 Job 划分为若干 Stages,具体划分策略是,由最终的 RDD 不断通过依赖回溯判断父依赖是否是宽依赖,即以 Shuffle 为界,划分 Stage,窄依赖的 RDD 之间被划分到同一个 Stage 中,可以进行 pipeline 式的计算。划分的 Stages 分两类,一类叫 做 ResultStage , 为 DAG 最 下 游 的 Stage , 由 Action 方 法 决 定 , 另 一 类 叫 做ShuffleMapStage,为下游 Stage 准备数据,下面看一个简单的例子 WordCount。

在这里插入图片描述

Job 由 saveAsTextFile 触发,该 Job 由 RDD-3 和 saveAsTextFile 方法组成,根据 RDD 之间的依赖关系从 RDD-3 开始回溯搜索,直到没有依赖的 RDD-0。在回溯搜索过程中,RDD-3 依赖 RDD-2,并且是宽依赖,所以在 RDD-2 和 RDD-3 之间划分 Stage,RDD-3 被划到最后一个 Stage,即 ResultStage 中,RDD-2 依赖 RDD-1,RDD-1 依赖 RDD-0,这些依赖都是窄依赖,所以将 RDD-0、RDD-1 和 RDD-2 划分到同一个 Stage,形成 pipeline 操作。即ShuffleMapStage 中,实际执行的时候,数据记录会一气呵成地执行 RDD-0 到 RDD-2 的转化。不难看出,其本质上是一个深度优先搜索(Depth First Search)算法。Spark中阶段的划分等于shuffle依赖的数量+1。

一个 Stage 是否被提交,需要判断它的父 Stage 是否执行,只有在父 Stage 执行完毕才能提交当前 Stage,如果一个 Stage 没有父 Stage,那么从该 Stage 开始提交。Stage 提交时会将 Task 信息(分区信息以及方法等)序列化并被打包成 TaskSet 交给 TaskScheduler,一个Partition 对应一个 Task,另一方面 TaskScheduler 会监控 Stage 的运行状态,只有 Executor 丢失或者 Task 由于 Fetch 失败才需要重新提交失败的 Stage 以调度运行失败的任务,其他类型 的 Task 失败会在 TaskScheduler 的调度过程中重试。

相对来说 DAGScheduler 做的事情较为简单,仅仅是在 Stage 层面上划分 DAG,提交Stage 并监控相关状态信息。TaskScheduler 则相对较为复杂。

任务的总数量等于各个阶段最后一个任务的分区之和,每个阶段的任务数量就是阶段最后一个任务的分区数。

Spark-Task级调度

Spark Task 的调度是由 TaskScheduler 来完成,由前文可知,DAGScheduler 将 Stage 打包到交给 TaskScheTaskSetduler,TaskScheduler 会将 TaskSet 封装为 TaskSetManager 加入到调度队中,TaskSetManager 结构如下图所示。

在这里插入图片描述

TaskSetManager 负 责 监 控 管 理 同 一 个 Stage 中 的 Tasks , TaskScheduler 就 是 以TaskSetManager 为单元来调度任务。

前面也提到,TaskScheduler 初始化后会启动 SchedulerBackend,它负责跟外界打交道,接收 Executor 的注册信息,并维护 Executor 的状态,所以说 SchedulerBackend 是管“粮食”的,同时它在启动后会定期地去“问”TaskScheduler 有没有任务要运行,也就是说,它会定期地“问”TaskScheduler“我有这么余粮,你要不要啊”, TaskScheduler 在 SchedulerBackend“问”它的时候,会从调度队列中按照指定的调度策略选择 TaskSetManager 去调度运行,大致方法调用流程如下图所示:

在这里插入图片描述

上图中,将 TaskSetManager 加入 rootPool 调度池中之后,调用 SchedulerBackend 的riviveOffers 方法给 driverEndpoint 发送 ReviveOffer 消息; driverEndpoint 收到 ReviveOffer 消息后调用 makeOffers 方法,过滤出活跃状态的 Executor(这些 Executor 都是任务启动时反向注册到 Driver 的 Executor),然后将 Executor 封装成 WorkerOffer 对象;准备好计算资源(WorkerOffer)后,taskScheduler 基于这些资源调用 resourceOffer 在 Executor 上分配 task。

调度策略

TaskScheduler 支持两种调度策略,一种是 FIFO,也是默认的调度策略,另一种是 FAIR。在 TaskScheduler 初始化过程中会实例化 rootPool,表示树的根节点,是 Pool 类型。

FIFO 调度策略

如果是采用 FIFO 调度策略,则直接简单地将 TaskSetManager 按照先来先到的方式入队,出队时直接拿出最先进队的 TaskSetManager,其树结构如下图所示,TaskSetManager 保存在一个 FIFO 队列中。

在这里插入图片描述

FAIR 调度策略

FAIR 调度策略的树结构如下图所示:
在这里插入图片描述

FAIR 模式中有一个 rootPool 和多个子 Pool,各个子 Pool 中存储着所有待分配的TaskSetMagager。

在 FAIR 模式中,需要先对子 Pool 进行排序,再对子 Pool 里面的 TaskSetMagager 进行排序,因为 Pool 和 TaskSetMagager 都继承了 Schedulable 特质,因此使用相同的排序算法。

排 序 过 程 的 比 较 是 基 于 Fair-share 来 比 较 的 , 每 个 要 排 序 的 对 象 包 含 三 个 属 性 :runningTasks 值(正在运行的 Task 数)、minShare 值、 weight 值,比较时会综合考量 runningTasks值,minShare 值以及 weight 值。

注意,minShare、weight 的值均在公平调度配置文件 fairscheduler.xml 中被指定,调度池在构建阶段会读取此文件的相关配置。

1)如果 A 对象的 runningTasks 大于它的 minShare,B 对象的 runningTasks 小于它的 minShare,那么 B 排在 A 前面;(runningTasks 比 minShare 小的先执行)

2)如果 A、B 对象的 runningTasks 都小于它们的 minShare,那么就比较 runningTasks 与minShare 的比值(minShare 使用率),谁小谁排前面;(minShare 使用率低的先执行)

3)如果 A、B 对象的 runningTasks 都大于它们的 minShare,那么就比较 runningTasks 与weight 的比值(权重使用率),谁小谁排前面。(权重使用率低的先执行)

4)如果上述比较均相等,则比较名字。

整体上来说就是通过 minShare 和 weight 这两个参数控制比较过程,可以做到让 minShare使用率和权重使用率少(实际运行 task 比例较少)的先运行。

FAIR 模式排序完成后,所有的 TaskSetManager 被放入一个 ArrayBuffer 里,之后依次被取出并发送给 Executor 执行。

从调度队列中拿到 TaskSetManager 后,由于 TaskSetManager 封装了一个 Stage 的所有Task,并负责管理调度这些 Task,那么接下来的工作就是 TaskSetManager 按照一定的规则一个个取出 Task 给 TaskScheduler,TaskScheduler 再交给 SchedulerBackend 去发到 Executor上执行。

本地化调度

DAGScheduler 切割 Job,划分 Stage, 通过调用 submitStage 来提交一个 Stage 对应的tasks, submitStage 会调用 submitMissingTasks, submitMissingTasks 确定每个需要计算的 task的 preferredLocations,通过调用 getPreferrdeLocations()得到 partition 的优先位置,由于一个partition 对应一个 Task,此 partition 的优先位置就是 task 的优先位置,对于要提交到TaskScheduler 的 TaskSet 中的每一个 Task,该 task 优先位置与其对应的 partition 对应的优先位置一致。

从调度队列中拿到 TaskSetManager 后,那么接下来的工作就是 TaskSetManager 按照一定的规则一个个取出 task 给 TaskScheduler,TaskScheduler 再交给 SchedulerBackend 去发到Executor 上执行。前面也提到,TaskSetManager 封装了一个 Stage 的所有 Task,并负责管理调度这些 Task。

根据每个 Task 的优先位置,确定 Task 的 Locality 级别(本地化级别),Locality 一共有五种,优先级由高到低顺序:

在这里插入图片描述

在调度执行时,Spark 调度总是会尽量让每个 task 以最高的本地性级别来启动,当一个task 以 X 本地性级别启动,但是该本地性级别对应的所有节点都没有空闲资源而启动失败,此时并不会马上降低本地性级别启动而是在某个时间长度内再次以 X 本地性级别来启动该task,若超过限时时间则降级启动,去尝试下一个本地性级别,依次类推。

可以通过调大每个类别的最大容忍延迟时间,在等待阶段对应的 Executor 可能就会有相应的资源去执行此 task,这就在在一定程度上提到了运行性能。

失败重试与黑名单机制

除了选择合适的 Task 调度运行外,还需要监控 Task 的执行状态,前面也提到,与外部打交道的是 SchedulerBackend,Task 被提交到 Executor 启动执行后,Executor 会将执行状态上报给 SchedulerBackend,SchedulerBackend 则告诉 TaskScheduler,TaskScheduler 找到该Task 对应的 TaskSetManager,并通知到该 TaskSetManager,这样 TaskSetManager 就知道 Task的失败与成功状态,对于失败的 Task,会记录它失败的次数,如果失败次数还没有超过最大重试次数,那么就把它放回待调度的 Task 池子中,否则整个 Application 失败。

在记录 Task 失败次数过程中,会记录它上一次失败所在的 Executor Id 和 Host,这样下次再调度这个 Task 时,会使用黑名单机制,避免它被调度到上一次失败的节点上,起到一定的容错作用。黑名单记录 Task 上一次失败所在的 Executor Id 和 Host,以及其对应的“拉黑”时间,“拉黑”时间是指这段时间内不要再往这个节点上调度这个 Task 了。

任务的调度

在这里插入图片描述

Spark-Shuffle解析

Shuffle一定会有数据落盘,通过shuffleWriterProcessor(写处理器)进行数据的落盘。

通过**ShuffleManager:(Hash(早期版本) & Sort(当前版本))**进行写管理

在这里插入图片描述

要提高Shuffle的效率,可以通过减少落盘的数据量;算子如果存在预聚合功能,可以提高Shuffle的性能,预聚合算子有:reduceByKey、aggregateByKey、combineByKey(最好不要用groupByKey,因为该算子只有Shuffle没有预聚合)。

Shuffle的核心要点

ShuffleMapStage与ResultStage

在这里插入图片描述

在划分 stage 时,最后一个 stage 称为 finalStage,它本质上是一个 ResultStage 对象,前面的所有 stage 被称为 ShuffleMapStage。

ShuffleMapStage 的结束伴随着 shuffle 文件的写磁盘。

ResultStage 基本上对应代码中的 action 算子,即将一个函数应用在 RDD 的各个 partition的数据集上,意味着一个 job 的运行结束。

HashShuffle解析

未优化的HashShuffle

这里我们先明确一个假设前提:每个 Executor 只有 1 个 CPU core,也就是说,无论这个 Executor 上分配多少个 task 线程,同一时间都只能执行一个 task 线程。

如下图中有 3 个 Reducer,从 Task 开始那边各自把自己进行 Hash 计算(分区器:hash/numreduce 取模),分类出 3 个不同的类别,每个 Task 都分成 3 种类别的数据,想把不同的数据汇聚然后计算出最终的结果,所以 Reducer 会在每个 Task 中把属于自己类别的数据收集过来,汇聚成一个同类别的大集合,每 1 个 Task 输出 3 份本地文件,这里有 4 个Mapper Tasks,所以总共输出了 4 个 Tasks x 3 个分类文件 = 12 个本地小文件。
在这里插入图片描述

优化后的 HashShuffle

优化的 HashShuffle 过程就是启用合并机制,合并机制就是复用 buffer,开启合并机制的配置是 spark.shuffle.consolidateFiles。该参数默认值为 false,将其设置为 true 即可开启优化机制。通常来说,如果我们使用 HashShuffleManager,那么都建议开启这个选项。

这里还是有 4 个 Tasks,数据类别还是分成 3 种类型,因为 Hash 算法会根据你的 Key进行分类,在同一个进程中,无论是有多少个 Task,都会把同样的 Key 放在同一个 Buffer里,然后把 Buffer 中的数据写入以 Core 数量为单位的本地文件中,(一个 Core 只有一种类型的 Key 的数据),每 1 个 Task 所在的进程中,分别写入共同进程中的 3 份本地文件,这里有 4 个 Mapper Tasks,所以总共输出是 2 个 Cores x 3 个分类文件 = 6 个本地小文件。

在这里插入图片描述

SortShuffle解析

普通 SortShuffle

在该模式下,数据会先写入一个数据结构,reduceByKey 写入 Map,一边通过 Map 局部聚合,一遍写入内存。Join 算子写入 ArrayList 直接写入内存中。然后需要判断是否达到阈值(默认文件是否大于5M且能够整除32(溢写的时候的用到了32K的缓冲区),默认条数是否大于int的最大值),如果达到就会将内存数据结构的数据写入到磁盘,清空内存数据结构。

在溢写磁盘前,先根据 key 进行排序,排序过后的数据,会分批写入到磁盘文件中。默认批次为 10000 条,数据会以每批一万条写入到磁盘文件。写入磁盘文件通过缓冲区溢写的方式,每次溢写都会产生一个磁盘文件,也就是说一个 Task 过程会产生多个临时文件

最后在每个 Task 中,将所有的临时文件合并,这就是 merge 过程,此过程将所有临时文件读取出来,一次写入到最终文件。意味着一个 Task 的所有数据都在这一个文件中。同时单独写一份索引文件,标识下游各个 Task 的数据在文件中的索引, start offset 和 end offset。

在这里插入图片描述

bypass SortShuffle

bypass 运行机制的触发条件如下:

1)shuffle reduce task 数量小于等于 spark.shuffle.sort.bypassMergeThreshold 参数的值,默认为 200。

2)不是预聚合类的 shuffle 算子(比如 reduceByKey)。

此时 task 会为每个 reduce 端的 task 都创建一个临时磁盘文件,并将数据按 key 进行hash 然后根据 key 的 hash 值,将 key 写入对应的磁盘文件之中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。

该过程的磁盘写机制其实跟未经优化的 HashShuffleManager 是一模一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的HashShuffleManager 来说,shuffle read 的性能会更好。

而该机制与普通 SortShuffleManager 运行机制的不同在于:不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write 过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值