Spark-core知识体系总结

什么是RDD

RDD(Resilient Distributed Dataset)叫做弹性分布式数据集,是Spark中最基本的数据抽象,它代表一个不可变、可分区、里面的元素可并行计算的集合。RDD具有数据流模型的特点:自动容错、位置感知性调度和可伸缩性。RDD允许用户在执行多个查询时显式地将工作集缓存在内存中,后续的查询能够重用工作集,这极大地提升了查询速度。

RDD包含5个特征:
在这里插入图片描述
1、一个分区的列表
2、一个计算函数compute,对每个分区进行计算
3、对其他RDDs的依赖(宽依赖、窄依赖)列表
4、对key-value RDDs来说,存在一个分区器(Partitioner)【可选的】
5、对每个分区有一个优先位置的列表【可选的】

  • 一组分片(Partition),即数据集的基本组成单位。对于RDD来说,每个分片都会被一个计算任务处理,并决定并行计算的粒度。用户可以在创建RDD时指定RDD的分片个数,如果没有指定,那么就会采用默认值。默认值就是程序所分配到的CPU Core的数目。
  • 一个计算每个分片的函数。Spark中RDD的计算是以分片为单位的,每个RDD都会实现compute函数以达到这个目的。compute函数会对迭代器进行复合,不需要保存每次计算的结果。
  • RDD之间的依赖关系。RDD的每次转换都会生成一个新的RDD,所以RDD之间就会形成类似于流水线一样的前后依赖关系。在部分分区数据丢失时,Spark可以通过这个依赖关系重新计算丢失的分区数据,而不是对RDD的所有分区进行重新计算。
  • 一个Partitioner,即RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
  • 一个列表,存储存取每个Partition的优先位置(preferred location)。对于一个HDFS文件来说,这个列表保存的就是每个Partition所在的块的位置。按照“移动数据不如移动计算”的理念,Spark在进行任务调度的时候,会尽可能地将计算任务分配到其所要处理数据块的存储位置。

创建RDD

