Spark面积题汇总

一、Spark RDD 的 五个特性

RDD(Resilient Distributed Dataset)弹性分布式数据集,是spark中最基本的数据抽象,它代表一个不可变,可分区,里面的元素可以并行计算的集合。RDD 最重要的特性就是容错性,可以自动从节点失败中恢复过来。即如果某个结点上的 RDD partition 因为节点故障,导致数据丢失,那么 RDD 可以通过自己的数据来源重新计算该 partition。 这一切对使用者都是透明的 RDD 的数据默认存放在内存中,但是当内存资源不足时,Spark 会自动将 RDD 数据写入磁盘。

五大特点如下:

1. A list of partitions

一组分区:RDD由一系列partition构成,缓存在不同节点的内存中,partition的数据对应task的数量

2. A function for computing each split

一个函数:对RDD做计算,相当于对RDD的每个partition做计算

3. A list of dependencies on other RDDs

RDD之间有依赖关系,可溯源

4. Optionally, a Partitioner for key-value RDDs (e.g. to say that the RDD is hash-partitioned)

一个Partitioner:即RDD的分片函数,如果RDD里面存的数据是key-value形式,则可以传递一个自定义的Partitioner进行重新分区

5. Optionally, a list of preferred locations to compute each split on (e.g. block locations for an HDFS file)

一个列表:存储存取每个Partition的优先位置(preferred location),计算每个split时,在split所在机器的本地上运行task是最好的,避免了数据的移动,split有多个副本,所以preferred location不止一个

二、Spark 和 Hive(Hadoop) 对比差异

1.计算架构不同:

        Hadoop 底层使用 MapReduce 计算架构,只有 map 和 reduce 两种操作,表达能力比较弱,而且在 MR 过程中会重复的读写 hdfs,造成大量的磁盘 io 读写操作,所以适合高时延环境下批处理计算的应用;

        Spark 是基于内存的分布式计算架构,提供更加丰富的数据集操作类型,主要分成转化 操作和行动操作,包括 map、reduce、filter、flatmap、groupbykey、reducebykey、union 和 join 等,数据分析更加快速,所以适合低时延环境下计算的应用; Spark 与 Hadoop 最大的区别在于迭代式计算模型。

2.任务执行阶段不同

        基于 mapreduce 框架的 Hadoop 主要 分为 map 和 reduce 两个阶段,两个阶段完了就结束了,所以在一个 job 里面能做的处理很有限;

        Spark 计算模型是基于内存的迭代式计算模型,可以分为 n 个阶段,根据用户编写的 RDD 算子和程序,在处理完一个阶段后可以继续往下处理很多个阶段,而不只是两个阶段。

所以 Spark 相较于 MapReduce,计算模型更加灵活,可以提供更强大的功能。

注:但是 spark 也有劣势,由于 spark 基于内存进行计算,虽然开发容易,但是真正面对大数据的时候,在没有进行调优的轻局昂下,可能会出现各种各样的问题,比如 OOM 内存溢出等情况,导致 Spark 程序可能无法运行起来,而 MapReduce 虽然运行缓慢,但是至少可以慢慢运行完。

三、Hadoop 和 Spark 使用场景

Hadoop/MapReduce 和 Spark 最适合的都是做离线型的数据分析,但 Hadoop 特别适合是 单次分析的数据量“很大”的情景,而 Spark 则适用于数据量不是很大的情景。

(1)一般情况下,对于中小互联网和企业级的大数据应用而言,单次分析的数量都不会“很 大”,因此可以优先考虑使用 Spark。

(2)业务通常认为 Spark 更适用于机器学习之类的“迭代式”应用,80GB 的压缩数据(解压后 超过 200GB),10 个节点的集群规模,跑类似“sum+group-by”的应用,MapReduce 花了 5 分钟,而 spark 只需要 2 分钟。

三、 Spark中的常用算子区别(map、mapPartitions、foreach、foreachPatition)

map:用于遍历RDD,将函数应用于每一个元素,返回新的RDD(transformation算子)

mapPatitions:用于遍历操作RDD中的每一个分区partition,返回生成一个新的RDD

foreach:用于遍历RDD,将函数应用于每一个元素,无返回值(action算子)

foreachPatition:用于遍历操作RDD中的每一个分区partition,无返回值(action算子)

