![](/img/trans.png)
[英]Scala: Error reading Kafka Avro messages from spark structured streaming
[英]Reading Avro messages from Kafka with Spark 2.0.2 (structured streaming)
我有一个spark 2.0应用程序,它使用spark streaming(使用spark-streaming-kafka-0-10_2.11)从kafka读取消息。
结构化流看起来很酷,所以我想尝试迁移代码,但我无法弄清楚如何使用它。
在常规流媒体中,我使用kafkaUtils来创建Dstrean,在我传递的参数中是值deserializer。
在结构化流媒体中,doc说我应该使用DataFrame函数进行反序列化,但我无法确切地知道这意味着什么。
我查看了这个示例,例如我在Kafka中的Avro对象是退出复杂的,不能简单地像示例中的String一样进行转换。
到目前为止,我尝试了这种代码(我在这里看到了另一个问题):
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(....”
我怎样才能反序化值?
如上所述,从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)
})
我还不太熟悉Spark的序列化如何与新的/实验性结构化流程结合使用,但下面的方法确实有效 - 尽管我不确定它是否是最好的方法(恕我直言,这种方法有点尴尬的样子'n感觉)。
我将尝试以自定义数据类型(此处: 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读取数据,其中输入主题包含消息值为二进制编码String
Kafka消息,您的目标是根据这些消息值创建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问题,你需要做的是:
Encoder
。 Foo(new String(row.getAs[Array[Byte]]("value"))
替换为将二进制编码的Avro数据反序列化为Avro POJO的代码,即将二进制编码的Avro数据从消息中取出的代码value( row.getAs[Array[Byte]]("value")
)并返回,例如,Avro GenericRecord
或您在别处定义的任何SpecificCustomAvroObject
。 如果其他人知道更简洁/更好/ ......回答Tal的问题的方式,我全都听见了。 :-)
也可以看看:
所以实际上我公司的某个人为我解决了这个问题所以我会在这里发布给未来的读者。
基本上我错过了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对象。
希望这有助于某人
使用以下步骤:
卡夫卡消息:
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
}
}
更新
utils的:
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]
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.