一、介绍

在默认情况下,当Spark在集群的多个不同节点的多个任务上并行运行一个函数时,它会把函数中涉及到的每个变量,在每个任务上都生成一个副本。但是,有时候需要在多个任务之间共享变量,或者在任务(Task)和任务控制节点(Driver Program)之间共享变量。为了满足这种需求,Spark提供了两种类型的变量

二、广播变量Broadcast Variables

广播变量用来把变量在所有节点的内存之间进行共享,在每个机器上缓存一个只读的变量,而不是为机器上的每个任务都生成一个副本

  • 原理
  • 广播变量允许开发人员在每个节点(Worker or Executor)缓存只读变量,而不是在Task之间传递这些变量。使用广播变量能够高效地在集群每个节点创建大数据集的副本。同时Spark还是用高效的广播算法分发这些变量,从而减少通信的开销
  • 可以通过调节sc.broadcast(v)创建一个广播变量,该广播变量的值封装在v变量中,可使用获取该变量的方法进行访问
  • 使用
from pyspark import SparkContext, SparkConf

if __name__ == '__main__':
    print("PySpark Broadcast Program")
    # 创建应用程序入口SparkContext实例对象
    conf = SparkConf().setAppName("miniProject").setMaster("local[*]")
    sc = SparkContext.getOrCreate(conf)
    # 定义累加器
    kvFruit = sc.parallelize([(1, "apple"), (2, "orange"), (3, "banana"), (4, "grape")])
    print(kvFruit.collect())
    fruitMap = kvFruit.collectAsMap()
    # print(fruitMap)     #{1: 'apple', 2: 'orange', 3: 'banana', 4: 'grape'}
    # print(type(fruitMap))   # 字典类型
    fruitlds = sc.parallelize([2, 4, 1, 3])
    # 定义累加函数实现累加功能
    fruitNames = fruitlds.map(lambda x: fruitMap[x]) # 这里根据字典的键得到value
    print(fruitNames.collect())
    print("停止PySpark SparkSession对象")
    # 关闭SparkContext
    sc.stop()

三、累加器Accumulators

累加器支持在所有不同节点之间进行累加计算(比如计数或者求和)

  • 原理
  • Spark提供的Accumulator,主要用于多个节点对一个变量进行共享性的操作。Accumulator只提供了累加的功能,即提供了多个task对一个变量并行操作的功能。但是task只能对Accumulator进行累加操作,不能读取Accumulator的值,只有Driver程序可以读取Accumulator的值。创建的Accumulator变量的值能够在Spark Web UI上看到,在创建时应该尽量为其命名
  • Spark内置了三种类型的Accumulator,分别是LongAccumulator用来累加整数型,DoubleAccumulator用来累加浮点型,CollectionAccumulator用来累加集合元素
  • 特性:
  • 累加器能保证在Spark任务出现问题被重启的时候不会出现重复计算
  • 累加器只有在Action执行的时候才会被触发
  • 累加器只能在Driver端定义,在Executor端更新,不能再Executor端定义,不能在Executor端.value获取值
  • 实例
  • 不使用累加器
from pyspark import SparkContext, SparkConf

if __name__ == '__main__':
    print("PySpark RDD Program")
    # 创建应用程序入口SparkContext实例对象
    conf = SparkConf().setAppName("miniProject").setMaster("local[*]")
    sc = SparkContext.getOrCreate(conf)

    # 定义累加器
    num = 10    # 如果这里改变变量为10,就得不到150的累加值

    # 定义累加函数实现累加功能
    def f(x):
        global num
        num += x

    rdd = sc.parallelize([20, 30, 40, 50])
    rdd.foreach(f)
    print(num)  # 如果num=10,此时打印num可以查看并没有实现分布式数据的累加
    print("停止Pyspark SparkSession对象")
    # 关闭SparkContext
    sc.stop()
  • 使用累加器
from pyspark import SparkConf, SparkContext

if __name__ == '__main__':
    print('PySpark RDD Program')
    # 创建应用程序入口SparkContext实例对象
    conf = SparkConf().setAppName("miniProject").setMaster("local[*]")
    sc = SparkContext.getOrCreate(conf)

    # 定义累加器
    num = sc.accumulator(10)    # 如果这里改变为变量10,就得不到150的累加值

    # 定义累加函数实现累加功能
    def f(x):
        global num
        num += x

    rdd = sc.parallelize([20, 30, 40, 50])
    rdd.foreach(f)
    final = num.value
    print("Accumulated value is -> %i" % (final))
    print("停止PySpark SparkSession对象")
    # 关闭SparkContext
    sc.stop()