总结:一般使用mapPatitions和foreachPatition算子比map和foreach更加高效,例如在map函数处理中需要链接redis做处理。

四、Spark中的宽窄依赖

RDD和它的父RDD的关系有两种类型:窄依赖和宽依赖

宽依赖:指的是多个子RDD的Partition会依赖同一个父RDD的Partition,关系是一对多,父RDD的一个分区的数据去到子RDD的不同分区里面,有shuffle的过程

窄依赖:指的是每一个父RDD的Partition最多被子RDD的一个partition使用,是一对一的,也就是父RDD的一个分区去到了子RDD的一个分区中,没有shuffle过程

区分的标准就是看父RDD的一个分区的数据的流向,要是流向一个partition的话就是窄依赖,否则就是宽依赖。

四、Spark中如何划分stage

在DAG调度的过程中,Stage阶段的划分是根据是否有shuffle过程,也就是存在ShuffleDependency宽依赖的时候,需要进行shuffle,这时候会将作业job划分成多个Stage;

spark 划分 stage 的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个 stage;遇到窄依赖就将这个 RDD 加入该 stage 中。

Spark 任务会根据 RDD 之间的依赖关系,形成一个 DAG 有向无环图,DAG 会提交给DAGScheduler 。DAGScheduler 会把 DAG 划分相互依赖的多个 stage,划分依据就是宽窄依赖,遇到宽依赖就划分stage,每个 stage 包含一个或多个 task,然后将这些 task 以 taskSet 的形式提交给 TaskScheduler 运行,stage 是由一组并行的task组成

stage 的 task 的并行度是由 stage 的最后一个 RDD 的分区数来决定的,一般来说,一个 partition对应一个 task,但最后 reduce 的时候可以手动改变 reduce 的个数,也就是改变最后一个 RDD 的分区数,也就改变了并行度。

DAGScheduler分析:

DAGScheduler 是一个面向stage 的调度器

主要入参有:dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, allowLocal, resultHandler,  localProperties.get)

rdd: final RDD;

cleanedFunc: 计算每个分区的函数;

resultHander: 结果侦听器;

主要功能:

    1.接受用户提交的job;

    2.将job根据类型划分为不同的stage,记录那些RDD,stage被物化,并在每一个stage内产生一系列的task,并封装成taskset;

    3.决定每个task的最佳位置,任务在数据所在节点上运行,并结合当前的缓存情况,将taskSet提交给TaskScheduler;

    4.重新提交shuffle输出丢失的stage给taskScheduler;(一个stage内部的错误不是由shuffle输出丢失造成的,DAGScheduler是不管的,由TaskScheduler负责尝试重新提交task执行。)

    5.Job的生成:一旦driver程序中出现action,就会生成一个job,比如count等,向DAGScheduler提交job,如果driver程序后面还有action,那么其他action也会对应生成相应的job,所以,driver端有多少action就会提交多少job,这可能就是为什么spark将driver程序称为application而不是job 的原因。每一个job可能会包含一个或者多个stage,最后一个stage生成result,在提交job 的过程中,DAGScheduler会首先从后往前划分stage,划分的标准就是宽依赖,一旦遇到宽依赖就划分,然后先提交没有父阶段的stage们,并在提交过程中,计算该stage的task数目以及类型,并提交具体的task,在这些无父阶段的stage提交完之后,依赖该stage 的stage才会提交。

    6.有向无环图:DAG,有向无环图,简单的来说,就是一个由顶点和有方向性的边构成的图中,从任意一个顶点出发,没有任意一条路径会将其带回到出发点的顶点位置。通俗说就是所有任务的依赖关系。为每个spark job计算具有依赖关系的多个stage任务阶段,通常根据shuffle来划分stage,如reduceByKey,groupByKey等涉及到shuffle的transformation就会产生新的stage ,然后将每个stage划分为具体的一组任务,以TaskSets的形式提交给底层的任务调度模块来执行,其中不同stage之前的RDD为宽依赖关系,TaskScheduler任务调度模块负责具体启动任务,监控和汇报任务运行情况。
 

五、 RDD缓存

