在Spark ML中,为什么在具有百万个不同值的列上拟合StringIndexer会产生OOM错误?

时间:2018-08-24 08:12:46

标签: apache-spark pyspark apache-spark-ml

我正在尝试在具有约15.000.000唯一字符串值的列上使用Spark的StringIndexer功能转换器。无论我投入多少资源,Spark都会因内存不足异常而死在我身上。

from pyspark.ml.feature import StringIndexer

data = spark.read.parquet("s3://example/data-raw").select("user", "count")

user_indexer = StringIndexer(inputCol="user", outputCol="user_idx")

indexer_model = user_indexer.fit(data) # This never finishes

indexer_model \
    .transform(data) \
    .write.parquet("s3://example/data-indexed")

驱动程序上会生成一个错误文件,其开头如下所示:

#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 268435456 bytes for committing reserved memory.
# Possible reasons:
#   The system is out of physical RAM or swap space
#   In 32 bit mode, the process size limit was hit
# Possible solutions:
#   Reduce memory load on the system
#   Increase physical memory or swap space
#   Check if swap backing store is full
#   Use 64 bit Java on a 64 bit OS
#   Decrease Java heap size (-Xmx/-Xms)
#   Decrease number of Java threads
#   Decrease Java thread stack sizes (-Xss)
#   Set larger code cache with -XX:ReservedCodeCacheSize=
# This output file may be truncated or incomplete.
#
#  Out of Memory Error (os_linux.cpp:2657)

现在,如果我尝试手动将值编入索引并将其存储在数据框中,则一切工作都像魅力一样,都在几个Amazon c3.2xlarge工人上完成。

from pyspark.sql.functions import row_number
from pyspark.sql.window import Window

data = spark.read.parquet("s3://example/data-raw").select("user", "count")

uid_map = data \
    .select("user") \
    .distinct() \
    .select("user", row_number().over(Window.orderBy("user")).alias("user_idx"))

data.join(uid_map, "user", "inner").write.parquet("s3://example/data-indexed")

我真的很想使用Spark提供的正式转换器,但是目前看来这是不可能的。关于如何进行这项工作的任何想法?

1 个答案:

答案 0 :(得分:1)

出现OOM错误的原因是在幕后,Spark的StringIndexer调用“用户”列上的countByValue以获取所有不同的值。

具有15M个不同的值,实际上是在驱动程序上创建了一个巨大的Map,并且该Map用完了内存...一个简单的解决方法是增加驱动程序的内存。如果您使用提交火花,则可以使用--driver-memory 16g。您还可以在配置文件中使用spark.driver.memory属性。

但是,随着不同值的数量增加,问题将再次发生。不幸的是,使用Spark的变压器您无能为力,这就是原因。实际上,在适应数据之后,这些变压器应进行序列化以备将来使用。因此,它们的设计并不是那么大(带有15M字符串的映射至少要重100MB)。我认为您需要为这么多类别重新考虑使用StringIndexer。在这里使用散列技巧可能会更合适。

最后,让我评论您的解决方法。使用窗口,您实际上将所有15M类别都放在一个分区上,因此放在了一个执行程序上。如果该数字增加,它将不会扩展。同样,使用非分区窗口通常也是一个坏主意,因为它会阻止并行计算(除了将所有内容放在同一分区上之外,这可能会导致OOM错误)。我会这样计算您的uid_map

# if you don't need consecutive indices
uid_map = data\
    .select("user")\
    .distinct()\
    .withColumn("user_idx", monotonically_increasing_id())

# if you do, you need to use RDDs
uid_rdd = data\
    .select("user")\
    .distinct()\
    .rdd.map(lambda x : x["user"])\
    .zipWithIndex()
uid_map = spark.createDataFrame(uid_rdd, ["user", "user_idx"])