Java操作spark-自定义累加器
spark的累加器
背景:
如果我们在Driver端定义一个变量,然后将该变量发送Executor端进行累加赋值操作,那么Driver端的变量值会发生改变吗?答案是不会,因为Executor端操作的是变量的副本,并不能影响Driver端的变量值。如何在这样的分布式系统中实现变量的共写呢?这就要用到累加器
简介:
累加器:分布式共享只写变量。(Executor和Executor之间不能读数据)
累加器是Spark 计算框架为了能够进行高并发和高吞吐的数据处理封装的三大数据结构之一,功能是实现分布式共享只写变量。累加器用来把 Executor 端变量信息聚合到 Driver 端。在 Driver 程序中定义的变量,在Executor 端的每个 Task 都会得到这个变量的一份新的副本,每个 task 更新这些副本的值后,传回 Driver 端进行merge。以此来实现变量的共写。
需要注意的是:
Executor端的任务不能读取累加器的值(例如:在Executor端调用sum.value,获取的值不是累加器最终的值)。因此我们说,累加器是一个分布式共享只写变量。
累加器要放在行动算子中,因为转换算子执行的次数取决于job的数量,如果一个spark应用有多个行动算子,那么转换算子中的累加器可能会发生不止一次更新,导致结果错误。所以,如果想要一个无论在失败还是重复计算时都绝对可靠的累加器,我们必须把它放在foreach()这样的行动算子中。
在很多场合我们都可以巧妙利用累加器替代转换算子实现一些功能,避免转换算子带来的shuffle操作,从而提升程序性能
自定义累加器实现wordCount
网上找到的都是scala语言,java语言代码如下:
package com.cjy.bigdata.spark.core.wc;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.util.AccumulatorV2;
import java.util.*;
/**
* @author cjy
* @description: 累加器实现wordCount
* @date: 2022$ 06/17$
*/
public class LearnAccumulator {
public static void main(String[] args) {
//环境准备
SparkConf sparkConf = new SparkConf().setAppName("java word count").setMaster("local");
JavaSparkContext jsc = new JavaSparkContext(sparkConf);
List<String> strings = Arrays.asList("hello java", "hello world", "hello spark");
JavaRDD<String> javaRDD = jsc.parallelize(strings);
//创建累加器
MyAccumulator myAccumulator = new MyAccumulator();
//注册累加器
jsc.sc().register(myAccumulator,"my");
javaRDD.foreach(datas -> {
//使用累加器
myAccumulator.add(datas);
});
//获取结果
Map<String, Integer> value = myAccumulator.value();
value.forEach((k,v) -> {
System.out.println(k + ":" + v);
});
}
}
class MyAccumulator extends AccumulatorV2<String, Map<String, Integer>>{
//定义输出类型变量
private Map<String ,Integer> output = new HashMap<>();
//是否为初始状态
@Override
public boolean isZero() {
return this.output.isEmpty();
}
//复制累加器
@Override
public AccumulatorV2<String, Map<String, Integer>> copy() {
MyAccumulator myAccumulator = new MyAccumulator();
//将此累加器中的数据赋值给新创建的累加器
myAccumulator.output = this.output;
return myAccumulator;
}
//重置累加器
@Override
public void reset() {
this.output.clear();
}
//累加器添加元素
@Override
public void add(String v) {
String[] split = v.split(" ");
for (String s : split){
//存在则加一,不存在则为一
int value = this.output.getOrDefault(s, 0) + 1;
this.output.put(s,value);
}
}
//合并累加器元素
@Override
public void merge(AccumulatorV2<String, Map<String, Integer>> other) {
other.value().forEach((k,v) -> {
if (this.output.containsKey(k)){
Integer i1 = this.output.get(k);
Integer i2 = other.value().get(k);
this.output.put(k,i1+i2);
}else {
this.output.put(k,v);
}
});
}
//输出
@Override
public Map<String, Integer> value() {
return this.output;
}
}
运行结果如下:
用大数据集测试一下累加器的好处,主要是消除shuffle。使用600万的数据,将上述代码中的strings替换成如下表示。
List<String> strings = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 6000000; i++) {
int salt = random.nextInt(3000000);
strings.add("asd"+salt+"fdksl");
}
ui界面图如下:时间是20s,未发生shuffle。
通常的reduceByKey结果图如下:可见shuffle机制导致时间的延长。