web-dev-qa-db-fra.com

Spark Streaming - lecture et écriture sur Kafka topic

J'utilise Spark Le streaming pour traiter des données entre deux Kafka files d'attente, mais je n'arrive pas à trouver un bon moyen d'écrire Kafka de Spark, j'ai essayé ceci:

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

et cela fonctionne comme prévu, mais instiller un nouveau KafkaProducer pour chaque message est clairement irréalisable dans un contexte réel et j'essaie de le contourner.

Je souhaite conserver une référence à une seule instance pour chaque processus et y accéder lorsque j'ai besoin d'envoyer un message. Comment puis-je écrire à Kafka à partir de Spark Streaming?

33
Chobeat

Mon premier conseil serait d'essayer de créer une nouvelle instance dans foreachPartition et de déterminer si elle est assez rapide pour vos besoins (instancier des objets lourds dans foreachPartition est ce que la documentation officielle suggère).

Une autre option consiste à utiliser un pool d'objets comme illustré dans cet exemple:

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

J'ai toutefois trouvé difficile à mettre en œuvre lors de l'utilisation de points de contrôle.

Une autre version qui fonctionne bien pour moi est une usine telle que décrite dans l'article suivant, il vous suffit de vérifier si elle fournit suffisamment de parallélisme pour vos besoins (consultez la section commentaires):

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

18
Marius Soutier

Oui, malheureusement Spark (1.x, 2.x) ne permet pas de savoir comment écrire Kafka de manière efficace.

Je suggérerais l'approche suivante:

  • Utilisez (et réutilisez) une instance KafkaProducer par processus exécuteur/JVM.

Voici la configuration de haut niveau pour cette approche:

  1. Tout d'abord, vous devez "emballer" le KafkaProducer de Kafka car, comme vous l'avez dit, il n'est pas sérialisable. L'emballer vous permet de "l'expédier" aux exécuteurs. L'idée clé ici est d'utiliser un lazy val afin que vous retardiez l’instanciation du producteur jusqu’à sa première utilisation, ce qui est en fait une solution de contournement afin que vous n’ayez pas à vous soucier de KafkaProducer qui n’est pas sérialisable.
  2. Vous "expédiez" le producteur encapsulé à chaque exécuteur en utilisant une variable de diffusion.
  3. Dans votre logique de traitement réelle, vous accédez au producteur encapsulé via la variable de diffusion et vous l'utilisez pour écrire les résultats du traitement dans Kafka.

Les extraits de code ci-dessous fonctionnent avec Spark en streaming à partir de Spark 2.0.

Étape 1: Habillage 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)

}

Étape 2: Utilisez une variable de diffusion pour donner à chaque exécuteur sa propre instance KafkaProducer enveloppée

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

Étape 3: écrivez à partir de Spark Diffusion en continu sur Kafka, réutilisation du même exemple KafkaProducer enveloppé (pour chaque exécuteur)

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

J'espère que cela t'aides.

28
Michael G. Noll

Il y a un Streaming Kafka Writer maintenu par Cloudera (en réalité dérivé d'un Spark JIRA [1] )). Il crée fondamentalement un producteur par partition, qui amortit le temps consacré à la création d'objets "lourds" par rapport à une collection d'éléments (espérons-le volumineux).

Le rédacteur peut être trouvé ici: https://github.com/cloudera/spark-kafka-writer

8
maasg

Avec Spark> = 2.2

Les deux opérations de lecture et d'écriture sont possibles sur Kafka à l'aide de l'API Structured Streaming

Créer un flux à partir de Kafka topic

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

Lisez la clé et la valeur et appliquez le schéma aux deux. Pour simplifier, nous convertissons les deux en String type.

val dsStruc = ds.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")
  .as[(String, String)]

Puisque dsStruc a le schéma, il accepte toutes les opérations de type SQL telles que filter, agg, select ..etc dessus.

Écrire le flux dans Kafka topic

dsStruc
  .writeStream // use `write` for batch, like DataFrame
  .format("kafka")
  .option("kafka.bootstrap.servers", "brokerhost1:port1,brokerhost2:port2")
  .option("topic", "target-topic1")
  .start()

Plus configuration pour Kafka en lecture ou en écriture

Principaux artefacts à ajouter dans l'application

 "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,
7
mrsrinivas

J'avais le même problème et j'ai trouvé ce post .

L'auteur résout le problème en créant un producteur par exécuteur. Au lieu d’envoyer le producteur lui-même, il n’envoie qu’une "recette" pour créer un producteur dans un exécuteur en le diffusant.

    val kafkaSink = sparkContext.broadcast(KafkaSink(conf))

Il utilise un wrapper qui crée paresseusement le producteur:

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

Le wrapper est sérialisable car le producteur Kafka est initialisé juste avant la première utilisation sur un exécuteur. Le pilote garde la référence au wrapper et celui-ci envoie les messages à l'aide du producteur de chaque exécuteur:

    dstream.foreachRDD { rdd =>
      rdd.foreach { message =>
        kafkaSink.value.send("topicName", message)
      }
    }
7
gcaliari

Pourquoi est-ce infaisable? Fondamentalement, chaque partition de chaque RDD s'exécutera indépendamment (et peut s'exécuter sur un nœud de cluster différent), de sorte que vous devez pour rétablir la connexion (et toute synchronisation) au début de la tâche de chaque partition. Si les frais généraux sont trop élevés, augmentez la taille du lot dans votre StreamingContext jusqu'à ce que cela devienne acceptable (dans ce cas, il y a un coût en latence).

(Si vous ne gérez pas des milliers de messages dans chaque partition, êtes-vous sûr de vouloir diffuser en continu des étincelles? Feriez-vous mieux avec une application autonome?)

3
lmm

Cela pourrait être ce que vous voulez faire. Vous créez essentiellement un producteur pour chaque partition d’enregistrements.

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

J'espère que ça t'as aidé

2
sainath reddy