Spark可以使用 cache 方法将任意 RDD 缓存到内存、磁盘文件系统中。主要用于提升读取的效率,以及针对一个数据流中存在多个 action 操作时(action 操作会触发 RDD 从读取数据源开始重新计算)。缓存是容错的,如果一个 RDD 分片丢失,可以通过构建它的 transformation 自动重构。

Spark中,RDD可以使用 cache() 和 persist() 方法来缓存。

cache:默认存储级别 MEMORY_ONLY。在第一次 action 操作时触发,第二次 action 操作时,会直接从内存中提取数据进行计算,可以避免重复计算生存对应的 RDD。
    def cache(): this.type = persist()

persist:手动选择持久化级别,并使用指定的方式进行持久化

    def persist(): this.type = persist(StorageLevel.MEMORY_ONLY)
    默认缓存级别是 StorageLevel.MEMORY_ONLY
 

注:在不会使用cached RDD的时候,及时使用unpersist方法来释放它。

RDD缓存级别

持久化级别
MEMORY_ONLY使用未序列化的Java对象格式,将数据保存在内存中。如果内存不够存放所有的数据,则数据可能就不会进行持久化。那么下次对这个RDD执行算子操作时,那些没有被持久化的数据,需要从源头处重新计算一遍。这是默认的持久化策略,使用cache()方法时,实际就是使用的这种持久化策略。
MEMORY_AND_DISK使用未序列化的Java对象格式,优先尝试将数据保存在内存中。如果内存不够存放所有的数据,会将数据写入磁盘文件中,下次对这个RDD执行算子时,持久化在磁盘文件中的数据会被读取出来使用。
MEMORY_ONLY_SER基本含义同MEMORY_ONLY。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
MEMORY_AND_DISK_SER基本含义同MEMORY_AND_DISK。唯一的区别是,会将RDD中的数据进行序列化,RDD的每个partition会被序列化成一个字节数组。这种方式更加节省内存,从而可以避免持久化的数据占用过多内存导致频繁GC。
DISK_ONLY使用未序列化的Java对象格式,将数据全部写入磁盘文件中。
MEMORY_ONLY_2, MEMORY_AND_DISK_2, 等等对于上述任意一种持久化策略,如果加上后缀_2,代表的是将每个持久化的数据,都复制一份副本,并将副本保存到其他节点上。这种基于副本的持久化机制主要用于进行容错。假如某个节点挂掉,节点的内存或磁盘中的持久化数据丢失了,那么后续对RDD计算时还可以使用该数据在其他节点上的副本。如果没有副本的话,就只能将这些数据从源头处重新计算一遍了。

六、checkpoint 检查点机制

应用场景:当 spark 应用程序特别复杂,从初始的 RDD 开始到最后整个应用程序完成有很多的步骤,而且整个应用运行时间特别长,这种情况下就比较适合使用 checkpoint 功能。 原因:对于特别复杂的 Spark 应用,会出现某个反复使用的 RDD,即使之前持久化过 但由于节点的故障导致数据丢失了,没有容错机制,所以需要重新计算一次数据。

Checkpoint 首先会调用 SparkContext 的 setCheckPointDIR()方法,设置一个容错的文件 系统的目录,比如说 HDFS;然后对 RDD 调用 checkpoint()方法。之后在 RDD 所处的 job 运行结束之后,会启动一个单独的 job,来将 checkpoint 过的 RDD 数据写入之前设置的文件 系统,进行高可用、容错的类持久化操作。

检查点机制是我们在 spark streaming 中用来保障容错性的主要机制,它可以使 spark streaming 阶段性的把应用数据存储到诸如 HDFS 等可靠存储系统中,以供恢复时使用。

具 体来说基于以下两个目的服务:

  • 控制发生失败时需要重算的状态数。Spark streaming 可以通过转化图的谱系图来重算状 态,检查点机制则可以控制需要在转化图中回溯多远。
  • 提供驱动器程序容错。如果流计算应用中的驱动器程序崩溃了,你可以重启驱动器程序 并让驱动器程序从检查点恢复,这样 spark streaming 就可以读取之前运行的程序处理数据的 进度,并从那里继续。

七、checkpoint 检查点机制和 cache 持久化机制的区别

最主要的区别在于:持久化只是将数据保存在 BlockManager 中,但是 RDD 的 lineage(血 缘关系,依赖关系)是不变的。但是 checkpoint 执行完之后,RDD 已经没有之前所谓的依赖 RDD,而只有一个强行为其设置的 checkpointRDD,checkpoint 之后 RDD 的 lineage 就改变了。

