Spark分布式计算框架介绍

目录

1.Spark基本概述

2.Spark编程模型

[2.1 RDD概念](#2.1 RDD概念)

[2.2 RDD算子](#2.2 RDD算子)

[2.2.1 常用转换算子](#2.2.1 常用转换算子)

[2.2.2 常用动作算子](#2.2.2 常用动作算子)

[2.3 RDD依赖](#2.3 RDD依赖)

[2.3.1 窄依赖【Narrow Dependency】](#2.3.1 窄依赖【Narrow Dependency】)

[2.3.2 宽依赖【Shuffle/Wide Dependency】](#2.3.2 宽依赖【Shuffle/Wide Dependency】)

3.Spark程序架构

[3.1 Spark运行模式](#3.1 Spark运行模式)

[3.1.1 Local模式](#3.1.1 Local模式)

[3.1.2 Standalone模式](#3.1.2 Standalone模式)

[3.1.3 Yarn模式](#3.1.3 Yarn模式)

Yarn-Cluster模式

Yarn-Client模式

[3.2 Spark执行流程](#3.2 Spark执行流程)

[3.2.1 生成逻辑查询计划](#3.2.1 生成逻辑查询计划)

[3.2.2 生成物理查询计划](#3.2.2 生成物理查询计划)

[3.2.3 任务调度与执行](#3.2.3 任务调度与执行)

[3.3 DAG任务规划与调度](#3.3 DAG任务规划与调度)


1.Spark基本概述

由于MapReduce仅支持Map、Reduce两种语义操作,执行效率低,时间开销大,主要用于大规模离线批处理,不适合迭代计算、交互式计算、实时流处理等场景,有较大的局限性。整个Spark是一个all in one的框架,它提供了多种计算场景。底层的Spark Core对标的是MapReduce, 做批处理运算,它是把数据全部拉到内存里面进行计算,解决了MapReduce计算效率慢的问题;上层提供Spark SQL接口做数仓,可以把SQL转换成底层的Spark Core代码进行运算;spark streaming是做实时流处理的,也是转换成底层的Spark Core去运行。Spark具有高吞吐、低延时、通用易扩展、高容错等特点,采用Scala语言开发,提供多种运行模式。

2.Spark编程模型

2.1 RDD概念

RDD(Resilient Distributed Datesets)是弹性分布式数据集,Spark是基于RDD进行计算的。RDD特点:

  • 分布在集群中的只读对象集合

  • 由多个Partition组成

  • 通过转换操作构造

  • 失效后自动重构(弹性)

  • 存储在内存或磁盘中

下面以WordCount的例子来理解spark是如何基于rdd来计算的。

Scala 复制代码
val rdd1 = sc.textFile("hdfs://node01:9000/data/wc/in")
val rdd2 = rdd1.flatMap(_.split("\t"))
val rdd3 = rdd2.map((_,1))
val rdd4 = rdd3.reduceByKey((_+_))
rdd4.saveAsTextFile("hdfs://node01:9000/data/wc/out")

首先RDD是只读的,如果对RDD进行处理之后,一定要把RDD的处理结果保存成新的RDD。在spark进行处理的时候,首先用spark的一个对象spark context,简称sc对象,用textFile算子把HDFS某个目录下的数据文件读进来,然后变成rdd1;接着对rdd1进行flatMap处理,把文本拆分成多个单词,保存成新的rdd2;对rdd2进行map处理,再把结果存到了rdd3;接下来对rdd3进行reduceByKey处理,即把Key相同的数据分发到同一个reduce节点,分发到同一个reduce节点之后再把Key相同的value值做一个求和。得到最终结果之后,把这个结果保存成一个新的rdd4。最后用saveAsTextFile把rdd4保存成一个结果文件存到HDFS的out目录下。

spark是基于rdd进行运算的,它把数据读进来以后变成一个rdd,再对这个rdd进行反复的操作,最后一个rdd基本上是最终结果,把这个结果做一个输出,这是spark的一个计算流程。中间大量依赖rdd,这样的编程范式和单机编程基本上没有什么区别。spark基于rdd的运算逻辑可以绘制成一张dag有向无环图,先从目录下把数据读出来,假设这个目录下数据存了4个block块,每个block块就是rdd的一个partition分区,对每个rdd的partition进行一次操作就变成了新的rdd,得到最后结果做一个输出。

这些rdd的数据都存在内存中,一旦某个rdd的某个partition计算错误,在数据恢复的时候,不需要返回到源头让程序再从HDFS的某个节点把数据重新读出来,只需要返回到上一个阶段,上一个阶段的数据还在内存里而且是只读的,所以只需要拿到这个数据再重新执行一下就能快速恢复数据,这就是rdd的弹性。

2.2 RDD算子

RDD算子分为转换算子Transformation和动作算子Action,转换算子实际上是把RDD进行转换构造,把已有的RDD从一个RDD转换成新的一个RDD;动作算子是触发计算,触发结果的生成或结果输出。一句话总结就是:没有动作算子,所有转换算子都不会执行。

特性 转换算子 动作算子
定义 从一个RDD生成一个新的RDD的操作。 触发计算,将RDD的计算结果返回给Driver程序或保存到外部存储系统的操作。
本质 惰性的。只记录计算逻辑,不立即执行。 触发的。会触发整个RDD依赖链上的所有转换算子的执行。
返回值 返回一个新的RDD。 返回一个非RDD类型的值(如Scala/Java集合、具体数值)或不返回值(如保存到文件)。
例子 map, filter, flatMap, groupByKey, reduceByKey count, collect, saveAsTextFile, reduce, take
2.2.1 常用转换算子
  • map(func) :对RDD中的每个元素应用func函数,返回一个新的RDD。

  • filter(func) :筛选出满足func函数(返回true)的元素

  • flatMap(func) :对每个元素应用func,然后将结果"扁平化"(压平)。func应返回一个序列(如列表)

  • reduceByKey(func):对键值对RDD中相同键的值进行聚合。这是一个宽依赖操作(会发生Shuffle)

  • groupByKey():将键值对RDD中相同键的值分组到一个迭代器中。宽依赖。

  • union(otherRdd):合并两个RDD。

  • distinct():去重。

2.2.2 常用动作算子
  • count():返回RDD中元素的个数。

  • collect():将RDD中的所有数据以数组的形式拉取到Driver程序中。但如果数据量极大,会导致Driver内存溢出。

  • take(n):取RDD中的前n个元素到Driver端。

  • first() :取RDD中的第一个元素,等同于take(1)

  • reduce(func) :使用func函数(接受两个参数,返回一个同类型值)对RDD中的元素进行两两聚合。

  • saveAsTextFile(path):将RDD以文本文件形式保存到HDFS或本地文件系统。

  • foreach(func) :对RDD中的每个元素应用func,通常用于将数据写入外部数据库等副作用操作。

2.3 RDD依赖

对RDD的操作会让RDD之间产生一些关系,宽依赖和窄依赖描述的是RDD中一个父RDD的分区与子RDD的分区之间的依赖关系。简单来说,就是看一个父RDD的分区的数据,会被子RDD的多少个分区所使用。

2.3.1 窄依赖【Narrow Dependency】

如下图,父RDD的每一个分区最多只能被一个子RDD的分区所依赖,子RDD如果有部分分区数据丢失或损坏,只需从对应的父RDD重新计算恢复。

【窄依赖特点】:

  • 计算可以在单个节点内独立完成,无需跨节点移动数据。

  • 高效的容错:如果子RDD的某个分区丢失,只需要重新计算其对应的唯一父RDD分区即可。

  • 是 "管道化" 执行的基础,支持连续多个窄依赖操作合并成一个阶段(Stage)并行执行。

【属于窄依赖的算子】:

  • map:父分区中的每个元素独立转换,输出到子分区的同一位置。

  • filter:父分区中过滤掉一些元素,输出到子分区的同一位置。

  • flatMap:一个父元素可能映射为0个、1个或多个子元素,但依然在同一个子分区内。

  • mapPartitions:针对整个分区进行操作,输入输出分区一一对应。

  • union:简单地将多个RDD合并,分区关系不变。

  • coalesce(N)(且N>原分区数,即减少分区):在节点内合并父分区,不产生Shuffle。例如,父分区4个 -> 子分区2个,一个父分区只贡献给一个子分区。

2.3.2 宽依赖【Shuffle/Wide Dependency】

如下图,父RDD的一个分区被多个子RDD的分区所依赖,子RDD如果部分或全部分区数据丢失或损坏,必须从所有父RDD分区重新计算。相对于窄依赖,宽依赖付出的代价要高很多,应尽量避免使用。举例:groupByKey、reduceByKey、sortByKey

【宽依赖特点】:

  • 计算必须跨节点,需要将具有相同Key的数据从所有父分区中拉取到同一个子分区进行处理。这个过程就是 Shuffle(洗牌)。

  • 容错成本高:如果子RDD的某个分区丢失,由于其数据来源于多个父分区(可能在不同节点),需要重新计算所有这些父分区,并重新进行Shuffle。

  • 宽依赖是划分Stage的边界。

【属于宽依赖的算子】:

  • groupByKey:相同Key的数据必须拉到同一个分区,必然Shuffle。

  • reduceByKey :聚合相同Key的数据,需要Shuffle(但比groupByKey优化,因为有Map端预聚合)。

  • join(非共同分区情况下):两个RDD中关联Key的数据需要拉到一起,必须Shuffle。

  • distinct (底层实现为reduceByKey):去重本质是相同Key的聚合,需要Shuffle。

  • repartition(N):无论增减分区,都通过Shuffle实现数据的完全重新分布。

  • coalesce(N)(且N<原分区数,即增加分区):增加分区必然需要Shuffle来打散数据。

3.Spark程序架构

  • **Driver:**负责Spark程序的解析、划分Stage、调度任务到Executor上执行,一个Spark程序有一个Driver,一个Driver创建一个Spark Context,程序的main函数在Driver中运行。

  • **SparkContext:**负责加载配置信息,初始化运行环境,创建DAGScheduler和TaskScheduler

  • **Executor:**负责执行Driver分发的任务,一个节点可以启动多个Executor,每个Executor通过多线程运行多个任务。

  • **Task:**Spark运行的基本单位,一个Task负责处理一个RDD分区的计算逻辑。

spark集群的主节点是Master,从节点是Worker,也是主从架构。客户端向Master主节点发起提交任务请求,Master就会在所有的worker节点里分配一个运行资源,第一个运行资源运行作业自己的管理进程Driver,Driver运行起来后就开始解析作业,解析完以后发现需要4个task的资源,于是Driver向Master申请分配4个task的资源,Master就会在所有的Worker节点里面分配资源,把资源分装成Executor,Driver拿到新分配的资源后就把解析出来的task分发到Executor执行,执行过程中task会实时向Driver汇报,Driver发现所有的task运行完成之后,就向Master申请资源释放,Master就把所有的Executor资源全部释放掉。

Spark程序架构原理跟Yarn很相似,在Yarn中,作业的管理进程叫Application Master,申请出来的资源叫Container,Container里只能运行一个 task;在Spark中,作业的管理进程叫Driver,申请出来的资源叫Executor,Executor里可以运行多个task。

3.1 Spark运行模式

Spark运行模式指的是Spark应用程序的Driver进程、Executor进程、以及集群管理器这三者之间的组织和管理方式。简单说,就是Spark程序在哪里、以什么方式运行。

3.1.1 Local模式

local模式是单机执行,通常用于测试。比如本地写好spark代码之后,在本地测试,就用local模式跑一下,如果跑通了,说明代码没有问题。

3.1.2 Standalone模式

Standalone模式是已经装了spark集群,不依赖于第三方资源管理系统(如:YARN、Mesos),就可以把代码,以Standalone模式提交到spark集群里面运行。它的运行机制首先是客户端向Master主节点发起提交任务请求,Master就会在所有的worker节点里分配一个运行资源,第一个运行资源运行作业自己的管理进程Driver,Driver运行起来后就开始解析作业,解析完以后发现需要4个task的资源,于是Driver向Master申请分配4个task的资源,Master就会在所有的Worker节点里面分配资源,把资源分装成Executor,Driver拿到新分配的资源后就把解析出来的task分发到Executor执行,执行过程中task会实时向Driver汇报,Driver发现所有的task运行完成之后,就向Master申请资源释放,Master就把所有的Executor资源全部释放掉。

3.1.3 Yarn模式

如果没有单独的spark集群,只有Hadoop集群,就用Yarn模式。其本质是将Spark作为Yarn上的一个应用程序来运行。Yarn负责整体的集群资源管理和调度。

Yarn-Cluster模式

Yarn-Cluster模式是Driver运行在Yarn集群的一个容器中(由Yarn的ApplicationMaster兼任),Executor运行在其他容器中,适用于生产环境。

如下图,首先客户端向Resource Manager提交作业申请,Resource Manager在所有的Node Manager里面分配一个container出来,第一个container运行application master,前面提到spark的管理进程是driver,yarn的管理进程是application master,两者的协调只需要在driver外面套一个application master的壳子,application master运行起来后,负责解析作业的还是driver,driver把spark作业解析完以后,发现spark作业解析成了8个task,需要8个task的资源,但是driver没办法申请Hadoop的资源,这个时候driver就通知application master需要8个task的资源,application master再向resource manager申请资源,resource manager就给application maste分配container,application maste拿到资源之后就通知driver已申请到作业资源,于是driver就把解析好的task分发到这些container里运行,执行过程中,task会实时向driver进行汇报,driver发现所有的task运行完成之后,再通知application master释放资源,于是resource manager就把所有的资源给释放掉了。

Yarn-Cluster模式的driver在某一个从节点上,客户端看不到返回来的信息,所以为了便于调试,希望客户端这一块也能看到driver,实时收集到task的运行信息,观察作业情况,这个时候就可以用Yarn-Client模式。

Yarn-Client模式

Yarn-Client模式是Driver运行在提交任务的客户端机器上,Executor运行在Yarn的容器中,适用于交互和调试。Client模式和Cluster模式的区别在于Client模式的driver放到了客户端里,把driver放到了客户端里,这样driver会实时监控task返回来的信息,这种方式更适合调试。

3.2 Spark执行流程

整体的spark程序执行流程首先是Driver解析代码,先生成逻辑查询计划,再生成物理查询计划,最后打包成任务,即task的集合,打包成task集合之后,就把task集合调度到executor里去执行,执行过程中driver还会实时监控task的运行信息,进行相应的调度。

下面以一段用scala语言编写好的一个spark词频统计代码来理解spark程序是如何解析成task的。这段代码的逻辑首先是spark context用textFile把文本文件读进来,然后把文本文件拆分成单词,再给每个单词标数字"1",再按照Key值进行reduce,也就是把相同的单词聚合到同一个节点里面,再把对应的value值累加,得到最终的词频统计结果,然后输出到一个数据目录里保存。

Scala 复制代码
sc.textFile(inputArg)
    .flatMap(_.split("\t"))
    .map((_,1))
    .reduceByKey((_+_))
    .saveAsTextFile(outArg)
3.2.1 生成逻辑查询计划

逻辑查询计划仅仅关注RDD的状态,首先driver把这段代码生成逻辑查询计划,从Hdfs把文本读出来时,RDD里的数据是string类型的,对这个string类型的数据进行flatMap处理成单词形式,每个单词也是属于string类型的,接着进行map处理,给每个单词标数字"1",这个时候RDD的数据类型就变成了key-value的类型,key是string类型的单词,value是int类型的,最后reduceByKey按照Key值聚合,统计出词频结果并输出,这个就是逻辑查询计划,把RDD的状态解析出来。

3.2.2 生成物理查询计划

接下来做物理查询计划,物理查询关注底层数据的状态,比方说底层数据是由4个block块,这个时候就要根据4个block块来划分DAG图。如下图,从HDFS里面读出来4个block块,这样第一个RDD里面就有4个partition,每个block块对应一个partition,接下来进行一系列处理,第一次处理的时候是一对一的转换,把文本拆成单词,partition变成了一个新的partition,生成新的RDD;接着给每个单词标数字"1",转换完以后,reduceByKey按照Key值聚合到reduce节点处理,生成最后一个RDD并输出。

3.2.3 任务调度与执行

整个DAG图出来之后就要根据DAG图是否有宽窄依赖进行stage的切分,如下图前面都是一对一转换,一对一都是窄依赖啊,于是窄依赖的放在stage1里面。中间一旦走过宽依赖,在宽依赖处就切一刀,把剩下的这个rdd变成了新的stage2。可以理解为它是按照宽窄依赖进行拆分的,或者说是根据是否有shuffle进行拆分的,因为一旦有shuffle,实际上就是形成了一个宽依赖,前面是窄依赖,前后不是一个stage的。

划分stage的作用在于,同一个stage里的rdd可以放在同一个节点处理,一对一的转换可以打包成一个task,只需要把数据拿过来,在task里一对一的转换,并不需要其他的数据,这样stage1可以画成4个task,右边stage2可以画成3个task,一共打包成7个task,再把7个task放到一个set集合里,然后打包发送到executor里执行,执行过程中这些task会实时向driver汇报,这是整个调度流程。

3.3 DAG任务规划与调度

DAG(Directed Acyclic Graph)有向无环图:受制于某些任务必须比另一些任务较早执行的约束,可排序为一个队列的任务集合,该队列可由一个DAG图呈现,一个有向图无法从任意顶点出发经过若干条边回到该点。Spark程序的内部执行逻辑可由DAG描述,顶点代表任务,边代表任务间的依赖约束。

DAGScheduler:根据任务的依赖关系建立DAG,根据依赖关系是否为宽依赖,即是否存在Shuffle,将DAG划分为不同的阶段(Stage),将各阶段中的Task组成的TaskSet提交到TaskScheduler

TaskScheduler:负责Application的任务调度;重新提交失败的Task;为执行速度慢的Task启动备用Task。

下面以一个DAG图来展示stage是如何去切分的。首先是根据宽窄依赖去切分的,宽窄依赖是针对RDD与RDD之间来说的,图中有ABCDEF总共有6个RDD,rdd-A和rdd-B之间是宽依赖,中间走了一次shuffle,那么在rdd-A和rdd-B之间切一刀,左边变成Stage1;rdd-C、rdd-D和rdd-E之间是一对一的窄依赖,可以放在一个stage里;rdd-E和rdd-F之间是宽依赖,中间走了一次shuffle,那么在rdd-E和rdd-F之间切一刀,左边rdd-CDE变成Stage2;剩下的rdd-B和rdd-F之间是一对一的窄依赖,形成Stage3。

stage数量越多,shuffle越多,性能越慢,所以在调优过程中,重点关注整个spark任务的stage个数。

相关推荐
Leon Cheng2 小时前
工作流引擎在AI Agent中的应用
大数据·人工智能
华南首席酱油官2 小时前
精工筑净 标杆引领:净化板厂家赋能净化彩钢板行业新高度
大数据·人工智能
【赫兹威客】浩哥2 小时前
【赫兹威客】Hadoop完全分布式克隆文件部署教程
大数据·hadoop·分布式
编程彩机2 小时前
互联网大厂Java面试:从Spring Boot到分布式缓存的技术场景解析
java·redis·分布式·缓存·大厂面试·技术解析·sprint boot
你才是臭弟弟3 小时前
Apache Flink+Apache Iceberg(协作关系)
大数据·flink·apache
王九思3 小时前
SQL网关 Kyuubi 系列——基本介绍
数据仓库·hive·hadoop·sql·spark
KdanMin3 小时前
“日志抓不到”到“全链路可追溯”:一次 Android 系统级日志体系的工程化实践
大数据·人工智能
珠海西格电力3 小时前
零碳园区如何实现能源互联
大数据·人工智能·物联网·架构·能源
StarChainTech3 小时前
一站式租车平台革新:从信用免押到全流程可视化管理的技术实践
大数据·人工智能·微信小程序·小程序·软件需求