Accumulator累加器(重要)
累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱 动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本, 更新这些副本的值也不会影响驱动器中的对应变量。 如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
- Spark提供了一个默认的累加器,只能用于求和没啥用
- 如何使用:
2.1.通过SparkContext对象.accumulator(0) var sum = sc.accumulator(0)
通过accumulator声明一个累加器,0为初始化的值
2.2.通过转换或者行动操作,通过sum +=n 来使用
2.3.如何取值? 在Driver程序中,通过 sum .value来获取值
3.累加器是懒执行,需要行动触发
例子: 数据计算相加
val numbers = sc .parallelize(List(1,2,3,4,5,6),2)
println(numbers.partitions.length)
//为什么sum值通过计算过后还是0
//因为foreach是没有返回值,整个计算过程都是在executor端完后
//foreach是在driver端运行所以打印的就是 0,foreach没有办法获取数据
//var sum = 0
// numbers.foreach(num =>{
// sum += num
// })
// println(sum)
//建议点击看原码 可以发现当前方法已经过时了,@deprecated("use AccumulatorV2", "2.0.0")
//所以以后使用时候需要使用自定义累加器
var sum = sc.accumulator(0)
numbers.foreach(num =>{
sum += num
})
println(sum.value)
}
自定义累加器
自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。官方同时给出了一个实现的示例:CollectionAccumulator类,这个类允许以集合的形式收集spark应用执行过程中的一些信息。例如,我们可以用这个类收集Spark处理数据时的一些细节,当然,由于累加器的值最终要汇聚到driver端,为了避免 driver端的outofmemory问题,需要对收集的信息的规模要加以控制,不宜过大。
案例1:已经定义好的数值类型可以直接使用
//这些类都是AccumulatorV2的子类可以直接使用
val conf = new SparkConf().setAppName("SparkWordCount").setMaster("local[*]")
//2.创建SparkContext 提交SparkApp的入口
val sc = new SparkContext(conf)
val num1 = sc.parallelize(List(1, 2, 3, 4, 5, 6), 2)
val num2 = sc.parallelize(List(1.1, 2.2, 3.3, 4.4, 5.5, 6.6), 2)
//创建并注册一个long accumulator, 从“0”开始,用“add”累加
def longAccumulator(name: String): LongAccumulator = {
val acc = new LongAccumulator
sc.register(acc, name)
acc
}
val acc1 = longAccumulator("kk")
num1.foreach(x => acc1.add(x))
println(acc1.value)
//创建并注册一个double accumulator, 从“0”开始,用“add”累加
def doubleAccumulator(name: String): DoubleAccumulator = {
val acc = new DoubleAccumulator
sc.register(acc, name)
acc
}
val acc2 = doubleAccumulator("kk")
num1.foreach(x => acc2.add(x))
println(acc2.value)
案例2:
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
//在继承的时候需要执行泛型 即 可以计算IN类型的输入值,产生Out类型的输出值
//继承后必须实现提供的方法
class MyAccumulator extends AccumulatorV2[Int,Int]{
//创建一个输出值的变量
private var sum:Int = _
//必须重写如下方法:
//检测方法是否为空
override def isZero: Boolean = sum == 0
//拷贝一个新的累加器
override def copy(): AccumulatorV2[Int, Int] = {
//需要创建当前自定累加器对象
val myaccumulator = new MyAccumulator()
//需要将当前数据拷贝到新的累加器数据里面
//也就是说将原有累加器中的数据拷贝到新的累加器数据中
//ps:个人理解应该是为了数据的更新迭代
myaccumulator.sum = this.sum
myaccumulator
}
//重置一个累加器 将累加器中的数据清零
override def reset(): Unit = sum = 0
//每一个分区中用于添加数据的方法(分区中的数据计算)
override def add(v: Int): Unit = {
//v 即 分区中的数据
//当累加器中有数据的时候需要计算累加器中的数据
sum += v
}
//合并每一个分区的输出(将分区中的数进行汇总)
override def merge(other: AccumulatorV2[Int, Int]): Unit = {
//将每个分区中的数据进行汇总
sum += other.value
}
//输出值(最终累加的值)
override def value: Int = sum
}
object MyAccumulator{
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName("MyAccumulator").setMaster("local[*]")
//2.创建SparkContext 提交SparkApp的入口
val sc = new SparkContext(conf)
val numbers = sc .parallelize(List(1,2,3,4,5,6),2)
val accumulator = new MyAccumulator()
//需要注册
sc.register(accumulator,"acc")
//切记不要使用Transformation算子 会出现无法更新数据的情况
//应该使用Action算子
//若使用了Map会得不到结果
numbers.foreach(x => accumulator.add(x))
println(accumulator.value)
}
}
案例3:使用累加器做单词统计
import org.apache.spark.{SparkConf, SparkContext}
import org.apache.spark.util.AccumulatorV2
import scala.collection.mutable
class MyAccumulator2 extends AccumulatorV2[String, mutable.HashMap[String, Int]] {
private val _hashAcc = new mutable.HashMap[String, Int]()
// 检测是否为空
override def isZero: Boolean = {
_hashAcc.isEmpty
}
// 拷贝一个新的累加器
override def copy(): AccumulatorV2[String, mutable.HashMap[String, Int]] = {
val newAcc = new MyAccumulator2()
_hashAcc.synchronized {
newAcc._hashAcc ++= (_hashAcc)
}
newAcc
}
// 重置一个累加器
override def reset(): Unit = {
_hashAcc.clear()
}
// 每一个分区中用于添加数据的方法 小SUM
override def add(v: String): Unit = {
_hashAcc.get(v) match {
case None => _hashAcc += ((v, 1))
case Some(a) => _hashAcc += ((v, a + 1))
}
}
// 合并每一个分区的输出 总sum
override def merge(other: AccumulatorV2[String, mutable.HashMap[String, Int]]): Unit = {
other match {
case o: AccumulatorV2[String, mutable.HashMap[String, Int]] => {
for ((k, v) <- o.value) {
_hashAcc.get(k) match {
case None => _hashAcc += ((k, v))
case Some(a) => _hashAcc += ((k, a + v))
}
}
}
}
}
// 输出值
override def value: mutable.HashMap[String, Int] = {
_hashAcc
}
}
object MyAccumulator2 {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("MyAccumulator2").setMaster("local[*]")
val sc = new SparkContext(sparkConf)
val hashAcc = new MyAccumulator2()
sc.register(hashAcc, "abc")
val rdd = sc.makeRDD(Array("a", "b", "c", "a", "b", "c", "d"),2)
rdd.foreach(hashAcc.add(_))
for ((k, v) <- hashAcc.value) {
println("【" + k + ":" + v + "】")
}
sc.stop()
}
}
总结:
1.累加器的创建:
1.1.创建一个累加器的实例
1.2.通过sc.register()注册一个累加器
1.3.通过累加器实名.add来添加数据
1.4.通过累加器实例名.value来获取累加器的值
2.最好不要在转换操作中访问累加器(因为血统的关系和转换操作可能执行多次),最好在行动操作中访问
作用:
1.能够精确的统计数据的各种数据例如:
可以统计出符合userID的记录数,在同一个时间段内产生了多少次购买,可以使用ETL进行数据清洗,并使用Accumulator来进行数据的统计
2.作为调试工具,能够观察每个task的信息,通过累加器可以在sparkIUI观察到每个task所处理的记录数
Broadcast广播变量(重要)
广播变量用来高效分发较大的对象。向所有工作节点发送一个 较大的只读值,以供一个或多个 Spark 操作使用。比如,如果你的应用需要向所有节点发 送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起 来都很顺手。
问题说明:
/**
以下代码就会出现一个问题:
list是在driver端创建的,但是因为需要在executor端使用,所以driver会把list以task的形式发送到excutor端,也就相当于在executor需要复制一份,如果有很多个task,就会有很多给excutor端携带很多个list,如果这个list非常大的时候,就可能会造成内存溢出
*/
val conf = new SparkConf().setAppName("BroadcastTest").setMaster("local")
val sc = new SparkContext(conf)
//list是在driver端创建也相当于是本地变量
val list = List("hello java")
//算子部分是在Excecutor端执行
val lines = sc.textFile("dir/file")
val filterStr = lines.filter(list.contains(_))
filterStr.foreach(println)
广播变量的好处,不是每个task一份变量副本,而是变成每个节点的executor才一份副本。这样的话, 就可以让变量产生的副本大大减少。
task在运行的时候,想要使用广播变量中的数据,此时首先会在自己本地的Executor对应的BlockManager中,
尝试获取变量副本;如果本地没有,那么就从Driver远程拉取变量副本,并保存在本地的BlockManager中;
此后这个executor上的task,都会直接使用本地的BlockManager中的副本。
executor的BlockManager除了从driver上拉取,也可能从其他节点的BlockManager上拉取变量副本。
HttpBroadcast TorrentBroadcast(默认)
BlockManager
负责管理某个Executor对应的内存和磁盘上的数据,尝试在本地BlockManager中找map
val conf = new SparkConf().setAppName("BroadcastTest").setMaster("local")
val sc = new SparkContext(conf)
//list是在driver端创建也相当于是本地变量
val list = List("hello java")
//封装广播变量
val broadcast = sc.broadcast(list)
//算子部分是在Excecutor端执行
val lines = sc.textFile("dir/file")
//使用广播变量进行数据处理 value可以获取广播变量的值
val filterStr = lines.filter(broadcast.value.contains(_))
filterStr.foreach(println)
总结:
广播变量的过程如下:
(1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。 任何可序列化的类型都可以这么实现。
(2) 通过 value 属性访问该对象的值(在 Java 中为 value() 方法)。
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
能不能将一个RDD使用广播变量广播出去?
不能,因为RDD是不存储数据的。可以将RDD的结果广播出去。
广播变量只能在Driver端定义,不能在Executor端定义。
广播变量的好处:
举例来说
50个executor,1000个task。一个map,10M。
默认情况下,1000个task,1000份副本。10G的数据,网络传输,在集群中,耗费10G的内存资源。
如果使用了广播变量。50个execurtor,50个副本。500M的数据,网络传输,
而且不一定都是从Driver传输到每个节点,还可能是就近从最近的节点的executor的bockmanager
上拉取变量副本,网络传输速度大大增加;500M的内存消耗。
10000M,500M,20倍。20倍~以上的网络传输性能消耗的降低;20倍的内存消耗的减少。
对性能的提升和影响,还是很客观的。
虽然说,不一定会对性能产生决定性的作用。比如运行30分钟的spark作业,可能做了广播变量以后,速度快了2分钟,或者5分钟。但是一点一滴的调优,积少成多。最后还是会有效果的。