相对来说cache持久化的数据丢失的可能性更大,因为节点的故障会导致磁盘、内存的数据丢失。但是 checkpoint 的数据通常是保存在高可用的文件系统中,比如 HDFS 中,所以数据丢失可能性 比较低。

八、Spark SQL执行计划

从 3.0 开始,explain 方法有一个新的参数 mode,该参数可以指定执行计划展示格式:

  •  explain(mode="simple"):只展示物理执行计划。

  •  explain(mode="extended"):展示物理执行计划和逻辑执行计划。

  • explain(mode="codegen"):展示要Codegen生成的可执行Java代码。

  • explain(mode="cost"):展示优化后的逻辑执行计划以及相关的统计。

  • explain(mode="formatted"):以分隔的方式输出,它会输出更易读的物理执行计划,并展示每个节点的详细信息。

执行计划处理流程

核心的执行过程一共有5个步骤:

这些操作和计划都是 Spark SQL 自动处理的,会生成以下计划:

Unresolved 逻辑执行计划:== Parsed Logical Plan ==
        Parser 组件检查 SQL 语法上是否有问题,然后生成 Unresolved(未决断)的逻辑计划,

不检查表名、不检查列名。


Resolved 逻辑执行计划:== Analyzed Logical Plan ==
        通过访问 Spark 中的 Catalog 存储库来解析验证语义、列名、类型、表名等。

优化后的逻辑执行计划:== Optimized Logical Plan ==

        Catalyst 优化器根据各种规则进行优化。        

物理执行计划:== Physical Plan ==

        a. HashAggregate 运算符表示数据聚合,一般 HashAggregate 是成对出现,第一个 HashAggregate 是将执行节点本地的数据进行局部聚合,另一个 HashAggregate 是 将各个分区的数据进一步进行聚合计算。
        b.Exchange 运算符其实就是 shuffle,表示需要在集群上移动数据。很多时候 HashAggregate 会以 Exchange 分隔开来。

        c.Project运算符是SQL中的投影操作,就是选择列(例如:select name, age...)。                 d.BroadcastHashJoin 运算符表示通过基于广播方式进行 HashJoin。

        e.LocalTableScan 运算符就是全表扫描本地的表。

SparkSQL 在整个执行计划处理的过程中,使用了 Catalyst 优化器:

1. 基于 RBO 的优化:

        在 Spark 3.0 版本中,Catalyst 总共有 81 条优化规则(Rules),分成 27 组(Batches), 其中有些规则会被归类到多个分组里。因此,如果不考虑规则的重复性,27 组算下来总共 会有 129 个优化规则。

如果从优化效果的角度出发,这些规则可以归纳到以下 3 个范畴:

1. 谓词下推(Predicate Pushdown)

        将过滤条件的谓词逻辑都尽可能提前执行,减少下游处理的数据量。对应 PushDownPredicte 优化规则,对于 Parquet、ORC 这类存储格式,结合文件注脚(Footer) 中的统计信息,下推的谓词能够大幅减少数据扫描量,降低磁盘 I/O 开销。

2. 列剪裁(Column Pruning)

        列剪裁就是扫描数据源的时候,只读取那些与查询相关的字段。

3. 常量替换(Constant Folding)

        假设我们在年龄上加的过滤条件是 “age < 12 + 18”,Catalyst 会使用 ConstantFolding

规则,自动帮我们把条件变成 “age < 30”。再比如,我们在 select 语句中,掺杂了一些

常量表达式,Catalyst 也会自动地用表达式的结果进行替换。

2 基于 CBO 的优化

CBO 优化主要在物理计划层面,原理是计算所有可能的物理计划的代价,并挑选出代 价最小的物理执行计划。充分考虑了数据本身的特点(如大小、分布)以及操作算子的特 点(中间结果集的分布及大小)及代价,从而更好的选择执行代价最小的物理执行计划。

通过 "spark.sql.cbo.enabled" 来开启,默认是 false。配置开启 CBO 后,CBO 优化器可以基于表和列的统计信息,进行一系列的估算,最终选择出最优的查询计划。比如:Build侧选择、优化 Join 类型、优化多表 Join 顺序等。

