《设计数据密集型应用》第四章(1) 编码和演化

数据在内存中,一般是用对象、结构体、列表、Hash表、树等处理的,但在数据存储和传输时,需要对数据进行编码解码才能实现。互联网应用是持续在更新的,应用的更新通常会伴随着数据结构的变化,一般情况下的升级模式可能会有出现以下的问题:

  • 服务端:采用滚动升级的方式,新旧代码读写数据库中的同一份表结构
  • 客户端:可能并不会立即跟进升级

因此,这就要求数据格式支持以下的两个特性:

  • 后向兼容:新版本代码可以读取旧版本代码写入的数据
  • 前向兼容:旧版本代码可以读取新版本代码写入的数据

一般后向兼容不难实现,因为新代码是已知旧代码写入的数据格式的,可以进行显式处理;前向兼容更困难一些,它需要旧代码忽略新代码写入的部分数据。

本章我们会介绍一些常见的数据编码格式,比如JSON、XML、Protocol Buffer、Thrift和Avro等,是如何处理schema变化的。然后介绍数据存储和通信的方式,比如REST、RPC和消息队列等。

JSON、XML和二进制变体

JSON和XML是被广泛使用的通用编码方式。XML具有良好的可读性,并且并不复杂;JSON被web浏览器内置支持,并且更加简单。但它们有一些其他问题:

  • 数字处理的问题。XML无法确定是数字还是字符串;JSON虽然可以区分数字还是字符串,但不能区分整型和浮点型,并且不能指定浮点型的精度;
  • 不支持二进制字符串。JSON和XML支持Unicode编解码,不支持用Base64等方式编码得到的二进制字符串;
  • 无schema定义或者schema定义语言复杂,有些JSON/XML的schema定义方式需要在代码中进行硬编码;
  • CSV由于不存在schema,每列的含义是由应用定义的,很难进行行列的变化。

尽管存在上述的问题,由于JSON、XML和CSV在一些场景,比如数据传输等,已经可以满足需求了,因此仍将是广泛流行的编码方式。

二进制编码

JSON有很多支持二进制编码的衍生编码格式,比如MessagePack、BSON、BJSON、UBSJON、BISON等,编码的原理并不复杂,这里不做详细介绍了,下面贴出一个MessagePack的编码实例图:


MessagePack的编码实例图

Thrift和Protocol Buffers

Thrift和Protobuf是基于相同的原理的二进制编码方式,Thrift是Facebook提出的,Protobuf是Google提出的,它们都是由一种叫接口定义语言(IDL,interface define language)确定的数据结构:


Thrift

Protobuf

Thrift和Protobuf都有对应的代码生成工具,可以生成多种语言支持的类代码。

编码方式

Thrift有两种编码方式,分别为BinaryProtocol和CompactProtocol,先来看下BinaryProtocol的编码结果:


Thrift BinaryProtocol

BinaryProtocol的编码特点:

  • 每个字段都有类型和长度标识,数据中的字符串使用ASCII或UTF-8编码;
  • 用字段编号替换了字段名称,压缩了名称需要的空间。

再来看下CompactProtocol的编码结果:


Thrift CompactProtocol

CompactProtocol的编码特点:

  • 使用变长字节进行数字编码,用sign标识编码是否结束,减少数字的编码结果使用的空间。

最后看下Protobuf的编码结果:


Protobuf

Protobuf支持某字段可选的语法optional,但不影响编码的过程,区别在于required的字段在运行时检查是否存在。

schema变化

对于Thrift格式,每个字段都有相应的数字tag,因此当需要添加新的字段时,只需要按照顺序,为新的字段添加新的tag即可,这里需要注意的点是:

  • 不能修改已有tag的字段,这样会造成格式混乱;
  • 新的字段要声明成为optional的,保证旧代码写入不会报错;
  • 删除已有字段,也只能删除声明为optional的,并且字段的tag不能被再次使用。

如果要进行数据类型的变化,可能会导致精度的丢失或者超精度结果被丢弃,比如32位整形和64位整形的转换。Protobuf和Thrift在处理列表类型时有一些区别:

  • Protobuf支持列表和非列表类型的转换。在Protobuf中,列表是通过关键字repeated定义的,当一个字段从非repeated修改为repeated时,新的代码读取的历史数据是0或者1个长度的数组,旧代码读取的新数据是列表的最后一个元素;
  • Thrift自带数组类型,因此不允许像Protobuf这样可以进行数组和非数组类型的转换。

Avro

Avro主要是针对Hadoop的处理场景产生的编码格式,Avro的格式和数据样例如下:


Avro IDL

Avro Data

Avro的编码结果如下所示:


Avro编码结果
编码方式

Avro将读schema和写schema分离,分别用于解码和编码。当数据解码时,Avro会对比读schema和写schema,并完成数据从写schema到读schema的转换。这里对两份schema中字段的顺序是不关心的。


Avro的读取过程
schema变化

添加或者删除字段,必须要该字段支持以null作为默认值,这样才可以保证后向兼容和前向兼容,null为默认值在Avro的schema中必须进行显式指定。

修改字段的数据类型比较容易,因为Avro会自动根据schema进行类型转换。修改字段名称会麻烦一点,读schema支持为字段设置别名,可以设置为修改前的写schema的字段名。因此修改字段支持后向兼容,但不支持前向兼容。

如何获取写schema

在进行数据解码时,如何获取写数据时使用的schema呢?这里有几种方式:

  • 文件:对于Hadoop的场景,可能是一个大的文件,包含很多记录,那是可以把写schema存储在整个文件的开头。
  • 数据库:写入到数据库中的每条记录使用的写schema可能不一样,因此在每条记录的开头保存写schema的版本号,然后在数据库中保存版本号和schema的对应关系。
  • 网络传输:在连接建立时先将写schema传输过来,然后在整个连接过程都使用这个写schema。
动态生成schema

Avro和前两个格式相比,由于避免了必须显式的指定每个字段的标签顺序,因此可以很方便的由代码动态生成schema,在schema变化时是很方便的。

Schema的好处

在学习完JSON、XML和Thrift、Protobuf和Avro这些格式后,我们看到后者的schema定义要比前者更简单和直接,并且包含更加丰富的校验信息。

类似JSON这种,schemaless或者schema-on-read的格式灵活性是更好的,但二进制数据格式的schema可以带来以下的好处:

  • 压缩比:摒弃了编码后数据中的字段名称;
  • 可读性:schema是可读的,可以很直接的看到schema是否是最新的;
  • 兼容性检查:将所有的schema保存在数据库中,可以直观的进行后向兼容和前向兼容的判断;
  • 代码生成:为静态语言生成类代码,可在编译时进行代码检查。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。