马丁-克莱普曼于2012年12月5日发表。
你有一些数据,你想存储在一个文件中或通过网络发送。你可能会发现自己经历了几个阶段的演变。
一旦你到了第四阶段,你的选择通常是 Thrift, Protocol Buffers或 Avro。所有这三个都提供了高效的、跨语言的、使用模式的数据序列化,并为Java生成代码。
已经有很多关于它们的比较文章然而,许多文章忽略了一个乍看起来很平凡的细节,但实际上是至关重要的。如果模式发生变化会怎样?
在现实生活中,数据总是在不断变化。当你认为你已经敲定了一个模式的时候,有人会想出一个没有预料到的用例,并希望 "只是快速添加一个字段"。幸运的是,Thrift、Protobuf和Avro都支持模式演进:你可以改变模式,你可以让生产者和消费者同时使用不同版本的模式,而且都能继续工作。当你处理一个大的生产系统时,这是一个非常有价值的功能,因为它允许你在不同的时间独立地更新系统的不同组件,而不用担心兼容性问题。
这把我们带到了今天文章的主题。我想探讨一下Protocol Buffers、Avro和Thrift实际上是如何将数据编码成字节的--这也将有助于解释它们各自如何处理模式变化。每个框架的设计选择都很有趣,通过比较,我认为你可以成为一个更好的工程师(通过一点点)。
我将使用的例子是一个描述一个人的小对象。在JSON中我将这样写。
{
"userName": "Martin",
"favouriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
这个JSON编码可以作为我们的基线。如果我去掉所有的空白,它消耗了82个字节。
人物对象的Protobuf模式可能看起来像这样。
message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}
当我们 encode上面的数据使用这种模式时,它使用了33个字节,如下所示。
准确地看一下二进制表示法的结构,逐个字节地看。这个人的记录只是其字段的连接。每个字段以一个字节开始,表示它的标签号(上述模式中的数字1、2、3),以及字段的类型。如果一个字段的第一个字节表明该字段是一个字符串,那么它后面是该字符串的字节数,然后是该字符串的UTF-8编码。如果第一个字节表明该字段是一个整数,那么接下来是一个可变长度的数字编码。没有数组类型,但一个标签号可以出现多次,以代表一个多值字段。
这种编码对模式的进化有影响。
这种用一个标签号来代表每个字段的方法简单而有效。但我们马上就会看到,这并不是唯一的方法。
Avro模式可以用两种方式编写,一种是JSON格式。
{
"type": "record",
"name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favouriteNumber", "type": ["null", "long"]},
{"name": "interests", "type": {"type": "array", "items": "string"}}
]
}
...或在一个IDL中。
record Person {
string userName;
union { null, long } favouriteNumber;
array<string> interests;
}
请注意,在模式中没有标签号!在模式中没有标签号。那么,它是如何工作的呢?
下面是同一个例子的数据 encoded只用了32个字节。
字符串只是一个长度前缀,后面是UTF-8字节,但字节流中没有任何东西告诉你它是一个字符串。它也可能是一个变长的整数,或者完全是其他的东西。你能解析这个二进制数据的唯一方法是通过与模式一起阅读,而模式告诉你接下来应该期待什么类型。你需要拥有与所用数据的编写者完全相同的模式版本。如果你有错误的模式,解析器将不能对二进制数据进行首尾呼应。
那么,Avro是如何支持模式演变的呢?好吧,尽管你需要知道写入数据的确切模式(写入者的模式),但这并不一定与消费者所期望的模式(读者的模式)相同。实际上,你可以给Avro分析器提供两种不同的模式,它用 resolution rules来将数据从写模式翻译成读模式。
这对模式的进化有一些有趣的影响。
这就给我们留下了一个问题,就是要知道某条记录是用什么模式写的。最好的解决方案取决于你的数据被使用的环境。
一种看法是:在Protocol Buffers中,记录中的每个字段都被标记,而在Avro中,整个记录、文件或网络连接都被标记为模式版本。
乍一看,Avro的方法似乎有更大的复杂性,因为你需要付出额外的努力来分配模式。然而,我开始认为Avro的方法也有一些明显的优势。
Thrift是一个比Avro或Protocol Buffers更大的项目,因为它不仅仅是一个数据序列化库,也是一个完整的RPC框架。它也有一些不同的文化:Avro和Protobuf标准化了一个单一的二进制编码,而Thrift embraces有各种不同的序列化格式(它称之为 "协议")。
事实上,Thrift有两种不同的JSON编码,以及不少于三种不同的二进制编码。(然而,其中一种二进制编码,DenseProtocol,是只支持C++的实现的;由于我们对跨语言的序列化感兴趣,我将专注于其他两种编码)。
所有的编码都有相同的模式定义,在Thrift IDL中。
struct Person {
1: string userName,
2: optional i64 favouriteNumber,
3: list<string> interests
}
BinaryProtocol的编码非常直接,但也相当浪费(它需要59个字节来编码我们的示例记录)。
CompactProtocol编码在语义上是等同的,但它使用可变长度的整数和比特打包,将大小减少到34字节。
正如你所看到的,Thrift的模式演化方法与Protobuf的相同:每个字段在IDL中被手动分配一个标签,标签和字段类型被存储在二进制编码中,这使得解析器可以跳过未知字段。Thrift定义了一个明确的列表类型,而不是Protobuf的重复字段方法,但除此之外,两者非常相似。
就哲学而言,这些库是非常不同的。Thrift倾向于 "一站式服务 "的风格,给你一个完整的RPC框架和许多选择,而Protocol Buffers和Avro似乎更倾向于遵循一种 “do one thing and do it well”风格。
来源:
https://www.toutiao.com/article/7078084133943001631/?log_from=f72a76d35dc64_1648180420342
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!