web-dev-qa-db-ja.com

Kafka with Spark 2.0.2(構造化ストリーミング)からのA​​vroメッセージの読み取り

spark 2.0アプリケーションがあり、kafka using spark Streaming(with spark-streaming-kafka-0 -10_2.11)。

構造化ストリーミングは本当にクールに見えるので、コードを移行してみたかったのですが、使用方法がわかりません。

通常のストリーミングでは、kafkaUtilsを使用してcreateDstreanを作成し、渡したパラメーターでは、値のデシリアライザーでした。

構造化ストリーミングでは、DataFrame関数を使用して逆シリアル化する必要があるとドキュメントに記載されていますが、それが何を意味するのか正確にはわかりません。

このような例を見ました example しかし、KafkaのAvroオブジェクトは非常に複雑で、例の文字列のように単純にキャストすることはできません。

これまでのところ、私はこの種のコードを試しました(これは別の質問でここで見ました):

import spark.implicits._

  val ds1 = spark.readStream.format("kafka").
    option("kafka.bootstrap.servers","localhost:9092").
    option("subscribe","RED-test-tal4").load()

  ds1.printSchema()
  ds1.select("value").printSchema()
  val ds2 = ds1.select($"value".cast(getDfSchemaFromAvroSchema(Obj.getClassSchema))).show()  
  val query = ds2.writeStream
    .outputMode("append")
    .format("console")
    .start()

