在spark中,当我们将一个function传递给算子去执行的时候,是会在集群的从节点执行的,例如map算子:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
JavaRDD<Integer> numberRDD = sc.parallelize(numbers);// 得到一个RDD
JavaRDD<String> results = numberRDD.map(new Function<Integer, String>() {//使用map操作将Integer类型转换成String
private static final long serialVersionUID = 1L;
@Override
public String call(Integer number) throws Exception {
return "number:" + number;
}
});
这个map算子里面的Function方法中的call会在集群的从节点上执行。同时,如果这个function存在或者依赖某些变量的话,也就需要将这些变量传送到从节点中去。并且在每个从节点上可能会有多个executor执行多个task,然而每个task都会通过网络传输序列化和反序列这个变量,最后在每个task中都保存一份变量。这样的话就会造成数据的大量冗余。如果只有一份变量,但是所有的task都可以使用会大大提高效率。
spark存在两种共享变量,分别是:Broadcast Variables和Accumulators。两种共享变量的特点都是在每个机器上会拷贝一份变量,然后这个机器上的所有task会使用这一份变量,这样的话就不会造成数据的冗余。
Broadcast Variables。
首先广播变量只会在每个机器上保存一份只读的文件,那么也就是说一旦使用了广播变量,那么它就不能够修改了.
并且官方文档中有这么一句话:
Spark actions are executed through a set of stages, separated by distributed “shuffle” operations. Spark automatically broadcasts the common data needed by tasks within each stage. The data broadcasted this way is cached in serialized form and deserialized before running each task. This means that explicitly creating broadcast variables is only useful when tasks across multiple stages need the same data or when caching the data in deserialized form is important.
简单的说,因为spark中有窄依赖和宽依赖,而宽依赖会对应的进行一次shuffle,此时这个job就会被切割开来,分成两个stage,每次shuffle都会进行一次切割。窄依赖和宽依赖介绍,在文档山也说有,当在一个stage中的多个task共同需要一个变量的时候,spark会自动的将该变量使用广播变量的方式进行传递使用。而且是当这个task需要使用共享变量的时候,才会通过序列化的反序列化的方式在task执行之前得到这个变量,如果不需要那么就不会加载了。
在java中使用广播变量的方法如下:
Broadcast<int[]> broadcastVar = sc.broadcast(new int[] {1, 2, 3});
broadcastVar.value();
// returns [1, 2, 3]
使用列子:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int num = 100;
Broadcast<Integer> broadcast = sc.broadcast(num);//将num做成广播变量
JavaRDD<Integer> numberRDD = sc.parallelize(numbers);// 得到一个RDD
JavaRDD<Integer> results = numberRDD.map(new Function<Integer, Integer>() {
private static final long serialVersionUID = 1L;
@Override
public Integer call(Integer arg0) throws Exception {
//return num * arg0;//这样也可以,但此时使用的不是广播变量
return broadcast.value() * arg0;//这里直接从广播变量中取值
}
});
Accumulators。
累加器与广播变量不同的是累加器不能进行读取(在call方法中不能读取,因为call方法是在从节点执行的,而累加器应该是在driver端的,所以在从节点中读取不到数据)。官方中也有提到这点:
Tasks running on a cluster can then add to it using the add method. However, they cannot read its value. Only the driver program can read the accumulator’s value, using its value method.
但是创建累加器的方法有好几种,本人在测试的时候只有最原始的Accumulator在call方法中读取的时候会抛出异常,其他的累加器都可以正常读取。
多个task会将数据直接作用在这个累加器上,所以每当一个task修改了累加器,我们就可以直接看到最新的结果。累加器原始支持数字累加,但是我们可以使用自定义的类型。在运行的时候可以在Web界面看到。
在spark2.0之前可以使用以下两种方法创建一个基础的数字累加器(sc代表JavaSparkContext):
Accumulator<Integer> sum = sc.accumulator(0,"Integer Accumulator");
LongAccumulator sum1 = sc.sc().longAccumulator();
但是在2.0之后这个方法就被弃用了,从API介绍,可以看到使用了AccumulatorV2这个抽象类,包含一个输入和一个输出参数以及五个我们需要实现的抽象方法和其他方法。五个方法分别是:
add(IN v):Takes the inputs and accumulates. 输入类型并且执行累加。
copy():Creates a new copy of this accumulator. 返回一个这个累加器的副本。
isZero() :Returns if this accumulator is zero value or not. 当这个累加器为零的时候返回什么。
merge(AccumulatorV2< IN,OUT> other) :Merges another same-type accumulator into this one and update its state, i.e. 将另一个累加器合并到这个累加器中并更新状态。
reset():Resets this accumulator, which is zero value. 将累加器重置为零.
完整示例:
import java.util.Arrays;
import java.util.List;
import org.apache.spark.Accumulator;
import org.apache.spark.SparkConf;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.api.java.function.Function;
import org.apache.spark.api.java.function.VoidFunction;
import org.apache.spark.broadcast.Broadcast;
import org.apache.spark.util.AccumulatorV2;
import org.apache.spark.util.LongAccumulator;
class MyAccumulatorV2 extends AccumulatorV2<String, String> {
//初始值
private String value = "value0";
//将原来的value值添加一个,然后将新的值再加上去
@Override
public void add(String arg0) {
value = value + "," + arg0;
}
//创建一个新的类,然后将value值赋值给新的并返回。
@Override
public AccumulatorV2<String, String> copy() {
MyAccumulatorV2 newAccumulatorV2 = new MyAccumulatorV2();
newAccumulatorV2.value = this.value;
return newAccumulatorV2;
}
//当累加器为零时返回true。
@Override
public boolean isZero() {
return true;
}
//直接将新的的内容添加到value上面
@Override
public void merge(AccumulatorV2<String, String> arg0) {
System.out.println("merge:" + arg0.value());
value = value + arg0.value();
}
//重置值为value0
@Override
public void reset() {
value = "value0";
}
//返回value的值
@Override
public String value() {
return this.value;
}
}
public class Accumulators {
public static void main(String[] args) throws InterruptedException {
SparkConf conf = new SparkConf().setMaster("local").setAppName("BroadcastVariables");
JavaSparkContext sc = new JavaSparkContext(conf);
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
JavaRDD<Integer> numberRDD = sc.parallelize(numbers);// 得到一个RDD
//Accumulator<Integer> sum = sc.accumulator(0,"Integer Accumulator");
//LongAccumulator sum1 = sc.sc().longAccumulator();
MyAccumulatorV2 myacc = new MyAccumulatorV2();
sc.sc().register(myacc, "MyAccumulatorV2");//需要注册,否则会抛出异常
if (myacc.isZero()) {
System.out.println("此时累加器为零!");
System.out.println(myacc.value());
}
numberRDD.foreach(new VoidFunction<Integer>() {
private static final long serialVersionUID = 1L;
@Override
public void call(Integer arg0) throws Exception {
//第一种累加器
//sum.add(arg0);
//System.out.println("累加器1" + sum.value());//此时会抛出异常
//第二种累加器
//sum1.add(arg0);
//System.out.println("累加器2:" + sum1.value());//此时不会抛出异常
//自定义累加器
myacc.add(String.valueOf(arg0));//将此时的值进行累加
System.out.println( "自定义累加器:" + myacc.value());//不会出现异常
System.out.println(arg0);
}
});
//System.out.println(sum.value());
//System.out.println(sum1.value());
System.out.println(myacc.value());//此时调用的myacc其实是新建了一个然后复制的
Thread.currentThread().sleep(60 * 1000 * 1000);//暂停,可以在UI界面看到结果
sc.close();
}
}
在运行示例出现的情况:
1.在本人测试的时候只有第一种累加器能够在UI(默认为4040端口)界面看到结果,其余的两种都没有结果显示,查了下也不太清楚是什么原因。。。
2.需要注意的时:当使用自定义累加器的时候,当最后调用value放的时候,可以从打印的输出内容中看到执行了依次merge方法。