(1)在你的 driver program(驱动程序)中 parallelizing 一个已经存在的Scala集合。
val rdd1 = sc.parallelize(Array(1,2,3,4,5,6,7,8))
查看该rdd的分区数量,默认是程序所分配的cpu core的数量,也可以在创建的时候指定
rdd1.partitions.length
创建的时候指定分区数量:
val rdd1 = sc.parallelize(Array(1,2,3.4),3)
(2)由外部存储系统的数据集创建,包括本地的文件系统,还有所有Hadoop支持的数据集,比如HDFS、Cassandra、HBase等
val rdd2 = sc.textFile(“hdfs://hadoop141:8020/words.txt”)

RDD总结

RDD是Spark的最基本抽象,是对分布式内存的抽象使用,实现了以操作本地集合的方式来操作分布式数据集的抽象实现。RDD是Spark最核心的东西,它表示已被分区,不可变的并能够被并行操作的数据集合,不同的数据集格式对应不同的RDD实现。RDD必须是可序列化的。RDD可以cache到内存中,每次对RDD数据集的操作之后的结果,都可以存放到内存中,下一个操作可以直接从内存中输入,省去了MapReduce大量的磁盘IO操作。这对于迭代运算比较常见的机器学习算法, 交互式数据挖掘来说,效率提升非常大。

RDD是Spark中的抽象数据结构类型,任何数据在Spark中都被表示为RDD。从编程的角度来看,RDD可以简单看成是一个数组。和普通数组的区别是,RDD中的数据是分区存储的,这样不同分区的数据就可以分布在不同的机器上,同时可以被并行处理。因此,Spark应用程序所做的无非是把需要处理的数据转换为RDD,然后对RDD进行一系列的变换和操作从而得到结果。

tips:spark-shell --total-executor-cores 1 只在standalone 和 mesos模式下有用

在这里插入图片描述

rdd1.mapPartitionsWithIndex((idx, iter)=>{
	Iterator(s"[$idx;${iter.toArray.mkString(",")}]\n")
}).collect

spark源码之类型参数

  • Scala的类和方法、函数都可以是泛型,在Spark源码中可以到处看到类和方法的泛型,在实际实例化的时候指定具体的类型,例如Spark最核心、最基础、最重要的抽象数据结构RDD里面关于RDD的类的定义是泛型的,RDD的几乎所有方法的定义也都是泛型的,之所以这么做,是因为RDD会派生很多子类,通过子类适配了各种不同的数据源以及业务逻辑操作;
  • 关于对类型边界的限定,分为上边界和下边界:
    上边界:表达了泛型的类型必须是某种类型或者某种类的子类,语法为<: ,这里的一个新的现象是对类型进行限定;
    下边界:表达了泛型的类型必须是某种类型或者某种类的父类,语法为>: ;
  • View Bounds,可以进行某种神秘的转换,把你的类型可以在没有知觉的情况下转换成为目标类型,其实你可以认为View Bounds是上边界和下边界的加强补充版本,例如在SparkContext这个Spark的核心类中有T <% Writable方式的代码,这个代码所表达的是T必须是Writable类型的,但是T有没有直接继承自Writable接口,此时就需要通过“implicit”的方式来实现这个功能;
  • T:ClassTag,例如Spark源码中的RDD class RDD[T:ClassTag] 这个其实也是一种类型转换系统,只是在编译的时候类型信息不够,需要借助于JVM的runtime来通过运行时信息获得完整的类型信息,这在Spark中是非常重要的,因为Spark的程序的编程和运行是区分了Driver和Executor的,只有在运行的时候才知道完整的类型信息。

RDD的特点

分区:RDD逻辑上是分区的,每个分区的数据是抽象存在的,计算的时候会通过一个compute函数得到每个分区的数据。如果RDD是通过已有的文件系统构建,则compute函数是读取指定文件系统中的数据,如果RDD是通过其他RDD转换而来,则compute函数是执行转换逻辑将其他RDD的数据进行转换。

只读:RDD是只读的,要想改变RDD中的数据,只能在现有的RDD基础上创建新的RDD;
由一个RDD转换到另一个RDD,可以通过丰富的操作算子(map、filter、union、join、reduceByKey… …)实现,不再像MR那样只能写map和reduce了。

依赖:RDDs通过操作算子进行转换,转换得到的新RDD包含了从其他RDDs衍生所必需的信息,RDDs之间维护着这种血缘关系(lineage),也称之为依赖。依赖包括两种,一种是窄依赖,RDDs之间分区是一一对应的;另一种是宽依赖,下游RDD的每个分区与上游RDD(也称之为父RDD)的每个分区都有关,是多对多的关系。

持久化(缓存):可以控制存储级别(内存、磁盘等)来进行持久化。如果在应用程序中多次使用同一个RDD,可以将该RDD缓存起来,该RDD只有在第一次计算的时候会根据血缘关系得到分区的数据,在后续其他地方用到该RDD的时候,会直接从缓存处取而不用再根据血缘关系计算,这样就加速后期的重用。

扫描二维码关注公众号,回复: 4412510 查看本文章

Spark编程模型

RDD被表示为对象;
通过对象上的方法调用来对RDD进行转换;
最后输出结果 或是 向存储系统保存数据;
RDD转换算子被称为Transformation;
只有遇到Action算子,才会执行RDD的计算(懒执行)
在这里插入图片描述
在Executor中完成数据的处理,数据有以下几种:
1、Scala集合数据(测试)
2、文件系统、DB(SQL、NOSQL)的数据
3、RDD
4、网络
Driver 主要是对SparkContext进行配置、初始化以及关闭。初始化SparkContext是为了构建Spark应用程序的运行环境,在初始化SparkContext,要先导入一些Spark的类和隐式转换;在Executor部分运行完毕后,需要将SparkContext关闭。
SparkContext是编写Spark程序用到的第一个类,是Spark的主要入口点,它负责和整个集群的交互;

如把Spark集群当作服务端,那么Driver就是客户端,SparkContext则是客户端的核心; SparkContext是Spark的对外接口,负责向调用者提供Spark的各种功能。

SparkContext用于连接Spark集群、创建RDD、累加器、广播变量;
1、SparkConf。SparkConf为Spark配置类,配置已键值对形式存储;配置项包括:master、appName、Jars、ExecutorEnv等等;

2、SparkEnv。SparkEnv可以说是Context中非常重要的类,它维护着Spark的执行环境,包含有:serializer、RpcEnv、Block Manager、内存管理等;

3、DAGScheduler。高层调度器,将Job按照RDD的依赖关系划分成若干个TaskSet,也称为Stage;之后结合当前缓存情况及数据就近的原则,将Stage提交给TaskScheduler;

4、TaskScheduler。负责任务调度资源的分配

5、SchedulerBackend。负责集群资源的获取和调度。
// SparkConf
sc.getConf.toDebugString
sc.getConf.getOption(“spark.app.name”)
sc.getConf.getOption(“spark.master”)
sc.getConf.getAll

// SparkEnv
import org.apache.spark._
SparkEnv.get
SparkEnv.get.memoryManager
SparkEnv.get.shuffleManager

RDD编程API—包含两种算子

参考文档:http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html
转换得到的RDD是惰性求值的。也就是说,整个转换过程只是记录了转换的轨迹,并不会发生真正的计算,只有遇到行动操作时,才会发生真正的计算,开始从血缘关系(lineage)源头开始,进行物理的转换操作;

常见Transformation算子

map(func):对调用map的RDD数据集中的每个element都使用func,然后返回一个新的RDD,这个返回的数据集是分布式的数据集

filter(func):对调用filter的RDD数据集中的每个元素都使用func,然后返回一个包含使func为true的元素构成的RDD

flatMap(func):和map差不多,但是flatMap生成的是多个结果

mapPartitions(func):和map很像,但是map是每个element,而mapPartitions是每个partition

mapPartitionsIndex(func):逐个处理每一个partition,使用迭代器it访问每个partition的行,index保存partition的索引

sample(withReplacement, fraction, seed):抽样

union(otherDataset):返回一个新的dataset,包含源dataset和给定dataset的元素的集合

distinct([numTasks]):返回一个新的dataset,这个dataset含有的是源dataset中的distinct的element

groupByKey(numTasks):返回(K,Seq[V]),也就是hadoop中reduce函数接受的key-valuelist

reduceByKey(func,[numTasks]):就是用一个给定的reducefunc再作用在groupByKey产生的(K,Seq[V]),比如求和,求平均数

sortByKey([ascending],[numTasks]):按照key来进行排序,是升序还是降序,ascending是boolean类型

join(otherDataset,[numTasks]):当有两个KV的dataset(K,V)和(K,W),返回的是(K,(V,W))的dataset,numTasks为并发的任务数

cogroup(otherDataset,[numTasks]):当有两个KV的dataset(K,V)和(K,W),返回的是(K,Seq[V],Seq[W])的dataset,numTasks为并发的任务数

cartesian(otherDataset):笛卡尔积就是m*n

行动(Action)操作及常见算子

Action触发了Job的执行,application中如果有多个Action,那么对应多个job。(看源码)

reduce(func):传入的函数是两个参数输入返回一个值,传入函数必须满足交换律和结合律
collect():一般在filter或者足够小的结果的时候,再用collect封装返回一个数组

count():返回的是dataset中的element的个数
first():返回的是dataset中的第一个元素
take(n):返回前n个elements

takeSample(withReplacement,num,seed):抽样返回一个dataset中的num个元素
备注:与sample类似,但第二个参数不是百分比

saveAsTextFile(path):把dataset写到一个textfile中,或者hdfs,或者hdfs支持的文件系统中,spark把每条记录都转换为一行记录,然后写到file中

countByKey():返回的是key对应的个数的一个map,作用于一个RDD
foreach(func):对dataset中的每个元素都使用func

注意:reduceByKey用于对每个key对应的多个value进行merge操作,最重要的是它能够在本地先进行merge操作;
当采用groupByKey时,Spark将所有的键值对(key-value pair)都移动,集群节点之间的开销很大
reduceByKey的效率高,在能使用reduceByKey的地方就不要使用groupByKey。

常见的Pair RDD转换操作

keys
把Pair RDD中的key返回形成一个新的RDD
values
把Pair RDD中的value返回形成一个新的RDD
sortByKey
返回一个根据键排序的RDD
mapValues(func)
对键值对RDD中的每个value都应用一个函数,但是,key不会发生变化
join
join表示内连接。对于给定的两个输入数据集(K,V1)和(K,V2),在两个数据集中都存在的key会被输出,最终得到一个(K,(V1,V2))类型的数据集

RDD控制算子

Spark中控制算子也是懒执行的,需要Action算子触发才能执行,主要是为了对数据进行缓存。
控制算子有三种,cache,persist,checkpoint,以上算子都可以将RDD持久化,持久化的单位是partition。RDD控制算子都是懒执行的。必须有一个action类算子触发执行。checkpoint算子不仅能将RDD持久化到磁盘,还能切断RDD之间的依赖关系。

RDD的缓存(持久化)

缓存是将计算结果写入不同的介质,用户定义可定义存储级别(存储级别定义了缓存存储的介质,目前支持内存、堆外内存、磁盘);

通过缓存,Spark避免了RDD上的重复计算,能够极大地提升计算速度;

Spark速度非常快的原因之一,就是在内存中持久化(或缓存)一个数据集。当持久化一个RDD后,每一个节点都将把计算的分片结果保存在内存中,并在对此数据集(或者衍生出的数据集)进行的其他动作(action)中重用。这使得后续的动作变得更加迅速;

RDD相关的持久化或缓存,是Spark最重要的特征之一。可以说,缓存是Spark构建迭代式算法和快速交互式查询的关键因素;

使用persist()方法对一个RDD标记为持久化。之所以说“标记为持久化”,是因为出现persist()语句的地方,并不会马上计算生成RDD并把它持久化,而是要等到遇到第一个行动操作触发真正计算以后,才会把计算结果进行持久化;
通过persist()或cache()方法可以标记一个要被持久化的RDD,一旦首次被触发,该RDD将会被保留在计算节点的内存中并重用;

什么时候该缓存数据,需要对空间和速度进行权衡,垃圾回收开销的问题让情况变的更复杂。一般情况下,如果多个动作需要用到某个 RDD,而它的计算代价又很高,那么就应该把这个 RDD 缓存起来;
缓存有可能丢失,或者存储于内存的数据由于内存不足而被删除。RDD的缓存的容错机制保证了即使缓存丢失也能保证计算的正确执行。通过基于RDD的一系列的转换,丢失的数据会被重算。RDD的各个Partition是相对独立的,因此只需要计算丢失的部分即可,并不需要重算全部Partition。

persist()的参数可以指定持久化级别参数;

persist(MEMORY_ONLY):表示将RDD作为反序列化的对象存储于JVM中,如果内存不足,就要按照LRU(Least recently used,最近最少使用)原则替换缓存中的内容;

persist(MEMORY_AND_DISK)表示将RDD作为反序列化的对象存储在JVM中,如果内存不足,超出的分区将会被存放在硬盘上【备注:并不是在内存和磁盘上都放,而是优先使用内存,如果内存不够才会使用磁盘】;

使用cache()方法时,会调用persist(MEMORY_ONLY),即:
cache() == persist(MEMORY_ONLY)

可使用unpersist()方法手动地把持久化的RDD从缓存中移除;
在这里插入图片描述

Storage的级别

在这里插入图片描述

如何选择缓存级别

应该如何选取持久化的存储级别,实际上存储级别的选取就是Memory与CPU之间的双重权衡,可参考以下内容:
(1)如果RDD的数据可以很好的兼容默认存储级别(MEMORY_ONLY),那么优先使用它,这是CPU工作最为高效的一种方式,可以很好地提高运行速度;
(2)如果(1)不能满足,则尝试使用MEMORY_ONLY_SER,且选择一种快速的序列化工具,也可以达到一种不错的效果;
(3)一般情况下不要把数据持久化到磁盘,除非计算是非常“昂贵”的或者计算过程会过滤掉大量数据,因为重新计算一个分区数据的速度可能要高于从磁盘读取一个分区数据的速度;
(4)如果需要快速的失败恢复机制,则使用备份的存储级别,如MEMORY_ONLY_2、MEMORY_AND_DISK_2;虽然所有的存储级别都可以通过重新计算丢失的数据实现容错,但是缓存机制使得大部分情况下应用无需中断,即数据丢失情况下,直接使用缓存数据,而不需要重新计算数据的过程;
(5)如果处于大内存或多应用的场景下,OFF_HEAP可以带来以下的好处:
它允许Spark Executors可以共享Tachyon的内存数据;
它很大程序上减少JVM垃圾回收带来的性能开销;
Spark Executors故障不会导致数据丢失。
最后,Spark可以自己监测“缓存”空间的使用,并使用LRU算法移除旧的分区数据。也可以通过显式调用RDD unpersist()手动移除数据。
在这里插入图片描述

RDD分区

分区的目的:设置合理的并行度,提高数据处理的性能。
在分布式程序中,通信的代价是很大的,因此控制数据分布以获得最少的网络传输可以提升整体性能。对RDD进行合理的分区,可以减少网络传输的代价,进而提高系统性能;

RDD分区的一个分区原则是:
尽可能使得分区的个数,等于集群核心数目;
尽可能使同一 RDD 不同分区内的记录的数量一致;

创建操作中,开发者可以手动指定分区的个数,例如:
sc.parallelize(arr, 2) 表示创建得到的 RDD 分区个数为 2,在没有指定分区个数的情况下,Spark 会根据集群部署模式,来确定一个分区个数默认值。

rdd1.getNumPartitions
rdd1.partitions.size
对于 parallelize(makeRDD) 方法,默认情况下,分区的个数会受 Spark 配置参数 spark.default.parallelism 的影响,该参数也用于控制 Shuffle 过程中默认使用的任务数量。

无论是local模式、Standalone 模式、Yarn 模式或者是 Mesos 模式来运行 Spark,分区的默认个数等于 spark.default.parallelism 的指定值,若该值未设置,Spark 根据不同集群模式,来确定这个值。

  • local模式,默认分区个数等于本地机器的 CPU 核心总数(或者是用户通过 local[N] 参数指定分配给 Apache Spark 的核心数目)。这样把每个分区的计算任务交付给单个核心执行,能够保证最大的计算效率;
  • Standalone 或者 Yarn,默认分区个数等于集群中所有核心数目的总和,或者 2,取两者中的较大值;
  • 若使用 Apache Mesos 作为集群的资源管理系统,默认分区个数等于 8;
    对于 textFile 方法,默认情况下:
    每个HDFS的分区文件(默认块大小128M),每个都会创建一个RDD分区;
    对于本地文件,默认分区个数等于 min(defaultParallelism, 2);

可以使用下列方式对RDD的分区数进行修改:
rdd.textFile("", n)
rdd.parallelize(arr, n)
还可以使用 repartition(有shuffle)、coalesce 对RDD进行重分区

备注:调用data.repartition后,data的分区数并不会改变,而是返回一个新的RDD,其分区数等于repartition后的分区数。

宽依赖、窄依赖

RDD的依赖分为两种:窄依赖(Narrow Dependencies)与宽依赖(Wide Dependencies,源码中称为Shuffle Dependencies)
依赖有2个作用,其一用来解决数据容错;其二用来划分stage。
窄依赖:每个父RDD的一个Partition最多被子RDD的一个Partition所使用(1:1 或 n:1)。例如map、filter、union等操作会产生窄依赖;
宽依赖:一个父RDD的Partition会被多个子RDD的Partition所使用,例如groupByKey、reduceByKey、sortByKey等操作会产生宽依赖;

相比于宽依赖,窄依赖对优化很有利,主要基于以下两点:
1、宽依赖对应着shuffle操作,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输;而窄依赖的每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换。
2、当RDD分区丢失时(某个节点故障),spark会对数据进行重算。对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%;
对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的父RDD分区都要进行重新计算。

重算的效用不仅在于算的多少,还在于有多少是冗余的计算
窄依赖允许在一个集群节点上以流水线的方式(pipeline)计算所有父分区。例如,逐个元素地执行map、然后filter操作;而宽依赖则需要首先计算好所有父分区数据,然后在节点之间进行Shuffle。

窄依赖能够更有效地进行失效节点的恢复,即只需重新计算丢失RDD分区的父分区,而且不同节点之间可以并行计算;而对于一个宽依赖关系的Lineage图,单个节点失效可能导致这个RDD的所有祖先丢失部分分区,因而可能导致整体重新计算。

RDD容错

为什么做checkpoint

分布式计算中难免因为网络,存储等原因出现计算失败的情况,RDD中的lineage信息常用来在task失败后重计算使用,为了防止计算失败后从头开始计算造成的大量开销,RDD会checkpoint计算过程的信息,这样作业失败后从checkpoint点重新计算即可,提高效率。
Checkpoint是针对整个RDD计算链条中特别需要数据持久化的环节(后面会反复使用当前环节的RDD)开始基于HDFS等的数据持久化复用策略,通过对RDD启动Checkpoint机制来实现容错和高可用;

什么时候写checkpoint数据

当RDD的action算子触发计算结束后会执行checkpoint。
只有在Action触发Job的时候才会进行checkpoint。Spark在执行完Job之后会判断是否需要checkpoint。

什么时候读checkpoint数据

task计算失败的时候会从checkpoint读取数据
会被重复使用的(但是)不能太大的RDD需要persist或者cache 。
哪些 RDD 需要 checkpoint?运算时间很长或运算量太大才能得到的 RDD,computing chain 过长或依赖其他 RDD 很多的 RDD。

checkpoint与persist或者cache的区别在于,持久化只是将数据保存在BlockManager中但是其lineage是不变的,但是checkpoint执行完后,rdd已经没有依赖RDD,只有一个checkpointRDD,checkpoint之后,RDD的lineage就改变了(斩断依赖)。而且,持久化的数据丢失的可能性较大,因为可能磁盘或内存被清理,但是checkpoint的数据通常保存到hdfs上,放在了高容错文件系统。

rdd.persist(StorageLevel.DISK_ONLY) 与 checkpoint 也有区别。前者虽然可以将 RDD 的 partition 持久化到磁盘,但该 partition 由 blockManager 管理。一旦 driver program 执行结束,也就是 executor 所在进程 CoarseGrainedExecutorBackend stop,blockManager 也会 stop,被 cache 到磁盘上的 RDD 也会被清空(整个 blockManager 使用的 local 文件夹被删除)。
而 checkpoint 将 RDD 持久化到 HDFS 或本地文件夹,如果不被手动 remove 掉,是一直存在的。

分区器(partitioner)

在Spark中分区器直接决定了:
RDD中分区的个数;
RDD中每条数据经过Shuffle过程属于哪个分区;
reduce的个数;

只有Key-Value类型的RDD才有分区器,非Key-Value类型的RDD分区器的值是None。
分区器的作用及分类:
在PairRDD(key,value)中,很多操作都是基于key的,系统会按照key对数据进行重组,如groupbykey;
数据重组需要规则,最常见的就是基于Hash分区,Spark还提供了一种复杂的基于抽样的Range分区方法;
HashPartitioner:是最简单也是默认提供的分区器。对于给定的key,计算其hashCode,并除以分区的个数取余,如果余数小于0,则用余数+分区的个数,最后返回的值就是这个key所属的分区ID。该分区方法可以保证key相同的数据出现在同一个分区中。
用户可通过partitionBy主动使用分区器,通过partitions参数指定想要分区的数量。

RangePartitioner:简单的说就是将一定范围内的数映射到某一个分区内。算法比较复杂。sortByKey会使用RangePartitioner。

自定义分区器:Spark允许用户通过自定义的Partitioner对象,灵活的来控制RDD的分区方式。

共享变量

当Spark在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本;

有时候需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量;

为了满足这种需求,Spark提供了两种类型的变量:

  • 广播变量(broadcast variables)
  • 累加器(accumulators)

广播变量将变量在节点的Executor之间进行共享(driver广播出去);

累加器则支持在所有不同节点之间进行累加计算(比如计数或者求和);

广播变量

广播变量(broadcast variables)允许在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本;

Spark的Action操作会跨越多个阶段(stage),对于每个阶段内的所有任务所需要的公共数据,Spark都会自动进行广播;

可以通过调用SparkContext.broadcast(v)来从一个普通变量v中创建一个广播变量。这个广播变量就是对普通变量v的一个包装器,通过调用value方法就可以获得这个广播变量的值,具体代码如下:
val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar.value

这个广播变量被创建以后,在集群中的任何函数中,都可以使用广播变量中的值,这样就不会把v重复分发到这些节点上
此外,一旦广播变量创建后,普通变量v的值就不能再发生修改,从而确保所有节点都获得这个广播变量的相同的值

累加器

累加器是仅仅被相关操作累加的变量,通常可以被用来实现计数器(counter)和求和(sum)。Spark原生地支持数值型(numeric)的累加器,程序开发人员可以编写对新类型的支持;

一个数值型的累加器,可以通过调用SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()来创建;

运行在集群中的任务,就可以使用add方法来把数值累加到累加器上。但是,这些任务只能做累加操作,不能读取累加器的值,只有任务控制节点(Driver Program)可以使用value方法来读取累加器的值。
val accum = sc.longAccumulator(“My Accumulator”)
sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
accum.value

Spark Core原理

Shuffle

Shuffle的本意是洗牌,目的是为了把牌弄乱。

Spark、Hadoop中的shuffle可不是为了把数据弄乱,而是为了将随机排列的数据转换成具有一定规则的数据。

Shuffle是MapReduce计算框架中的一个特殊的阶段,介于Map 和 Reduce 之间。当Map的输出结果要被Reduce使用时,输出结果需要按key排列,并且分发到Reducer上去,这个过程就是shuffle。

shuffle涉及到了本地磁盘(非hdfs)的读写和网络的传输,大多数Spark作业的性能主要就是消耗在了shuffle环节。因此shuffle性能的高低直接影响到了整个程序的运行效率

猜你喜欢

转载自blog.csdn.net/qq_39429714/article/details/84571254