Spark Streaming - 读写Kafka主题

时间:2015-07-23 14:39:29

标签: scala apache-kafka spark-streaming spark-streaming-kafka

我正在使用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?

8 个答案:

答案 0 :(得分:27)

是的,不幸的是Spark(1.x,2.x)并没有直截了当地如何以有效的方式写信给Kafka。

我建议采用以下方法:

  • 每个执行程序进程/ JVM使用(并重复使用)一个KafkaProducer实例。

以下是此方法的高级设置:

  1. 首先,你必须“包装”Kafka的KafkaProducer,因为正如你所提到的,它不是可序列化的。包装它允许您将其“运送”给执行者。这里的关键思想是使用lazy val,以便延迟实例化生成器直到它第一次使用,这实际上是一种解决方法,因此您不必担心KafkaProducer不可序列化。 / LI>
  2. 使用广播变量将包装的生产者“运送”给每个执行者。
  3. 在您的实际处理逻辑中,您可以通过广播变量访问包装的生产者,并使用它将处理结果写回Kafka。
  4. 下面的代码片段与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中实例化重型对象是官方文档建议的那样)。

另一种选择是使用对象池,如下例所示:

https://github.com/miguno/kafka-storm-starter/blob/develop/src/main/scala/com/miguno/kafkastorm/kafka/PooledKafkaProducerAppFactory.scala

然而,我发现在使用检查点时很难实现。

另一个适合我的版本是工厂,如以下博文中所述,您只需检查它是否提供了足够的并行性以满足您的需求(请查看评论部分):

http://allegro.tech/2015/08/spark-kafka-integration.html

答案 2 :(得分:8)

有一个由Cloudera维护的Streaming Kafka Writer(实际上是从Spark JIRA [1]分离出来的)。它基本上为每个分区创建一个生产者,这可以分摊创建“重”的时间。对象(希望很大)的元素集合。

可以在这里找到作家:https://github.com/cloudera/spark-kafka-writer

答案 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上都可以进行读写操作

从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类型的操作,例如filteraggselect ..etc。

将流写入Kafka主题

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

创建一个KafkaSinkWritter

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()
      }
   }

使用SinkWriter编写消息

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