「データ型の不一致:BinaryTypeをStructType(StructField(....)にキャストできません」というメッセージが表示されます。

どうすれば値を逆シリアル化できますか?

8
Tal Joffe

Sparkのシリアル化が新しい/実験的な構造化ストリーミングと組み合わせてどのように機能するかはまだよくわかりませんが、以下のアプローチは機能します-それが最善の方法かどうかはわかりませんが(IMHOのアプローチはやや厄介な外観です 'n感じる)。

特にAvroではなく、カスタムデータ型(ここではFooケースクラス)の例であなたの質問に答えようとしますが、とにかく役立つことを願っています。アイデアは、Kryoシリアル化を使用して、カスタムタイプをシリアル化/逆シリアル化することです。Sparkドキュメントの チューニング:データシリアル化 を参照してください。

注:Sparkは、_import spark.implicits.__を介してインポートできる組み込み(暗黙的)エンコーダーを介して、すぐに使用できるケースクラスのシリアル化をサポートします。ただし、このため、この機能は無視してください。例。

次のFooケースクラスをカスタムタイプとして定義したとします(TL; DRヒント:奇妙なSparkシリアル化の苦情/エラーに遭遇しないようにするには、コードを別の_Foo.scala_ファイル):

_// This could also be your auto-generated Avro class/type
case class Foo(s: String)
_

これで、Kafkaからデータを読み取るための次の構造化ストリーミングコードができました。入力トピックにはKafkaメッセージ値がバイナリエンコードされたStringであるメッセージが含まれ、目標はそれです。これらのメッセージ値に基づいてFooインスタンスを作成するには(つまり、バイナリデータをAvroクラスのインスタンスに逆シリアル化する方法と同様):

_val messages: DataFrame = spark.readStream
    .format("kafka")
    .option("kafka.bootstrap.servers", "broker1:9092,broker2:9092")
    .option("subscribe", "my-input-topic")
    .load()
_

これで、 逆シリアル化 値をカスタムFoo型のインスタンスに変換します。最初に、暗黙の_Encoder[Foo]_を定義する必要があります。

_implicit val myFooEncoder: Encoder[Foo] = org.Apache.spark.sql.Encoders.kryo[Foo]
val foos: Dataset[Foo] = messages.map(row => Foo(new String(row.getAs[Array[Byte]]("value")))
_

Avroの質問に戻ると、次のことを行う必要があります。

  1. ニーズに合った適切なEncoderを作成します。
  2. Foo(new String(row.getAs[Array[Byte]]("value"))を、バイナリエンコードされたAvroデータをAvro POJOに逆シリアル化するコードに置き換えます。つまり、バイナリエンコードされたAvroデータをメッセージ値(row.getAs[Array[Byte]]("value"))から取り出して、たとえば、Avro GenericRecordまたは他の場所で定義したSpecificCustomAvroObject

他の誰かがタルの質問に答えるより簡潔な/より良い/ ...方法を知っているなら、私はすべての耳です。 :-)

参照:

3
Michael G. Noll

上記のように、Spark 2.1.0以降、バッチリーダーではavroがサポートされていますが、SparkSession.readStream()ではサポートされていません。他の回答に基づいて、Scalaで動作させる方法は次のとおりです。簡潔にするためにスキーマを簡略化しました。

package com.sevone.sparkscala.mypackage

import org.Apache.spark.sql._
import org.Apache.avro.io.DecoderFactory
import org.Apache.avro.Schema
import org.Apache.avro.generic.{GenericDatumReader, GenericRecord}

object MyMain {

    // Create avro schema and reader
    case class KafkaMessage (
        deviceId: Int,
        deviceName: String
    )
    val schemaString = """{
        "fields": [
            { "name":  "deviceId",      "type": "int"},
            { "name":  "deviceName",    "type": "string"},
        ],
        "name": "kafkamsg",
        "type": "record"
    }""""
    val messageSchema = new Schema.Parser().parse(schemaString)
    val reader = new GenericDatumReader[GenericRecord](messageSchema)
    // Factory to deserialize binary avro data
    val avroDecoderFactory = DecoderFactory.get()
    // Register implicit encoder for map operation
    implicit val encoder: Encoder[GenericRecord] = org.Apache.spark.sql.Encoders.kryo[GenericRecord]

    def main(args: Array[String]) {

        val KafkaBroker =  args(0);
        val InTopic = args(1);
        val OutTopic = args(2);

        // Get Spark session
        val session = SparkSession
                .builder
                .master("local[*]")
                .appName("myapp")
                .getOrCreate()

        // Load streaming data
        import session.implicits._
        val data = session
                .readStream
                .format("kafka")
                .option("kafka.bootstrap.servers", KafkaBroker)
                .option("subscribe", InTopic)
                .load()
                .select($"value".as[Array[Byte]])
                .map(d => {
                    val rec = reader.read(null, avroDecoderFactory.binaryDecoder(d, null))
                    val deviceId = rec.get("deviceId").asInstanceOf[Int]
                    val deviceName = rec.get("deviceName").asInstanceOf[org.Apache.avro.util.Utf8].toString
                    new KafkaMessage(deviceId, deviceName)
                })
3
Ralph Gonzalez

だから実際に私の会社の誰かが私のためにこれを解決したので、将来の読者のためにここに投稿します。

基本的に、migunoが提案したものに加えて私が見逃したのは、デコード部分です。

_def decodeMessages(iter: Iterator[KafkaMessage], schemaRegistryUrl: String) : Iterator[<YourObject>] = {
val decoder = AvroTo<YourObject>Decoder.getDecoder(schemaRegistryUrl)
iter.map(message => {
  val record = decoder.fromBytes(message.value).asInstanceOf[GenericData.Record]
  val field1 = record.get("field1Name").asInstanceOf[GenericData.Record]
  val field2 = record.get("field1Name").asInstanceOf[GenericData.String]
        ...
  //create an object with the fields extracted from genericRecord
  })
}
_

これで、kafkaからメッセージを読み取り、次のようにデコードできます。

_val ds = spark
  .readStream
  .format(config.getString(ConfigUtil.inputFormat))
  .option("kafka.bootstrap.servers", config.getString(ConfigUtil.kafkaBootstrapServers))
  .option("subscribe", config.getString(ConfigUtil.subscribeTopic))
  .load()
  .as[KafkaMessage]

val decodedDs  = ds.mapPartitions(decodeMessages(_, schemaRegistryUrl))
_

* KafkaMessageは、Kafka _(key,value,topic,partition,offset,timestamp)_から読み取るときに取得する汎用オブジェクトを含む単なるケースクラスです。

_AvroTo<YourObject>Decoder_は、スキーマレジストリURLを指定してオブジェクトをデコードするクラスです。

たとえば、Confluentの KafkaAvroDeserializer とスキーマレジストリを使用します。

_val kafkaProps = Map("schema.registry.url" -> schemaRegistryUrl)
val client = new CachedSchemaRegistryClient(schemaRegistryUrl, 20)

// If you have Avro encoded keys
val keyDeserializer = new KafkaAvroDeserializer(client)
keyDeserializer.configure(kafkaProps.asJava, true) //isKey = true

// Avro encoded values
valueDeserializer = new KafkaAvroDeserializer(client)
valueDeserializer.configure(kafkaProps.asJava, false) //isKey = false
_

これらから、.deserialize(topicName, bytes).asInstanceOf[GenericRecord]を呼び出してavroオブジェクトを取得します。

これが誰かを助けることを願っています

2
Tal Joffe

次の手順を使用します。

  • Kafkaメッセージを定義します。
  • YourAvroObjectのDataSetを返すコンシューマユーティリティを定義します。
  • 論理コードを定義します。

カフカメッセージ:

case class KafkaMessage(key: String, value: Array[Byte],
                                    topic: String, partition: String, offset: Long, timestamp: Timestamp)

カフカ消費者:

import Java.util.Collections

import com.typesafe.config.{Config, ConfigFactory}
import io.confluent.kafka.serializers.KafkaAvroDeserializer
import org.Apache.avro.Schema
import org.Apache.avro.generic.GenericRecord
import org.Apache.spark.sql.SparkSession

import scala.reflect.runtime.universe._


object KafkaAvroConsumer {

  private val conf: Config = ConfigFactory.load().getConfig("kafka.consumer")
  val valueDeserializer = new KafkaAvroDeserializer()
  valueDeserializer.configure(Collections.singletonMap("schema.registry.url",
    conf.getString("schema.registry.url")), false)

  def transform[T <: GenericRecord : TypeTag](msg: KafkaMessage, schemaStr: String) = {
    val schema = new Schema.Parser().parse(schemaStr)
    Utils.convert[T](schema)(valueDeserializer.deserialize(msg.topic, msg.value))
  }

  def createDataStream[T <: GenericRecord with Product with Serializable : TypeTag]
  (schemaStr: String)
  (subscribeType: String, topics: String, appName: String, startingOffsets: String = "latest") = {

    val spark = SparkSession
      .builder
      .master("local[*]")
      .appName(appName)
      .getOrCreate()

    import spark.implicits._

    // Create DataSet representing the stream of KafkaMessage from kafka
    val ds = spark
      .readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", conf.getString("bootstrap.servers"))
      .option(subscribeType, topics)
      .option("startingOffsets", "earliest")
      .load()
      .as[KafkaMessage]
      .map(msg => KafkaAvroConsumer.transform[T](msg, schemaStr)) // Transform it Avro object.

    ds
  }

}

更新

ユーティリティ:

import org.Apache.avro.Schema
import org.Apache.avro.file.DataFileReader
import org.Apache.avro.generic.{GenericDatumReader, GenericRecord}
import org.Apache.avro.specific.SpecificData

import scala.reflect.runtime.universe._

object Utils {


  def convert[T <: GenericRecord: TypeTag](targetSchema: Schema)(record: AnyRef): T = {
      SpecificData.get.deepCopy(targetSchema, record).asInstanceOf[T]
  }


}
2
user2550587