参数描述默认值

spark.sql.cbo.enabled

CBO 总开关。
true 表示打开,false 表示关闭。 要使用该功能,需确保相关表和列的统计信息已经生成

false

spark.sql.cbo.joinReorder.enabled

使用 CBO 来自动调整连续的 inner join 的顺序。 true:表示打开,false:表示关闭 要使用该功能,需确保相关表和列的统计信息已经生成,且 CBO 总开关打开。

false

spark.sql.cbo.joinReorder.dp.threshold

使用 CBO 来自动调整连续 inner join 的表的个数阈值。 如果超出该阈值,则不会调整 join 顺序。

12

3. 广播 Join

        Spark join 策略中,如果当一张小表足够小并且可以先缓存到内存中,那么可以使用 Broadcast Hash Join,其原理就是先将小表聚合到driver端,再广播到各个大表分区中,那么 再次进行 join 的时候,就相当于大表的各自分区的数据与小表进行本地 join,从而规避了 shuffle。

广播 join 默认值为 10MB,由 spark.sql.autoBroadcastJoinThreshold 参数控制。

3.4 SMB Join

        SMB JOIN是sort merge bucket操作,需要进行分桶,首先会进行排序,然后根据key 值合并,把相同 key 的数据放到同一个 bucket 中(按照 key 进行 hash)。分桶的目的其实 就是把大表化成小表。相同 key 的数据都在同一个桶中之后,再进行 join 操作,那么在联 合的时候就会大幅度的减小无关项的扫描。

使用条件:

a. 两表进行分桶,桶的个数必须相等

b. 两边进行 join 时,join 列=排序列=分桶列

九、Spark 性能优化

1. 使用 Kryo 进行序列化

在 spark 中主要有三个地方涉及到序列化:

第一:在算子函数中使用到外部变量时,该变量会被序列化后进行网络传输;

第二:将自定义的类型作为 RDD 的泛型数据时(JavaRDD,Student 是自定义类型),所有自定义类型对象,都会进行序列化。因此这种情况下,也要求自定义的类必须实现 serializable 借口;

第三: 使用可序列化的持久化策略时,Spark 会将 RDD 中的每个 partition 都序列化成为一个大的字节数组。

对于这三种出现序列化的地方,我们都可以通过 Kryo 序列化类库,来优化序列化和反序列化的性能。Spark 默认采用的是 Java 的序列化机制。

但是 Spark 同时支持使用 Kryo 序列化库,而且 Kryo 序列化类库的性能比 Java 的序列化类 库要高。官方介绍,Kryo 序列化比 Java 序列化性能高出 10 倍。

Spark 之所以默认没有使用 Kryo 作为序列化类库,是因为 Kryo 要求最好要注册所有需要进行序列化的自定义类型,因此对于开发者来说这种方式比较麻烦。

 2. 优化数据结构

Java 中有三种类型比较耗费内存:

        对象:每个 Java 对象都有对象头、引用等 额外的信息,因此比较占用内存空间;

        字符串:每个字符串内部都有一个字符数 组以及长度等额外信息;

        集合类型:比如 HashMap、LinkedList 等,因为集合类型内部通常会使用一些内部类来封装集合元素,比如 Map.Entry。

因此 Spark 官方建议,在 Spark 编码实现中,特别对于算子函数中的代码, 尽量不要使用上述三种数据结构,尽量使用字符串替代对象,使用原始类型(比 如 int、long)替代字符串,使用数组替代集合类型,这样尽可能地减少内存占用, 从而降低 GC 频率,提升性能。

        使用数组替代集合类型: 有个 List list=new ArrayList(),可以将其替换为 int[] arr=new int[].这样 array 既比 list 少了额外信息的存储开销,还能使用原始数据类 型(int)来存储数据,比 list 中用 Integer 这种包装类型存储数据,要节省内存的多。

        使用字符串替代对象: 通常企业级应用中的做法是,对于 HashMap、List 这种数据,统一 用 String 拼接成特殊格式的字符串,比如 Map persons = new HashMap(),可以优化为特殊的字符串格式:id:name,address|id: name,address。

