序列化格式比较

选择要写入队列,日志或其他地方的消息的序列化格式时,经常会出现许多问题,这些问题会以某种方式影响最终选择。 这些关键问题之一是序列化的速度和收到的消息的大小。 由于有许多用于这种目的的格式,所以我决定测试其中一些并共享结果。

测试准备


将测试以下格式:

  1. Java序列化
  2. 杰森
  3. 阿夫罗
  4. 原虫
  5. 节俭(二进制,紧凑型)
  6. 消息包


Scala被选为PL。
主要测试工具将是Scalameter

将测量和比较以下参数:序列化和反序列化所花费的时间,以及生成的文件的大小。

易用性,电路发展的可能性以及其他比较中的重要参数将不参与。

输入产生


为了保证实验的纯度,必须首先生成一个数据集。 输入格式为CSV文件。 使用简单的`Random.next [...]`作为数字值,`UUID.randomUUID()`作为字符串来生成数据。 使用kantan将生成的数据写入csv文件。 总共生成了3个数据集,每个集有10万条记录:

  1. 混合数据-28 mb

    混合数据
    final case class MixedData( f1: Option[String], f2: Option[Double], f3: Option[Long], f4: Option[Int], f5: Option[String], f6: Option[Double], f7: Option[Long], f8: Option[Int], f9: Option[Int], f10: Option[Long], f11: Option[Float], f12: Option[Double], f13: Option[String], f14: Option[String], f15: Option[Long], f16: Option[Int], f17: Option[Int], f18: Option[String], f19: Option[String], f20: Option[String], ) extends Data 

  2. 仅线-71 mb

    Onlystrings
     final case class OnlyStrings( f1: Option[String], f2: Option[String], f3: Option[String], f4: Option[String], f5: Option[String], f6: Option[String], f7: Option[String], f8: Option[String], f9: Option[String], f10: Option[String], f11: Option[String], f12: Option[String], f13: Option[String], f14: Option[String], f15: Option[String], f16: Option[String], f17: Option[String], f18: Option[String], f19: Option[String], f20: Option[String], ) extends Data 

  3. 仅数字(长)-20 mb

    隆隆
     final case class OnlyLongs( f1: Option[Long], f2: Option[Long], f3: Option[Long], f4: Option[Long], f5: Option[Long], f6: Option[Long], f7: Option[Long], f8: Option[Long], f9: Option[Long], f10: Option[Long], f11: Option[Long], f12: Option[Long], f13: Option[Long], f14: Option[Long], f15: Option[Long], f16: Option[Long], f17: Option[Long], f18: Option[Long], f19: Option[Long], f20: Option[Long], ) extends Data 


每个条目包含20个字段。 每个字段的值是可选的。

测试中


进行测试的PC的特性,scala和java的版本:
PC:1.8 GHz Intel Core i5-5350U(2个物理内核),8 GB 1600 MHz DDR3,SSD SM0128G
Java版本:1.8.0_144-b01; 热点:版本25.144-b01
Scala版本:2.12.8

Java序列化


混合数据只有多头仅字符串
序列化,毫秒3444.532586,235548.63
反序列化,毫秒852.62617.652006.41
大小,mb362486

杰森


混合数据只有多头仅字符串
序列化,毫秒5280.674358.135958.92
反序列化,毫秒3347,202730.194039.24
大小,mb5236124

阿夫罗


Avro电路是在直接测试之前随时随地生成的。 为此,使用了avro4s库。
混合数据只有多头仅字符串
序列化,毫秒2146.721546.952829.31
反序列化,毫秒692.56535.96944.27
大小,mb221173

原虫


Protobuf模式
 syntax = "proto3"; package protoBenchmark; option java_package = "protobufBenchmark"; option java_outer_classname = "data"; message MixedData { string f1 = 1; double f2 = 2; sint64 f3 = 3; sint32 f4 = 4; string f5 = 5; double f6 = 6; sint64 f7 = 7; sint32 f8 = 8; sint32 f9 = 9; sint64 f10 = 10; double f11 = 11; double f12 = 12; string f13 = 13; string f14 = 14; sint64 f15 = 15; sint32 f16 = 16; sint32 f17 = 17; string f18 = 18; string f19 = 19; string f20 = 20; } message OnlyStrings { string f1 = 1; string f2 = 2; string f3 = 3; string f4 = 4; string f5 = 5; string f6 = 6; string f7 = 7; string f8 = 8; string f9 = 9; string f10 = 10; string f11 = 11; string f12 = 12; string f13 = 13; string f14 = 14; string f15 = 15; string f16 = 16; string f17 = 17; string f18 = 18; string f19 = 19; string f20 = 20; } message OnlyLongs { sint64 f1 = 1; sint64 f2 = 2; sint64 f3 = 3; sint64 f4 = 4; sint64 f5 = 5; sint64 f6 = 6; sint64 f7 = 7; sint64 f8 = 8; sint64 f9 = 9; sint64 f10 = 10; sint64 f11 = 11; sint64 f12 = 12; sint64 f13 = 13; sint64 f14 = 14; sint64 f15 = 15; sint64 f16 = 16; sint64 f17 = 17; sint64 f18 = 18; sint64 f19 = 19; sint64 f20 = 20; } 

