将mapPartitions与Mongo连接器配合使用时,“ IllegalStateException:状态应为:打开”

时间:2019-04-25 12:41:16

标签: mongodb apache-spark

设置

我有一个简单的Spark应用程序,它使用mapPartitions来转换RDD。作为此转换的一部分,我从Mongo数据库中检索了一些必要的数据。使用适用于Spark的MongoDB连接器(https://docs.mongodb.com/spark-connector/current/)管理Spark工作者到Mongo数据库的连接。

我使用的是mapPartitions而不是简单的map,因为存在一些相对昂贵的设置,对于分区中的所有元素只需要一次。如果我改用map,则必须分别对每个元素重复此设置。

问题

当源RDD中的分区之一足够大时,转换失败并显示消息

IllegalStateException: state should be: open

或偶尔

IllegalStateException: The pool is closed

代码

下面是一个简单的Scala应用程序的代码,可以用来重现该问题:

package my.package

import com.mongodb.spark.MongoConnector
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.SparkSession
import org.bson.Document

object MySparkApplication {
    def main(args: Array[String]): Unit = {
        val sparkSession: SparkSession = SparkSession.builder()
            .appName("MySparkApplication")
            .master(???) // The Spark master URL
            .config("spark.jars", ???) // The path at which the application's fat JAR is located.
            .config("spark.scheduler.mode", "FAIR")
            .config("spark.mongodb.keep_alive_ms", "86400000")
            .getOrCreate()

        val mongoConnector: MongoConnector = MongoConnector(Map(
            "uri" -> ??? // The MongoDB URI.
            , "spark.mongodb.keep_alive_ms" -> "86400000"
            , "keep_alive_ms" -> "86400000"
        ))

        val localDocumentIds: Seq[Long] = Seq.range(1L, 100L)
        val documentIdsRdd: RDD[Long] = sparkSession.sparkContext.parallelize(localDocumentIds)

        val result: RDD[Document] = documentIdsRdd.mapPartitions { documentIdsIterator =>
            mongoConnector.withMongoClientDo { mongoClient =>
                val collection = mongoClient.getDatabase("databaseName").getCollection("collectionName")
                // Some expensive query that should only be performed once for every partition.
                collection.find(new Document("_id", 99999L)).first()

                documentIdsIterator.map { documentId =>
                    // An expensive operation that does not interact with the Mongo database.
                    Thread.sleep(1000)
                    collection.find(new Document("_id", documentId)).first()
                }
            }
        }

        val resultLocal = result.collect()
    }
}

堆栈跟踪

下面是当我运行上面的应用程序时Spark返回的堆栈跟踪:

Driver stacktrace:
    [...]
    at my.package.MySparkApplication.main(MySparkApplication.scala:41)
    at my.package.MySparkApplication.main(MySparkApplication.scala)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.spark.deploy.SparkSubmit$.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:775)
    at org.apache.spark.deploy.SparkSubmit$.doRunMain$1(SparkSubmit.scala:180)
    at org.apache.spark.deploy.SparkSubmit$.submit(SparkSubmit.scala:205)
    at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:119)
    at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala)
