在默认情况下,当Spark在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本。但是,有时候,需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量。为了满足这种需求,Spark提供了两种类型的变量:广播变量(broadcast variables)和累加器(accumulators)。广播变量用来把变量在所有节点的内存之间进行共享。累加器则支持在所有不同节点之间进行累加计算(比如计数或者求和)。
广播变量
广播变量(broadcast variables)允许程序开发人员在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本。通过这种方式,就可以非常高效地给每个节点(机器)提供一个大的输入数据集的副本。Spark的“动作”操作会跨越多个阶段(stage),对于每个阶段内的所有任务所需要的公共数据,Spark都会自动进行广播。通过广播方式进行传播的变量,会经过序列化,然后在被任务使用时再进行反序列化。这就意味着,显式地创建广播变量只有在下面的情形中是有用的:当跨越多个阶段的那些任务需要相同的数据,或者当以反序列化方式对数据进行缓存是非常重要的。
可以通过调用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)
这个广播变量被创建以后,那么在集群中的任何函数中,都应该使用广播变量broadcastVar的值,而不是使用v的值,这样就不会把v重复分发到这些节点上。此外,一旦广播变量创建后,普通变量v的值就不能再发生修改,从而确保所有节点都获得这个广播变量的相同的值。
累加器
累加器是仅仅被相关操作累加的变量,通常可以被用来实现计数器(counter)和求和(sum)。Spark原生地支持数值型(numeric)的累加器,程序开发人员可以编写对新类型的支持。如果创建累加器时指定了名字,则可以在Spark UI界面看到,这有利于理解每个执行阶段的进程。
一个数值型的累加器,可以通过调用SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()来创建。运行在集群中的任务,就可以使用add方法来把数值累加到累加器上,但是,这些任务只能做累加操作,不能读取累加器的值,只有任务控制节点(Driver Program)可以使用value方法来读取累加器的值。
下面是一个代码实例,演示了使用累加器来对一个数组中的元素进行求和:
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))
scala> accum.value
res1: Long = 10
广播变量运用场景
spark官方建议超过2G的数据,就不适合使用广播变量,应将变量做成RDD。如果一个RDD中引用到外部变量,而这个变量比较大,并且任务并行度较多,应使用广播变量
为什么要用广播变量?
Driver进程
其实就是我们写的Spark作业,打成jar运行起来的主进程。
比如一个1M的map(随机抽取的map) ,创建1000个副本,网络传输!分到1000个机器上,则占用了1G内存。 又比如如果你是从哪个表里面读取了一些维度数据,比方说,所有商品的品类的信息,在某个算子函数中使用到100M。1000个task 。100G的数据,要进行网络传输,集群瞬间性能下降。
如果说,task使用大变量(1M-100M),明知道会导致大量消耗。该怎么做呢?
使用广播!!
广播变量里面会在Driver有一份初始副本。一个executor 会对应一份blockManager!
task在运行的时候,想要使用 广播变量中的数据,此时会首先在本地的Executor对应的BlockManager上 获取,如果没有。
则:blockManager会Driver上拉取map(也有可能从距离比较近的其他节点的Executor的BlockManager上获取!这样效率更高)
使用广播变量的好处:
不是每个task一份副本,而是变成每个节点Executor上一个副本。
举例来说:
50个Executor 1000个task。
一个map10M
默认情况下,1000个task 1000个副本
1000 * 10M = 10 000M = 10 G
10G的数据,网络传输,在集群中,耗费10G的内存资源。
如果使用 广播变量,
50个Executor ,50个副本,10M*50 = 500M的数据。
网络传输,而且不一定是从Drver传输到各个节点,还可能是从就近的节点
的Executor的BlockManager上获取变量副本,网络传输速度大大增加。
假设Spark集群每个Work上面有连个Executor,每个Executor上面有4个Task
为什么会出现广播变量?
因为图中,a=3普通情况下会复制到每个task中,只是一个变量还好,假设有1千个task,就是1千个a=3的复制,但是如果是a=一个list集合,大小为1G,那么就是1000G,这些数据存储都是问题,所以出现了广播变量。
假设a=list是1G,普通情况下,标号3所属的work会有4份a,一共4G,但是标号4所属的worker里面的worker共享一个a,一共1G,明显减少了网络的传输,以及存储,少了3个G的数据。而且数据是存放到Executor中的。