为了生成protobuf3类,使用了ScalaPB插件。
混合数据只有多头仅字符串
序列化,毫秒1169.40865.061856.20
反序列化,毫秒113.5677.38256.02
大小,mb221173

节俭


节俭模式
 namespace java thriftBenchmark.java #@namespace scala thriftBenchmark.scala typedef i32 int typedef i64 long struct MixedData { 1:optional string f1, 2:optional double f2, 3:optional long f3, 4:optional int f4, 5:optional string f5, 6:optional double f6, 7:optional long f7, 8:optional int f8, 9:optional int f9, 10:optional long f10, 11:optional double f11, 12:optional double f12, 13:optional string f13, 14:optional string f14, 15:optional long f15, 16:optional int f16, 17:optional int f17, 18:optional string f18, 19:optional string f19, 20:optional string f20, } struct OnlyStrings { 1:optional string f1, 2:optional string f2, 3:optional string f3, 4:optional string f4, 5:optional string f5, 6:optional string f6, 7:optional string f7, 8:optional string f8, 9:optional string f9, 10:optional string f10, 11:optional string f11, 12:optional string f12, 13:optional string f13, 14:optional string f14, 15:optional string f15, 16:optional string f16, 17:optional string f17, 18:optional string f18, 19:optional string f19, 20:optional string f20, } struct OnlyLongs { 1:optional long f1, 2:optional long f2, 3:optional long f3, 4:optional long f4, 5:optional long f5, 6:optional long f6, 7:optional long f7, 8:optional long f8, 9:optional long f9, 10:optional long f10, 11:optional long f11, 12:optional long f12, 13:optional long f13, 14:optional long f14, 15:optional long f15, 16:optional long f16, 17:optional long f17, 18:optional long f18, 19:optional long f19, 20:optional long f20, } 

为了生成类似scala的节俭类,使用了Scrooge插件。
二元混合数据只有多头仅字符串
序列化,毫秒1274.69877.982168.27
反序列化,毫秒220.58133.64514.96
大小,mb371698

紧凑型混合数据只有多头仅字符串
序列化,毫秒1294.87900,022199.94
反序列化,毫秒240.23232.53505.03
大小,mb311498

消息包


混合数据只有多头仅字符串
序列化,毫秒1142.56791.551974.73
反序列化,毫秒289.6080.36428.36
大小,mb219.673

最终比较


序列化

反序列化

大小

结果的准确性
重要提示:序列化和反序列化速度的结果并非100%准确。 有个大错误。 尽管在JVM预热的情况下测试已经运行了很多次,但是很难将结果称为稳定和准确的。 因此,我强烈建议您不要针对特定​​的序列化格式做出最终结论,而应以时间表为重点。


考虑到结果并非绝对准确的事实,仍可以根据其观察结果:

  1. 再次,我们确保Java序列化是缓慢的,并且就输出而言不是最经济的。 工作缓慢的主要原因之一是使用反射访问对象的字段。 顺便说一句,这些字段的访问和记录方式不是按照您在类中声明它们的顺序,而是按照字典顺序排序。 这只是一个有趣的事实。
  2. Json是此比较中提供的唯一文本格式。 为什么在json中序列化的数据会占用大量空间是显而易见的-每个记录都与电路一起写入。 这也会影响写入文件的速度:需要写入的字节越多,花费的时间就越多。 另外,不要忘记为每个记录创建一个json对象,这也不会减少时间。
  3. 序列化对象时,Avro会分析电路,以进一步决定如何处理特定的字段。 这些都是额外的费用,导致总序列化时间增加;
  4. 与例如protobuf和msgpack相比,节俭算法需要大量内存来写入一个字段,因为它的元信息与字段值一起保存。 此外,如果查看thrift的输出文件,您会发现记录的开头和结尾的各种标识符以及整个记录的大小(作为分隔符)占据了总容量的一小部分。 所有这些当然只会增加包装时间。
  5. 像节俭一样,Protobuf可以打包元信息,但可以使其更加优化。 同样,打包和解包算法的差异使这种格式在某些情况下比另一些情况下工作更快;
  6. Msgpack运行速度非常快。 速度的一个原因是没有序列化其他元信息的事实。 这有好有坏:好是因为它只占用很少的磁盘空间并且不需要额外的记录时间,而坏处是因为通常对记录结构一无所知,因此请确定如何打包对每个记录的每个字段执行不同的值。

