如果在一个算子的函数中使用到了某个外部的变量,那么这个变量的值会被拷贝到每个task中,此时每个task只能操作自己的那份变量副本。如果多个task想要共享某个变量,那么这种方式是做不到的
3大数据结构
rdd
广播变量 分布式只读共享变量
累加器 分布式只写共享变量
广播变量(调优策略)
Broadcast Variable会将用到的变量,仅仅为每个节点拷贝一份,即每个Executor拷贝一份,更大的用途是优化性能,减少网络传输以及内存损耗
特点
- 能不能将一个RDD使用广播变量广播出去? 不能 ,因为RDD是不存数据的。可以将RDD的结果广播出去。
- 广播变量是在driver端定义,在Executor端只读的共享变量
- 我们可以将大型外部变量封装为广播变量,此时一个Executor保存一个变量副本,此Executor上的所有task共用此变量,减少变量到各个节点的网络传输消耗,以及在各个节点上的内存消耗
不时用广播变量时,如果executor端用到了Driver的变量,每个task都会保存一份它所使用的外部变量的副本,当一个Executor上的多个task都使用一个大型外部变量时,对于Executor内存的消耗是非常大的 - 经典应用是大表与小表的join中通过广播变量小表来实现以brodcast join 取代reduce join
- 直接从本地的BlockManager中获取变量,不需要再从远程节点上获取,减少变量到各个节点的网络传输消耗,以及在各个节点上的内存消耗
作用
广播变量用来高效分发较大的对象。向所有工作节点发送一个较大的只读值,以供一个或多个Spark操作使用。比如,如果你的应用需要向所有节点发送一个较大的只读查询表,甚至是机器学习算法中的一个很大的特征向量,广播变量用起来都很顺手。 在多个并行操作中使用同一个变量,但是 Spark会为每个任务分别发送。
使用
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(2)
scala> broadcastVar.value
res2: Array[Int] = Array(1, 2, 3)
使用广播变量的过程如下:
(1) 通过对一个类型 T 的对象调用 SparkContext.broadcast 创建出一个 Broadcast[T] 对象。 任何可序列化的类型都可以这么实现。
(2) 通过 value 属性访问该对象的值(在 Java 中为 value() 方法)。
(3) 变量只会被发到各个节点一次,应作为只读值处理(修改这个值不会影响到别的节点)。
累加器
Accumulator则可以让多个task共同操作一份变量,主要可以进行累加操作
累加器场景
定义一个count=0,对rdd遍历累加
当只有一个分区,count累加成功
否者count还是0
此时用到累加器
累加器支持加法,支持数值类型和自定义类型 参数可以为Object
累加器类型除Spark自带的int、float、Double外,也支持开发人员自定义。
并行计算
累加器特点
- Accumulator是存在于Driver端的,集群上运行的task进行Accumulator的累加,随后把值发到Driver端,在Driver端汇总
- 由于Accumulator存在于Driver端,从节点读取不到Accumulator的数值,task只能对Accumulator进行累加操作,不能读取它的值,只有Driver程序可以读取Accumulator的值
- 主要用于多个节点对一个变量进行共享性的操作
- 累加之后要缓存然后调用action算子
创建的Accumulator变量的值能够在Spark Web UI上看到,在创建时应该尽量为其命名
LongAccumulator fooCount = spark.sparkContext().longAccumulator("fooCount");
累加器遇到的问题
累加器在算子中,比如在map算子里面累加,如果没有遇到action算子,map算子不会执行,那么累加器的值一直不变
连续两次调用action算子会执行两次代码,累加器是在driver端的,他是会被累加的,所以需要缓存,避免重新计算
比如下面这段代码,理想应该是两个2,结果却是0和4
val list_rdd: RDD[String] = sc.parallelize( List("rose is beautiful", "jisoo is beautiful"))
val map_rdd = list_rdd.map(x => {
longAccumulator.add(1)
})
println(longAccumulator.value) //以为没有action算子,没有执行,所以为0
map_rdd.collect()
map_rdd.collect()
println(longAccumulator.value) //两个action算子,相当于对driver端的累加器变量执行了两次累加,所以是4
正确写法
val list_rdd: RDD[String] = sc.parallelize( List("rose is beautiful", "jisoo is beautiful"))
val map_rdd = list_rdd.map(x => {
longAccumulator.add(1)
})
map_rdd.cache()
map_rdd.collect()
map_rdd.collect()
println(longAccumulator.value) //2
Accumulator三种类型
Spark内置了三种类型的Accumulator,分别是LongAccumulator用来累加整数型,DoubleAccumulator用来累加浮点型,CollectionAccumulator用来累加集合元素。
// 内置的累加器有三种,LongAccumulator、DoubleAccumulator、CollectionAccumulator
// LongAccumulator: 数值型累加
LongAccumulator longAccumulator = sc.longAccumulator("long-account");
// DoubleAccumulator: 小数型累加
DoubleAccumulator doubleAccumulator = sc.doubleAccumulator("double-account");
// CollectionAccumulator:集合累加
CollectionAccumulator<Integer> collectionAccumulator = sc.collectionAccumulator("double-account");
自定义累加器
当内置的Accumulator无法满足要求时,可以继承AccumulatorV2实现自定义的累加器。
实现自定义累加器的步骤:
- 继承AccumulatorV2,实现相关方法
- 创建自定义Accumulator的实例,然后在SparkContext上注册它
import org.apache.spark.util.AccumulatorV2
import org.apache.spark.{SparkConf, SparkContext}
import scala.collection.JavaConversions._
// AccumulatorV2[in,out] 输入类型,输出类型
class SetAccumulator extends org.apache.spark.util.AccumulatorV2[String, mutable.Set[String]] {
//定义一个返回值
private val logArray: mutable.Set[String] = mutable.Set[String]()
//累机器是否是初始化状态,set最开始就是empty
override def isZero: Boolean = {
logArray.isEmpty
}
//复制累加器对象,返回的就是个一样的累加器
override def copy(): org.apache.spark.util.AccumulatorV2[String, mutable.Set[String]] = {
//new了一个新的累加器,但是是不是要把值也copy进去,多线程是不是要加锁,要不然我copy时候进来值怎么办
val newAccumulator = new SetAccumulator //和new SetAccumulator()好像也没啥区别
// 加锁copy value到新的累加器里
//这个地方没有()不要写成logArray.synchronized() {
logArray.synchronized {
newAccumulator.logArray ++= this.logArray
}
newAccumulator
}
//重置累加器
override def reset(): Unit = {
logArray.clear()
}
//添加数据
override def add(v: String): Unit = {
logArray.add(v)
}
//合并
override def merge(other: org.apache.spark.util.AccumulatorV2[String, mutable.Set[String]]): Unit = {
//模式匹配,相当于java的switch
// switch(other){
// case LogAccumulator :
// _logArray.addAll(o.value)
// }
other match {
case setAccumulator: SetAccumulator => logArray ++= setAccumulator.value
}
}
//取出累机器的值
override def value: mutable.Set[String] = {
//我们内部使用可以直接用.logArray访问或者操作
//对于外部给一个value方法可以访问,但是不想让他们操作,返回不可变
//java.util.Collections.unmodifiableSet(logArray)
logArray
}
}
object Demo {
def main(args: Array[String]): Unit = {
val sparkConf = new SparkConf().setAppName("Test").setMaster("local[2]")
val sc = new SparkContext(sparkConf)
val accum = new LogAccumulator
sc.register(accum, "logAccum")
val sum = sc.parallelize(Array("1", "2a", "3", "4b", "5", "6", "7cd", "8", "9"), 2).filter(line => {
//过滤掉不是数字的,然后放到累加器里,查看过滤的数据
val pattern = """^-?(\d+)"""
val flag = line.matches(pattern)
if (!flag) {
accum.add(line)
}
flag
}).map(_.toInt).reduce(_ + _)
println("sum: " + sum)
for (v <- accum.value) print(v + " ")
println()
sc.stop()
}
}
sum; 32
7cd 4b 2a
思考:内置的累加器LongAccumulator、DoubleAccumulator、CollectionAccumulator和我上面的自定义BigIntegerAccumulator,它们都有一个共同的特点,就是最终的结果不受累加数据顺序的影响(对于CollectionAccumulator来说,可以简单的将结果集看做是一个无序Set),看到网上有博主举例子StringAccumulator,这个就是一个错误的例子,就相当于开了一百个线程,每个线程随机sleep若干毫秒然后往StringBuffer中追加字符,最后追加出来的字符串是无法被预测的。总结一下就是累加器的最终结果应该不受累加顺序的影响,否则就要重新审视一下这个累加器的设计是否合理。
累加器陷阱
package cc11001100.spark.sharedVariables.accumulators;
import org.apache.spark.SparkContext;
import org.apache.spark.api.java.function.MapFunction;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.util.LongAccumulator;
import java.util.Arrays;
public class AccumulatorTrapDemo {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
SparkContext sc = spark.sparkContext();
LongAccumulator longAccumulator = sc.longAccumulator("long-account");
// ------------------------------- 在transform算子中的错误使用 -------------------------------------------
Dataset<Integer> num1 = spark.createDataset(Arrays.asList(1, 2, 3), Encoders.INT());
Dataset<Integer> nums2 = num1.map((MapFunction<Integer, Integer>) x -> {
longAccumulator.add(1);
return x;
}, Encoders.INT());
// 因为没有Action操作,nums.map并没有被执行,因此此时累加器的值还是0
System.out.println("num2 1: " + longAccumulator.value()); // 0
// 调用一次action操作,num.map得到执行,累加器被改变
nums2.count();
System.out.println("num2 2: " + longAccumulator.value()); // 3
// 又调用了一次Action操作,累加器所在的map又被执行了一次,所以累加器又被累加了一遍,就悲剧了
nums2.count();
System.out.println("num2 3: " + longAccumulator.value()); // 6
// ------------------------------- 在transform算子中的正确使用 -------------------------------------------
// 累加器不应该被重复使用,或者在合适的时候进行cache断开与之前Dataset的血缘关系,因为cache了就不必重复计算了
longAccumulator.setValue(0);
Dataset<Integer> nums3 = num1.map((MapFunction<Integer, Integer>) x -> {
longAccumulator.add(1);
return x;
}, Encoders.INT()).cache(); // 注意这个地方进行了cache
// 因为没有Action操作,nums.map并没有被执行,因此此时累加器的值还是0
System.out.println("num3 1: " + longAccumulator.value()); // 0
// 调用一次action操作,广播变量被改变
nums3.count();
System.out.println("num3 2: " + longAccumulator.value()); // 3
// 又调用了一次Action操作,因为前一次调用count时num3已经被cache,num2.map不会被再执行一遍,所以这里的值还是3
nums3.count();
System.out.println("num3 3: " + longAccumulator.value()); // 3
// ------------------------------- 在action算子中的使用 -------------------------------------------
longAccumulator.setValue(0);
num1.foreach(x -> {
longAccumulator.add(1);
});
// 因为是Action操作,会被立即执行所以打印的结果是符合预期的
System.out.println("num4: " + longAccumulator.value()); // 3
}
}
Accumulator另类使用
累加器并不是只能用来实现加法,也可以用来实现减法,直接把要累加的数值改成负数就可以了:
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.util.LongAccumulator;
import java.util.Arrays;
public class AccumulatorSubtraction {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
Dataset<Integer> nums = spark.createDataset(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8), Encoders.INT());
LongAccumulator longAccumulator = spark.sparkContext().longAccumulator("AccumulatorSubtraction");
nums.foreach(x -> {
if (x % 3 == 0) {
longAccumulator.add(-2);
} else {
longAccumulator.add(1);
}
});
System.out.println("longAccumulator: " + longAccumulator.value()); // 2
}
}