四、累加器注意事项

spark 广播变量的使用 pyspark广播变量_spark


spark 广播变量的使用 pyspark广播变量_spark_02


spark 广播变量的使用 pyspark广播变量_函数实现_03

from pyspark.sql import SparkSession

if __name__ == '__main__':
    spark = SparkSession.builder.appName("broadcast").getOrCreate()
    sc = spark.sparkContext

    acc = sc.accumulator(0)

    def judge_even(row_data):
        """
        过滤奇数,计数偶数个数
        """
        global acc
        if row_data%2 == 0:
            acc += 1
            return 1
        else:
            return 0

    a_list = sc.parallelize([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    even_num = a_list.filter(judge_even)
    print(f'the accumulator value is {acc}')
    # the accumulator value is 0

    """    
    分析:为什么会出现这个结果呢?
    这是因为Spark中的一系列的转化(transform)算子操作会构成一长串的任务链,只有当存在行动(action)算子操作时,才会进行真正的运算
    累加器也同理
    上述代码中并没有action算子,因此累加器并没有进行累加
    """


    # 增加一个action算子count操作
    print(f'even_num.count {even_num.count()}')  # even_num.count 5
    print(f'the accumulator value is {acc}')    # accumulator value is 5

    # 扩展2
    # print(f'even_num.count {even_num.count()}')   # even_num.count 5
    print(f'even_num.collect {even_num.collect()}') # even_num.collect [2, 4, 6, 8, 10]
    print(f'the accumulator value is {acc}')        # the accumulator value is 10

    """    
    分析:我们可以看到实际上经过过滤之后的偶数为5个,但是累加器给出的数值是10个,为两倍的关系,那么为什么会是这种结果呢?
    这就涉及到Spark运行机制的问题了
    当我们遇到第一个action算子count的时候,它就会从头开始计算,这时累加器就会累加到5,直到输出count的值
    当我们遇到第二个action算子collect时,由于前面没有缓存数据可以直接加载,因此也只能从头计算,在从头计算时,这是accumulator已经是5了,
    在计算过程中累加器同样会被再执行一次,因此最后会输出10
    """

    # 扩展3:继续验证
    print(f'even_num.count {even_num.count()}')     # even_num.count 5
    print(f'after the first action operator the accumulator is {acc}')      # after the first action operator the accumulator is 5
    print(f'even_num.collect {even_num.collect()}') # even_num.collect [2, 4, 6, 8, 10]
    print(f'after the second action operator the accumulator is {acc}')     # after the second action operator the accumulator is 10

    # 扩展4
    """
    分析:遇到以上问题我们应该怎么解决这个问题呢?
    解决这个问题只需要切断他们之间的依赖关系即可
    即:在累加器计算之后进行持久化操作,这样的话,第二次action操作就会从缓存的数据开始计算,不会再重复进行累加器计数
    """
    # 增加cache
    even_num = a_list.filter(judge_even).cache()
    print(f'even_num.count {even_num.count()}')     # even_num.count 5
    print(f'after the first action operator the accumulator is {acc}')  # after the first action operator the accumulator is 5
    print(f'even_num.collect {even_num.collect()}') # even_num.collect [2, 4, 6, 8, 10]
    print(f'after the second action operator the accumulator is {acc}') # after the second action operator the accumulator is 5

    # 扩展5:释放缓存位置不对
    even_num = a_list.filter(judge_even).cache()
    print(f'even_num.count {even_num.count()}') # even_num.count 5
    print(f'after the first action operator the accumulator is {acc}')  # after the first action operator the accumulator is 5

    # 对缓存进行释放
    even_num.unpersist()
    print(f'even_num.collect {even_num.collect()}')     # ven_num.collect [2, 4, 6, 8, 10]
    print(f'after the second action operator the accumulator is {acc}') # after the second action operator the accumulator is 10

    """
    分析:这是因为第一次action算子操作后,存在一步释放内存的操作,当执行第二个action算子时,
    首先会将rdd的缓存释放,然后再对rdd进行collect操作,而由于rdd没有被缓存,
    因此想要被collect必须从头计算,那么累加器又一次被重新计算,因此又变为两倍
    """

    sc.stop()