一、概述
在spark程序中,当一个传递给Spark操作(例如map和reduce)的函数在远程节点上面运行时,Spark操作实际上操作的是这个函数所用变量的一个独立副本。这些变量会被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的,但是,Spark还是为两种常见的使用模式提供了两种有限的共享变量:广播变(broadcast variable)和累加器(accumulator)
二、广播变量broadcast variable
2.1 为什么要将变量定义成广播变量?
如果我们要在分布式计算里面分发大对象,例如:字典,集合,黑白名单等,这个都会由Driver端进行分发,一般来讲,如果这个变量不是广播变量,那么每个task就会分发一份,这在task数目十分多的情况下Driver的带宽会成为系统的瓶颈,而且会大量消耗task服务器上的资源,如果将这个变量声明为广播变量,那么知识每个executor拥有一份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。
2.2 广播变量图解
错误的,不使用广播变量
正确的,使用广播变量的情况
2.3 如何定义一个广播变量?
val a = 3
val broadcast = sc.broadcast(a)
2.4 如何还原一个广播变量?
val c = broadcast.value
2.5 定义广播变量需要的注意点?
变量一旦被定义为一个广播变量,那么这个变量只能读,不能修改
2.6 注意事项
1、能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
2、 广播变量只能在Driver端定义,不能在Executor端定义。
3、 在Driver端可以修改广播变量的值,在Executor端无法修改广播变量的值。
4、如果executor端用到了Driver的变量,如果不使用广播变量在Executor有多少task就有多少Driver端的变量副本。
5、如果Executor端用到了Driver的变量,如果使用广播变量在每个Executor中只有一份Driver端的变量副本。
从代码层面看使用广播变量和不使用广播变量的区别
不使用广播变量
package com.soul.bigdata.spark.core4
import org.apache.spark.{SparkConf, SparkContext}
object BroadCastApp {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("AccumulatorApp").setMaster("local[2]")
val sc = new SparkContext(conf)
commonJoin(sc)
Thread.sleep(30000 * 10)
sc.stop()
}
def commonJoin(sc: SparkContext): Unit = {
val info1 = sc.parallelize(Array(("601", "张三"), ("602", "李四")))
val info2 = sc.parallelize(Array(("601", "哈弗", "25"), ("603", "浙大", "22"), ("603", "深大", "26")))
.map(x => (x._1, (x._2, x._3)))
//TODO 需得到 601,张三,哈弗
info1.join(info2).map(x => {
//(601,(张三,(哈弗,25)))
x._1 + "," + x._2._1 + "," + x._2._2._1
}).foreach(println)
}
}
使用广播变量后
package com.soul.bigdata.spark.core4
import org.apache.spark.{SparkConf, SparkContext}
object BroadCastApp {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("AccumulatorApp").setMaster("local[2]")
val sc = new SparkContext(conf)
broadcastJoin(sc)
Thread.sleep(30000 * 10)
sc.stop()
}
def broadcastJoin(sc: SparkContext): Unit = {
//小数据 -> 广播
val info1 = sc.parallelize(Array(("601", "张三"), ("602", "李四"))).collectAsMap() //转成Map 可以通过get得到key
//Driver数据才需广播
val broadcastinfo1 = sc.broadcast(info1)
//大数据
val info2 = sc.parallelize(Array(("601", "哈弗", "25"), ("603", "浙大", "22"), ("603", "深大", "26")))
.map(x => (x._1, (x._2, x._3)))
//broadcst以后就不会用Join实现。而是大表数据读取出来一条就和广播出去的小表记录做匹配
info2.mapPartitions(x => {
val broadcastMap = broadcastinfo1.value
for ((key, value) <- x if broadcastMap.contains(key))
//TODO 需得到 601,张三,哈弗
yield (key, broadcastMap.get(key).getOrElse(), value._1)
}).foreach(println)
}
}
一个存在shuffle,一个不存在shuffle。性能对比一目了然,但是广播的前提是你的数据不能太大,否则也会发生OOM。
注意事项:
只能广播RDD的结果数据,不能直接广播RDD
广播变量只能在Driver端定义,不能在Executor端定义
三、累加器
3.1 为什么要将一个变量定义为一个累加器?
在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在被改变时不会在driver端进行全局汇总,即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。
3.2 图解累加器
错误的图解
正确的图解
3.3 如何定义一个累加器?
val a = sc.accumulator(0)
3.4 如何还原一个累加器?
val b = a.value
3.5 注意事项
1、 累加器在Driver端定义赋初始值,累加器只能在Driver端读取最后的值,在Excutor端更新。
2、累加器不是一个调优的操作,因为如果不这样做,结果是错的
看一个列子
package com.soul.bigdata.spark.core4
import org.apache.spark.{SparkConf, SparkContext}
object AccumulatorApp {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("AccumulatorApp").setMaster("local[2]")
val sc = new SparkContext(conf)
val line = sc.textFile("file:///D:\\RZ-G6\\2019G6\\data\\wordcount.txt")
var i = 0
val result = line.map(x => {
i = i + 1
x
})
result.collect().foreach(println)
//0 driver端数据与executor端数据不能共享导致 所以Spark就引出累加器
println("不用累加器统计 word lines is " + i)
sc.stop()
}
}
运行结果为0
依然是因为driver端数据与executor端数据不能共享导致,所以Spark就引出累加器。
使用累加器之后
package com.soul.bigdata.spark.core4
import org.apache.spark.{SparkConf, SparkContext}
object AccumulatorApp {
def main(args: Array[String]): Unit = {
val conf = new SparkConf()
.setAppName("AccumulatorApp").setMaster("local[2]")
val sc = new SparkContext(conf)
var accu = sc.longAccumulator("MyAccumulator")
println("累加器原始值: " + accu.value)
val line = sc.textFile("file:///D:\\RZ-G6\\2019G6\\data\\wordcount.txt")
val result2 = line.map(x => {
accu.add(1)//有一行数据就增加1
x
})
//.foreach(println) //必须触发一个action算子 将结果返回到Driver 累加器的值只有Driver可以读取 executor端只能累加计数器得到值,但不能获取
result2.collect()
println("使用累加器统计 word lines is " +accu.value)
sc.stop()
}
}
累加器的作用:提供了将工作节点中的值聚合到驱动器程序中的简单语法
注意事项:累加器在Driver端定义赋初始值,累加器只能在Driver端读取,在Excutor端更新