一、累加器
- 累加器:分布式共享只写变量。(Task和Task之间不能读数据)
- 累加器用来对数据的聚合,通常在向Spark传递函数时,比如使用map()函数或者用 filter()传条件时,可以使用驱动器程序中定义的变量,但是集群中运行的每个任务都会得到这些变量的一份新的副本,更新这些副本的值也不会影响驱动器中的对应变量。如果我们想实现所有分片处理时更新共享变量的功能,那么累加器可以实现我们想要的效果。
1.1 系统累加器
- 代码实现
package com.spark.day06
import org.apache.spark.rdd.RDD
import org.apache.spark.util.LongAccumulator
import org.apache.spark.{Accumulator, SparkConf, SparkContext}
import scala.util.parsing.json.JSON
/*
* 累加器:
* 分布式共享只写变量——task之间不能读取数据
*
* */
object Spark06_Accumulator {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Test").setMaster("local[*]")
val sc = new SparkContext(conf)
// 创建一个RDD,单值RDD,并对其进行求和,不存在shuffle过程
// val rdd: RDD[Int] = sc.makeRDD(List(1, 2, 3, 4))
// println(rdd.sum())
// println(rdd.reduce(_+_))
// 存在shuffle,效率低
// val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)))
// val resRDD: RDD[(String, Int)] = rdd.reduceByKey(_ + _)
//
// resRDD.map(_._2).collect().foreach(println)
// 如果定义一个普通变量,那么在Driver定义,Executor会创建变量的副本,算子都是对副本进行操作,Driver端的变量不会更新
// 在driver端创建一个sum。
// var sum:Int = 0
// val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)))
// rdd.foreach{
// case (word, count) => {
// sum += count // 在这里会在每个executor上创建一个sum的副本,每个执行自己的
// }
// }
//
// println(sum)
// 如果需要通过excutor,对Driver端定义的变量进行更新,需要定义为累加器
// 累加器和普通变量相比,会将Excutor端的结果,收集到Driver端进行汇总
// 创建累加器
// val sum: Accumulator[Int] = sc.accumulator(10) // 过时了
val sum: LongAccumulator = sc.longAccumulator("myAcc")
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3), ("d", 4)), 2)
rdd.foreach{
case (word, count) => {
sum.add(count)
println(sum.value)
}
}
// 获取累加器的值
println(sum.value)
sc.stop()
}
}
1.2 自定义累加器
- 自定义累加器类型的功能在1.X版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后,累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式。
1.2.1 自定义累加器步骤
- 继承AccumulatorV2,设定输入、输出泛型
- 重写方法
1.2.2 举例
- 需求:自定义累加器,统计集合中首字母为“H”单词出现的次数。
- 代码实现
package com.spark.day06
import org.apache.spark.rdd.RDD
import org.apache.spark.util.{AccumulatorV2, LongAccumulator}
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.mutable
/*
* 累加器:
* 自定义累加器
* 自定义累加器,统计出RDD中,所有以"H"开头的单词以及出现的次数
*
* */
object Spark07_Accumulator {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("Test").setMaster("local[*]")
val sc = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("Hello", "Hello", "Haha", "Haha", "Hello", "Spark", "Spark"))
// 创建累加器对象
val myAcc = new MyAccumulator
// 注册累加器
sc.register(myAcc)
// 使用累加器
rdd.foreach{
word => {
myAcc.add(word)
}
}
// 输出累加器结果
println(myAcc.value)
sc.stop()
}
}
// 定义一个类,继承AccumulatorV2
// 泛型累加器输入和输出数据的类型
// 执行过程:先copy、再reset、然后isZero,
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
// 定义一个集合,记录单词以及出现次数
var map = mutable.Map[String, Int]()
// 是否为初始状态
override def isZero: Boolean = map.isEmpty
// 拷贝,在不同的Excutor上都有副本,通过这个方法进行拷贝
override def copy(): AccumulatorV2[String, mutable.Map[String, Int]] = {
val newAcc = new MyAccumulator
newAcc.map = this.map
newAcc
}
// 重置,回到初始状态
override def reset(): Unit = map.clear()
// 向累加器中添加元素
override def add(elem: String): Unit = {
// 向可变集合中添加或者更新元素
if (elem.startsWith("H")) {
map(elem) = map.getOrElse(elem, 0) + 1
}
}
// 合并
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
// 当前Excutor的map
var map1 = map
// 另一个Excutor的map
var map2 = other.value
map = map1.foldLeft(map2) {
// mm表示map2,kv表示map1中的每一个元素
(mm, kv) => {
// 指定合并规则
val k: String = kv._1
val v: Int = kv._2
// 根据map1中元素的key,到map2中获取元素
mm(k) = mm.getOrElse(k, 0) + v
mm
}
}
}
// 获取累加器的值
override def value: mutable.Map[String, Int] = map
}
二、广播变量
- 广播变量:分布式共享只读变量。
- 在多个并行操作中(Executor)使用同一个变量,Spark默认会为每个任务(Task)分别发送,这样如果共享比较大的对象,会占用很大工作节点的内存。广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。
2.1 使用广播变量步骤
- 通过对一个类型T的对象调用SparkContext.broadcast创建出一个Broadcast[T]对象,任何可序列化的类型都可以这么实现。
- 通过value属性访问该对象的值(在Java中为value()方法)。
- 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
2.2 原理说明
2.3 具体实现
package com.spark.day06
import org.apache.spark.broadcast.Broadcast
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
/*
* 广播变量
*
* */
object Spark08_Broadcast {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")
val sc = new SparkContext(conf)
// 想实现类似join功能, (a, (1, 4))
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("b", 2), ("c", 3)))
val list = List(("a", 1), ("b", 2), ("c", 3))
// 创建一个广播变量
val broadcastList: Broadcast[List[(String, Int)]] = sc.broadcast(list)
val resRDD: RDD[(String, (Int, Int))] = rdd.map {
case (k1, v1) => {
var v3 = 0
// for ((k2, v2) <- list) {
for ((k2, v2) <- broadcastList.value) {
if (k1 == k2) {
v3 = v2
}
}
(k1, (v1, v3))
}
}
resRDD.collect().foreach(println)
sc.stop()
}
}