关于kafka中的序列化

前言

如前面的示例所示,生产者配置包括强制序列化器。 我们已经了解了如何使用默认的String序列化程序。 Kafka还包括integers和ByteArrays的序列化程序,但这并不包括大多数用例。 最终,你将希望能够序列化更多通用格式的记录。
我们将首先展示如何编写自己的序列化程序,然后介绍Avro序列化程序作为推荐的替代方案。

自定义序列化器

当你需要发送给Kafka的对象不是简单的字符串或整数时,你可以选择使用AvroThriftProtobuf等通用序列化库来创建记录,或者为已经使用的对象创建自定义序列化。 我们强烈建议使用通用序列化库。 为了理解序列化程序的工作原理以及为什么使用序列化库是个好主意,让我们看看编写自己的自定义序列化程序需要什么。
假设你不是仅记录客户名称,而是创建一个简单的类来表示客户:

public class Customer {
            private int customerID;
            private String customerName;
            public Customer(int ID, String name) {
                    this.customerID = ID;
                    this.customerName = name;
}
      public int getID() {
        return customerID;
}
      public String getName() {
       return customerName;
} }

现在假设我们要为此类创建自定义序列化程序。 它看起来像这样:

import org.apache.kafka.common.errors.SerializationException;
import java.nio.ByteBuffer;
import java.util.Map;
public class CustomerSerializer implements Serializer<Customer> {
      @Override
      public void configure(Map configs, boolean isKey) {
       // nothing to configure
      }
      @Override
      /**
      We are serializing Customer as:
      4 byte int representing customerId
      4 byte int representing length of customerName in UTF-8 bytes (0 if name is
      Null)
      N bytes representing customerName in UTF-8
      */
      public byte[] serialize(String topic, Customer data) {
		try {
                      byte[] serializedName;
                      int stringSize;
          if (data == null)
			return null;
		  else {
            if (data.getName() != null) {
              serializeName = data.getName().getBytes("UTF-8");
              stringSize = serializedName.length;
            } else {
              serializedName = new byte[0];
			  stringSize = 0;
			}
		  }
          ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
          buffer.putInt(data.getID());
          buffer.putInt(stringSize);
          buffer.put(serializedName);
          return buffer.array();
	   } catch (Exception e) {
         throw new SerializationException("Error when serializing Customer to byte[] " + e);
	 	} 
	 }
    
     @Override
     public void close() {
              // nothing to close
	  } 
}

使用此CustomerSerializer配置生产者将允许你定义ProducerRecord <String,Customer>,并发送Customer数据并将Customer对象直接传递给生产者。这个例子很简单,但你可以看到代码是多么脆弱。例如,如果我们有太多的客户,并且需要将customerID更改为Long,或者如果我们决定将一个startDate字段添加到Customer,我们将在维护旧消息和新消息之间的兼容性方面遇到严重问题。

调试不同版本的串行器和反序列化器之间的兼容性问题是相当具有挑战性的 —— 你需要比较原始字节的数组。更糟糕的是,如果同一家公司的多个团队最终将Customer数据写入Kafka,他们都需要使用相同的序列化程序并在同一时间修改代码。
出于这些原因,我们建议使用现有的序列化程序和反序列化程序,如JSON,Apache Avro,Thrift或Protobuf。在下一节中,我们将介绍Apache Avro,然后展示如何序列化Avro记录并将其发送到Kafka。

使用Apache Avro进行序列化

Apache Avro是一种与语言无关的数据序列化格式。 该项目由Doug Cutting创建,旨在提供与大量受众共享数据文件的方法。
Avro数据在与语言无关的模式中描述。 Schema通常用JSON描述,序列化通常用于二进制文件,但也支持序列化为JSON。 Avro假定在读取和写入文件时存在schema,通常是通过将模式嵌入文件本身
Avro最有趣的功能之一,以及它非常适合在像Kafka这样的消息传递系统中使用,是当正在编写消息的应用程序切换到新的schema时,读取数据的应用程序可以继续处理消息而不需要需要任何更改或更新。

