一、RDD
1.1 什么是RDD?
RDD(Resilient Distributed Dataset),意为弹性分布式数据集,为什么说它是弹性呢?因为当计算过程中内存不够时,它会和磁盘进行数据交换,同样的,分布式的意思是它分布在不同的节点中进行计算,我们可以将RDD看成是一个分布式对象的集合,它本质上是一个只读的分区记录的集合,每个RDD可以分成多个分区,每个分区都是一个独立的数据集片段,一个RDD的不同分区可以保存到集群的不同节点上,从而可以实现并行计算。
从源码的角度看,RDD是一个类,它包含了数据应该在哪里,应该怎么计算,算完了应该放在哪里。它能被序列化,也能被反序列化,给人的感觉就是,它是一个数据集,但RDD存储的并不是数据集,而是数据存储的位置,读取的方法,数据的类型,分区的方法等。
1.2 RDD里包含了什么?
1.2.1 一组分区(partition)
RDD是一组分区的集合,RDD的每一个分区都会被一个计算任务处理,这也是保证RDD能并行计算的根本原因。用户可以在创建RDD时显式的指定RDD的分区数,如果不指定,那么会默认将partition的值设成程序分配到的CPU core数。
1.2.2 compute(计算函数)
RDD有一个计算每个分区的函数,在spark中,RDD的计算是以分区为单位的,每个RDD都会实现一个compute()
函数来进行计算,compute()是以迭代计算为基础的计算函数,可以迭代地拉取父RDD对应分区的数据,不需要每次都从头开始计算。
1.2.3 依赖关系(dependences)
RDD的每次转换操作都会生成一个新的RDD,所以这些RDD之间就构成了流水线(pipeline),RDD在这条流水线中经过一步一步的“加工”,最终成为结果,如果在这条流水线中的任何一步出现了分区数据的丢失,那么可以通过它的依赖关系得到它,而不用对RDD的所有分区重新计算。
1.2.4 一个分区函数(Partitioner)
Partition是RDD的分片函数。当前Spark中实现了两种类型的分片函数,一个是基于哈希的HashPartitioner,另外一个是基于范围的RangePartitioner。只有对于于key-value的RDD,才会有Partitioner,非key-value的RDD的Parititioner的值是None。Partitioner函数不但决定了RDD本身的分片数量,也决定了parent RDD Shuffle输出时的分片数量。
1.2.5 一个存放partition位置的list
按照分布式计算“计算向数据移动”原则,spark在进行任务调度时,会尽可能地将任务分配到距离数据最近的节点中,而一个block在HDFS上通常有多个副本,所以需要一个list来存放距离最近的RDD partition的块的位置。
1.3 RDD的血缘关系(Lineage)
Lineage是RDD最重要的特性,它描述了一个RDD是如何从它的父RDD计算得来的,如果一个RDD数据丢失了,则可以从它的父RDD重新拉取数据,重新计算来得到它,而不用从第一个RDD计算,这也是spark计算速度比MapReduce快的一大原因。
上图描述了一组RDD之间的血缘关系,系统从逻辑输入中得到了A、C两个RDD,并经过一系列计算,最终得到了F这个RDD,假设E这个RDD发生了数据丢失,那么程序会从E的父RDD,也就是B和D中拉取数据重新计算,而不用从A和C开始计算。
值得一提的是,spark的RDD操作是一个惰性计算的过程,如果没有遇到action算子,这个过程就只是搭了一条“流水线”,并不会发生计算。
1.4 RDD的依赖类型
根据不同的转换操作,spark又将RDD的血缘关系划分为宽依赖和窄依赖,划分依据就是子RDD的计算是否涉及到所有的父RDD,以及是否会产生shuffle过程。
1.4.1 宽依赖(ShuffleDependency)
所谓宽依赖,就是子RDD的计算过程涉及到所有D父RDD,即父RDD的每个分区都被多个子RDD所依赖。会产生宽依赖的操作包括sortBy、groupBy、reduceBy等。
1.4.2 窄依赖(narrow Dependency)
所谓窄依赖,就是子RDD的计算过程依赖于常数个父RDD,也可以认为,一个父RDD的分区数据只发送到一个子RDD中去计算了。窄依赖又分为onetooneDependency
和narrowDependency
。
- onetooneDependency:父RDD和子RDD是一对一的关系,一个父RDD的分区数据只发送给一个子RDD,一个子RDD的分区数据也只源于一个父RDD,常见的onetooneDependency有map、flatmap、filter。
- rangeDependency:父RDD和子RDD是多对一的关系,一个父RDD的分区数据只发送给一个子RDD,一个子RDD的分区数据可能源于多个父RDD。常见的rangeDependency有Union、join等。
1.5 RDD缓存
刚才提到,RDD是惰性计算的,有时候我们希望能多次复用一个RDD,但是复用的过程中仍然需要action算子的参与,spark每次都会重复计算这个RDD和它的依赖,这样会带来很大的性能消耗,所以为了避免多次重复计算一个RDD,可以使用RDD缓存,对RDD进行持久化。
spark使用cache和persist来对RDD进行缓存,它们既可以缓存到内存中,也可以缓存到磁盘中,缓存策略由用户自定义。被缓存的 RDD 被使用时,存取速度会被大大加速。一般情况下,Executor 内存的 60% 会分配给 cache,剩下的 40% 用来执行任务。
缓存级别 | 使用空间 | cpu时间 | 是否在内存 | 是否在磁盘 |
---|---|---|---|---|
MEMORY_ONLY | 高 | 低 | 是 | 否 |
MEMORY_ONLY_SER | 低 | 高 | 是 | 否 |
MEMORY_AND_DISK | 高 | 中 | 部分 | 部分 |
MEMORY_AND_DISK_SER | 低 | 高 | 部分 | 部分 |
DISK_ONLY | 低 | 高 | 否 | 是 |
其中对于MEMORY_AND_DISK
和MEMORY_AND_DISK_SER
缓存级别,系统会首先将RDD的分区数据保存到内存中,无法保存的部分才会保存到磁盘中。
对于 MEMORY_AND_DISK
和MEMORY_AND_DISK_SER
级别,系统会首先把数据保存在内存中,如果内存不够则把溢出部分写入磁盘中。
另外,为了提高缓存的容错性,可以在持久化级别名称的后面加上“_2”来把持久化数据存为两份,如 MEMORY_ONLY_2
。Spark 的不同 StorageLevel 的目的是为了满足内存使用和CPU效率权衡上的不同需求。可以通过以下步骤来选择合适的持久化级别。
1) 如果 RDD 可以很好地与默认的存储级别(MEMORY_ONLY)契合,就不需要做任何修改了。这已经是 CPU 使用效率最高的选项,它使得 RDD 的操作尽可能快。
2) 如果 RDD 不能与默认的存储级别很好契合,则尝试使用 MEMORY_ONLY_SER,并且选择一个快速序列化的库使得对象在有比较高的空间使用率的情况下,依然可以较快被访问。
3) 尽可能不要将数据存储到硬盘上,除非计算数据集函数的计算量特别大,或者它们过滤了大量的数据。否则,重新计算一个分区的速度与从硬盘中读取的速度基本差不多。
4) 如果想有快速故障恢复能力,则使用复制存储级别。所有的存储级别都有通过重新计算丢失数据恢复错误的容错机制,但是复制存储级别可以让任务在 RDD 上持续运行,而不需要等待丢失的分区被重新计算。
5) 在不使用 cached RDD 的时候,及时使用 unpersist 方法来释放它。
2、广播变量(broadcast)
在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。
如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么支持每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。
根据官方定义,广播变量是一个共享变量,它允许程序员在每台机器上保留一个只读变量,而不是随副本一起发送它的副本。例如,它们可用于以有效的方式为每个节点提供大输入数据集的副本。Spark还尝试使用有效的广播算法来分发广播变量,以降低通信成本。
2.1 定义一个广播变量
val x = 1
val broadcast = sc.broadcast(x)
2.2 还原一个广播变量
val a = broadcast.value
2.3 注意事项
1) 能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
2) 广播变量只能在Driver端定义,不能在Executor端定义。
3) 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
4) 如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
5) 如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。
参考资料:
https://blog.csdn.net/dsdaasaaa/article/details/94181269
https://blog.csdn.net/BeiisBei/article/details/103882893