我正在尝试找出从Spark调用Rest端点的最佳方法。
我当前的方法(解决方案[1])看起来像这样-
val df = ... // some dataframe
val repartitionedDf = df.repartition(numberPartitions)
lazy val restEndPoint = new restEndPointCaller() // lazy evaluation of the object which creates the connection to REST. lazy vals are also initialized once per JVM (executor)
val enrichedDf = repartitionedDf
.map(rec => restEndPoint.getResponse(rec)) // calls the rest endpoint for every record
.toDF
我知道我本可以使用.mapPartitions()而不是.map(),但是从DAG来看,似乎spark可以优化重新分区->无论如何映射到mapPartition。
在第二种方法(解决方案[2])中,将为每个分区创建一次连接,并对该分区中的所有记录重新使用。
val newDs = myDs.mapPartitions(partition => {
val restEndPoint = new restEndPointCaller /*creates a db connection per partition*/
val newPartition = partition.map(record => {
restEndPoint.getResponse(record, connection)
}).toList // consumes the iterator, thus calls readMatchingFromDB
restEndPoint.close() // close dbconnection here
newPartition.iterator // create a new iterator
})
在第三种方法(解决方案[3])中,每个JVM(执行程序)都会在执行程序处理的所有分区之间重用一次创建连接。
lazy val connection = new DbConnection /*creates a db connection per partition*/
val newDs = myDs.mapPartitions(partition => {
val newPartition = partition.map(record => {
readMatchingFromDB(record, connection)
}).toList // consumes the iterator, thus calls readMatchingFromDB
newPartition.iterator // create a new iterator
})
connection.close() // close dbconnection here
[a]与解决方案[1]和[3]非常相似,我对 lazy val 的工作方式的理解正确吗?目的是将每个执行者/ JVM的连接数限制为1,并重用打开的连接来处理后续请求。我将为每个JVM创建1个连接还是为每个分区创建1个连接? [b]有什么方法可以控制我们向其余端点发出的请求数(RPS)? [b]请让我知道是否有更好,更有效的方法。谢谢!
答案 0 :(得分:2)
IMO使用mapPartitions
的第二个解决方案更好。首先,您明确地告诉您期望实现的目标。转换的名称和实现的逻辑可以很清楚地告诉它。对于第一个选项,您需要了解Apache Spark如何优化处理。也许对您来说这很明显,但是您还应该考虑将要使用您的代码的人员,或者仅仅考虑6个月,1年,2年等等。而且他们应该比mapPartitions
+ repartition
更了解map
。
此外,使用地图进行分区的优化可能会在内部发生变化(我不相信,但是您仍然可以将其作为有效的观点),这时您的工作将会做得更糟。
最后,使用第二种解决方案,可以避免序列化可能遇到的许多问题。在您编写的代码中,驱动程序将创建终结点对象的一个实例,对其进行序列化并发送给执行者。所以是的,也许只有一个实例,但前提是它是可序列化的。
[编辑]
感谢您的澄清。您可以通过不同的方式来实现您想要的东西。要使每个JVM仅有1个连接,可以使用一种称为单例的设计模式。在Scala中,它很容易表示为object
(我在Google https://alvinalexander.com/scala/how-to-implement-singleton-pattern-in-scala-with-object上找到的第一个链接)
这非常好,因为您不需要序列化任何东西。单例直接从执行器端的类路径读取。有了它,您可以确定只有给定对象的一个实例。
[a]与解决方案[1]和[3]非常相似,是我的 了解val延迟工作如何正确?目的是 将每个执行器/ JVM的连接数限制为1,然后重用 用于处理后续请求的开放连接。我会吗 每个JVM创建1个连接还是每个分区创建1个连接? 每个分区将创建1个连接。您可以执行此小测试以查看:
class SerializationProblemsTest extends FlatSpec {
val conf = new SparkConf().setAppName("Spark serialization problems test").setMaster("local")
val sparkContext = SparkContext.getOrCreate(conf)
"lazy object" should "be created once per partition" in {
lazy val restEndpoint = new NotSerializableRest()
sparkContext.parallelize(0 to 120).repartition(12)
.mapPartitions(numbers => {
//val restEndpoint = new NotSerializableRest()
numbers.map(nr => restEndpoint.enrich(nr))
})
.collect()
}
}
class NotSerializableRest() {
println("Creating REST instance")
def enrich(id: Int): String = s"${id}"
}
它应该打印创建REST实例 12次(分区数)
[b]有什么方法可以控制请求数(RPS) 我们对其余端点进行操作?
要控制请求数,可以使用类似于数据库连接池的方法:HTTP连接池(一个快速找到的链接:HTTP connection pooling using HttpClient)。
但是也许另一种有效的方法是处理较小的数据子集?因此,您无需将其处理30000行,而是可以将其拆分为不同的较小的微型批次(如果是流式作业)。它应该给您的Web服务更多的“休息”。
否则,您也可以尝试发送批量请求(Elasticsearch这样做是为了一次索引/删除多个文档https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html)。但这取决于Web服务。