Caused by: java.lang.IllegalStateException: state should be: open
    at com.mongodb.assertions.Assertions.isTrue(Assertions.java:70)
    at com.mongodb.connection.BaseCluster.getDescription(BaseCluster.java:152)
    at com.mongodb.Mongo.getConnectedClusterDescription(Mongo.java:885)
    at com.mongodb.Mongo.createClientSession(Mongo.java:877)
    at com.mongodb.Mongo$3.getClientSession(Mongo.java:866)
    at com.mongodb.Mongo$3.execute(Mongo.java:823)
    at com.mongodb.FindIterableImpl.first(FindIterableImpl.java:193)
    at my.package.MySparkApplication$$anonfun$1$$anonfun$apply$1$$anonfun$apply$2.apply(MySparkApplication.scala:36)
    at my.package.MySparkApplication$$anonfun$1$$anonfun$apply$1$$anonfun$apply$2.apply(MySparkApplication.scala:33)
    at scala.collection.Iterator$$anon$11.next(Iterator.scala:409)
    at scala.collection.Iterator$class.foreach(Iterator.scala:893)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1336)
    at scala.collection.generic.Growable$class.$plus$plus$eq(Growable.scala:59)
    at scala.collection.mutable.ArrayBuffer.$plus$plus$eq(ArrayBuffer.scala:104)
    at scala.collection.mutable.ArrayBuffer.$plus$plus$eq(ArrayBuffer.scala:48)
    at scala.collection.TraversableOnce$class.to(TraversableOnce.scala:310)
    at scala.collection.AbstractIterator.to(Iterator.scala:1336)
    at scala.collection.TraversableOnce$class.toBuffer(TraversableOnce.scala:302)
    at scala.collection.AbstractIterator.toBuffer(Iterator.scala:1336)
    at scala.collection.TraversableOnce$class.toArray(TraversableOnce.scala:289)
    at scala.collection.AbstractIterator.toArray(Iterator.scala:1336)
    at org.apache.spark.rdd.RDD$$anonfun$collect$1$$anonfun$13.apply(RDD.scala:936)
    at org.apache.spark.rdd.RDD$$anonfun$collect$1$$anonfun$13.apply(RDD.scala:936)
    at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:2069)
    at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:2069)
    at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:87)
    at org.apache.spark.scheduler.Task.run(Task.scala:108)
    at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:338)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

我所做的研究

我发现有几个人在问这个问题,看来在所有情况下,问题都出在他们关闭Mongo客户端后,他们才开始使用。据我所知,这在我的应用程序中没有发生-打开和关闭连接应该由Mongo-Spark连接器处理,我希望客户端仅在函数传递给mongoConnector.withMongoClientDo之后关闭返回。

我确实设法发现该问题并不是RDD中的第一个元素出现的。相反,似乎许多元素都已成功处理,并且仅在过程花费了一定时间后才会发生失败。这个时间似乎在5到15秒左右。

上面的内容使我相信,一旦客户端处于活动状态一定时间,就会自动关闭该客户端,即使该客户端仍在使用中。

您可以从我的代码中看出,我发现一个事实,即Mongo-Spark连接器公开了一个配置spark.mongodb.keep_alive_ms,根据连接器文档,该配置控制“使MongoClient保持可用状态的时间长度分享”。它的默认值为5秒,因此尝试尝试似乎很有用。在上面的应用程序中,我尝试以三种不同的方式将其设置为一整天,并且效果为零。文档确实声明此特定属性“只能通过系统属性进行配置”。我认为这就是我正在做的事情(通过在初始化Spark会话和/或Mongo连接器时设置属性),但是我不确定。初始化Mongo连接器后,似乎无法验证设置。

另一个StackOverflow问题提到我应该尝试在maxConnectionIdleTime中设置MongoClientOptions选项,但据我所知无法通过连接器设置这些选项。

作为健全性检查,我尝试将mapPartitions的使用替换为功能上等效的map的使用。问题消失了,这可能是因为针对RDD的每个单独元素重新初始化了与Mongo数据库的连接。但是,如上所述,这种方法的性能会大大降低,因为我最终将对RDD中的每个元素重复进行昂贵的设置工作。

出于好奇,我还尝试将对mapPartitions的呼叫替换为对foreachPartition的呼叫,也将对documentIdsIterator.map的呼叫替换为documentIdsIterator.foreach。在这种情况下,问题也消失了。我不知道为什么会这样,但是因为我需要转换RDD,所以这也不是可接受的方法。

