目录

  • ​​spark outline​​
  • ​​spark 3大数据结构​​
  • ​​为什么要使用累加器​​
  • ​​spark 累加器功能​​
  • ​​自定义累加器案例演示​​
  • ​​spark 累加器执行原理​​

spark outline

​​大纲目录​​

spark 3大数据结构

  1. RDD:弹性分布式数据集
  2. 累加器:分布式共享只写变量
  3. 广播变量:分布式共享只读变量

为什么要使用累加器

首先来看一段代码(统计单词出现的次数)

def main(args: Array[String]): Unit = {

// 1.创建SparkConf并设置App名称
val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[1]")

// 2.创建SparkContext,该对象是提交Spark App的入口
val sc: SparkContext = new SparkContext(conf)

// 3.创建RDD
val rdd: RDD[(String, Int)] = sc.makeRDD(List(("a", 1), ("a", 2), ("a", 3), ("a", 4)),numSlices = 2)

// 3.1 打印单词出现的次数(a,10)
rdd.reduceByKey(_ + _).collect().foreach(println) // 其中有shuffle过程

// 有shuffle过程,那么意味着执行效率低下
// 3.2 如果不用shuffle,怎么处理呢?
var sum = 0
rdd.foreach {
case (word, count) => {
sum = sum + count
println("sum=" + sum) // 打印是在Executor端
}
}
println(("a", sum)) // 打印是在Driver端
sc.stop()
}

输出结果:

(a,10)
sum=1
sum=3
sum=3
sum=7
(a,0)

​问题1:​​​使用reduceByKey会有shuffle,有shuffle则意味着执行效率低下
​​​问题2:​​如果不用reduceByKey,怎么处理呢?

通常的思路是:自己去定义一个全局sum变量,每遍历到相同key的单词后,就去做累加(sum+count),​​但为什么最终结果会是(a,0)?​

spark 累加器_数据

因为numSlices = 2,所以会由2个Executor来处理数据,然后每个Executor会去复制一份Driver端的sum变量,每个Executor内的sum变量互不干扰,所以就有:第一个分区中的数据为(“a”, 1), (“a”, 2),输出sum=1,sum=3;第二个分区中的数据为(“a”, 3), (“a”, 4),输出sum=3,sum=7;因为Driver端的sum变量是被复制Executor端过去的,Executor端并没有直接操作Driver端的sum变量,所以Driver端的sum变量仍然为0,如果想要Executor端的sum变量影响到Driver端的sum变量,则需要使用累加器

// 3.3 使用累加器实现数据的聚合功能
// Spark自带常用的累加器
// 3.3.1 声明累加器
val sum1: LongAccumulator = sc.longAccumulator("myAcc")

rdd.foreach {
case (word, count) => {
// 3.3.2 使用累加器
sum1.add(count)
}
}

//3.3.3 获取累加器的值
println(sum1.value)

spark 累加器_spark_02

spark 累加器功能

累加器用来对信息进行聚合。​​比如上述使用 foreach 算子时,该算子内部使用到了 Dirver 端定义的变量,并且集群中运行的每个任务的 Executor 端都会得到变量的一个副本,在 Executor 端更新副本的值不会影响 Dirver 端对应的变量,如果想要影响,则需要使用累加器​

自定义累加器案例演示

自定义累加器在1.x版本中就已经提供了,但是使用起来比较麻烦,在2.0版本后累加器的易用性有了较大的改进,而且官方还提供了一个新的抽象类:AccumulatorV2来提供更加友好的自定义类型累加器的实现方式

需求:统计集合List(“Hello”, “World”, “Hi”, “Spark”)中首字母为“H”单词出现的次数

输出:Map(Hello -> 1, Hi -> 1)

package com.xcu.bigdata.spark.core.pg02_accumulator

import org.apache.spark.rdd.RDD
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}

import scala.collection.mutable

/**
* @Desc : 累加器的使用 统计特定形式的wordCount
*/
object Spark01_Accumulator {
def main(args: Array[String]): Unit = {
//创建配置文件
val conf: SparkConf = new SparkConf().setAppName("Spark01_Accumulator").setMaster("local[*]")
//创建SparkContext,该对象是提交的入口
val sc = new SparkContext(conf)
//创建RDD
val rdd: RDD[String] = sc.makeRDD(List("Hello", "World", "Hi", "Spark"))
//创建累加器
val myAcc = new MyAccumulator()
//注册累加器
sc.register(myAcc, name = "myAcc")
//使用累加器
rdd.foreach(
word => {
myAcc.add(word)
}
)
//获取累计器的累加结果
println(myAcc.value)
//释放资源
sc.stop()
}
}


//AccumulatorV2[输入数据的类型, 输出数据的类型]
class MyAccumulator extends AccumulatorV2[String, mutable.Map[String, Int]] {
//定义一个集合,记录单词以及出现次数
var map: mutable.Map[String, Int] = mutable.Map[String, Int]()

//初始化状态
override def isZero: Boolean = map.isEmpty

//拷贝(不同的excutor需要去拷贝一份driver上的累加器,作为副本)
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(e: String): Unit = {
if (e.startsWith("H")) {
//向可变集合中添加或者更新元素
map(e) = map.getOrElse(e, 0) + 1
}
}


//分区间计算逻辑
override def merge(other: AccumulatorV2[String, mutable.Map[String, Int]]): Unit = {
//当前excutor的map
var map1: mutable.Map[String, Int] = map
//另一个excutor的map
var map2: mutable.Map[String, Int] = other.value
map = map1.foldLeft(map2) {
(map2, kv) => {
map2(kv._1) = map2.getOrElse(kv._1, 0) + kv._2
map2
}
}
}

//获取累加器的值
override def value: mutable.Map[String, Int] = map
}

spark 累加器执行原理

首先序列化 driver 端 accumulator 到 executor ,序列化前调用 reset 重置 value 并使用 isZero 检测是否重置成功。单个 executor 内使用 add 进行累加,最终 driver 端对多个 executor 间的 accumulaotr 使用merge 进行合并得到结果