至于输出文件的大小,观察结果非常清楚:

  1. 从msgpack获得了用于数字拨号的最小文件。
  2. 原来,用于字符串设置的最小文件位于源文件中:)除了源文件之外,avro从msgpack和protobuf中获得了少量利润;
  3. 混合集的最小文件再次来自msgpack。 但是,差距并不是那么明显,并且avro和protobuf非常接近;
  4. 最大的文件来自json。 但是,必须指出一个重要的问题-json文本格式,并将其与二进制文件进行比较(就串行化速度而言)并不完全正确;
  5. 用于数字拨号的最大文件是从标准java序列化获得的。
  6. 字符串集的最大文件是节俭二进制文件;
  7. 混合集的最大文件是节俭二进制文件。 随后是标准的Java序列化。

格式分析


现在让我们尝试通过序列化36个字符的字符串(UUID)的示例来理解结果,而无需考虑记录之间的分隔符,记录的开头和结尾的各种标识符-仅记录1个字符串字段,但考虑到诸如字段类型和数字之类的参数。 字符串序列化的考虑一次完全涵盖了几个方面:

  1. 数字序列化(在这种情况下为字符串的长度)
  2. 字符串序列化

让我们从avro开始。 由于所有字段均为“选项”类型,因此此类字段的方案如下:`union:[“ null”,“ string”]`。 知道这一点,您可以获得以下结果:
1个字节表示记录的类型(空或字符串),每行长度1个字节(1个字节,因为avro使用可变长度写整数),每行本身36个字节。 总计:38个字节。

现在考虑msgpack。 Msgpack使用类似于可变长度的方法来写整数: spec 。 让我们尝试计算实际写入一个字符串字段需要花费多少:每行长度2个字节(因为字符串> 31个字节,所以需要2个字节),每个数据36个字节。 总计:38个字节。

Protobuf还使用可变长度对数字进行编码。 但是,除了字符串的长度外,protobuf还添加了另一个字节,其中包含字段的数量和类型。 总计:38个字节。

Thrift 二进制文件不使用任何优化来写字符串的长度,而是使用Thrift而不是每个字段编号和类型1个字节,因此,需要花费3。因此,将获得以下结果:每个字段编号1个字节,每种类型2个字节,每行长度4个字节,每个字段36个字节字符串。 总计:43个字节。

节俭契约与二进制不同,它使用可变长度方法来写整数,并且如果可能,还使用缩写的字段头记录。 基于此,我们得到:1个字节表示字段的类型和编号,1个字节表示长度,36个字节表示数据。 总计:38个字节。

Java序列化花了45个字节来写一个字符串,其中36个字节-一个字符串,9个字节-2个字节的长度和7个字节用于一些其他信息,我无法解密。

仅保留了avro,msgpack,protobuf和thrift compact。 这些格式中的每一种都将需要38个字节来写入长度为36个字符的utf-8行。 那么,为什么在打包10万个字符串记录时,尽管未压缩的方案与数据一起写入,但avro却得到的量却较小? Avro与其他格式的差距很小,造成此差距的原因是整个记录的每个打包长度都缺少额外的4个字节。 事实是,msgpack,protobuf和节俭都没有特殊的记录分隔符。 因此,为了让我正确地解压缩记录,我需要知道每个记录的确切大小。 如果不是这个事实,则msgpack很有可能具有较小的文件。

对于数字数据集,msgpack获胜的主要原因是打包数据中缺少模式信息,并且数据稀疏。 由于需要打包有关字段类型和编号的信息,节俭和protobuf甚至需要占用1个字节以上的空值。 Avro和msgpack恰好需要1个字节来写入一个空值,但是如上所述,avro将数据保存在电路中。

Msgpack还打包在一个较小的文件和一个混合集中,这也是稀疏的。 原因都是相同的因素。

因此,事实证明,打包在msgpack中的数据占用的空间最少。 这是一个公平的声明-选择msgpack作为tarantool和aerospike的数据存储格式并不是没有道理的。

结论


经过测试,我可以得出以下结论:

  1. 很难获得稳定的基准结果;
  2. 选择格式是序列化速度和输出大小之间的权衡。 同时,不要忘记重要的参数,例如使用格式的便利性和方案发展的可能性(通常这些参数起主要作用)。

可以在这里找到源代码: github

Source: https://habr.com/ru/post/zh-CN458026/


All Articles