我正在寻找的答案

  • “您实际上正在过早关闭客户,这是这里:[...]”
  • “这是Mongo-Spark连接器中的一个已知问题,这是其问题跟踪器的链接:[...]”
  • “您错误地设置了spark.mongodb.keep_alive_ms属性,这是您应该这样做的方式:[...]”
  • “可以在您的Mongo连接器上验证spark.mongodb.keep_alive_ms的值,方法如下:[...]”
  • “可以通过Mongo连接器设置MongoClientOptions,例如maxConnectionIdleTime,方法如下:[...]”

编辑

进一步的调查得出以下见解: 连接器文档中使用的短语“系统属性”是指Java系统属性,可使用System.setProperty("spark.mongodb.keep_alive_ms", desiredValue)或命令行选项-Dspark.mongodb.keep_alive_ms=desiredValue进行设置。然后,MongoConnector单例对象读取该值,并将其传递给MongoClientCache。但是,设置此属性的两种方法均无效:

  • 从驱动程序调用{​​{1}}仅在JVM中为Spark驱动程序设置值,而在Spark worker的JVM中需要该值。
  • 从工作程序中调用System.setProperty()只会在System.setProperty()读取值之后才设置值。
  • 再次将命令行选项MongoConnector传递给Spark选项-Dspark.mongodb.keep_alive_ms只会在驱动程序的JVM中设置该值。
  • 将命令行选项传递给Spark选项spark.driver.extraJavaOptions会导致Spark产生错误消息:
spark.executor.extraJavaOptions

引发此错误的Spark代码位于Exception in thread "main" java.lang.Exception: spark.executor.extraJavaOptions is not allowed to set Spark options (was '-Dspark.mongodb.keep_alive_ms=desiredValue'). Set them directly on a SparkConf or in a properties file when using ./bin/spark-submit. 中,在其中检查包含字符串org.apache.spark.SparkConf#validateSettings的任何工作程序选项值。

这似乎是对Mongo连接器设计的疏忽;该属性应通过Spark会话设置(如我最初预期的那样),或者应将其重命名为不以-Dspark开头的名称。我将此信息添加到评论中提到的JIRA票证中。

2 个答案:

答案 0 :(得分:1)

核心问题是MongoConnector使用MongoClients缓存,并遵循借阅模式来管理该缓存。一旦归还所有借出的MongoClients,并且keep_alive_ms时间过去了,MongoClient将被关闭并从缓存中删除。

由于RDD的实现方式的本质(它们遵循Scala的惰性集合语义),因此以下代码:documentIdsIterator.map { documentId => ... }仅在一次处理。到那时,借用的MongoClient已经返回到缓存,并且keep_alive_ms之后MongoClient将关闭。这会导致客户端上出现state should be open异常。

如何解决?

  1. 一旦固定SPARK-246,就可以将keep_alive_ms设置得足够高,以使MongoClient在RDD处理期间不会关闭。但是,这仍然违反了MongoConnector使用的贷款模式的合同-因此应避免使用。
  2. 根据需要重新使用MongoConnector获取客户端。如果客户端可用,但由于某种原因客户端超时,则仍然可以使用这种方式使用缓存:
documentIdsRdd.mapPartitions { documentIdsIterator =>
   mongoConnector.withMongoClientDo { mongoClient =>
      // Do some expensive operation
      ...

      // Return the lazy collection
      documentIdsIterator.map { documentId => 
         // Loan the mongoClient
         mongoConnector.withMongoClientDo { mongoClient => ... }
      }
   }
 }

答案 1 :(得分:0)

连接对象通常紧密地绑定到上下文,在上下文中对其进行初始化。您不能简单地序列化这些对象并传递它们。相反,您应该在mapPartitions中就地初始化它们:

val result: RDD[Document] = documentIdsRdd.mapPartitions { documentIdsIterator =>
  val mongoConnector: MongoConnector = MongoConnector(Map(
    "uri" -> ??? // The MongoDB URI.
    , "spark.mongodb.keep_alive_ms" -> "86400000"
    , "keep_alive_ms" -> "86400000"
  ))
  mongoConnector.withMongoClientDo { mongoClient =>
   ...
 }
}