再比如,避免使用多层嵌套的对象结构。比如说,public class Teacher{private List students =new ArrayList()}.就是非常不好的例子。因为 Teacher 类的内部又嵌套了大量的小 Student 对象。解决措施是,完全可以使用特 殊的字符串来进行数据的存储,比如用 json 字符串来存储数据就是一个好的选 择。{"teacherId":1,"teacherName":"leo",students:[{},{}]}

        使用原始类型(比如 int、long)替代字符串: 对于有些能够避免的场景,尽量使用 int 代替 String。因为 String 虽然比 ArrayList、HashMap 等数据结构高效多了,占用内存上少多了,但是还是有额外 信息的消耗。比如之前用 String 表示 id,那么现在完全可以用数字类型的 int, 来进行替代。这里提醒,在 spark 应用中,id 就不要使用常用的 uuid,因为无法 转成 int,就用自增的 int 类型的 id 即可。

但是在编码实践中要做到上述原则其实并不容易。因为要同时考虑到代码的 可维护性,如果一个代码中,完全没有任何对象抽象,全部是字符串拼接的方式, 那么对于后续的代码维护和修改,无疑是一场巨大的灾难。同理,如果所有操作 都基于数组实现,而不是用 HashMap、LinkedList 等集合类型,那么对于我们编 码的难度以及代码的可维护性,也是一个极大的挑战。因此建议是在保证代码可 维护性的前提下,使用占用内存较少的数据结构。

3. 对多次使用的 RDD 进行持久化并序列化

Spark中每一个 action 操作都会触发 RDD 重头开始计算,如果代码逻辑中针对同一份 RDD 存在多个action 操作就会导致 RDD 重复计算,效率很低。

因此对于这种情况,建议是对多次使用的 RDD 进行持久化。此时 spark 就会根据你的持久化策略,将 RDD 中的数据保存到内存或者磁盘中。 以后每次对这个 RDD 进行算子操作时,都会直接从内存或磁盘中提取持久化的 RDD 数据,然后执行算子,而不会从源头出重新计算一遍这个 RDD。

4. 数据本地化

数据本地化对于 Spark job 性能有着巨大的影响。

如果数据以及要计算它的代码是在一 起的,那么性能自然会高。但是如果数据和计算它的代码是分开的,那么其中之一必须要另 外一方的机器上。

通常来说,移动代码到其他节点,会比移动数据到所在节点上去,速度要快的多,因为代码比较小。Spark 也正是基于整个数据本地化的原则来构建 task 调度算法的。

数据本地化,指的是数据距离它的代码有多近。基于数据距离代码的距离,有几种数据本地 化级别:

PROCESS_LOCAL:数据和计算它的代码在同一个 JVM 进程里面;

NODE_LOCAL:数据和计算它的代码在一个节点上,但是不在一个进程中,比如不在同 一个 executor 进程中,或者是数据在 hdfs 文件的 block 中;

NO_PREF:数据从哪里过来,性能都是一样的;

RACK_LOCAL:数据和计算它的代码在一个机架上;

ANY:数据可能在任意地方,比如其他网络环境内,或者其他机架上。 Spark 倾向于使用最好的本地化级别来调度 task,但是这是不可能的;

如果没有任何未处理的数据在空闲的 executor 上,那么 Spark 就会放低本地化级别。这时有两个选择:

第一,等待,直到 executor 上的 cpu 释放出来,那么就分配 task 过去;

第二,立即在任意一个 executor 上启动一个 task。Spark 默认会等待一会,来期望 task 要处理的数据所在的节点上的 executor 空闲出一个 cpu,从而将 task 分配过去。只要超过了时间,那么 spark 就会将 task 分配到其他任意一个空闲的 executor 上。

可以设置参数,spark.locality 系列参数,来调节 spark 等待 task 可以进行数据本地化的时间。

saprk.locality.wait(3000ms) 、 spark.locality.wait.node 、 spark.locality.wait.pr

5. 使用高性能的算子

