Spark广播变量和累加器

Spark广播变量和累加器

广播变量broadcast

广播变量顾名思义,由Driver端发送数据,所有Executor端接收并保存这份数据,用于每个Executor上的数据计算工作。
在这里插入图片描述

广播变量的几点特性:

  1. 广播变量是保存在Executor内存中的,每个Executor一份。如果一个Executor上执行多个Task,那么多个Task将共享一份广播变量
  2. 广播变量是只读变量,即Driver端将变量发送到Executor端后,Executor端只能读取变量数据,而不能修改变量数据
  3. 当多个Executor上需要同一份数据时,可以考虑使用广播变量形式发送数据。尤其当该份数据在多个Stage中使用时,通过广播变量一次性发送的方案,执行性能将明显优于当数据需要时再发送的多次发送方案

广播变量时使用SparkContext对象调用broadcast函数,传入要广播的变量。在Executor端使用广播变量时,使用Broadcast对象,调用value函数就可以获得广播的数值了

def main(args: Array[String]): Unit = {
  val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
  val factor = 3
  val b: Broadcast[Int] = sc.broadcast(factor)
  val source = sc.makeRDD(Seq(1, 2, 3, 4, 5))
  source.foreach(number => println(number * b.value))
}
/*
12
15
6
3
9
*/

成功打印出了结果,但是这段代码很难体现出sc.broadcast(factor)的效果,要怎么证明数值确实是被广播出去了呢?

我们可以在广播之后,任务执行之前插入一行代码,修改factor的值,再来看看效果

def main(args: Array[String]): Unit = {
  val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
  var factor = 3
  val b: Broadcast[Int] = sc.broadcast(factor)
  factor = 5
  val source = sc.makeRDD(Seq(1, 2, 3, 4, 5))
  source.foreach(number => println(number * b.value))
}
/*
15
6
9
3
12
*/

这次就比较清楚了,factor先被广播出去后,才被修改为5,所以被广播出去的数值是3。只是在Driver端将factor修改为了5,对于真正执行运行的Executor端没有影响

这个案例证明了两点:

  1. 运算的逻辑由Driver端发送到Executor端,执行运算是在Executor端
  2. 广播变量是单独由Driver端发往Executor端,而不是在计算逻辑发往Executor端时,闭包(closure)后一同发送过去的

当然,广播变量同样可以发送AnyRef类型的变量,但是必须是可序列化的对象:

def main(args: Array[String]): Unit = {
  val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
  val map = Map(1 -> 'a', 2 -> 'b', 3 -> 'c')
  val user = new User(1, "zs") with Serializable
  val b = sc.broadcast(user)
  user.uName = "ls"
  val source = sc.makeRDD(Seq(1, 2, 3, 4, 5))
  source.foreach(number => println(b.value))
}

class User(idIn: Int, nameIn: String) {
  var uid = idIn
  var uName = nameIn

  override def toString: String = s"uid:$uid , uName:$uName"
}
/*
uid:1 , uName:ls
uid:1 , uName:ls
uid:1 , uName:ls
uid:1 , uName:ls
uid:1 , uName:ls
*/

这里uName改变了的原因是,广播发送出去的是user的地址值,对应地址的uName属性发生变化,对Executor端产生了影响

累加器

累加器与广播变量类似,也是定义好后由Driver发到各Executor上辅助运算。累加器区别于广播变量的特性是:累加器在Executor端无法被读取,只能执行累加操作。

累加器是一种比较高效的计数或者求和的工具

下面先演示累加器的使用方式:

def main(args: Array[String]): Unit = {
  val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
  val oddCnt = sc.longAccumulator("oddCnt")
  val rdd = sc.makeRDD(List(1, 2, 3, 4, 5))
  rdd.foreach(number => if (number % 2 == 1) oddCnt.add(1))
  println("奇数的数量是:" + oddCnt.value)
}
/*
奇数的数量是:3
*/

当然也可以用来求和,只要加上不同的数字就行了

