Spark-Core之共享变量

参考官网:http://spark.apache.org/docs/latest/rdd-programming-guide.html#shared-variables

默认情况下,如果在一个算子函数中使用到了某个外部的变量,那么这个变量的值会被拷贝到每个task中。此时每个task只能操作自己的那份变量副本。意思就是说当Spark在集群的多个不同节点的多个任务上并行运行一个函数的时候,它会把函数中涉及到的每个变量在每个节点每个任务上都生成一个副本。Spark 操作实际上操作的是这个函数所用变量的一个独立副本。这些变量被复制到每台机器上,并且这些变量在远程机器上的所有更新都不会传递回驱动程序。通常跨任务的读写变量是低效的(就是多线程的去操作这些变量)。
如果需要在多个任务之间共享变量,就需要共享变量了。

Spark为此提供了两种有限类型的共享变量,一种是Broadcast Variable(广播变量),另一种是Accumulator(累加变量)。Broadcast Variable会将使用到的变量,仅仅为每个节点拷贝一份,更大的用处是优化性能,减少网络传输以及内存消耗。Accumulator则可以让多个task共同操作一份变量,主要可以进行累加操作。

Broadcast Variable

广播变量允许开发人员在每个节点(Worker or Executor)缓存只读变量,而不是在Task之间传递这些变量。使用广播变量能够高效地在集群每个节点创建大数据集的副本。同时Spark还使用高效的广播算法分发这些变量,从而减少通信的开销。

广播变量是将变量复制到每一台机器上而不是像普通变量那样每个task复制,它是高效的,广播变量只能读取,并不能修改。 经典应用是大表与小表的join中通过广播变量小表来实现以brodcast join取代reduce join。

在这里插入图片描述

举个例子,假如说有在函数的外部定义了一个普通的变量,这个变量的大小为10M,那么现在你去用一个函数操作一个RDD,比如这样:RDD.foreach(x =>{对这个变量进行一波操作}), 里面有1000个task,那么每个task都会有一个变量的拷贝,现在就需要10G的资源,这样肯定是不行的。如果用广播变量的话,这个数据集比较大,只是每个机器拷贝一个副本,很多的task共同使用这个副本。

Spark的任务操作一般会跨越多个阶段,对于每个阶段内的所有任务所需要的公共数据,Spark都会自动进行广播。

看官网例子:

//把这个值广播出去
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)

用的时候取出来
scala> broadcastVar.value
res0: Array[Int] = Array(1, 2, 3)

上面只是简单的,生产上是如何使用广播变量呢?

broadcast join举例

在spark core里面,如何实现广播变量?
经典应用比如:大表与小表的join中通过广播变量小表来实现以brodcast join 取代reduce join
用map join可以实现它,不过map join在广播变量里是叫broadcast join。
(mapjoin在hive优化里讲到过)
举个例子:
现在两个表
表1:

学号 名字
601 zhansan
602 lisi
603 wangwu

表2:

学号 年龄 性别
601 25 man
602 20 woman
604 18 man
605 30 woman

通过学号进行关联,想要得到的结果:

学号 名字 年龄 性别
601 zhansan 25 man
602 lisi 20 woman
先看一下普通的join如何实现?

定义一个函数:

  def commonJoin(sc:SparkContext)={
    val info1 = sc.parallelize(Array(("601","zhangsan"),("602","lisi"),("603","wangwu")))
    val info2 = sc.parallelize(Array(("601",25,"man"),("602",20,"woman"),("604",18,"man"),("605",30,"woman")))
        .map(x =>(x._1,x))

    info1.join(info2).foreach(println)
  }

输出为:

(602,(lisi,(602,20,woman)))
(601,(zhangsan,(601,25,man)))

这不是我们想要的,需要修改一下,整个代码为:

package com.ruozedata.spark.com.ruozedata.spark.core
import org.apache.spark.{SparkConf, SparkContext}