a. 使用 reduceBykey/aggregateBykey 替代 groupByKey。

        map-side 预聚合:是指在每个节点本地对 相同的 key 进行一次聚合操作,类似于 MR 的本地 combiner。

        map-side 预聚合之后,每个节点本地就只会有一条相同的 key,因为多条相同的 key 都被聚合起来了。其他节点在拉取所有节点上的相同 key 时,就会大大减少需要拉取的数据 量,从而也就减少了磁盘 IO 以及网络传输开销。通过来说,在可能的情况下, 建议尽量使用 reduceByKey 或者 aggregateByKey 算子来替代 groupBykey 算子。

        因为 reduceBykey 和 aggregateBykey 算子都会使用用户自定义的函数对每个节点 本地相同的 key 进行预聚合。但是 groupbykey 算子是不会进行预聚合的,全量的数据会在集群的各个节点之间分发和传输,性能相对来说比较差。

b. 使用 mapPartitions 替代普通 map;

c. 使用 foreachPartitions 替代 foreach;

d. 使用 filter 之后进行 coalesce 操作:通常对一个 RDD 执行 filter 算子过滤掉 RDD 中以后比较多的数据后,建议使用 coalesce 算子,手动减少 RDD 的 partitioning 数量,将 RDD 中的数据压缩到更少的 partition 中去,只要使用更少 的 task 即可处理完所有的 partition,在某些场景下对性能有提升。

e. 使用 repartitionAndSortWithinPartitions 替代 repartition 与 sort 类操作: repartitionAndSortWithinPartitions 是 spark 官网推荐的一个算子。官方建议,如果需要在 repartition 重分区之后,还要进行排序,建议直接使用是这个算子。

        因为该算子可以一边进行重分区的 shuffle 操作,一边进行排序,Shuffle 和 Sort 两个 操作同时进行,比先 shuffle 再 sort 来说,性能更高。

十、Spark 共享变量

        我们知道Spark是多机器集群部署的,分为Driver/Master/Worker,Master负责资源调度,Worker是不同的运算节点,由Master统一调度,而Driver是我们提交Spark程序的节点,并且所有的reduce类型的操作都会汇总到Driver节点进行整合。节点之间会将map/reduce等操作函数传递一个独立副本到每一个节点,这些变量也会复制到每台机器上,而节点之间的运算是相互独立的,变量的更新并不会传递回Driver程序。那么有个问题,如果我们想在节点之间共享一份变量,比如一份公共的配置项,该怎么办呢?Spark为我们提供了两种特定的共享变量:累加器和广播变量。简单说,累加器是用来对信息进行聚合的,而广播变量则是用来高效分发较大对象的。
1.累加器

        累加器是对信息进行聚合的,通常在向 Spark 传递函数时,比如使用 map() 或者 filter() 传条件时,可以使用 Driver 中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,然而,正如前面所述,更新这些副本的值,并不会影响到 Driver 中对应的变量。

累加器则突破了这个限制,可以将工作节点中的值聚合到 Driver 中。它的一个典型用途就是对作业执行过程中的特定事件进行计数。

在2.0.0之前版本中,累加器的声明使用方式如下:

scala> val accum = sc.accumulator(0, "My Accumulator")
accum: spark.Accumulator[Int] = 0

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x)
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Int = 10

累加器的声明在2.0.0发生了变化,到2.1.0也有所变化,具体可以参考官方文档,我们这里以2.1.0为例将代码贴一下:

scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Long = 10

2.广播变量

        Spark 会自动把task中所有引用到的自由变量发送到工作节点上,那么每个 Task 都会持有自由变量的副本。如果自由变量的内容很大且 Task 很多的情况下,为每个 Task 分发这样的自由变量的代价将会巨大,必然会对网络 IO 造成压力。

        广播变量则突破了这个限制,不是把变量副本发给所有的 Task ,而是将其分发给所有的工作节点一次,这样节点上的 Task 可以共享一个变量副本。

Spark 使用的是一种高效的类似 BitTorrent 的通信机制,可以降低通信成本。广播的数据只会被发动各个节点一次,除了 Driver 可以修改,其他节点都是只读,并且广播数据是以序列化形式缓存在系统中的,当 Task 需要数据时对其反序列化操作即可。

一个广播变量可以通过调用SparkContext.broadcast(v)方法从一个初始变量v中创建。广播变量是v的一个包装变量,它的值可以通过value方法访问,下面的代码说明了这个过程:

scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

总结:spark中的共享变量让我们能够在全局做出一些操作,比如record总数的统计更新,一些大变量配置项的广播等等。而对于广播变量,我们也可以监控数据库中的变化,做到定时的重新广播新的数据表配置情况。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值