Pyspark共享变量: 累加器和广播变量

Pyspark版本: V3.2.1

1. 共享变量

一般来说,当一个被传递给Spark操作的函数在一个远程集群上运行时,该函数实际上操作的是它用到的所有变量的独立副本。这些变量会被复制到每一台机器上,在远程机器上对变量的所有更新都不会传回主驱动程序。举例如下:

而有时我们需要变量能够在任务中共享,或者在任务与驱动程序之间共享。Spark提供了两种模式的共享变量:

  • 广播变量(Broadcast):可以在内存的所有节点中被访问,用于缓存变量;
  • 累加器(Accumulator):只能用来做加法的变量;

2. 累加器

累加器是一种只能通过关联操作进行“加”操作的变量,可以将Worker节点中的值聚合Driver端中。常规的累加器使用方法如下:

from pyspark import SparkConf,SparkContext
from pyspark.accumulators import AccumulatorParam
conf=SparkConf()
conf.setMaster('local[1]').setAppName('test')
sc=SparkContext(conf=conf)
sc.setLogLevel('ERROR')

accu=sc.accumulator(0)
rdd=sc.parallelize([1,2,3,4,5,6,7,8,9,10])

def fun1(x):
    global accu
    if x>5:
        accu+=1
rest=rdd.foreach(fun1)
print(accu.value) #值为5

累加器需要遇到行动算子才会执行,所以这个例子中直接使用了foreach算子。

 2.1 累加器变量的方法/属性

在Pyspark中,累加器变量具有的方法和属性主要如下:

方法/属性作用
Accumulator.value获取累加器的值,只能在Driver端使用;
Accumulator.add(term)将term加入到累加器中,只能在Executor端使用

基于以上方法和属性的累加器的定义及使用方法如下:

accu=sc.accumulator(0)
rdd=sc.parallelize([1,2,3,4,5,6,3],2)

def fun2(x):
    #这个函数在Executor端执行,
    accu.add(x)

rdd.foreach(fun2)
print(accu.value) #Driver端

fun2函数的定义相比于fun1函数,不需要使用global来声明accu。另外, Accumulator类的定义使用__iadd__()重载了运算符“+=”,所以fun1中累加器的具体实现是由Accumulator.__iadd__()方法来实现的,__iadd__()方法的源码如下(用到了Accumulator.add方法):

    def __iadd__(self, term: T) -> "Accumulator[T]":
        """The += operator; adds a term to this accumulator's value"""
        self.add(term)
        return self

2.2 累加器类型

目前使用SparkContext.accumulator创建累加器时初始值的默认类型为:Int型、Double型及Complex型(复数类型)。Pyspark中的AddingAccumulatorParam类即为默认累加器类型。其源码如下:

class AddingAccumulatorParam(AccumulatorParam[U]):

    """
    An AccumulatorParam that uses the + operators to add values. Designed for simple types
    such as integers, floats, and lists. Requires the zero value for the underlying type
    as a parameter.
    """

    def __init__(self, zero_value: U):
        self.zero_value = zero_value

    def zero(self, value: U) -> U:
        return self.zero_value

    def addInPlace(self, value1: U, value2: U) -> U:
        value1 += value2  # type: ignore[operator]
        return value1

INT_ACCUMULATOR_PARAM = AddingAccumulatorParam(0)  # type: ignore[type-var]
FLOAT_ACCUMULATOR_PARAM = AddingAccumulatorParam(0.0)  # type: ignore[type-var]
COMPLEX_ACCUMULATOR_PARAM = AddingAccumulatorParam(0.0j)  # type: ignore[type-var]

SparkContext.accumulator(value,accum_param)会根据value的类型将accum_param设置为对应的INT_ACCUMULATOR_PARAM、FLOAT_ACCUMULATOR_PARAM、COMPLEX_ACCUMULAOTR_PARAM类型。若value初始值类型不符合这三种形式,并且没有指定accum_param参数时将会直接提示TypeErrorl错误。举例如下:

2.3 自定义累加器

通过观察AddingAccumulatorParam类的源码可以发现,可以利用这个累加器实现一些特殊功能。举例如下:

from pyspark.accumulators import AddingAccumulatorParam
rdd_1=sc.parallelize([[1,2],[2,3,4],[3,4]])
accu=sc.accumulator([],accum_param=AddingAccumulatorParam([]))
res=rdd_1.foreach(lambda x:accu.add(x))
print(accu.value) #值为:[1,2,2,3,4,3,4]

Tips:因为AddingAccumulatorParam中实现了__init__()方法,所以在使用该类给accum_param赋值时,要给AddingAccumulatorParam赋初值。

