我正在使用Spark Streaming处理两个Kafka队列之间的数据,但我似乎找不到从Spark写Kafka的好方法。我试过这个:
input.foreachRDD(rdd =>
rdd.foreachPartition(partition =>
partition.foreach {
case x: String => {
val props = new HashMap[String, Object]()
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer")
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer")
println(x)
val producer = new KafkaProducer[String, String](props)
val message = new ProducerRecord[String, String]("output", null, x)
producer.send(message)
}
}
)
)
它按预期工作,但是为每条消息实例化一个新的KafkaProducer在真实环境中显然是不可行的,我正在尝试解决它。
我想为每个进程保留一个实例的引用,并在需要发送消息时访问它。如何从Spark Streaming写入Kafka?
答案 0 :(得分:27)
是的,不幸的是Spark(1.x,2.x)并没有直截了当地如何以有效的方式写信给Kafka。
我建议采用以下方法:
KafkaProducer
实例。以下是此方法的高级设置:
KafkaProducer
,因为正如你所提到的,它不是可序列化的。包装它允许您将其“运送”给执行者。这里的关键思想是使用lazy val
,以便延迟实例化生成器直到它第一次使用,这实际上是一种解决方法,因此您不必担心KafkaProducer
不可序列化。 / LI>
下面的代码片段与Spark 2.0的Spark Streaming一起使用。
第1步:结束KafkaProducer
import java.util.concurrent.Future
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord, RecordMetadata}
class MySparkKafkaProducer[K, V](createProducer: () => KafkaProducer[K, V]) extends Serializable {
/* This is the key idea that allows us to work around running into
NotSerializableExceptions. */
lazy val producer = createProducer()
def send(topic: String, key: K, value: V): Future[RecordMetadata] =
producer.send(new ProducerRecord[K, V](topic, key, value))
def send(topic: String, value: V): Future[RecordMetadata] =
producer.send(new ProducerRecord[K, V](topic, value))
}
object MySparkKafkaProducer {
import scala.collection.JavaConversions._
def apply[K, V](config: Map[String, Object]): MySparkKafkaProducer[K, V] = {
val createProducerFunc = () => {
val producer = new KafkaProducer[K, V](config)
sys.addShutdownHook {
// Ensure that, on executor JVM shutdown, the Kafka producer sends
// any buffered messages to Kafka before shutting down.
producer.close()
}
producer
}
new MySparkKafkaProducer(createProducerFunc)
}
def apply[K, V](config: java.util.Properties): MySparkKafkaProducer[K, V] = apply(config.toMap)
}
第2步:使用广播变量为每个执行者提供自己的包裹KafkaProducer
实例
import org.apache.kafka.clients.producer.ProducerConfig
val ssc: StreamingContext = {
val sparkConf = new SparkConf().setAppName("spark-streaming-kafka-example").setMaster("local[2]")
new StreamingContext(sparkConf, Seconds(1))
}
ssc.checkpoint("checkpoint-directory")
val kafkaProducer: Broadcast[MySparkKafkaProducer[Array[Byte], String]] = {
val kafkaProducerConfig = {
val p = new Properties()
p.setProperty("bootstrap.servers", "broker1:9092")
p.setProperty("key.serializer", classOf[ByteArraySerializer].getName)
p.setProperty("value.serializer", classOf[StringSerializer].getName)
p
}
ssc.sparkContext.broadcast(MySparkKafkaProducer[Array[Byte], String](kafkaProducerConfig))
}
步骤3:从Spark Streaming写入Kafka,重新使用相同的包装KafkaProducer
实例(对于每个执行者)
import java.util.concurrent.Future
import org.apache.kafka.clients.producer.RecordMetadata
val stream: DStream[String] = ???
stream.foreachRDD { rdd =>
rdd.foreachPartition { partitionOfRecords =>
val metadata: Stream[Future[RecordMetadata]] = partitionOfRecords.map { record =>
kafkaProducer.value.send("my-output-topic", record)
}.toStream
metadata.foreach { metadata => metadata.get() }
}
}
希望这有帮助。
答案 1 :(得分:18)
我的第一个建议是尝试在foreachPartition中创建一个新实例,并测量它是否足够快以满足您的需求(在foreachPartition中实例化重型对象是官方文档建议的那样)。
另一种选择是使用对象池,如下例所示:
然而,我发现在使用检查点时很难实现。
另一个适合我的版本是工厂,如以下博文中所述,您只需检查它是否提供了足够的并行性以满足您的需求(请查看评论部分):
答案 2 :(得分:8)
有一个由Cloudera维护的Streaming Kafka Writer(实际上是从Spark JIRA [1]分离出来的)。它基本上为每个分区创建一个生产者,这可以分摊创建“重”的时间。对象(希望很大)的元素集合。
答案 3 :(得分:7)
我遇到了同样的问题并找到this post。
作者通过为每个执行者创建1个生成器来解决问题。他只发送一个“配方”,而不是发送生产者本身,如何通过广播来在执行者中创建生产者。
val kafkaSink = sparkContext.broadcast(KafkaSink(conf))
他使用了一个懒洋洋地创建生产者的包装器:
class KafkaSink(createProducer: () => KafkaProducer[String, String]) extends Serializable {
lazy val producer = createProducer()
def send(topic: String, value: String): Unit = producer.send(new ProducerRecord(topic, value))
}
object KafkaSink {
def apply(config: Map[String, Object]): KafkaSink = {
val f = () => {
val producer = new KafkaProducer[String, String](config)
sys.addShutdownHook {
producer.close()
}
producer
}
new KafkaSink(f)
}
}
包装器是可序列化的,因为Kafka生成器在首次使用执行程序之前初始化。驱动程序保留对包装器的引用,包装器使用每个执行程序的生产者发送消息:
dstream.foreachRDD { rdd =>
rdd.foreach { message =>
kafkaSink.value.send("topicName", message)
}
}
答案 4 :(得分:6)
使用Spark> = 2.2
使用结构化流API在Kafka上都可以进行读写操作
// Subscribe to a topic and read messages from the earliest to latest offsets
val ds= spark
.readStream // use `read` for batch, like DataFrame
.format("kafka")
.option("kafka.bootstrap.servers", "brokerhost1:port1,brokerhost2:port2")
.option("subscribe", "source-topic1")
.option("startingOffsets", "earliest")
.option("endingOffsets", "latest")
.load()
读取键和值并为两者应用模式,为简单起见,我们将它们都转换为String
类型。
val dsStruc = ds.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
.as[(String, String)]
由于dsStruc
具有架构,因此可以接受所有SQL类型的操作,例如filter
,agg
,select
..etc。
dsStruc
.writeStream // use `write` for batch, like DataFrame
.format("kafka")
.option("kafka.bootstrap.servers", "brokerhost1:port1,brokerhost2:port2")
.option("topic", "target-topic1")
.start()
更多configuration for Kafka integration to read or write
"org.apache.spark" % "spark-core_2.11" % 2.2.0,
"org.apache.spark" % "spark-streaming_2.11" % 2.2.0,
"org.apache.spark" % "spark-sql-kafka-0-10_2.11" % 2.2.0,
答案 5 :(得分:3)
为什么不可行?从根本上说,每个RDD的每个分区都将独立运行(并且可能在不同的集群节点上运行),因此您有在每个分区的任务开始时重做连接(以及任何同步)。如果开销太高,那么你应该增加StreamingContext
中的批量大小,直到它变得可接受为止(显然这样做有延迟成本)。
(如果您没有在每个分区中处理数千条消息,您确定需要火花流吗?使用独立应用程序会做得更好吗?)
答案 6 :(得分:2)
这可能就是你想要做的。您基本上为每个记录分区创建一个生产者。
input.foreachRDD(rdd =>
rdd.foreachPartition(
partitionOfRecords =>
{
val props = new HashMap[String, Object]()
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers)
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer")
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
"org.apache.kafka.common.serialization.StringSerializer")
val producer = new KafkaProducer[String,String](props)
partitionOfRecords.foreach
{
case x:String=>{
println(x)
val message=new ProducerRecord[String, String]("output",null,x)
producer.send(message)
}
}
})
)
希望有所帮助
答案 7 :(得分:0)
使用Spark <2.2
由于没有直接方法可以将消息从Spark Streaming写入Kafka
import java.util.Properties
import org.apache.kafka.clients.producer._
import org.apache.spark.sql.ForeachWriter
class KafkaSink(topic:String, servers:String) extends ForeachWriter[(String, String)] {
val kafkaProperties = new Properties()
kafkaProperties.put("bootstrap.servers", servers)
kafkaProperties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
kafkaProperties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")
val results = new scala.collection.mutable.HashMap[String, String]
var producer: KafkaProducer[String, String] = _
def open(partitionId: Long,version: Long): Boolean = {
producer = new KafkaProducer(kafkaProperties)
true
}
def process(value: (String, String)): Unit = {
producer.send(new ProducerRecord(topic, value._1 + ":" + value._2))
}
def close(errorOrNull: Throwable): Unit = {
producer.close()
}
}
val topic = "<topic2>"
val brokers = "<server:ip>"
val writer = new KafkaSink(topic, brokers)
val query =
streamingSelectDF
.writeStream
.foreach(writer)
.outputMode("update")
.trigger(ProcessingTime("25 seconds"))
.start()
参考link