Apache spark 在循环中评估Spark数据帧会随着每次迭代而变慢,所有工作都由控制器完成

Apache spark 在循环中评估Spark数据帧会随着每次迭代而变慢,所有工作都由控制器完成,apache-spark,pyspark,pyspark-sql,Apache Spark,Pyspark,Pyspark Sql,我正在尝试使用Spark cluster(在AWS EMR上运行)链接具有公共元素的项目组。基本上,我有一些元素的组,如果其中一些元素在多个组中,我想创建一个包含所有这些组中的元素的组 我了解GraphX库,并尝试使用包(ConnectedComponentsalgorithm)来解决此任务,但发现graphframes包还不够成熟,资源非常浪费。。。在我的数据集(cca 60GB)上运行它,无论我如何调整Spark参数、如何对数据进行分区和重新分区,或者创建了多大的集群(图是巨大的),它都会耗

我正在尝试使用Spark cluster(在AWS EMR上运行)链接具有公共元素的项目组。基本上,我有一些元素的组,如果其中一些元素在多个组中,我想创建一个包含所有这些组中的元素的组

我了解GraphX库,并尝试使用包(
ConnectedComponents
algorithm)来解决此任务,但发现
graphframes
包还不够成熟,资源非常浪费。。。在我的数据集(cca 60GB)上运行它,无论我如何调整Spark参数、如何对数据进行分区和重新分区,或者创建了多大的集群(图是巨大的),它都会耗尽内存

所以我写了自己的代码来完成任务代码工作正常,解决了我的问题,但每次迭代都会减慢速度。由于有时需要大约10次迭代才能完成,因此可能会运行很长时间,我无法找出问题所在。

我从一个表(DataFrame)
item\u链接开始,它有两列:
item
group\u name
。项目在每个组中是唯一的,但在此表中不是唯一的。一个项目可以在多个组中。如果两个项目各有一行具有相同的组名,则它们都属于同一组

我首先对每个项目进行分组,并从它所属的所有组中找到所有组名中最小的一个。我将此信息作为附加列附加到原始数据帧。然后,我通过按组名分组并在每个组中找到这个新列的最小值来创建一个新的DataFrame。我将此数据框与组名上的原始表联接,并用新列中的最小值替换组名列。其思想是,如果一个组包含一个也属于某个较小组的项目,则该组将与它合并。在每一次迭代中,它都链接由越来越多的项目间接链接的组

我正在运行的代码如下所示:

print(" Merging groups that have common items...")

n_partitions = 32

merge_level = 0

min_new_group = "min_new_group_{}".format(merge_level)

# For every item identify the (alphabetically) first group in which this item was found
# and add a new column min_new_group with that information for every item.
first_group = item_links \
                    .groupBy('item') \
                    .agg( min('group_name').alias(min_new_group) ) \
                    .withColumnRenamed('item', 'item_id') \
                    .coalesce(n_partitions) \
                    .cache()

item_links = item_links \
                .join( first_group,
                       item_links['item'] == first_group['item_id'] ) \
                .drop(first_group['item_id']) \
                .coalesce(n_partitions) \
                .cache()

first_group.unpersist()

# In every group find the (alphabetically) smallest min_new_group value.
# If the group contains a item that was in some other group,
# this value will be different than the current group_name.
merged_groups = item_links \
                    .groupBy('group_name') \
                    .agg(
                        min(col(min_new_group)).alias('merged_group')
                    ) \
                    .withColumnRenamed('group_name', 'group_to_merge') \
                    .coalesce(n_partitions) \
                    .cache()

# Replace the group_name column with the lowest group that any of the item in the group had.
item_links = item_links \
                .join( merged_groups,
                       item_links['group_name'] == merged_groups['group_to_merge'] ) \
                .drop(item_links['group_name']) \
                .drop(merged_groups['group_to_merge']) \
                .drop(item_links[min_new_group]) \
                .withColumnRenamed('merged_group', 'group_name') \
                .coalesce(n_partitions) \
                .cache()

# Count the number of common items found
common_items_count = merged_groups.filter(col('merged_group') != col('group_to_merge')).count()

merged_groups.unpersist()

# just some debug output
print("  level {}: found {} common items".format(merge_level, common_items_count))

