一、介绍
在默认情况下,当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()
四、累加器注意事项
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()