我有一个简单的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,所以这也不是可接受的方法。
spark.mongodb.keep_alive_ms
属性,这是您应该这样做的方式:[...]” spark.mongodb.keep_alive_ms
的值,方法如下:[...]” MongoClientOptions
,例如maxConnectionIdleTime
,方法如下:[...]” 进一步的调查得出以下见解:
连接器文档中使用的短语“系统属性”是指Java系统属性,可使用System.setProperty("spark.mongodb.keep_alive_ms", desiredValue)
或命令行选项-Dspark.mongodb.keep_alive_ms=desiredValue
进行设置。然后,MongoConnector
单例对象读取该值,并将其传递给MongoClientCache
。但是,设置此属性的两种方法均无效:
System.setProperty()
只会在System.setProperty()
读取值之后才设置值。MongoConnector
传递给Spark选项-Dspark.mongodb.keep_alive_ms
只会在驱动程序的JVM中设置该值。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票证中。
答案 0 :(得分:1)
核心问题是MongoConnector
使用MongoClients缓存,并遵循借阅模式来管理该缓存。一旦归还所有借出的MongoClients,并且keep_alive_ms
时间过去了,MongoClient
将被关闭并从缓存中删除。
由于RDD的实现方式的本质(它们遵循Scala的惰性集合语义),因此以下代码:documentIdsIterator.map { documentId => ... }
仅在一次处理。到那时,借用的MongoClient
已经返回到缓存,并且keep_alive_ms
之后MongoClient
将关闭。这会导致客户端上出现state should be open
异常。
如何解决?
keep_alive_ms
设置得足够高,以使MongoClient
在RDD处理期间不会关闭。但是,这仍然违反了MongoConnector
使用的贷款模式的合同-因此应避免使用。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 =>
...
}
}