DDIA | 数据编码和演化

301 阅读9分钟

引言

一切都在改变,应用程序不可避免地需要随时间而变化。对于服务端程序,需要执行滚动升级,对于客户端程序,安装更新的时间并不确定。因此新旧版本的代码与新旧数据格式会同时在系统内共存,需要保持双向的兼容性。各种数据编码格式即用于支持新旧共存的系统,并用于数据存储和通信。

本文基于 DDIA 第四章整理。

数据编码格式

程序至少使用两种数据表示格式

  • 在内存中,数据保存在对象、结构体、哈希表等数据结构中,针对CPU的高效访问进行了优化(如使用指针)
  • 将数据写入文件或通过网络发送时,指针对其他进程没有意义,必须将其编码为自包含的字节序列

在这两种表示之间需要进行转化。从内存表示到字节序列的转化称为编码或序列化,相反的过程则称之为解码或反序列化。

语言特定的格式

许多编程语言都内置支持将内存中的对象编码为字节序列。如Java的java.io.Serializable,Python的pickle等。

这些编码库使用起来很方便,但是编码与特定的编程语言绑定在一起,同时缺乏向前向后的兼容性和效率。因此通常并不使用语言内置的编码方案。

JSON、XML与二进制变体

JSON和XML是可由不同语言编写和读取的标准化编码中的佼佼者。

JSON(JavaScript Object Notation)基于JavaScript的对象语法,但它的使用已经超越了JavaScript,成为许多编程语言支持的标准格式。JSON的基本结构包括:

  • 对象(Object) :由一对花括号包围的键值对集合,键(Key)为字符串,值(Value)可以是字符串、数字、数组、布尔值或者另一个对象。
  • 数组(Array) :由一对方括号包围的值的有序集合,这些值可以是上述任意类型。

XML(Extensible Markup Language)是一种标记语言,用于存储和传输数据。XML提供了一种非常灵活的方式来描述信息的结构,它的基本组成包括:

  • 元素(Element) :由开始标签和结束标签定义,可以包含文本、属性、其他元素或混合内容。
  • 属性(Attribute) :提供有关元素的额外信息,通常用于存储标识符或其他不适合作为元素内容的数据。

JSON和XML都是文本格式,提供了不错的可读性,但是也有一些缺点:

  • 类型支持有限:XML无法区分数字和由数字组成的字符串,JSON不区分整数和浮点数
  • 不支持二进制字符串:JSON和XML都不支持没有字符编码的字节序列,需要使用Base64来编码成文本
  • 占用过大空间:XML由于开闭标签过于冗长和复杂,JSON相比简单一些但仍然占用过大空间。

为处理占用过大空间的问题,有大量的二进制编码被开发出以支持JSON(MessagePack、BSON、BJSON)和XML(XBXML)。虽然这些格式已被很多细分领域所采用,但没有JSON和XML的广泛性。

{ 
    "userName": "Martin", 
    "favoriteNumber": 1337, 
    "interests": ["daydreaming", "hacking"]
}

以MessagePack为例,以上的JSON可以被编码为:

image.png

原JSON占用81字节,经二进制编码后的长度为66字节。

Thrift与Protocol Buffers

thrift和protobuf是基于相同原理的两种二进制编码库,其中thrift是由Facebook开发,protobuf是由Google开发,并都于2007~2008开源。

thrift和protobuf都需要模式来编码数据,需要使用IDL(Interface Definition Language)来描述模式,如:

struct Person { 
  1: required string userName, 
  2: optional i64 favoriteNumber,
  3: optional list interests 
}
message Person { 
  required string user_name = 1; 
  optional int64 favorite_number = 2; 
  repeated string interests = 3; 
}

thrift有两种二进制编码格式:BinaryProtocol和CompactProtocol

image.png

image.png

protobuf的编码格式与CompactProtocol相似,只需要33字节就可以完成相同的记录。

image.png

thrift和protobuf都使用了字段标签代替字段名和变长编码等方法,减小数据包的大小,从而提高网络传输效率和减少存储需求。

从示例中可知,一条编码记录只是一组编码字段的拼接,每个字段由其标签号(1、2、3)标识。因为编码不直接引用字段名称,因此可以随意更改字段名称或增加新的字段。当旧代码读取新数据时,遇到不能识别的号码,可以简单忽略该字段,从而实现向前兼容性。

因此,虽然JSON和XML等文本格式非常普遍,但基于模式的二进制编码依然是个不错的选择并拥有一些不错的属性

  • 比文本格式的二进制变体更紧凑
  • 允许在部署前检查向前兼容性和向后兼容性
  • 可以利用模式生成代码并在编译时进行类型检查

