第四章 编码和演化
一、为什么需要考虑编码?
在大型应用程序中,代码变更无法立刻生效。原因如下:
- 服务端程序:代码往往通过灰度发布方式,新版本主键部署到所有节点
- 客户端程序:取决于用户是否更新
上述过程意味着,新旧数据格式会在系统中共存,系统顺利运行的前提是 双向兼容,即: - 向后兼容:新的代码可以读取旧代码写入的数据
- 向前兼容:旧的代码可以读取新代码写入的数据
本章中介绍几种编码数据格式:JSON、XML、Protocol Buffers Thrift 和 Avro。重点关注:这些格式如何应对模式变化,如何对新旧代码数据需要共存的系统提供支持。
最后,本章介绍了如何使用数据进行存储和通信:REST、RPC、消息传递系统
大体上说,文章中提到的模式演变是指在系统迭代过程中,对接口的参数进行修改(例如 IDL 中定义了接口的模式,对 IDL 的修改就代表了文中说的模式转变)
一般来说,大型系统中为了兼容旧数据,不会修改现在正在使用的字段的类型或者将一个字段从可选转为必须,更多的是增加新的字段。
二、编码格式
程序通常有两种形式的数据:
- 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
- 如果要将数据写入文件,或通过网络发送,则必须将其 编码(encode) 为某种自包含的字节序列(例如,JSON文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同。
所以
- 编码:内存中表示从对象等转换为字节序列的过程成为编码
- 解码:编码的反过程
2.1 文本格式 json xml csv
文本格式编码的一个特点就是:具有很好的可读性并且跨平台性,但是这些文本格式也有一些问题
- 数字处理
- xml csv 中,无法区分数字和由数字组成的字符串
- json 无法区分整数和浮点数,不可以指定精度
- 二进制数据
- json xml 不支持二进制数据
- 模式
- xml json 提供的模式支持功能强大但是实现复杂
- csv 没有模式,每行每列含义均由应用程序确定,非常模糊
2.2 二进制编码
二进制编码的最大优势:占用空间小,效率高。
thrift 和 protocol buffers
两者是基于相同原理的二进制编码库,它们的构成如下:
- 通过 IDL (接口描述语言)描述模式
- 通过代码生成工具,生成不同编程语言的类
如何处理模式演变?
- 字段标签部分
- 添加新字段:
- 向前兼容:
- 可以添加新字段,必须具有新的标记号码
- 为了保证向后兼容,处理时必须有以下两种准则
- 标记为可选参数
- 一定会有默认值
- 删除字段:
- 只能选择可选字段
- 不能使用当前的 标签号码(这个在 thrift 中似乎做不到?)
- 添加新字段:
- 数据类型部分
- 数据类型可以改变(int32-int64)
- 向后兼容
- 满足,新代码可以读旧代码的数据
- 向前兼容
- 不满足,读取会截断
- 向后兼容
- Protobuf:没有列表类型,只有 repeated,因此可以把可选字段改为重复字段
- Thrift 不可以更改为参数列表,优点是可以嵌套列表。
- 数据类型可以改变(int32-int64)
Avro
Avro 是另一种二进制编码格式
Avro 中有两种模式语言:Avro IDL && JSON
IDL
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}
JSON
{
"type": "record",
"name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favoriteNumber", "type": ["null", "long"], "default": null},
{"name": "interests", "type": {"type": "array", "items": "string"}
]
}
Avro 中关键思想是:Writer 模式和 Reader 模式不必完全相同,只需要兼容。
模式演变规则:
- 为了兼容性,只能添加或者删除具有默认值的字段。
2.3 不同编码选型
- 公司内部系统间调用,性能要求 100ms 以内,可以考虑基于 XML 的 SOAP 协议
- 性能要求不高,或者数据负荷较小的,可以选择 JSON
- 性能要求较高,Protobuf Thrift Avro 均可
- 如果需要提供完整的 RPC 能力,选择 Thrift
- 如果对持久性要求在 T 级别,Protobuf 和 Avro 首选
- 序列化需要支持不同传输协议,优先考虑 Protobuf
三、数据流类型
数据流动的常见方式
- 数据库
- 服务调用
- 异步消息传递
3.1 数据库中的数据流
解码:读取数据库
编码:访问数据库
场景描述:
多个服务访问同一个数据库,其中服务a对数据行字段进行扩展,则服务b对行进行读写时,要保证仅对旧字段进行更新,不能影响新字段的值
3.2 服务中的数据流
RPC 的问题
- 远程调用会存在网络超时导致的没有结果返回,如何处理?
- 网络请求的时间相较于本地函数调用是不稳定的
- 超时失败,客户端进行重试时,服务端如何保证幂等?
- 通过网络,如何传递对象
RPC 和数据编码
- RPC 可能会使用上述编码格式中的任何一种,模式演化时是否能支持向前/后兼容,需要取决于编码格式
3.3 消息传递中的数据流
消息传递:不通过直接的网络连接发送信息,而是通过消息代理中介临时存储信息。
和 RPC 相比,消息传递的通信是单向的。
消息代理的使用方式如下:
- 一个进程将消息发送到指定的队列/主题
- 代理确保消息传递到订阅主题的消费者
消息代理通常不会执行特定的数据模型,可以使用任何编码。