假设原始模式是:


    {"namespace": "customerManagement.avro",
     "type": "record",
     "name": "Customer",
     "fields": [
         {"name": "id", "type": "int"},
         {"name": "name",  "type": "string""},
         {"name": "faxNumber", "type": ["null", "string"], "default": "null"} //[1]
] }

[1] id和name字段是必填字段,而传真号码是可选的,默认为null。

我们使用这个schema几个月,并以这种格式生成了几TB的数据。 现在假设我们决定在新版本中,我们将升级到新的格式,将不再包含传真号码字段,而是使用电子邮件字段。
新架构将是:

 {"namespace": "customerManagement.avro",
     "type": "record",
     "name": "Customer",
     "fields": [
         {"name": "id", "type": "int"},
         {"name": "name",  "type": "string"},
         {"name": "email", "type": ["null", "string"], "default": "null"}
] }

现在,升级到新版本后,旧记录将包含“faxNumber”,新记录将包含“email”。在许多组织中,升级过程缓慢且持续数月。因此,我们需要考虑仍然使用传真号码的preupgrade应用程序和使用电子邮件的postupgrade应用程序如何能够处理Kafka中的所有事件。
读取应用程序将包含对类似于getName()getId()getFaxNumber() 的方法的调用。如果遇到使用新schema编写的消息,getName()和getId()将继续工作而不进行任何修改,但getFaxNumber()将返回null,因为该消息不包含传真号。
现在假设我们升级了我们的阅读应用程序,它不再具有getFaxNumber()方法,而是getEmail()。如果遇到使用旧架构编写的消息,则getEmail()将返回null,因为旧消息不包含电子邮件地址。
此示例说明了使用Avro的好处:即使我们在不更改读取数据的所有应用程序的情况下更改了消息中的schema,也不会出现异常或突发错误,也不需要对现有数据进行昂贵的更新。

但是,这种情况有两点需要注意:

  • 用于编写数据的schema和读取应用程序所需的schema必须兼容。 Avro文档包含兼容性规则。
  • 反序列化器需要访问编写数据时使用的schema,即使它与访问数据的应用程序所期望的模式不同。 在Avro文件中,写入模式包含在文件本身中,但有一种更好的方法来处理Kafka消息。 我们接下来会看一下。

将Avro记录与Kafka一起使用

与Avro文件不同,将整个schema存储在数据文件中与相当合理的开销相关联,将整个schema存储在每个记录中通常会使记录大小增加一倍以上。 但是,Avro在读取记录时仍然需要存在整个schema,因此我们需要在其他位置找到模式。 为了解决这个问题,我们遵循一个通用的架构schema并使用Schema Registry。 Schema Registry不是Apache Kafka的一部分,但有几个开源选项可供选择。 我们将在此示例中使用Confluent Schema Registry。 你可以在GitHub上找到Schema Registry代码,也可以将其作为Confluent Platform的一部分安装。 如果你决定使用Schema Registry,那么我们建议你查看文档。

我们的想法是在注册表中存储用于将数据写入Kafka的所有schema。 然后我们只是将schema的标识符存储在我们为Kafka生成的记录中。 然后,消费者可以使用标识符将记录拉出Schema Registry并反序列化数据。

关键是所有这些工作 —— 在寄存器中存储模式并在需要时将其拉出 —— 在序列化器和反序列化器中完成。 向Kafka生成数据的代码只是使用Avro序列化程序,就像使用任何其他序列化程序一样。 图3-2演示了这个过程。

以下是如何为Kafka生成生成的Avro对象的示例(有关如何使用Avro生成代码,请参阅Avro文档):

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("value.serializer","io.confluent.kafka.serializers.KafkaAvroSerializer"); //[1]
props.put("schema.registry.url", schemaUrl); //[2]
String topic = "customerContacts";
int wait = 500;
Producer<String, Customer> producer = new KafkaProducer<String, Customer>(props); //[3]
// We keep producing new events until someone ctrl-c
while (true) {
    Customer customer = CustomerGenerator.getNext();
    System.out.println("Generated customer " + customer.toString());
    ProducerRecord<String, Customer> record =  ProducerRecord<>(topic, customer.getId(), customer); //[4]
    producer.send(record); //[5]
}

[1] 我们使用KafkaAvroSerializer来使用Avro序列化我们的对象。 请注意,AvroSerializer也可以处理原语,这就是我们以后可以使用String作为记录键和Customer对象作为值的原因。
[2] schema.registry.url是一个新参数。 这只是指向我们存储模式的位置。
[3] Customer是我们生成的对象。 我们告诉生产者我们的记录将包含Customer作为值。
[4] 我们还使用Customer将ProducerRecord实例化为值类型,并在创建新记录时传递Customer对象。
[5] 我们发送记录与我们的Customer对象,KafkaAvroSerializer将处理剩下的事情。

如果你更喜欢使用通用Avro对象而不是生成的Avro对象,该怎么办? 别担心。 在这种情况下,你只需提供schema:

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer","io.confluent.kafka.serializers.KafkaAvroSerializer"); //[1]
props.put("value.serializer", "io.confluent.kafka.serializers.KafkaAvroSerializer");
props.put("schema.registry.url", url); //[2]
String schemaString = "{\"namespace\": \"customerManagement.avro\",
\"type\": \"record\", " + //[3]
                           "\"name\": \"Customer\"," +
                           "\"fields\": [" +
                            "{\"name\": \"id\", \"type\": \"int\"}," +
                            "{\"name\": \"name\", \"type\": \"string\"}," +
                            "{\"name\": \"email\", \"type\": [\"null\",\"string
\"], \"default\":\"null\" }" +
                           "]}";
Producer<String, GenericRecord> producer = new KafkaProducer<String, GenericRecord>(props); //[4]
Schema.Parser parser = new Schema.Parser();
Schema schema = parser.parse(schemaString);
for (int nCustomers = 0; nCustomers < customers; nCustomers++) {
	 String name = "exampleCustomer" + nCustomers;
	 String email = "example " + nCustomers + "@example.com"
	 GenericRecord customer = new GenericData.Record(schema); //[5]
	 customer.put("id", nCustomer);
	 customer.put("name", name);
	 customer.put("email", email);
	 ProducerRecord<String, GenericRecord> data =  new ProducerRecord<String,GenericRecord("customerContacts", name, customer);
     producer.send(data);
    }
}

[1] 我们仍然使用相同的KafkaAvroSerializer。
[2] 我们提供相同模式注册表的URI。
[3] 但是现在我们还需要提供Avro架构,因为它不是由Avro生成的对象提供的。
[4] 我们的对象类型是Avro GenericRecord,我们使用我们的schema和我们想要编写的数据进行初始化。
[5] 然后,ProducerRecord的值只是一个GenericRecord,它计算我们的schema和数据。 序列化程序将知道如何从此记录中获取架构,将其存储在架构注册表中,以及序列化对象数据。

参考资料

Chapter 3. Kafka Consumers: Writing Message to Kafka#Serializers

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

Lestat.Z.

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值