# As long as the number of groups keep decreasing (groups are merged together), repeat the operation.
while (common_items_count > 0):
    merge_level += 1

    min_new_group = "min_new_group_{}".format(merge_level)

    # for every item find new minimal group...
    first_group = item_links \
                        .groupBy('item') \
                        .agg(
                            min(col('group_name')).alias(min_new_group)
                        ) \
                        .withColumnRenamed('item', 'item_id') \
                        .coalesce(n_partitions) \
                        .cache() 

    item_links = item_links \
                    .join( first_group,
                           item_links['item'] == first_group['item_id'] ) \
                    .drop(first_group['item']) \
                    .coalesce(n_partitions) \
                    .cache()

    first_group.unpersist()

    # find groups that have items from other groups...
    merged_groups = item_links \
                        .groupBy(col('group_name')) \
                        .agg(
                            min(col(min_new_group)).alias('merged_group')
                        ) \
                        .withColumnRenamed('group_name', 'group_to_merge') \
                        .coalesce(n_partitions) \
                        .cache()

    # merge the groups with items from other groups...
    item_links = item_links \
                    .join( merged_groups,
                           item_links['group_name'] == merged_groups['group_to_merge'] ) \
                    .drop(item_links['group_name']) \
                    .drop(merged_groups['group_to_merge']) \
                    .drop(item_links[min_new_group]) \
                    .withColumnRenamed('merged_group', 'group_name') \
                    .coalesce(n_partitions) \
                    .cache()

    common_items_count = merged_groups.filter(col('merged_group') != col('group_to_merge')).count()

    merged_groups.unpersist()

    print("  level {}: found {} common items".format(merge_level, common_items_count))
正如我所说,它是有效的,但问题是,每次迭代都会减慢速度。迭代1-3只运行几秒钟或几分钟。迭代5大约运行20-40分钟。迭代6有时甚至没有完成,因为控制器内存不足(控制器14 GB,整个集群有20个CPU核,大约140 GB的RAM…测试数据大约30 GB)

当我在Ganglia中监视集群时,我发现在每次迭代之后,工作人员执行的工作越来越少,控制器执行的工作越来越多。网络流量也降到零。在初始阶段之后,内存使用相当稳定

我读了很多关于重新分区、转换Spark参数和洗牌操作的背景知识,我尽了最大努力优化了所有内容,但我不知道这里发生了什么。下面是随着时间的推移,当上面的代码运行时,我的集群节点的负载(控制器节点为黄色)


您的代码具有正确的逻辑。只是您从未调用
item\u links.unpersist()
,所以首先它会变慢(尝试与本地磁盘交换),然后是OOM


神经节中的内存使用可能会产生误导。它不会改变,因为内存在脚本开始时分配给执行者,不管他们以后是否使用它。您可以检查Spark UI的存储/执行器状态。

我通过在每次迭代结束时将数据帧保存到HDFS,并在下一次迭代开始时从HDFS读取数据帧来解决此问题

因为我这样做了,程序运行起来很轻松,没有显示任何减速、过度使用内存或驱动程序过载的迹象


我仍然不明白为什么会发生这种情况,所以我把问题留给大家。

一个简单的复制场景:

import time
from pyspark import SparkContext

sc = SparkContext()

def push_and_pop(rdd):
    # two transformations: moves the head element to the tail
    first = rdd.first()
    return rdd.filter(
        lambda obj: obj != first
    ).union(
        sc.parallelize([first])
    )

def serialize_and_deserialize(rdd):
    # perform a collect() action to evaluate the rdd and create a new instance
    return sc.parallelize(rdd.collect())

def do_test(serialize=False):
    rdd = sc.parallelize(range(1000))
    for i in xrange(25):
        t0 = time.time()
        rdd = push_and_pop(rdd)
        if serialize:
            rdd = serialize_and_deserialize(rdd)
        print "%.3f" % (time.time() - t0)

do_test()
显示25次迭代期间的主要减速:

0.597 0.117 0.186 0.234 0.288 0.309 0.386 0.439 0.507 0.529 0.553 0.586 0.710 0.728 0.779 0.896 0.866 0.881 0.956 1.049 1.069 1.061 1.149 1.189 1.201

(由于初始化效应,第一次迭代相对较慢,第二次迭代较快,后续每次迭代较慢)

原因似乎是不断增长的惰性转换链。我们可以通过使用一个动作汇总RDD来检验这个假设

do_test(True)
0.897 0.256 0.233 0.229 0.220 0.238 0.234 0.252 0.240 0.267 0.260 0.250 0.244 0.266 0.295 0.464 0.292 0.348 0.320 0.258 0.250 0.201 0.197 0.243 0.230


collect()
parallelize()
为每次迭代增加约0.1秒,但完全消除了增量减速。

尝试打印数据帧。请解释以查看逻辑计划。 每次迭代,这个数据框架上的转换都会不断增加到逻辑计划中,因此计算时间也会不断增加

您可以使用以下解决方案作为解决方案:

dataFRame.rdd.localCheckpoint()

这会将此数据帧的RDD写入内存并删除沿袭,然后根据写入内存的数据创建RDD

这样做的好处是,您不需要将RDD写入HDFS或磁盘。
然而,这也带来了一些问题,这些问题可能会影响你,也可能不会影响你。您可以阅读“LocalCheckpoint”方法的文档了解详细信息。

我只是尝试在循环中取消持久化数据帧,但没有真正起到作用。这个问题与这里描述的情况非常相似:除了我使用spark参数(
spark.default.parallelism
spark.sql.shuffle.partitions
)将分区保持在正常的级别,甚至手动合并,这只是为了确保。还有其他想法吗?你一直在控制分区,所以这应该是原因。鉴于此