前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Avro、Protobuf和Thrift中的模式演变

Avro、Protobuf和Thrift中的模式演变

作者头像
IT大咖说
发布2022-03-29 10:40:07
1.2K0
发布2022-03-29 10:40:07
举报
文章被收录于专栏:IT大咖说

马丁-克莱普曼于2012年12月5日发表。

你有一些数据,你想存储在一个文件中或通过网络发送。你可能会发现自己经历了几个阶段的演变。

  1. 使用你的编程语言的内置序列化,例如 Java serialization, Ruby的 marshal或 Python 的 pickle. 或者你甚至可以发明你自己的格式。
  2. 然后你意识到被锁定在一种编程语言中是很糟糕的,所以你转而使用一种被广泛支持的、与语言无关的格式,如JSON(如果你喜欢像1999年那样狂欢,也可以使用XML)。
  3. 然后你决定JSON太冗长了,解析起来太慢了,你对它不区分整数和浮点感到恼火,并且认为你很喜欢二进制字符串和Unicode字符串。所以你发明了某种二进制格式,有点像JSON,但又是二进制(1, 2, 3, 4, 5, 6).
  4. 然后你发现人们把各种随机的字段塞进他们的对象中,使用不一致的类型,而你很想有一个模式和一些文档,非常感谢。也许你还在使用一种静态类型的编程语言,并想从模式中生成模型类。你也意识到你的二进制JSON-lookalike实际上并不那么紧凑,因为你仍然在重复存储字段名;嘿,如果你有一个模式,你可以避免存储对象的字段名,你可以节省一些字节

一旦你到了第四阶段,你的选择通常是 Thrift, Protocol Buffers或 Avro。所有这三个都提供了高效的、跨语言的、使用模式的数据序列化,并为Java生成代码。

已经有很多关于它们的比较文章然而,许多文章忽略了一个乍看起来很平凡的细节,但实际上是至关重要的。如果模式发生变化会怎样?

在现实生活中,数据总是在不断变化。当你认为你已经敲定了一个模式的时候,有人会想出一个没有预料到的用例,并希望 "只是快速添加一个字段"。幸运的是,Thrift、Protobuf和Avro都支持模式演进:你可以改变模式,你可以让生产者和消费者同时使用不同版本的模式,而且都能继续工作。当你处理一个大的生产系统时,这是一个非常有价值的功能,因为它允许你在不同的时间独立地更新系统的不同组件,而不用担心兼容性问题。

这把我们带到了今天文章的主题。我想探讨一下Protocol Buffers、Avro和Thrift实际上是如何将数据编码成字节的--这也将有助于解释它们各自如何处理模式变化。每个框架的设计选择都很有趣,通过比较,我认为你可以成为一个更好的工程师(通过一点点)。

我将使用的例子是一个描述一个人的小对象。在JSON中我将这样写。

代码语言:javascript
复制
{
    "userName": "Martin",
    "favouriteNumber": 1337,
    "interests": ["daydreaming", "hacking"]
}

这个JSON编码可以作为我们的基线。如果我去掉所有的空白,它消耗了82个字节。

Protobuf

人物对象的Protobuf模式可能看起来像这样。

代码语言:javascript
复制
message Person {
    required string user_name        = 1;
    optional int64  favourite_number = 2;
    repeated string interests        = 3;
}

当我们 encode上面的数据使用这种模式时,它使用了33个字节,如下所示。

准确地看一下二进制表示法的结构,逐个字节地看。这个人的记录只是其字段的连接。每个字段以一个字节开始,表示它的标签号(上述模式中的数字1、2、3),以及字段的类型。如果一个字段的第一个字节表明该字段是一个字符串,那么它后面是该字符串的字节数,然后是该字符串的UTF-8编码。如果第一个字节表明该字段是一个整数,那么接下来是一个可变长度的数字编码。没有数组类型,但一个标签号可以出现多次,以代表一个多值字段。

这种编码对模式的进化有影响。

  • 可选字段、必填字段和重复字段之间的编码没有区别(除了标签号可以出现的次数)。这意味着你可以将一个字段从可选字段改为重复字段,反之亦然(如果解析器期待一个可选字段,但在一条记录中多次看到相同的标签号,它就会丢弃除最后一个值以外的所有字段)。required有一个额外的验证检查,所以如果你改变它,你会有运行时错误的风险(如果消息的发送者认为它是可选的,但接收者认为它是必需的)。
  • 一个没有值的可选字段,或者一个值为零的重复字段,根本不会出现在编码数据中--带有该标签号的字段根本不存在。因此,从模式中删除这类字段是安全的。然而,你决不能在将来为另一个字段重复使用标签号,因为你可能仍然有存储的数据,这些数据在你删除的字段中使用了该标签。
  • 你可以向你的记录添加一个字段,只要给它一个新的标签号。如果Protobuf分析器看到一个在其模式版本中没有定义的标签号,它就没有办法知道这个字段叫什么。但是它确实大致知道它是什么类型,因为该字段的第一个字节中包含了一个3位类型代码。这意味着,即使解析器不能准确地解释这个字段,它也能算出需要跳过多少个字节,以便找到记录中的下一个字段。
  • 你可以重命名字段,因为字段名在二进制序列化中并不存在,但你永远不能改变标签号。

这种用一个标签号来代表每个字段的方法简单而有效。但我们马上就会看到,这并不是唯一的方法。

Avro

Avro模式可以用两种方式编写,一种是JSON格式。

代码语言:javascript
复制
{
    "type": "record",
    "name": "Person",
    "fields": [
        {"name": "userName",        "type": "string"},
        {"name": "favouriteNumber", "type": ["null", "long"]},
        {"name": "interests",       "type": {"type": "array", "items": "string"}}
    ]
}

...或在一个IDL中。

