数据序列化工具比较:Avro vs Protobuf


两种流行的数据序列化系统是Google 的 Protocol Buffers (Protobuf)Apache 的 Avro。虽然 Protobuf 和 A​​vro 都有各自的优点和缺点,但开发人员在选择它们时经常考虑的一个关键因素是性能。在这篇文章中,我们将在一系列指标上比较 Protobuf 和 A​​vro 的性能,包括编码时间、解码时间、序列化大小和吞吐量。

通过分析这些测试的结果,我希望能让开发人员更好地了解每个系统的性能特征以及如何根据自己的具体需求进行选择。

Protobuf 和 A​​vro 概述
Protobuf由Google于2008年开发,是开源软件。它的设计简单、快速、高效,支持多种编程语言,包括 C++、Java、Python 和 Ruby。
Protobuf 使用模式定义语言来定义数据结构,然后将其编译成可用于序列化和反序列化数据的代码。

Avro由Apache于2009年开发,也是开源软件。Avro 旨在支持模式演化,即使模式发生变化,也允许数据序列化和反序列化。Avro 支持与 Protobuf 类似的编程语言,并对 Java、Python 和 Ruby 提供强大支持。Avro 使用基于 JSON 的模式定义语言来定义数据结构,该结构可以与数据本身一起存储。

在比较 Protobuf 和 A​​vro 时,了解它们在不同行业和应用程序中的使用方式非常重要。以下是每种格式的一些用例示例。

Protobuf:

  • Protobuf 最初由 Google 开发,用于其内部系统,包括 Google 搜索、Google 地图和 Gmail。
  • 用于微服务通信的 gRPC 。
  • 金融机构进行高频交易、风险管理和欺诈检测。
  • 游戏开发人员使用它来优化网络性能并减少带宽使用。
  • IoT 应用程序在传感器、网关和云服务器等设备之间发送数据。
  • 汽车应用程序在汽车和基础设施(例如交通信号灯和停车计时器)之间交换数据。


Avro :

  • Avro 由开源大数据平台 Apache Hadoop 使用,用于 Hadoop 组件之间的数据序列化和交换。
  • 消息系统,例如 Apache Kafka 和 Apache Pulsar,用于在应用程序和服务之间传输数据。
  • 用于数据处理和分析的分析平台,例如 Apache Spark 和 Apache Flink。
  • 机器学习框架,例如 TensorFlow 和 PyTorch,用于训练和推理阶段之间的数据序列化和交换。
  • 日志收集工具,例如 Apache Flume 和 Apache NiFi,用于将日志数据从各种来源流式传输到集中位置。

总的来说,Protobuf 在低延迟、高性能场景中更受欢迎,而 Avro 在大数据、分布式系统和分析场景中更受欢迎。然而,这两种格式都可以用于各种应用程序,并且选择取决于每个用例的具体要求和限制。


先决条件
为了比较Protobuf和Avro的性能,我们将使用JMH(Java Microbenchmark Harness),一个广泛使用的Java代码基准测试工具。JMH提供了一种稳健可靠的方法来测量小代码片段在受控环境中的性能,使我们能够准确地比较Protobuf和Avro在一系列指标上的性能,如编码时间、解码时间、序列化大小和吞吐量。为了准备性能测试数据,我创建了一个样本对象,使用Protobuf和Avro进行序列化。该对象是一个带有地址和书籍字段的图书馆的简化表示。每本书都有作者、页数和可用性,作者由名字、姓氏和国籍组成。Protobuf和Avro模式的一个关键区别是它们如何处理集合字段。在Protobuf中,集合字段的定义使用了一种特殊的语法,表明该字段是重复的,如下面的代码片断所示。

syntax = "proto3";

package protobuf;

option java_multiple_files = true;
option java_package =
"com.szymon_kaluza.protobuf.proto.model";
option java_outer_classname =
"LibraryProtos";

message Author {
 optional string name = 1;
 optional string surname = 2;
 optional string nationality = 3;
}

message Book {
 optional string title = 1;
 optional Author author = 2;
 optional int64 pages = 3;
 optional bool available = 4;
}

message Library {
 optional string address = 1;
 repeated Book books = 2;
}

有了这个定义,我们就可以使用协议缓冲区编译器生成Java代码。

在Avro中,集合是用数组来定义的,数组是一个特定类型的集合。同一对象的Avro模式会是这样的:

