Spark Core 累加器、广播变量

Spark 专栏收录该内容
10 篇文章 0 订阅

Accumulator累加器(重要)

累加器用来对信息进行聚合,通常在向 Spark 传递函数时,比如使用 map() 函数或者用 filter() 传条件时,可以使用驱 动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本, 更新这些副本的值也不会影响驱动器中的对应变量。 如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。

  1. Spark提供了一个默认的累加器,只能用于求和没啥用

  2. 如何使用:

    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

[外链图片转存失败(img-6AfAWh0B-1565850929672)(./image/图片5.png)]

 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分钟。但是一点一滴的调优,积少成多。最后还是会有效果的。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值