object BroadcastApp {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("BroadcastApp").setMaster("local[2]")
    val sc = new SparkContext(sparkConf)
 
    //调用函数,把sc传进去
    commonJoin(sc)
    
    sc.stop()
  }

	//定义一个函数,实现普通join
  def commonJoin(sc:SparkContext)={
  
    //定义两个RDD
    val info1 = sc.parallelize(Array(("601","zhangsan"),("602","lisi"),("603","wangwu")))
    val info2 = sc.parallelize(Array(("601",25,"man"),("602",20,"woman"),("604",18,"man"),("605",30,"woman")))
        .map(x =>(x._1,x))
    //info2 的定义中,有三个值,无法判断哪个是key,哪个是value,所以需要转换一下,x的第一个值是key,整体是value


//上面输出的格式不是我们想要的:(602,(lisi,(602,20,woman))),(601,(zhangsan,(601,25,man)))
//所以需要按照下面来修改一下,整体是x,学号是x中第一个元素,名字是x中的第二个元素中的第一个元素,年龄是x中的第二个元素中的第二个元素中的第二个元素,所以需要这样写的,比较绕哈。。。
    info1.join(info2).map(x =>{
      "学号: " + x._1 + " 名字: " + x._2._1 + " 年龄: " + x._2._2._2 + " 性别: " + x._2._2._3
    }).foreach(println)
  }

}

输出结果:

学号: 601 名字: zhangsan 年龄: 25 性别: man
学号: 602 名字: lisi 年龄: 20 性别: woman

现在来看一下web UI,这里因为是在本地Windows的IDEA上写的代码运行的,在本地,所以需要通过http://localhost:4040这样来访问UI界面。但是需要在代码中加入Thread.sleep(200000)这一行,让程序多跑一会,(在 sc.stop()前一行加入),不然 sc.stop()掉之后就看不到页面了。加入这一行之后,再运行,看一下页面:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过上面图可以知道,总共有6个task,三个stage,stage0是对于info1而言的,stage1是对于info2而言的,stage0和stage1遇到join有shuffle,拆分成第三个stage,stage2中join之后,再做一次map。
可以看到,普通的join是存在shuffle的。

再用broadcast join来实现?
package com.ruozedata.spark.com.ruozedata.spark.core
import org.apache.spark.{SparkConf, SparkContext}

object BroadcastApp {
  def main(args: Array[String]): Unit = {
    val sparkConf = new SparkConf().setAppName("BroadcastApp").setMaster("local[2]")
    val sc = new SparkContext(sparkConf)

    broadcastJoin(sc)

    Thread.sleep(200000)
    sc.stop()
  }

  def broadcastJoin(sc:SparkContext)= {

    //假设info1是小变量,那么需要广播出去的
    //广播是需要先到driver端的,从driver端广播出去,但是最好以map的方式去广播出去,这种方式比较好一些
    //定义info1的时候,后面需要加个.collectAsMap(),不加也可以(一般不会这样直接广播),换成collect也可以,但是用.collectAsMap()最好
    //因为后面用的方式是map类型的,根据key来进行join,直接调用map.get就可以拿到key对应的值了
      //不能直接去广播RDD,要把RDD的结果广播出去
    val info1 = sc.parallelize(Array(("601", "zhangsan"), ("602", "lisi"), ("603", "wangwu")))
                    .collectAsMap()
    val info1Broadcast = sc.broadcast(info1)

    //假设info2是大变量
    //info2 的定义中,有三个值,无法判断哪个是key,哪个是value,所以需要转换一下,x的第一个值是key,整体是value
    val info2 = sc.parallelize(Array(("601", 25, "man"), ("602", 20, "woman"), ("604", 18, "man"), ("605", 30, "woman")))
      .map(x => (x._1, x))

    //broadcast出去以后就不会再用join来实现
    //和之前hive提到的MapReduce中的mapjoin一样
    //大表的数据读取出来一条数据就和广播出去的小表的记录做匹配,匹配上就ok,匹配不上就忽略

    //现在对info2的数据进行一个map,这里建议用mapPartitions
    info2.mapPartitions(x => {
      //把广播变量里的值拿出来,就拿到map了
      val broadcastMap = info1Broadcast.value

      //对info2里面的每一条数据进行一下迭代
      //info2里面元素是key,value结构,用for对里面的每个元素遍历一下
      //从info2里拿到一个元素,赋值给(key,value),然后根据广播变量broadcastMap进行过滤,看看广播变量里面是否存在这个key
      //如果存在就执行循环体,就存下来,如果不存在就忽略
      //yeild是将循环中的符合条件的元素先添加到缓存中,循环结束后把这个集合返回出去
      //broadcastMap.get(key).getOrElse("")是根据这个key获取这个值,如果找不到这个key就置空
      //因为broadcastMap是 (学号 -> 姓名)这样的key value结构,所以broadcastMap.get(key)就拿到姓名了
      //value._2是年龄,value._3是性别
      for((key,value) <- x if(broadcastMap.contains(key)))
        yield (key,broadcastMap.get(key).getOrElse(""),value._2,value._3)
    }).foreach(println)

  }
}

