Apache spark spark流媒体和连接池的实现

Apache spark spark流媒体和连接池的实现,apache-spark,spark-streaming,Apache Spark,Spark Streaming,spark streaming网站在中提到以下代码: dstream.foreachRDD { rdd => rdd.foreachPartition { partitionOfRecords => // ConnectionPool is a static, lazily initialized pool of connections val connection = ConnectionPool.getConnection() partitionOfR

spark streaming网站在中提到以下代码:

dstream.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    // ConnectionPool is a static, lazily initialized pool of connections
    val connection = ConnectionPool.getConnection()
    partitionOfRecords.foreach(record => connection.send(record))
    ConnectionPool.returnConnection(connection)  // return to the pool for future reuse
  }
}
我尝试使用org.apache.commons.pool2实现此功能,但运行应用程序失败,出现预期的java.io.NotSerializableException:

15/05/26 08:06:21 ERROR OneForOneStrategy: org.apache.commons.pool2.impl.GenericObjectPool
java.io.NotSerializableException: org.apache.commons.pool2.impl.GenericObjectPool
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
 ...
我想知道实现一个可序列化的连接池有多现实。有人成功地做到了这一点吗


多谢各位

下面的答案是错误的 我把答案留在这里作为参考,但答案是错误的,原因如下
socketPool
被声明为
lazy val
,因此它将在每次首次请求访问时被实例化。由于SocketPool案例类不是可序列化的,这意味着它将在每个分区中实例化。这使得连接池毫无用处,因为我们希望保持分区和RDD之间的连接。它是作为一个伴生对象实现还是作为一个case类实现都没有区别。底线是:连接池必须是可序列化的,而apachecommons池则不是

import java.io.PrintStream
import java.net.Socket

import org.apache.commons.pool2.{PooledObject, BasePooledObjectFactory}
import org.apache.commons.pool2.impl.{DefaultPooledObject, GenericObjectPool}
import org.apache.spark.streaming.dstream.DStream

/**
 * Publish a Spark stream to a socket.
 */
class PooledSocketStreamPublisher[T](host: String, port: Int)
  extends Serializable {

    lazy val socketPool = SocketPool(host, port)

    /**
     * Publish the stream to a socket.
     */
    def publishStream(stream: DStream[T], callback: (T) => String) = {
        stream.foreachRDD { rdd =>

            rdd.foreachPartition { partition =>

                val socket = socketPool.getSocket
                val out = new PrintStream(socket.getOutputStream)

                partition.foreach { event =>
                    val text : String = callback(event)
                    out.println(text)
                    out.flush()
                }

                out.close()
                socketPool.returnSocket(socket)

            }
        }
    }

}

class SocketFactory(host: String, port: Int) extends BasePooledObjectFactory[Socket] {

    def create(): Socket = {
        new Socket(host, port)
    }

    def wrap(socket: Socket): PooledObject[Socket] = {
        new DefaultPooledObject[Socket](socket)
    }

}

case class SocketPool(host: String, port: Int) {

    val socketPool = new GenericObjectPool[Socket](new SocketFactory(host, port))

    def getSocket: Socket = {
        socketPool.borrowObject
    }

    def returnSocket(socket: Socket) = {
        socketPool.returnObject(socket)
    }

}
您可以按如下方式调用:

val socketStreamPublisher = new PooledSocketStreamPublisher[MyEvent](host = "10.10.30.101", port = 29009)
socketStreamPublisher.publishStream(myEventStream, (e: MyEvent) => Json.stringify(Json.toJson(e)))
要解决这个“本地资源”问题,需要一个单例对象,即保证在JVM中实例化一次且仅实例化一次的对象。幸运的是,Scala
object
提供了这个现成的功能

要考虑的第二件事是,这个单独的一个将为运行在同一个JVM上的所有托管任务提供服务,因此,它必须关心并发和资源管理。 让我们试着画出(*)这样的服务:

class ManagedSocket(专用val池:对象池,val套接字:套接字){
def release()=pool.returnObject(套接字)
}
//单态对象
对象套接字池{
var hostPortPool:Map[(字符串,Int),ObjectPool]=Map()
sys.addShutdownHook{
hostPortPool.values.foreach{//终止每个池}
}
//工厂法
def应用(主机:字符串,端口:字符串):ManagedSocket={
val pool=hostPortPool.getOrElse{(主机,端口){
val p=???//为(主机、端口)创建新池
hostPortPool+=(主机,端口)->p
P
}
新ManagedSocket(池,池.对象)
}
}
然后用法变成:

val host = ???
val port = ???
stream.foreachRDD { rdd =>
    rdd.foreachPartition { partition => 
        val mSocket = SocketPool(host, port)
        partition.foreach{elem => 
            val os = mSocket.socket.getOutputStream()
            // do stuff with os + elem
        }
        mSocket.release()
    }
}
我假设问题中使用的
GenericObjectPool
负责并发性。否则,需要通过某种形式的同步来保护对每个
实例的访问


(*)提供的代码说明了如何设计此类对象的想法-需要额外的努力才能转换为工作版本。

实际上,您希望将池设置为
对象
,这是每个运行执行器的JVM的一个单例。这样,初始化只会发生一次,并且可以保持状态(如使用中的插座)在Spark任务之间。我如何在对象中配置主机和端口等属性?因为这正是我使用case类而不是对象的原因。您的说明是正确的-此版本将为每个执行器上的mapPartitions的每次交互重新实例化池-我花了几分钟时间草拟一个可行的解决方案;请参阅答案。这种方法似乎有效。非常感谢。我将其标记为首选答案。对于此答案的具体实现,请参阅在Java中使用ThreadLocal而不是Singleton更好。使用ThreadLocal,每个线程都有一个实例,因此可以避免并发问题。@CarlosVerdes取决于您的需要。如果实现缓存,拥有一个ThreadLocal实例将意味着使用该代码路径复制n倍于可能线程数的数据,这可能是一个选项。映射是否应该是并发的,并且池条目是否应该以原子方式使用
putIfAbsent
创建?这个解决方案是可以的。但是,为什么我们需要一个池呢?我们每个分区都有连接,通常每个分区只有一个线程,所以每个分区都不会有多个连接因为singleton是好的,因为它的主要目的是在spark流媒体中重用每个微批次的连接。所以我们不会在每个微批次中重新创建DBdao(在短时间间隔流媒体的情况下这会很糟糕)。