Spark从星火到燎原Ⅱ

Spark架构

spark架构图

驱动程序 (Driver program)

驱动程序运行应用程序的主要进程,负责创建 SparkContext、将用户程序转换为集群中的作业、跟踪执行程序的运行状态以及调度任务。

集群资源管理器 (Cluster manager)

集群资源管理器是外部服务,用于获取集群中的资源,例如独立管理器、Mesos、YARN等。

工作节点 (Worker node)

工作节点是集群中可以运行应用程序代码的任何节点。

执行程序 (Executor)

执行程序是在工作节点上为应用程序启动的进程,负责运行任务并在内存或磁盘存储中保存数据。每个应用程序都有自己的执行程序,它相当于一个Java虚拟机(JVM)。

任务 (Task)

被送到某个Executor上的工作单元,但hadoopMR中的MapTask和ReduceTask概念一样,是运行Application的基本单位,多个Task组成一个Stage,而Task的调度和管理等是由TaskScheduler负责。

作业 (Job)

作业是由多个任务组成的并行计算,响应于 Spark 的动作(例如保存、收集),你会在驱动程序的日志中看到这个术语。

阶段 (Stage)

阶段是将每个作业分成较小的任务集的过程,这些任务集相互依赖,类似于MapReduce中的映射和归约阶段,你会在驱动程序的日志中看到这个术语。

Spark DAG

Spark DAG是Spark作业的执行计划,用有向无环图表示。顶点是操作,边是操作之间的数据流依赖。Spark使用DAG优化作业执行,将其分解成一系列阶段并并行执行任务。通过DAG,Spark实现了惰性计算、任务调度优化和容错性,提高了作业执行效率和可靠性

job、stage和task的关系

Job、stage和task是spark任务执行流程中的三个基本单位。其中job是最大的单位,也是Spark Application任务执行的基本单元,由action算子划分触发生成。

stage隶属于单个job,根据shuffle算子(宽依赖)拆分。单个stage内部可根据数据分区数划分成多个task,由TaskScheduler分发到各个Executor上的task线程中执行。

img

DAGScheduler的Stage划分算法

官网的RDD执行流程图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

针对一段应用代码(如上),Driver会以Action算子为边界生成DAG调度图。DAGScheduler从DAG末端开始遍历划分Stage,封装成一系列的tasksets移交TaskScheduler,后者根据调度算法, 将taskset分发到相应worker上的Executor中执行。

1. DAGSchduler的工作原理

  • DAGScheduler是一个面向stage调度机制的高级调度器,为每个job计算stage的DAG(有向无环图),划分stage并提交taskset给TaskScheduler。
  • 追踪每个RDD和stage的物化情况,处理因shuffle过程丢失的RDD,重新计算和提交。
  • 查找rdd partition 是否cache/checkpoint。提供优先位置给TaskScheduler,等待后续TaskScheduler的最佳位置划分

2. Stage划分算法

  • 从触发action操作的算子开始,从后往前遍历DAG。
  • 为最后一个rdd创建finalStage
  • 遍历过程中如果发现该rdd是宽依赖,则为其生成一个新的stage,与旧stage分隔而开,此时该rdd是新stage的最后一个rdd。
  • 如果该rdd是窄依赖,将该rdd划分为旧stage内,继续遍历,以此类推,继续遍历直至DAG完成。

img

TaskScheduler的Task分配算法

TaskScheduler负责Spark中的task任务调度工作。TaskScheduler内部使用TasksetPool调度池机制存放task任务。TasksetPool分为FIFO(先进先出调度)和FAIR(公平调度)。

  • FIFO调度: 基于队列思想,使用先进先出原则顺序调度taskset
  • FAIR调度: 根据权重值调度,一般选取资源占用率作为标准,可人为设定

img

1. TaskScheduler的工作原理

  • 负责Application在Cluster Manager上的注册
  • 根据不同策略创建TasksetPool资源调度池,初始化pool大小
  • 根据task分配算法发送Task到Executor上执行
  1. Task分配算法
  • 首先获取所有的executors,包含executors的ip和port等信息
  • 将所有的executors根据shuffle算法进行打散
  • 遍历executors。在程序中依次尝试本地化级别,最终选择每个task的最优位置(结合DAGScheduler优化位置策略)
  • 序列化task分配结果,并发送RPC消息等待Executor响应

img

面试题:Spark如何划分stage,每个stage又根据什么决定task个数?

Stage:根据RDD之间的依赖关系的不同将Job划分成不同的Stage,遇到一个宽依赖则划分一个Stage。

Task:Stage是一个TaskSet,将Stage根据分区数划分成一个个的Task。

Spark工作流程

picture