数据流模式

数据可以通过多种方式从一个进程流向另一个进程,但无论以何种方式,都需要进行编解码,向前和向后的兼容性是可演化性的关键。

基于数据库的数据流

访问数据库的程序有两种

  1. 由同一个进程访问
  2. 由多个进程访问

由同一进程访问可以理解成向未来的自己发送消息,这时向后兼容性是重要的,否则未来无法解码自己曾经写过的东西。

一般而言,多个进程同时访问数据库是更常见的。这些进程可能是不同版本不同服务的程序,因此数据库可能被新代码的进程写入而被旧代码的进程读取,所以数据库也需要保证向前兼容性。

因此数据库提供了快照和添加默认空的新列作为模式更改来提供兼容性。

基于服务的数据流

对于需要通过网络进行通信的进程,最常见的方式通常涉及两种角色:服务器(server)和客户端(client)。客户端(Web浏览器、移动设备或桌面的本地应用)通过一组标准的协议向服务器发出请求获取一组标准的数据格式。

此外,服务器本身也可以是另一项服务的客户端(如Web服务器是数据库的客户端)。将大型应用程序按照功能区域分解为较小的服务,当一个服务需要另一个服务的某些功能时,会向另一个服务发出请求。这种构建应用程序的方式传统上被称为面向服务的架构(service-oriented architecture),最近则被称为微服务架构(microservices architecture)。

微服务架构一个关键设计目标是,通过使服务可独立部署和演化,让应用程序更易更改和维护。

网络服务

当HTTP被用作与服务通信的底层协议时,被称作Web服务。Web服务在不同的上下文中使用

  • 客户端:运行在用户设备上的客户端程序通过HTTP向服务发出请求,这些请求通过公共互联网进行。
  • 中间件:一个服务向同一组织内的另一服务提出请求,作为微服务架构的一部分。
  • 跨组织交换:一个服务向不同组织所拥有的服务提出请求。如在线服务提供的API或共享访问用户数据的OAuth。

对此,有两种流行的Web服务方法:REST和SOAP。

REST不是一种协议,而是一个基于HTTP的设计理念。REST强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制、身份验证和内容类型协商。根据REST原则设计的API被称为RESTful。

SOAP是一种基于XML的协议,用于发出网络API请求。虽然SOAP最常用于HTTP,但其目的是独立于HTTP,避免使用大多数HTTP功能。

SOAP的消息过于复杂且严重依赖工具支持、代码生成和IDE,而RESTful的API更倾向于简单的方法,如使用OpenAPI来描述RESTful API并生成文档。因此在跨组织数据交换和微服务架构的发展中,REST变得越来越受欢迎。

RPC

RPC(Remote Procedure Call)远程过程调用是一种技术,允许一个程序能够请求另一个程序在远程系统上执行一个过程或函数,而不需要开发者处理网络的细节。RPC抽象了网络通信的细节(位置透明),使得开发者可以像调用本地函数一样调用远程的函数或方法。

RPC看起来很方便,但网络请求和本地函数调用有一些根本的不同:

  • 网络请求是不可预测的:本地函数只可以成功或失败,而网络请求可能因为远程服务器宕机失败或网络问题而丢失,对此需要有所准备。
  • 网络请求可能超时:如果没有收到远程服务的响应,无法知道请求是成功还是失败。当因为请求实际已完成只是响应丢失时,重试会导致操作被执行多次。
  • 网络请求延迟波动大:本地函数执行时间大致相同,当网络拥塞或远程服务过载时,相同的操作完成时间会相差较多。
  • 网络请求需要编码:本地函数可以通过指针传递,但网络请求必须将参数编码为可以通过网络发送的字节序列。

虽然RPC有一些问题,但是RPC依然蓬勃发展,在各种编码的基础上构建了RPC框架:Thrift带有RPC支持,gRPC使用了Protocol Buffers的RPC实现。

除此以外,一些框架还集成了服务治理模块,用于服务注册和发现、可用性和可观测性。

基于消息传递的数据流

基于消息传递的数据流是位于服务和数据库之间的异步消息系统。一个进程的请求不会直接发送到另一个进程,而是会发送到暂存消息的中介,由中介发送到目标。与直接RPC相比,使用消息代理有以下优点:

  • 提高可靠性
  • 防止消息丢失
  • 支持单生产者多消费者
  • 逻辑解耦

这种通信方式是异步的,发送者不会等待消息被传递。