上述累加器可以将rdd_1中的值都汇集到一个List中。所以这个变量叫累加器,但是其实现的功能不仅仅只是字面意义上的“求和”。想要实现特殊的功能,就需要自定义累加器了。

在Pyspark中要先自定义累加器,必须定义一个继承AccumulatorParam类,并实现zero()和addInPlace()方法(AddingAccumulatorParam方法还自定义了__init__方法)。现在我们来看看这两个函数的作用。

  • zero(value):用来设置Executor端task内部累加操作时的初始化默认值(每个分区一个初始值)。

举例如下:

class MyAccum1(AccumulatorParam):
    def zero(self,value):
        return value
    def addInPlace(self,value1,value2):
        print(value1,value2)
        value1+=value2
        return value1

def fun1(x):
    accu.add(x)

rdd=sc.parallelize([1,2,3,4,5],2)
accu=sc.accumulator(2,accum_param=MyAccum1())
rdd.foreach(fun1)
print(accu.value) #值为21

Tips:定义累加器的时候参数accum_param接收的是实例化的变量,所以MyAccum1后面一定加括号。否则提示:PicklingError: Could not serialize object: TypeError: zero() missing 1 required positional argument: 'value'

这里rdd中的数据分了两个分区,每一个分区通过zero(value)获取累加器初始值。这里要注意,zero()方法中的value由SparkContext.accumulator()中指定的初始值对其传参。

上述累加器的最终结果为21。其计算流程如下(假设rdd中1和2在一个分区,3、4、5在一个分区):

对于分区1:该分区的计算结果:2(该分区的累加器初始值)+1+2=5

对于分区2:该分区的计算结果:2(该分区的累加器初始值)+3+4+5=14

累加器的结果:2(累加器初始值)+5+14=21

  • addInPlace(value1,value2):累加器计算

前文提到的累加器accu.add()方法,其实其具体实现就是由addInPlace()方法决定的。addInPlace()方法需要实现三种情况下得操作:分区内每个task内部的操作;分区间的第一次操作;分区间的非第一次操作。

以上文中的MyAccum1为例进行说明:addInPlace()方法中需要实现分区1和分区2内的操作,即每个task内的操作;而分区间的第一次操作即累加器初始值为一个分区的结果之间的操作,比如上述案例中“累加器结果”中的2+5部分;而分区间的非第一次操作即为“累加器结果”中的7+14部分(7为分区间的第一次操作结果)。

addInPlace(value1,value2)方法实现的时候要注意,累加器的初始值、每个task内的累加器初始值、每个rdd需要累加的数值,若这三种值的类型一致的话,那么addInPlace()实现起来会比较简单。举例如下:

class MyAccum2(AccumulatorParam):
    def zero(self,value):
        return value
    def addInPlace(self,value1,value2):
        for k,v in value2.items():
            value1[k]+=v
        return value1

rdd=sc.parallelize([1,2,3,4,5,6,7,8,9,10])
accu=sc.accumulator({'sum':0,'count':0},accum_param=MyAccum2())
rdd.foreach(lambda x:accu.add({'sum':x,'count':1}))
print(accu.value) #{'sum':55,'count':10}

当类型不一致时,addInPlace(value1,value2)就需要根据value1、value2的类型进行判断。举例如下:

class MyAccum2(AccumulatorParam):
    def zero(self,value):
        return {'sum':0,'count':0}
    def addInPlace(self,value1,value2):
        if value1=='': #分区间第一次操作
            return value2
        elif type(value2)==int: #每个task内的操作
            value1['sum']+=value2
            value1['count']+=1
        else:
            for k,v in value2.items(): #分区间非第一次操作
                value1[k]+=v
        return value1

rdd=sc.parallelize([1,2,3,4,5,6,7,8,9,10])
accu=sc.accumulator('',accum_param=MyAccum2())
rdd.foreach(lambda x:accu.add(x))
print(accu.value) #{'sum':55,'count':10}

在这个例子中,累加器初始值是一个空字符串,每个task内的累加器初始值为一个字典,但每个rdd分区内与累加器进行操作的变量上为Int型。

2.4 注意事项

前文提到累加器只有遇到行动算子才会执行。但是累加器在遇到多次action操作的时候会出现重复累加求和的问题。举例如下:

rdd=sc.parallelize([1,2,3,4,5,6,7,8,9,10])
accu=sc.accumulator(0)
def fun3(x):
    global accu
    accu+=x
    return x
