编码和演进

数据库中的数据格式会经常变化。

随着数据格式的变化,应用代码也应该随之改变。但是在大型应用中,代码变化通常不会瞬间完成:

  • 在服务端,有滚动发行的概念。
  • 在客户端,用户很可能不会能安装更新版本。

所以需要兼容性:

  • 向后兼容:
    新代码可以读老代码写的数据。

  • 向前兼容:
    老代码可以读新代码写的数据。

数据编码格式

数据通常以(至少)两种不同的表现形式和应用程序交互:

  1. 在内存中,数据以各种 objects, structs, lists, arrays, hash tables, trees 等结构存储。这些数据结构通常会根据 CPU 的操作来做优化(通常以指针的形式)。

  2. 当数据需要被写入文件或者通过网络传输时,需要被编码成某种独立的字节序列。

因此,我们需要在这两种表现形式之间做翻译:

  • 内存 –> 字节序列:称为编码(encoding),也叫序列化;
  • 字节序列 –> 内存: 称为解码(decoding), 也叫反序列化;

和编程语言相关的格式

许多语言都自带了序列化功能的支持:

  • Java: java.io.Serializable
  • Python: pickle

这些内置的序列化库在便捷好用的同时有一些很深的坑:

  • 序列化后的数据只能被同一种语言读取;
  • 为了恢复成任意的对象类型,解码进程需要能创建任意类。这会带来一种安全隐患:攻击者能初始化任意类,这意味着攻击者可以远程执行任意代码。
  • 数据的版本经常被忽视:兼容性在快速和便捷的需求面前经常被忽略。
  • 性能问题:编解码是一种 CPU 消耗大户。

JSON, XML 以及二进制

JSON, XML 和 CSV 格式都是人类可读的文本格式。除了表面的语法问题外,还有一些小问题:

  • 数字
    XML 和 CSV 不区分字符串和数字;JSON 虽然区分,但是不区分整数和浮点数,也没有精度;
    当处理的数值很大时,这个问题尤其困扰;

  • JSON 和 XML 对 UNICODE 字符串的支持很好,但是不支持二进制字符串;

  • 类型校验不好做

除了这些问题之外,JSON、XML 和 CSV 都足够好用了,特别是在做数据交换的时候。在这种时候,只要大家都认同这个格式,性能和格式是否优雅就一点都不重要了:毕竟,让不同组织都认同某件事的难度比啥都大

二进制编码

当数据只在一个组织内部流转时,可以采用不那么通用的编码格式:比如,可以考虑数据的大小和编解码的性能。在数据量小的时候,这个优势看似微不足道,但是当数据量了之后,带来好处不容忽视。

Thrift 和 Protocol Buffers

两者都是二进制编码格式;在数据被编码前都需要一个 Schema 来定义数据的格式。

两者都提供了代码生成工具来生成这个定义好的类型在不同语言内的代码实现。

Thrift 有两种二进制编码格式:

  • BinaryProtocol
  • CompactProtocol

CompactProtocol压缩更厉害了,通过:1. 字段类型和 Tag 合并成单个 byte;2. 整数的字节长度是变长的 ——- 数字越大长度越长。

Protocol Buffers

需要值得一提的是:requiredoptional 字段不在编码中体现,单纯地是 runtime 行为。

字段 tag 和 schema 演进

整个定义中,tag number 是最重要的。

只要 tag number 不被重新使用就可以保证 向前兼容: 老的代码可以读新的代码写的数据。
只要 tag number 不被重用并且保证新的字段是 optional 的就可以保证 向后兼容:新的代码可以读老的代码写的数据。

数据类型和 schema 演进

数据类型可以改变,但是会有精度丢失的风险。
另外,对于 list 类型的字段,不同的编码格式处理的方式不同。

Avro

Avro是另一种二进制编码格式。

Avro 同样使用 Schema 来定义被编码的数据的结构。Avro 有两种 Schema 语言:Avro IDL 是用于人类编辑的; 基于 JSON 的另一种是机器读的。

从下图可以看到,在编码数据中,没有任何字段标志和类型,只是单纯的把所有数据都拼接在一起,所以数据量非常小。

为了正确地解码,解码方必须使用和编码方一模一样的 Schema 格式。

编解码方 Schema

为了正常解码,关键的是 解码方需要理解编码方的 schema。

在 Avro 中,重要的是:编解码方的 Schema 不必完全一样 —– 只需要兼容就行。

在解码的时候,Avro Lib 会把编码方的 Schema 翻译成 解码方 Schema,如下所示:

编码方 Schema 到底是啥

由上可知,重要的一点是:解码方要怎么才能知道编码方 Schema ?

有几种情况:

  • 有超多记录的大文件
    当数据量超大时,多一份 Schema 文件也无所谓了。所以编码方在写文件时可以把编码方的 Schema 写在文件开头。

  • 小文件但是记录又很多时
    使用版本号来控制 Schema。

  • 通过网络连接传送记录
    当两个进程通过网络连接时,可以在建立链接前协商 Schema 版本号来控制 Schema。

动态生成 Schema

类似 Avro 这样的 Schema 方式的一大优势在于:如果编码方有 schema 的改动,不用手动修改 Avro 编码 Schema,而 PB 和 Thrift 都需要。

这些二进制编码的优势

  • 更压缩,因为没有字段名字了
  • schema 可以当做数据文档
  • 通过保存所有版本,可以保证兼容性
  • 对于静态语言,编译阶段的类型检查是很有用的

(TO BE CONTINUED…)