1.启动SparkContext:

  • 应用程序启动时,首先创建 SparkContext,它是与 Spark 集群通信的入口点。SparkContext向资源管理器(可以是Standalone、Mesos或YARN)注册,并请求运行 Executor 资源。

2.资源分配和Executor启动:

  • 资源管理器根据 SparkContext 的请求,分配 Executor 资源,并启动 StandaloneExecutorBackend。Executor 运行情况通过心跳发送到资源管理器上。

3.构建DAG图和分解成Stage:

  • SparkContext根据应用程序代码构建 DAG 图,并将其分解成阶段(Stage)。

4.任务调度:

  • SparkContext将 DAG 图中的任务(Task)分发给任务调度器(Task Scheduler)。Executor向 SparkContext 申请任务。

5.代码分发和任务执行:

  • SparkContext将应用程序代码分发给 Executor。任务调度器将任务发放给 Executor 运行。

6.任务执行和资源释放:

  • Executor 在本地运行任务,并在完成后释放所有相关资源

Spark中的两种核心Shuffle

Spark中触发Shuffle过程的算子包括:

  1. groupByKey:将具有相同键的元素分组到一起。
  2. reduceByKey:对具有相同键的元素执行聚合操作。
  3. sortByKey:根据键对RDD中的元素进行排序。
  4. join:连接两个RDD,根据它们的键将具有相同键的元素组合在一起。

HashShuffle

(在 Spark 2.0 版本中, Hash Shuffle 方式己经不再使用。)

hs

Shuffle Write 阶段:

  1. 当前 Stage 结束计算后,每个 Task 处理的数据会按照 key 进行“划分”。
  2. 数据按照 key 经过 hash 算法分配到相应的内存缓冲中。
  3. 当内存缓冲填满后,数据会溢写到磁盘文件中。每个 Task 会为下游 Stage 的每个 Task 创建一个磁盘文件,用于存储该 Task 处理的数据。
  4. 当前 Stage 有多个 Task,每个 Task 都会为下一个 Stage 的每个 Task 创建相应数量的磁盘文件。

Shuffle Read 阶段:

  1. 下一个 Stage 开始时,每个 Task 需要从上一个 Stage 的计算结果中拉取所有相同 key 的数据到本地节点。
  2. 每个 Shuffle Read Task 在拉取数据时一边进行聚合操作,一边将数据写入自己的内存缓冲中。
  3. 拉取的数据大小受到每个 Task 的缓冲大小限制,每次拉取一部分数据,然后进行聚合操作。
  4. 聚合完一批数据后,继续拉取下一批数据,直到所有数据都拉取完毕,并得到最终的结果。

优化后的Hash Shuffle

开启 spark.shuffle.consolidateFiles 参数后,在 HashShuffleManager 中会采用一种优化机制,称为 consolidate 机制。这个机制会将 shuffle write 过程中复用buffer和进行磁盘文件的合并,大幅减少了小文件数量,从而提升了性能。(复用buffer)

举例来说,假设第一个 stage 有 50 个 Task,第二个 stage 有 100 个 Task,总共有 10 个 Executor(每个 Executor 有 1 个 CPU Core),每个 Executor 执行 5 个 Task。未经优化时,每个 Executor 会创建 500 个磁盘文件,总共 5000 个磁盘文件。经过优化后,每个 Executor 只会创建 100 个磁盘文件,总共只有 1000 个磁盘文件。这种优化大幅减少了磁盘文件数量,提升了性能

yhs

SortShuffle

SortShuffleManager 的运行机制主要分成三种:

  1. 普通运行机制
  2. bypass 运行机制,当 shuffle read task 的数量小于等于spark.shuffle.sort.bypassMergeThreshold参数的值时(默认为 200),就会启用 bypass 机制;
  3. Tungsten Sort 运行机制,开启此运行机制需设置配置项 spark.shuffle.manager=tungsten-sort。开启此项配置也不能保证就一定采用此运行机制(后面会解释)。[了解]

普通运行机制


normal

在这种模式下,数据会先被写入一个内存数据结构中。根据不同的Shuffle算子,可能选择不同的数据结构。

例如,对于reduceByKey等聚合类的Shuffle算子,会选择Map数据结构,在内存中进行聚合并写入;而对于join等普通的Shuffle算子,则会选择Array数据结构直接写入内存。

每写入一条数据到内存数据结构后,都会检查是否达到了某个临界阈值。一旦达到临界阈值,会尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

溢写到磁盘文件之前,会先根据 key 对内存数据结构中已有的数据进行排序。