rdd1=rdd.map(fun3)
res=rdd1.collect() #第一次行动操作
print(accu.value) #值为55
res=rdd1.foreach(lambda x:print(x)) #第二次行动操作
print(accu.value) #值为110

第一次输出是accu.value的值为55,第二次输出是accu.value的值为110。所以转化操作map中的累加器被多次执行了。为了避免这种情况,可以在转化操作后面添加cache()操作。具体如下:

rdd2=rdd.map(fun3).cache()  #添加cache()操作
res=rdd2.collect() #第一次行动操作
print(accu.value) #值为55
res=rdd2.foreach(lambda x:print(x)) #第二次行动操作
print(accu.value) #值为55

3. 广播变量

广播变量是将一个变量通过广播的形式发送到每个Worker节点的缓存中,而不是发送到每个Task任务中,这样同一个Worker节点中的Task任务可以共享该变量的数据。举例如下:

from pyspark import SparkConf,SparkContext

conf=SparkConf()
conf.setMaster('local[1]').setAppName('test')
sc=SparkContext(conf=conf)
sc.setLogLevel('ERROR')

#不使用广播变量
val=10
rdd_1=sc.parallelize([1,3,45,6],2)
rest_1=rdd_1.map(lambda x:x+val).collect()
print(rest_1)

#使用广播变量
bc_val=sc.broadcast(10)
rest_2=rdd_1.map(lambda x:x+bc_val.value).collect()
print(rest_2)

上述代码中的bc_val即为广播变量。运行上述代码可以发现rest1和rest2的结果是完全相同的(这里不在展示结果)。当不使用广播变量时,val会发送到每个Task任务中(如下图所示):

当使用广播变量时bc_val会分发给每个Work中,而Work中的每一个Task共享一份变量,这样可以减少网络传输和内存开销(如下图所示):

3.1 广播变量的方法/属性

Pysparkh中提供的广播变量类型的基本方法及属性主要包括以下几种:

方法/属性作用
Broadcast.value返回广播变量的值;
Broadcast.destory()删除与该广播变量相关的所有数据及元数据等;
Broadcast.dump(value,f)
Broadcast.load(f)
Broadcast.load_from_path(path)
Broadcast.unpersist()删除广播变量在Worker节点上的缓存副本;

其具体用法举例如下:

  • 广播变量的创建和使用

在Pyspark中,除了能使用简单变量创建广播变量之外,还可以读入序列化后的数据来创建广播变量。具体代码如下:

from pyspark import Broadcast
import pickle
a=[1,2,3,4]
pickle.dump(a,open(r'test.pkl','wb')) #将变量a保存到文件test.pkl中

bc_val1=Broadcast(path='test.pkl')
print(bc_val1) #值为[1,2,3,4]

当使用序列化后的文件声明广播变量时,其value值的计算会用方法:load/load_from_path等。

  • 删除广播变量缓存

正如前文所述,广播变量会在每个Executo上保存一份副本,也就是说广播变量会持续占用内存,当暂时不需要使用该广播变量时,可以使用unpersist()方法暂时删除副本,释放内存资源。等到再需要使用时,Executor可以从Driver端或其他Executor重新拉取数据块。其用法如下:

rdd=sc.parallelize([1,2,3,4,5])
bc_val=sc.broadcast(10)
rest1=rdd.map(lambda x:x+bc_val.value).collect()
print(rest1)
bc_val.unpersist() #释放副本
rest2=rdd.map(lambda x:x+2*bc_val.value).collect() #Executo会重新拉取bc_val,所以代码执行不报错
print(rest2)

其结果如下:

[11, 12, 13, 14, 15]
[21, 22, 23, 24, 25]

  • 广播变量的销毁

当广播变量不再需要之后,需要将其销毁。具体用法如下:

bc_val1.destroy()
print(bc_val1.value) #代码仍能执行
rest2=rdd.map(lambda x:x+bc_val1.value[0]).collect() #报错
print(rest2) #报错

广播变量销毁之后,仍然可以通过value属性查看其值,但是不能在算子中继续使用。

参考资料:

  1. 《Spark核心技术与高级应用》
  2. 《Spark大数据分析实战》
  3. 累加器执行的过程 - 简书
  4. PySpark 累加器使用及自定义累加器_rgc_520_zyl的博客-CSDN博客_pyspark自定义累加器
  5. PySpark之Spark的共享变量(广播变量和累加器)_pyspark 广播变量_飞Link的博客-CSDN博客
  6. (5)pyspark----共享变量 - 吱吱了了 - 博客园
  7. Spark Core — PySpark 3.3.1 documentation
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值