输出结果:

(601,zhangsan,25,man)
(602,lisi,20,woman)

然后去看一下UI界面:http://localhost:4040/jobs/
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面可以看出,这个过程中有2个job,因为遇到collectAsMap和foreach这两个action,触发了两个job,每个job有2个task,只有1个stage。中间并不存在shuffle的。 在mapPartitions里,从info2里拿到每一条数据,然后都会跟广播变量做对比,进行过滤,并不存在shuffle。

这样处理性能会更好,但是小表的数据不能太大,如果太大,内存装不下,会发生OOM,内存溢出。
但是如果小表的数据量很小,就建议这种方法了。

Accumulator

可以叫计数器或者累加器

累加器是通过 一个关联和交换的操作 只能进行累加的变量,因此可以有效地支持并行,多个task对一个变量并行的操作。它们可以用来实现计数器(如MapReduce)或求和的操作。Spark 本身支持数字类型的累加器,程序员可以添加对新类型的支持,自定义的。
作为用户,您可以创建命名或匿名的累加器。
可以通过SparkContext.longAccumulator()或者SparkContext.doubleAccumulator()来创建累加器。运行在集群中的任务,就可以使用add()方法来把数值累加到累加器上。但是任务节点执行做累加操作,不能读取累加器的值,只有任务控制节点(Driver Program)可以使用value方法来读取。

参数有两个(Int,String),第一个参数为初始累加值,默认为0,第二个参数为累加器的名字。
在这里插入图片描述
在IDEA中可以看到定义的时候有一个带有名字的,有一个不带有名字的,看自己需要来选择。

可以看官网例子:

scala> val accum = sc.longAccumulator("My Accumulator")
accum: org.apache.spark.util.LongAccumulator = LongAccumulator(id: 0, name: Some(My Accumulator), value: 0)

scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum.add(x))
...
10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s

scala> accum.value
res2: Long = 10

在这里插入图片描述
在Web UI的stage界面里面,可以看到上面的,从界面可以看到,总共有8个task,8个并行度。我自己跑的话就只有两个task。

累加器只能由Spark内部进行更新,并保证每个任务在累加器的更新操作仅执行一次,也就是说重启任务也不应该更新。在转换操作中,用户必须意识到任务和作业的调度过程重新执行会造成累加器的多次更新。

参考博客:
https://blog.csdn.net/anbang713/article/details/81588829
https://www.cnblogs.com/zzhangyuhang/p/9005347.html
https://blog.csdn.net/qq_32641659/article/details/90183882#23_REST_API_64

猜你喜欢

转载自blog.csdn.net/liweihope/article/details/93300705
今日推荐