代码语言:javascript
复制
record Person {
    string               userName;
    union { null, long } favouriteNumber;
    array<string>        interests;
}

请注意,在模式中没有标签号!在模式中没有标签号。那么,它是如何工作的呢?

下面是同一个例子的数据 encoded只用了32个字节。

字符串只是一个长度前缀,后面是UTF-8字节,但字节流中没有任何东西告诉你它是一个字符串。它也可能是一个变长的整数,或者完全是其他的东西。你能解析这个二进制数据的唯一方法是通过与模式一起阅读,而模式告诉你接下来应该期待什么类型。你需要拥有与所用数据的编写者完全相同的模式版本。如果你有错误的模式,解析器将不能对二进制数据进行首尾呼应。

那么,Avro是如何支持模式演变的呢?好吧,尽管你需要知道写入数据的确切模式(写入者的模式),但这并不一定与消费者所期望的模式(读者的模式)相同。实际上,你可以给Avro分析器提供两种不同的模式,它用 resolution rules来将数据从写模式翻译成读模式。

这对模式的进化有一些有趣的影响。

  • Avro编码没有一个指示器来说明哪个字段是下一个;它只是按照它们在模式中出现的顺序,对一个又一个字段进行编码。因为解析器没有办法知道一个字段被跳过,所以在Avro中没有可选字段这种东西。相反,如果你想撇开一个值,你可以使用一个联合类型,比如上面的union { null, long }。这被编码为一个字节,告诉解析器要使用哪种可能的联合类型,然后是值本身。通过使用null类型的Union(简单地编码为零字节),你可以让一个字段变得可有可无。
  • Union类型很强大,但在改变它们时,你必须小心。如果你想给Union添加一个类型,你首先需要用新的模式更新所有的读者,这样他们就知道该怎么做了。只有当所有的读者都被更新后,写作者才可以开始把这个新的类型放在他们生成的记录中。
  • 你可以随心所欲地重新排列记录中的字段。尽管字段是按照它们被声明的顺序进行编码的,但解析器是按照名字来匹配读写器模式中的字段的,这就是为什么在Avro中不需要标签号。
  • 因为字段是按名称匹配的,所以改变字段的名称是很棘手的。你需要首先更新数据的所有读者以使用新的字段名,同时保留旧的名称作为别名(因为名称匹配使用来自读者模式的别名)。然后,你可以更新写作者的模式以使用新的字段名。
  • 你可以在一条记录中添加一个字段,只要你给它一个默认值(例如,如果字段的类型是与null联合的,则为null)。默认值是必要的,这样当使用新模式的读者解析用旧模式写的记录时(因此缺少字段),它就可以填入默认值来代替。
  • 相反,你可以从一条记录中删除一个字段,只要它以前有一个默认值。(这是一个很好的理由,如果可能的话,让你的所有字段都有默认值。)这样,当使用旧模式的读者解析用新模式写的记录时,它就可以返回到默认值。

这就给我们留下了一个问题,就是要知道某条记录是用什么模式写的。最好的解决方案取决于你的数据被使用的环境。

  • 在Hadoop中,你通常会有包含数百万条记录的大文件,这些记录都是用同一个模式编码的。 Object container files处理这种情况:他们只是在文件的开头包括一次模式,文件的其余部分就可以用该模式进行解码。
  • 在RPC上下文中,在每个请求和响应中发送模式的开销可能太大。但是,如果你的RPC框架使用长寿命的连接,它可以在连接开始时协商一次模式,并在许多请求中分摊开销。
  • 如果你在数据库中逐一存储记录,最终可能会出现在不同时间编写的不同模式版本,因此你必须在每条记录上注释其模式版本。如果存储模式本身的开销太大,你可以使用一个 hash的模式,或者一个连续的模式版本号。然后你需要一个 schema registry在这里,你可以为一个给定的版本号查找准确的模式定义。

一种看法是:在Protocol Buffers中,记录中的每个字段都被标记,而在Avro中,整个记录、文件或网络连接都被标记为模式版本。

乍一看,Avro的方法似乎有更大的复杂性,因为你需要付出额外的努力来分配模式。然而,我开始认为Avro的方法也有一些明显的优势。

  • 对象容器文件是很好的自我描述:文件中嵌入的作者模式包含了所有的字段名和类型,甚至还有文档字符串(如果模式的作者费心写了一些)。这意味着你可以将这些文件直接加载到交互式工具中,如 Pig等交互式工具中,而且无需任何配置就能正常工作。
  • 由于Avro模式是JSON格式,你可以在其中添加你自己的元数据,例如,描述一个字段的应用级语义。当你分发模式时,这些元数据也会自动分发。
  • 模式注册表在任何情况下都可能是一件好事,它可以作为 documentation并帮助你找到和重用数据。而且因为没有模式,你根本无法解析Avro数据,所以模式注册表可以保证是最新的。当然,你也可以建立一个protobuf模式注册表,但由于它不是操作所必需的,所以它最终将是在尽力而为的基础上。

Thrift

Thrift是一个比Avro或Protocol Buffers更大的项目,因为它不仅仅是一个数据序列化库,也是一个完整的RPC框架。它也有一些不同的文化:Avro和Protobuf标准化了一个单一的二进制编码,而Thrift embraces有各种不同的序列化格式(它称之为 "协议")。

事实上,Thrift有两种不同的JSON编码,以及不少于三种不同的二进制编码。(然而,其中一种二进制编码,DenseProtocol,是只支持C++的实现的;由于我们对跨语言的序列化感兴趣,我将专注于其他两种编码)。

所有的编码都有相同的模式定义,在Thrift IDL中。

代码语言:javascript
复制
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大咖说的热心支持!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 IT大咖说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ◆Protobuf
  • ◆Avro
  • ◆Thrift
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档