{
 "type": "record",
 
"name": "Library",
 
"namespace": "com.szymon_kaluza.avro.avro.model",
 
"fields": [
   {
     
"name": "address",
     
"type": [
       
"null",
       
"string"
     ],
     
"default": null
   },
   {
     
"name": "books",
     
"type": {
       
"type": "array",
       
"items": {
         
"type": "record",
         
"name": "Book",
         
"fields": [
           {
             
"name": "title",
             
"type": [
               
"null",
               
"string"
             ],
             
"default": null
           },
           {
             
"name": "author",
             
"type": {
               
"type": "record",
               
"name": "Author",
               
"fields": [
                 {
                   
"name": "name",
                   
"type": [
                     
"null",
                     
"string"
                   ],
                   
"default": null
                 },
                 {
                   
"name": "surname",
                   
"type": [
                     
"null",
                     
"string"
                   ],
                   
"default": null
                 },
                 {
                   
"name": "nationality",
                   
"type": [
                     
"null",
                     
"string"
                   ],
                   
"default": null
                 }
               ]
             }
           },
           {
             
"name": "pages",
             
"type": [
               
"null",
               
"long"
             ],
             
"default": null
           },
           {
             
"name": "available",
             
"type": [
               
"null",
               
"boolean"
             ],
             
"default": null
           }
         ]
       }
     }
   }
 ]
}

在Avro Java API的帮助下,我们可以创建上述模式,然后将其编译为Java。

对于基准,我选择使用两个版本的图书馆对象。一个是只有一本书的小版本,一个是有一千本书的大版本。为了防止JVM的优化,字符串字段在内容和长度上都是随机的(从3到13个字符)。一般来说,为了比较性能,应该避免随机性,但对于我们的用例来说,它是可以忽略不计的。我只用固定值来比较序列化的数据大小,因为我们在那里需要相同的对象。

值得注意的是,这些测试是专门在Java中进行的。在其他编程语言中比较Protobuf和Avro时,由于序列化库、特定语言的性能特征和运行时环境的变化,结果可能会有所不同。建议你在你所使用的编程语言中进行基准测试并评估性能。

测试结果:

                      第一列是Protobuf  后面一列数据是Avro
Serialized size with one book         56 bytes      54 bytes
Serialized size with ten books        479 bytes      441 bytes
Serialized size with thousand books    55441 bytes    51508 bytes
Serialized size with million books    68539057 bytes   64547317 bytes

结论
我们进行的测试表明,Protobuf在序列化和反序列化的速度方面要好一些

另一方面,Avro为我们提供了一个更紧凑的序列化数据。

当在这两者之间进行选择时,决定将主要取决于你的分布式系统的具体需求。如果性能是一个关键问题,那么Protobuf的速度和效率可能使它成为一个更好的选择。如果你需要更复杂的数据结构或内置的压缩选项,那么Avro可能更适合。

无论你选择哪个序列化框架,重要的是要记住,序列化只是分布式系统的一个组成部分。其他因素,如网络延迟、数据一致性、CPU使用率和使用的库,也将在决定你的系统的整体性能和可靠性方面发挥作用。通过仔细考虑所有这些因素,并根据你的需要选择正确的序列化框架,你可以帮助建立一个快速、高效和可靠的分布式系统。

事件溯源的最佳序列化策略
在实施事件溯源系统时,在某些时候,您将需要回答两个主要问题:

  1. 在哪里存储事件?
  2. 如何存储事件?

可能大多数人会选择 JSON——这是一个非常明显的偏好。JSON 格式被大量使用并且具有许多优点。这是作为默认选择的最佳选项。

二进制协议是一个非常诱人的选择:

Java 序列化——我们不要在这个可怕的错误上浪费时间。

Kryo — 非常快、非常紧凑,但它只能在 JVM 上运行,将我们的基础设施限制为仅 JVM 应用程序是没有意义的。也许一些疯狂的 NodeJS 开发人员也想阅读我们的活动。

Thrift——来自 Facebook,在功能方面几乎与 Google 的 Protocol Buffers 相同,但主观上 Protobuf 更容易使用。文档非常详细且广泛。对 Java 和 Scala 的支持和工具都处于非常好的水平。这就是为什么我选择Protocol Buffer 与 Avro(来自 Hadoop)进行最终比较。

详细的比较可以在 Martin Kleppmann 的这篇精彩文章中找到

事件溯源选择结果:
Avro,尤其是在开始时,似乎更容易使用。这样做的代价是您需要提供读取器和写入器架构来反序列化任何内容。

在使用 Avro 时可能面临的下一个问题是对域事件的整体影响。在某些时候,Avro 可能会泄漏到您的域中。Avro 模型不支持某些结构,例如带有非字符串键的映射。当序列化机制迫使您更改域模型中的某些内容时,这不是一个好兆头。

使用 Protocol Buffers,模式管理变得更加简单 - 您只需要模式数据结构工件,它可以作为任何其他工件发布到本地存储库。此外,您的域可以与序列化机制完美分离。成本是域层和序列化层之间转换所需的样板代码。

就我个人而言,我会使用 Avro 来处理主要为原始类型的简单域。
对于具有复杂类型和结构的丰富域,我已经使用Protocol Buffers相当长一段时间了。

没有序列化影响的干净的领域,确实值得付出模板代码的代价。这种换来的成本代价是值得的。