写入磁盘文件是通过 Java 的 BufferedOutputStream 实现的。BufferedOutputStream 是 Java 的缓冲输出流,首先会将数据缓冲在内存中,当内存缓冲满溢之后再一次写入磁盘文件中,这样可以减少磁盘 IO 次数,提升性能。

在这个过程中,一个task会多次进行磁盘溢写操作,生成多个临时文件。最后,会执行合并操作(merge),将所有之前生成的临时文件合并成一个最终的磁盘文件(大大减少了文件数量)。由于一个task对应一个磁盘文件,为了标识下游阶段的task所需的数据,会额外写入一个索引文件,记录了下游各个task的数据在文件中的起始偏移量和结束偏移量。

bypass 运行机制


Reducer 端任务数比较少的情况下,基于 Hash Shuffle 实现机制明显比基于 Sort Shuffle 实现机制要快,因此基于 Sort Shuffle 实现机制提供了一个带 Hash 风格的回退方案,就是 bypass 运行机制。对于 Reducer 端任务数少于配置属性spark.shuffle.sort.bypassMergeThreshold设置的个数时,使用带 Hash 风格的回退计划。

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

  • shuffle map task 数量小于spark.shuffle.sort.bypassMergeThreshold=200参数的值。
  • 不是聚合类的 shuffle 算子。

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

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

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

bypassSh

Tungsten Sort 运行机制


tungsten Sort是对普通Sort的一种优化,它通过对数据进行序列化后的字节数组的指针进行排序,而不是直接对内容进行排序,从而避免了序列化和反序列化的开销,大幅降低了内存消耗和GC开销。

Spark提供了配置属性,用于选择Shuffle的具体实现机制。默认情况下,Spark使用SortShuffle实现机制,但实际上,SortShuffleManager既支持SortShuffle也支持Tungsten Sort Shuffle。

对应非基于 Tungsten Sort 时,通过 SortShuffleWriter.shouldBypassMergeSort 方法判断是否需要回退到 Hash 风格的 Shuffle 实现机制,当该方法返回的条件不满足时,则通过 SortShuffleManager.canUseSerializedShuffle 方法判断是否需要采用基于 Tungsten Sort Shuffle 实现机制,而当这两个方法返回都为 false,即都不满足对应的条件时,会自动采用普通运行机制。

要使用Tungsten Sort Shuffle,需要满足以下条件:

  1. Shuffle操作没有聚合操作或排序要求。
  2. 序列化器支持序列化值的重定位,目前仅支持KryoSerializer。
  3. 输出分区数量少于16777216个。

此外,还有其他限制,如单条记录的长度不能超过128MB,这受到内存模型的影响。

Yarn Cluster&Yarn Client模式的区别

Spark中最普遍的一道面试题

Yarn Cluster模式的driver进程托管给Yarn(AppMaster)管理,通过yarn UI或者Yarn logs命令查看日志。

Yarn Client模式的driver进程运行在本地客户端,因资源调度、任务分发会和Yarn集群产生大量网络通信,出现网络激增现象,适合本地调试,不建议生产上使用。

  • Yarn Cluster模式

  • Yarn Client模式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Spark的广播变量和累加器

广播变量(Broadcast Variables)

  • 基本原理:广播变量是一种将较大的只读数据集(List、Array)在集群中所有节点间进行高效共享的机制。Spark会将广播变量的内容序列化后分发到每个Executor的内存中,并在任务执行时直接使用它们,而不是通过网络传输大量数据。
  • 用途:适用于需要在所有任务中共享的大型只读数据集,例如需要在每个任务中使用的模型参数、配置数据等。
  • 注意不能广播RDD,因为RDD不存储数据
  • 在Driver端使用broadcast()将一些大变量(List、Array)持久化,Executor根据broadcastid拉取本地缓存中的Broadcast对象,如果不存在,则尝试远程拉取Driver端持久化的那份Broadcast变量。这样所有的Executor均存储了一份变量的备份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

累加器(Accumulators)

  • 基本原理:累加器是一种仅能进行“加”的分布式变量,只有驱动程序能够读取它们的值,而任务只能将值加到累加器中。累加器的更新是在各个任务执行过程中进行的,因此可以用来实现分布式的计数、求和等操作。

  • 用途:适用于需要在所有任务中进行累加计算的场景,例如统计所有任务中某个条件满足的记录数、计算全局的计数器等

  • Spark累加器支持在Driver端进行全局汇总的计算需求,实现分布式计数的功能

参考:

https://bbs.huaweicloud.com/blogs/326863

面试题:

