Spark的三大核心数据结构:RDD、累加器(只写不读)、广播变量(只读不写)
在spark应用程序中,我们经常会有这样的需求,如异常监控,调试,记录符合某特性的数据的数目,这种需求都需要用到计数器,如果一个变量不被声明为一个累加器,那么它将在被改变时不会再driver端进行全局汇总,即在分布式运行时每个task运行的只是原始变量的一个副本,并不能改变原始变量的值,但是当这个变量被声明为累加器后,该变量就会有分布式计数的功能。
摘要:
(1)自定义变量在Spark中运算时,会从Driver中复制一份副本到Executor中运算,但变量的运算结果并不会返回给Driver,所以无法实现自定义变量的值改变,一直都是初始值,所以针对这个问题,引入了累加器的概念;
(2)系统累加器longAccumulator和自定义累加器(extends AccumulatorV2[类型,类型])实际都是两步,new累加器,然后sc.register注册累加器;
(3)先在Driver程序中创建一个值为0或者空的累加器对象,Task运算时,Executor中会copy一份累加器对象,在Executor中进行运算,累加器的运算结果返回给Driver程序并合并Merge,得出累加器最终结果;
(4)累加器.add(元素);具体对元素的操作包括数据sum、增加、删减、筛选等要求,都可以写在自定义累加器的.add()方法中。
目录
1. 累加器原理
贴一段代码如下,来说明累加器的原理
-
object Spark_Add {
-
def main(args:
Array[
String]): Unit = {
-
val conf =
new SparkConf().setMaster(
"local[*]").setAppName(
"Application")
-
//构建Spark上下文对象
-
val sc =
new SparkContext(conf)
-
-
var sum =
0
-
val rdd = sc.makeRDD(
Array(
1,
2,
3,
4,
5))
-
-
rdd.map(
item=>{
-
sum = sum + item
-
}).collect()
-
println(
"sum = "+sum)
-
-
//释放资源
-
sc.stop()
-
}
-
}
-
-
-----------------------------------
-
sum =
0
在Spark中声明SparkContext的类称为Driver,所以变量sum在Driver中;
而任务Task(即分区数据的运算)的执行是在Executor中进行,即sum = sum + item在Executor节点执行;
为什么Task运算完后,在Driver的IDEA客户端打印结果sum还是0呢?
问题的关键点在于:Executor只是做了运算,但并没有将sum运算后的值返回Driver中,也就是说Driver中的sum变量至始至终都保持初始值为0;
那么Spark中怎么解决将Executor中运算完毕的数据传回Driver中修改原始数据呢?这里就引入了“累加器”的概念
-
object Spark_Add {
-
def main(args:
Array[
String]): Unit = {
-
val conf =
new SparkConf().setMaster(
"local[*]").setAppName(
"Application")
-
//构建Spark上下文对象
-
val sc =
new SparkContext(conf)
-
-
//使用系统默认累加器,默认初始值为0
-
val sum = sc.longAccumulator(
"sum")
-
-
val rdd = sc.makeRDD(
Array(
1,
2,
3,
4,
5))
-
-
rdd.map(
item=>{
-
sum.add(item)
-
}).collect()
-
println(
"sum = "+sum.value)
-
-
//释放资源
-
sc.stop()
-
}
-
}
-
-------------------------------------
-
sum =
15
因为累加器sum在Driver中,Executor中运算Task时,会把sum作为副本从Driver传递到Executor中,通过sum.add方法累加rdd元素;
并且会将各个Executor运算结果返回Driver,并作Merge合并操作,所以Driver中最终sum数据就是各个Executor运算合并后的结果。
2. 累加器原理图:
直接在Driver中声明共享变量,运算时会将其copy一份副本到各个Executor中,但是运算后不会将其返回;
如果共享数据以累加器的方式存在,那么copy运算后,还会从Executor中返回给Driver,实现Merge操作。
累加器的传递途径:序列化
3. 自定义累加器
上面的代码使用了系统默认累加器val sum = sc.longAccumulator("sum")
查看其源码:
-
def longAccumulator(name: String): LongAccumulator = {
-
val acc = new LongAccumulator
-
register(acc, name)
-
acc
-
}
longAccumulator方法中,其实封装了两步,第一步new累加器对象,第二步完成累加器在当前SparkContext中注册
下面自定义累加器,也是用这两步
-
//累加器
-
object Spark_Add {
-
def main(args: Array[String]):
Unit = {
-
val conf = new SparkConf().setMaster(
"local[*]").setAppName(
"Application")
-
//构建Spark上下文对象
-
val sc = new SparkContext(conf)
-
-
//创建累加器
-
val sum = new MyAccumulator()
-
-
//注册累加器
-
sc.register(sum,
"accumulator")
-
-
val rdd = sc.makeRDD(Array(
1,
2,
3,
4,
5))
-
-
rdd.map(item=>{
-
sum.add(item)
-
}).collect()
-
println(
"sum = "+sum.value)
-
-
//释放资源
-
sc.stop()
-
}
-
}
-
-----------------------------------------------
-
sum =
15
-
-
//自定义累加器
-
class MyAccumulator extends AccumulatorV2[Int,Int]{
-
var sum =
0
-
-
//1. 是否初始状态(sum为0表示累加器为初始状态)
-
override def isZero:
Boolean = sum ==
0
-
-
//2. 执行器执行时需要拷贝累加器对象(把累加器对象序列化后,从Driver传到Executor)
-
override def copy(): AccumulatorV2[
Int,
Int] = {
-
val mine = new MyAccumulator
-
mine
-
}
-
-
//3. 重置数据(重置后看当前累加器是否为初始状态)
-
override def reset():
Unit = sum =
0
-
-
//累加数据
-
override def add(v:
Int):
Unit = {
-
sum = sum + v
-
}
-
-
//合并计算结果数据(把所有Executor中累加器value合并)
-
override def merge(other: AccumulatorV2[
Int,
Int]):
Unit = {
-
sum = sum + other.value
-
}
-
-
//累加器的结果
-
override def value:
Int = sum
-
}
自定义累加器需要继承AccumulatorV2[类型,类型],重写所有方法即可;
可以看出创建自定义累加器,后需要sc注册该累加器,与系统默认累加器longAccumulator源码中封装的两步一致。
4. 自定义累加器——用法2:筛选数据
原理:累加器变量初始值为0或者空,通过add方法增加元素;
数据增加、删减、筛选的逻辑都可以写在自定义累加器的add方法中。
eg:写一个小demo,给定一个场景,从字符串中筛选出含b的元素
-
object Spark_Add {
-
def main(args: Array[String]):
Unit = {
-
val conf = new SparkConf().setMaster(
"local[*]").setAppName(
"Application")
-
//构建Spark上下文对象
-
val sc = new SparkContext(conf)
-
-
//创建累加器
-
val acc2 = new MyBlackAccumulator()
-
-
//注册累加器
-
sc.register(acc2,
"accumulator")
-
-
val rdd = sc.makeRDD(Array(
"abc",
"bcd",
"efg"))
-
-
rdd.map(s => {
-
acc2.add(s)
-
}).collect()
-
println(
"sum = " + acc2.value)
-
-
//释放资源
-
sc.stop()
-
}
-
}
-
---------------------------------------------------
-
sum = [bcd, abc]
-
-
-
//自定义累加器
-
//传入元素String,返回的是String集合,要求无序不可重复,用java中hashSet
-
class MyBlackAccumulator extends AccumulatorV2[String, java.util.HashSet[String]] {
-
var blackList = new util.HashSet[String]()
-
-
override def isZero:
Boolean = {
-
blackList.isEmpty
-
}
-
-
override def copy(): AccumulatorV2[String, util.HashSet[String]] = {
-
val acc = new MyBlackAccumulator
-
acc
-
}
-
-
override def reset():
Unit = {
-
blackList.clear()
-
}
-
-
//包含b的加入黑名单,筛选逻辑写在add中
-
override def add(v: String):
Unit = {
-
if (v.contains(
"b")) {
-
blackList.add(v)
-
}
-
}
-
-
override def merge(other: AccumulatorV2[String, util.HashSet[String]]):
Unit = {
-
//把另外集合中数据合并,addAll方法
-
blackList.addAll(other.value)
-
}
-
-
override def value: util.HashSet[String] = blackList
-
}
5. 累加器的Shell实现
val a = sc.accumulator(0)
创建一个初始值为0的累加器