spark之共享变量

如果在一个算子的函数中使用到了某个外部的变量,那么这个变量的值会被拷贝到每个task中,此时每个task只能操作自己的那份变量副本。如果多个task想要共享某个变量,那么这种方式是做不到的

3大数据结构

rdd
广播变量 分布式只读共享变量
累加器 分布式只写共享变量

广播变量(调优策略)

Broadcast Variable会将用到的变量,仅仅为每个节点拷贝一份,即每个Executor拷贝一份,更大的用途是优化性能,减少网络传输以及内存损耗

特点

  1. 能不能将一个RDD使用广播变量广播出去? 不能 ,因为RDD是不存数据的。可以将RDD的结果广播出去。
  2. 广播变量是在driver端定义,在Executor端只读的共享变量
  3. 我们可以将大型外部变量封装为广播变量,此时一个Executor保存一个变量副本,此Executor上的所有task共用此变量,减少变量到各个节点的网络传输消耗,以及在各个节点上的内存消耗
    不时用广播变量时,如果executor端用到了Driver的变量,每个task都会保存一份它所使用的外部变量的副本,当一个Executor上的多个task都使用一个大型外部变量时,对于Executor内存的消耗是非常大的
  4. 经典应用是大表与小表的join中通过广播变量小表来实现以brodcast join 取代reduce join
  5. 直接从本地的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实现自定义的累加器。

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

  1. 继承AccumulatorV2,实现相关方法
  2. 创建自定义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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

orange大数据技术探索者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值