Spark 闭包、累加器和广播变量

Spark 闭包、累加器和广播变量


先看以下的 Spark 代码:

object Test {
  def main(args:Array[String]):Unit = {
      val conf = new SparkConf().setAppName("test");
      val sc = new SparkContext(conf)

      val rdd = sc.parallelize(List(1,2,3))
      var counter = 0
      //warn: don't do this
      rdd.foreach(x => counter += x)
      println("Counter value: "+counter)

      sc.stop()
    }
}

最终的结果为 counter = 0 ,为什么呢?

闭包


Spark 中的闭包: 跨作用域访问函数变量,特指 不在算子的作用域内部,但是在作用域外部却被算子处理和操作了的变量

上面的 counter 变量的作用域在 Driver,而 rdd.foreach() 是在 Executor 上进行运行的; 而现在 foreach() 函数里面使用了
Driver 端定义的变量,这就出现了跨作用域访问变量;

出现这种情况时,Spark 会在 Driver 端将这些变量进行拷贝,将 counter 副本序列化发送到 Executor 进行使用; 因此闭包变量发送到executor进程中之后,就变成了一个一个独立的变量副本了;

此时 Driver 中的 counter 变量和 Executor 变量就不是一个东西了,即使在 Executor 对 counter 变量进行修改了不会影响 Driver 中的 counter 变量。

Spark 提供了两种解决对统一变量进行处理的方式:累加器和广播变量

累加器


累加器的原理:将定义的变量分发到各个 task 进行计算,之后将每个副本变量的最终值传回 Driver,由 Driver 聚合后得到最终值,并更新原始变量。

Spark内置了三种类型的Accumulator,分别是LongAccumulator用来累加整数型,DoubleAccumulator用来累加浮点型,CollectionAccumulator用来累加集合元素。

基本使用:

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.CollectionAccumulator;
import org.apache.spark.util.DoubleAccumulator;
import org.apache.spark.util.LongAccumulator;
 
import java.util.Arrays;
 
 
/**
 * 累加器的基本使用
 *
 * @author CC11001100
 */
public class AccumulatorsSimpleUseDemo {
 
    public static void main(String[] args) {
 
        SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
        SparkContext sc = spark.sparkContext();
 
        // 内置的累加器有三种,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");
 
        Dataset<Integer> num1 = spark.createDataset(Arrays.asList(1, 2, 3), Encoders.INT());
        Dataset<Integer> num2 = num1.map((MapFunction<Integer, Integer>) x -> {
            longAccumulator.add(x);
            doubleAccumulator.add(x);
            collectionAccumulator.add(x);
            return x;
        }, Encoders.INT()).cache();
 
        num2.count();
 
        System.out.println("longAccumulator: " + longAccumulator.value());
        System.out.println("doubleAccumulator: " + doubleAccumulator.value());
        // 注意,集合中元素的顺序是无法保证的,多运行几次发现每次元素的顺序都可能会变化
        System.out.println("collectionAccumulator: " + collectionAccumulator.value());
 
    }
 
}
  • 自定义累加器

当内置的Accumulator无法满足要求时,可以继承AccumulatorV2实现自定义的累加器。

实现自定义累加器的步骤:

  1. 继承AccumulatorV2,实现相关方法

  2. 创建自定义Accumulator的实例,然后在SparkContext上注册它

自定义累加器的使用:

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.AccumulatorV2;
 
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
 
/**
 * 自定义累加器
 *
 * @author CC11001100
 */
public class CustomAccumulatorDemo {
 
    // 需要注意的是累加操作不能依赖顺序,比如类似于StringAccumulator这种则会得到错误的结果
    public static class BigIntegerAccumulator extends AccumulatorV2<BigInteger, BigInteger> {
 
        private BigInteger num = BigInteger.ZERO;
 
        public BigIntegerAccumulator() {
        }
 
        public BigIntegerAccumulator(BigInteger num) {
            this.num = new BigInteger(num.toString());
        }
 
        @Override
        public boolean isZero() {
            return num.compareTo(BigInteger.ZERO) == 0;
        }
 
        @Override
        public AccumulatorV2<BigInteger, BigInteger> copy() {
            return new BigIntegerAccumulator(num);
        }
 
        @Override
        public void reset() {
            num = BigInteger.ZERO;
        }
 
        @Override
        public void add(BigInteger num) {
            this.num = this.num.add(num);
        }
 
        @Override
        public void merge(AccumulatorV2<BigInteger, BigInteger> other) {
            num = num.add(other.value());
        }
 
        @Override
        public BigInteger value() {
            return num;
        }
    }
 
    public static void main(String[] args) {
 
        SparkSession spark = SparkSession.builder().master("local[*]").getOrCreate();
        SparkContext sc = spark.sparkContext();
 
        // 直接new自定义的累加器
        BigIntegerAccumulator bigIntegerAccumulator = new BigIntegerAccumulator();
        // 然后在SparkContext上注册一下
        sc.register(bigIntegerAccumulator, "bigIntegerAccumulator");
 
        List<BigInteger> numList = Arrays.asList(new BigInteger("9999999999999999999999"), new BigInteger("9999999999999999999999"), new BigInteger("9999999999999999999999"));
        Dataset<BigInteger> num = spark.createDataset(numList, Encoders.kryo(BigInteger.class));
        Dataset<BigInteger> num2 = num.map((MapFunction<BigInteger, BigInteger>) x -> {
            bigIntegerAccumulator.add(x);
            return x;
        }, Encoders.kryo(BigInteger.class));
 
        num2.count();
        System.out.println("bigIntegerAccumulator: " + bigIntegerAccumulator.value());
 
    }
 
}

广播变量


在使用累加器时,如果变量很大并且 task 数很多的话,那么将会占用 task 所在 Executor 的内存的大量内存以及消耗大量的网络IO; 为了解决这种情况,Spark 提供了广播变量(Broadcast Variable);

广播变量的原理是将变量发送到每个 Executor 上,每个 Executor 只要一份变量副本,Executor 上的 task 共用该变量数据(只读),这在变量较大 task 数较多的情况下,就减少了很多内存和网络IO的消耗;

简单使用:

package com.add

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

object BroadCastDemo1 {
  def main(args: Array[String]): Unit = {
    val bigArr = 1 to 1000 toArray
    val conf: SparkConf = new SparkConf().setAppName("BroadCastDemo1").setMaster("local[2]")
    val sc: SparkContext = new SparkContext(conf)
    // 广播出去.
    val bd = sc.broadcast(bigArr)
    val list1 = List(30, 50000000, 70, 600000, 10, 20)
    val rdd1: RDD[Int] = sc.parallelize(list1, 4)
    //获取 广播变量的引用
    // 过滤rdd1和bigArr有交集的变量
    val rdd2 = rdd1.filter(x => bd.value.contains(x))
    rdd2.collect.foreach(println)
    Thread.sleep(1000000)
    sc.stop()
  }
}

输出:

30
70
10
20

广播变量和累加器的区别:

广播变量是共享读变量,task不能去修改它,而累加器可以让多个task操作一个变量

参考:


https://www.cnblogs.com/cc11001100/p/9901606.html
https://blog.csdn.net/hu_lichao/article/details/112451982
https://juejin.cn/post/6844904047015624712#heading-22

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值