从Spark调用休息服务

时间:2018-07-18 06:41:56

标签: scala rest apache-spark

我正在尝试找出从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]请让我知道是否有更好,更有效的方法。谢谢!

1 个答案:

答案 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服务。