当我定义一个局部变量(例如大量复杂对象)并在pyspark的UDF中使用它时,会发生什么。让我以此为例:
huge_list = [<object_1>, <object_2>, ..., <object_n>]
@udf
def some_function(a, b):
l = []
for obj in huge_list:
l.append(a.operation(obj))
return l
df2 = df.withColumn('foo', some_function(col('a'), col('b')))
它是自动广播的吗?还是节点与主机进行通信以每次获取其数据?我使用这种方法会有哪些性能损失?有更好的吗? (考虑到每次应用UDF都从头开始构建huge_list
会更糟)
答案 0 :(得分:1)
查看代码,可以看到发生了以下情况:每个udf this function被调用一次,这通过CloudPickleSerializer
腌制this函数中的可调用对象。它还具有将可腌制的可调用大小与1Mb的hardcoded threshold进行比较的逻辑。如果大小较大,则会广播腌制命令,而改为腌制类型pyspark.broadcast.Broadcast
的对象(由于该对象几乎是一个引用,其序列化值显然很短)。读取腌制的可呼叫项的位置似乎是here。我的理解是执行者从头开始为每个新任务执行创建一个python进程。对于每个使用的udf,它将获得腌制的命令并将其解开,或者(对于广播)将需要从JVM获取广播的值并将其解开。
据我了解,如果在此处创建pyspark.broadcast.Broadcast
对象,则该执行者将通过python worker.py进程创建的所有执行者将其值保留在以后的所有查找中。
因此,如果您想回答是否将广播某些功能的问题,可以重复执行pyspark相同的操作,然后查看腌制对象是否大于1Mb,例如像这样:
from pyspark.serializers import CloudPickleSerializer
ser = CloudPickleSerializer()
x = [i**2 for i in range(10**5)]
v = ser.dumps(lambda : x)
print(len(v)) # 607434 - less than 1Mb, won't be broadcast
关于替代方法,我认为我看到的唯一替代方法(每次调用udf'ed函数时都会创建新对象,而这已经被解释为太昂贵了)将是创建一个模块,该模块将在此过程中创建有问题的对象输入。在这种情况下,将为每个任务执行创建一次对象。因此,这几乎可以让您选择(a)如果仅允许udf函数捕获对象,则每个任务执行一次通过CloudPickleSerializer
反序列化对象;或者(b)每个任务执行通过导入模块创建对象一次。更快的是一个单独的问题-但我想答案可能取决于所讨论的对象。在每种情况下,似乎都很容易测量。