如何使用Spark实现TopN的获取(描述思路或使用伪代码)

  1. 将数据按照需要排序的字段进行降序排序:使用Spark的sortBy算子对数据集进行排序,按照需要进行降序排序。

  2. 取前N个元素:使用take算子获取排序后的前N个元素,即为TopN结果。

    SELECT * FROM data ORDER BY value DESC LIMIT N
    
    // 假设有一个包含(key, value)对的RDD,需要根据value字段获取TopN
    val dataRDD: RDD[(String, Int)] = ...
    
    // 定义需要获取的TopN数量
    val topN: Int = ...
    
    // 对数据进行降序排序,并取前N个元素
    val topNElements: Array[(String, Int)] = dataRDD.sortBy(_._2, ascending = false).take(topN)
    
    // 打印TopN结果
    topNElements.foreach(println)
    
    

    京东:调优之前与调优之后性能的详细对比(例如调整map个数,map个数之前多少、之后多少,有什么提升)

    调优之前,假设有几百个文件,每个文件一个map任务,总共几百个map任务。在进行join操作时,每个map任务都需要读取一个文件的数据,然后进行join操作,这会导致大量的数据移动和shuffle操作,性能较低。

    调优之后,通过对map任务进行coalesce操作,将几百个map任务合并成较少的几十个map任务,即窄依赖。这样,在进行join操作时,每个map任务需要处理的数据量会增加,但由于减少了map任务的数量,整体上减少了shuffle操作产生的文件数。因此,join操作的性能会有所提升。

在 Spark 中,每个分区会被分配给一个 map 任务来处理。因此,调优的关键是合理设置分区数量,以便最大限度地利用集群资源,并最小化不必要的数据移动和 shuffle 操作。

对于你提到的情况,假设有几百个小文件,每个文件一个分区,而每个分区都会被分配给一个 map 任务来处理。在进行 join 操作时,如果分区数量过多,会导致大量的 map 任务,增加了 shuffle 操作的开销,降低了性能。

通过使用 coalesce 将分区数量减少到一个更合理的数量,可以减少 map 任务的数量,从而降低了 shuffle 操作的开销,提高了性能。将几百个分区合并成几十个分区,可以显著减少 map 任务的数量,加速 join 操作的执行。

键是合理设置分区数量**,以便最大限度地利用集群资源,并最小化不必要的数据移动和 shuffle 操作。

对于你提到的情况,假设有几百个小文件,每个文件一个分区,而每个分区都会被分配给一个 map 任务来处理。在进行 join 操作时,如果分区数量过多,会导致大量的 map 任务,增加了 shuffle 操作的开销,降低了性能。

通过使用 coalesce 将分区数量减少到一个更合理的数量,可以减少 map 任务的数量,从而降低了 shuffle 操作的开销,提高了性能。将几百个分区合并成几十个分区,可以显著减少 map 任务的数量,加速 join 操作的执行。

因此,在这种情况下,调优的重点是调整分区数量,而不是直接合并 map 任务。通过合理设置分区数量,可以达到减少 shuffle 操作、提高性能的目的

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spark从读取文件到Shuffle的过程可以概括为以下几个步骤: 1. 文件读取:Spark首先会将文件分割成若干个块(blocks),每个块的大小通常为HDFS或本地文件系统的块大小(默认为128MB)。每个块都会被存储在集群中的不同节点上。 2. 数据划分:接下来,Spark会将文件的每个块划分为若干行数据,并将这些数据分发到集群中的不同节点上。这个过程称为数据划分(Partitioning)。 3. 任务分配:Spark将一个或多个任务(Task)分配给每个节点,每个任务负责处理一个或多个数据划分的数据。任务的数量通常与集群中的节点数量相匹配。 4. 数据处理:每个节点上的任务会对其负责的数据进行处理,例如进行过滤、转换、聚合等操作。这些操作通常是在内存中进行,以提高计算效率。 5. Shuffle准备:在某些操作(例如groupByKey、reduceByKey等)中,需要对数据进行重新分区,以便在后续阶段进行合并或聚合操作。在Shuffle准备阶段,Spark会对需要重新分区的数据进行排序和分组,以便在后续阶段更高效地进行数据交换。 6. Shuffle数据交换:在Shuffle数据交换阶段,Spark会将每个节点上的数据按照指定的分区规则进行打包,并将其发送到其他节点上。这个过程会产生大量的数据传输,因此对于大规模数据集的Shuffle操作可能会成为性能瓶颈。 7. Shuffle合并:接收到Shuffle数据的节点会按照分区规则进行数据合并和聚合,以生成最终结果。这个过程通常涉及大量的磁盘IO和计算操作。 需要注意的是,Shuffle是一个开销较高的操作,因为它需要大量的数据传输和磁盘IO。因此,在Spark应用程序中,尽量减少Shuffle操作的次数和数据量,可以显著提高性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值