def main(args: Array[String]): Unit = {
  val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
  val oddSum = sc.longAccumulator("oddCnt")
  val rdd = sc.makeRDD(List(1, 2, 3, 4, 5))
  rdd.foreach(number => if (number % 2 == 1) oddSum.add(number))
  println("所有奇数之和是:" + oddSum.value)
}
/*
所有奇数之和是:9
*/

Spark提供了三种累加器,除了上面演示的longAccumulator外,还有doubleAccumulatorcollectionAccumulator,对于整型数值累加就采用longAccumulator,浮点型数值累加采用doubleAccumulator(整型数值都可以转化成浮点型数值,所以doubleAccumulator也可以进行整型数值的累加),collectionAccumulator的累加操作是将目标元素放入一个集合中。

留意下collectionAccumulator的使用,在创建时需要指定放入元素的泛型,否则无法执行累加操作:

val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
val map = sc.collectionAccumulator[String]("map")
val rdd = sc.makeRDD(List("hello", "world", "hi", "scala", "hallo", "spark"))
rdd.foreach(e => if (e.startsWith("h")) map.add(e))
println(map.value)
/*
[hallo, hello, hi]
*/

累加器是可以自定义的,官方提供了累加器自定义的方式,自定义类继承AccumulatorV2类并实现其抽象函数即可

下面使用自定义的累加器完成一个word count案例(万物皆可word count)

class WordAccumulator extends AccumulatorV2[String, util.HashMap[String, Int]] {
  // 定义一个用来存储中间过程的Map
  private val map = new util.HashMap[String, Int]()

  // 判断累加器是否是初始值,对于当前案例,执行过数据后map中肯定会有元素,所有累加器为初始值时map为空
  override def isZero: Boolean = map.isEmpty

  // 创建一个新的累加器
  override def copy(): AccumulatorV2[String, util.HashMap[String, Int]] = new WordAccumulator

  // 重置累加器,只需要将map置空就行了
  override def reset(): Unit = map.clear()

  // 添加一条数据,如果当前有该数据就使其value + 1,否则就新加入一条值为1的键值对
  override def add(v: String): Unit = if (map.containsKey(v)) map.put(v, map.get(v) + 1) else map.put(v, 1)

  // 当前累加器的map与另外一个累加器的map合并
  override def merge(other: AccumulatorV2[String, util.HashMap[String, Int]]): Unit = {
    other match {
      case o: WordAccumulator =>
        o.value.entrySet().forEach(new Consumer[util.Map.Entry[String, Int]] {
          override def accept(t: util.Map.Entry[String, Int]): Unit = {
            if (map.containsKey(t.getKey)) {
              map.put(t.getKey, map.get(t.getKey) + t.getValue)
            } else {
              map.put(t.getKey, t.getValue)
            }
          }
        })
      case _ => throw new UnsupportedOperationException(
        s"Cannot merge ${this.getClass.getName} with ${other.getClass.getName}")
    }
  }

  // 最终要返回的值,我们返回map就可以了
  override def value: util.HashMap[String, Int] = map
}

测试代码:

object AccTest {
  def main(args: Array[String]): Unit = {
    val sc = SparkContext.getOrCreate(new SparkConf().setMaster("local[*]").setAppName("broadcastTest"))
    // 创建自定义累加器对象
    val wordAccumulator = new WordAccumulator
    // 注册累加器后,就就可以直接使用了
    sc.register(wordAccumulator, "wc")

    sc.textFile("in/article.txt") // 读取源文件
      .filter(e => e.length != 0) // 过滤空行后切分为单词,再处理掉空的数据
      .flatMap(line => {
        line.trim.split("[\\s|\"|,|\\.|   ]+")
      })
      .filter(!_.isEmpty)
      .foreach(e => wordAccumulator.add(e.trim)) // 调用累加方法就可以执行上面定义的代码了

    println(wordAccumulator.value)
  }
}
/*
{reuse=1, Qiang=1, year=1, ...... engage=1}
*/

某些需要collect后执行的操作,可以考虑使用累加器代替,可以大幅提高执行效率

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值