【问题标题】:Reading Avro messages from Kafka with Spark 2.0.2 (structured streaming)使用 Spark 2.0.2(结构化流)从 Kafka 读取 Avro 消息
【发布时间】:2017-04-04 00:10:30
【问题描述】:

我有一个 spark 2.0 应用程序,它使用 spark 流(使用 spark-streaming-kafka-0-10_2.11)从 kafka 读取消息。

结构化流看起来很酷,所以我想尝试迁移代码,但我不知道如何使用它。

在常规流中,我使用 kafkaUtils 创建 Dstrean,在我传递的参数中,它是值反序列化器。

在结构化流中,文档说我应该使用 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(....”

如何反序列化该值?

【问题讨论】:

  • 有人找到可行的解决方案了吗?以下都不适合我!
  • 这个库支持使用 Avro 作为有效负载的结构化流,可能会有所帮助:ABRiS (Avro Bridge for Spark)。它仍在开发中,但支持您的用例。披露:我为 ABSA 工作,我是这个库背后的主要开发人员。

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


【解决方案1】:

我还不太熟悉 Spark 的序列化如何与新的/实验性的结构化流结合使用,但下面的方法确实有效——尽管我不确定它是否是最好的方法(恕我直言,这种方法有点尴尬看感觉)。

我将尝试在自定义数据类型的示例(此处:Foo 案例类)而不是专门的 Avro 中回答您的问题,但我希望它无论如何都会对您有所帮助。这个想法是使用 Kryo 序列化来序列化/反序列化您的自定义类型,请参阅 Spark 文档中的 Tuning: Data serialization

注意: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()

现在我们将deserializing 的值放入我们自定义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 的代码,即从消息值(row.getAs[Array[Byte]]("value")) 中取出二进制编码的Avro 数据并返回例如Avro @ 987654339@ 或您在其他地方定义的任何 SpecificCustomAvroObject

如果其他人知道更简洁/更好/...的方式来回答 Tal 的问题,我会全力以赴。 :-)

另见:

【讨论】:

  • 我认为 Tal 的用例是他的主题没有二进制编码的字符串,他有二进制编码的 avro。在这种情况下使用 bijection-avro 会起作用吗?
  • 是的@zzztimbo 是的。我不得不把这个项目积压一点,所以我没有机会尝试任何新的东西。希望我能尽快研究这个主题。什么时候做,我会研究 bijection-avro
  • @TalJoffe 请告诉我你的想法。我正在尝试阅读由 kstream 放在那里的 avro,而 bijection-avro 对我不起作用。
  • @zzztimbo 哦,好吧。正如我所说的,这可能需要我一段时间,但一旦我弄清楚了,我就在这里
【解决方案2】:

所以实际上我公司的某个人为我解决了这个问题所以我会在这里发布给未来的读者..

除了 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&lt;YourObject&gt;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 对象。

希望这对某人有所帮助

【讨论】:

  • 那么你的意思是除了avro生成的类之外,我们还需要提供相关的案例类吗?你能告诉我们你的进口声明吗?在这个语句中哪里可以得到“Deocder”类? val 解码器 = AvroToDecoder.getDecoder(schemaRegistryUrl)
【解决方案3】:

如上所述,从 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)
                })

【讨论】:

  • 它对我不起作用,原因是:java.io.EOFException 错误
  • 此解决方案不适用于启用架构注册表的 kafka。它报告“引起:org.apache.avro.AvroRuntimeException:格式错误的数据。长度为负:-13”
  • 确保此处使用的 avro 模式与来自 kafka 的模式相匹配。
【解决方案4】:

使用以下步骤:

  • 定义 Kafka 消息。
  • 定义一个消费者实用程序,它返回一个 YourAvroObject 的数据集。
  • 定义您的逻辑代码。

卡夫卡消息:

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


}

【讨论】:

  • 能否给个完整的例子或者分享github中的代码
  • Utils.convert 方法是如何定义的?
  • 什么是TypeTag?如果您不想硬编码阅读器架构字符串以及在消息中使用什么架构 ID,该怎么办?
  • @CaselChen,我已经添加了实用功能,希望对您有所帮助。抱歉,这是一个旧代码,我没有保留存储库。
  • @cricket_007,请阅读有关模式注册表和模式演变的信息。架构不是硬编码的。 docs.oracle.com/database/nosql-11.2.2.0/GettingStartedGuide/…
猜你喜欢
  • 1970-01-01
  • 2019-01-29
  • 2019-09-20
  • 1970-01-01
  • 2020-03-17
  • 2017-03-31
  • 1970-01-01
  • 1970-01-01
  • 2022-11-25
相关资源
最近更新 更多