应用程序总是增增改改,而修改程序大多数情况下也在修改存储的数据
- 数据格式发生改变时,需要代码更改:
服务端:rolling update/ staged rollout,即灰度发布
客户端:用户可能相当长一段时间都不会升级软件 - 存在问题:新旧版本的代码,以及新旧版本数据格式在系统中同时共存。为了系统正常运行,需要保持双向兼容性:
向后兼容:新代码可以读旧数据--可通过保留旧代码即可读取旧数据
向前兼容:旧代码可以读新数据--比较棘手,旧版程序需要忽略新版数据格式中新增的部分 - 解决方案:通过几种编码数据的格式,对新旧代码数据需要共存的系统提供支持
编码数据的格式
程序中至少使用两种形式的数据
- 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树中。这些数据结构针对CPU的高校访问和操作进行了优化(通常使用指针)
- 如果要将数据写入文件,或通过网络发送,则必须将其encoding为某种自包含的字节序列(如,JSON文档),由于每个进程都有自己独立的地址空间,一个进程中的只针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同(第三章中内存数据库更快的原因:省去了将内存数据结构编码为磁盘数据结构的开销)
语言特定的格式
编程语言对将内存对象编码为字节序列的支持:java中的java.io.Serializable,Python中的pickle,golang中的encoding/gob
存在问题:
- 与特定的编程语言绑定
- 为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这是安全问题的来源
- 数据版本控制不方便,通常事后才考虑,忽略了前向后向兼容性带来的问题
- 只适合临时使用,例如java其java.io.Serializable性能较差
JSON, XML,和二进制变体
- XML和CSV不能区分数字和字符串,JSON虽然能区分字符串和数字,但不区分整数和浮点数,而且不能指定精度
- 处理大量数据困难。大于(2^{53})的整数不能再IEEE 754双精度浮点数中精确表示
- JSON 和 XML 对 unicode(人类可读的文本)有很好的支持,但是不支持二进制。通过 base64 绕过这个限制。
- CSV没有模式,应用程序需要定义每行和每列的含义,格式模糊
二进制编码
JSON比XML简洁,但与二进制格式相比还是太占空间,现在有很多二进制格式的 JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)。
JSON,二进制编码长度为66
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
Thrift和Protocol Buffers
-
Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007~2008年都是开源的,都是二进制编码库
-
Thrift和Protocol Buffers都需要一个模式来编码任何数据
-
Thrift 有两种不同的二进制编码格式,分别称为 BinaryProtocol 和 CompactProtocol
BinaryProtocol: 对上面的信息编码只需要59个字节。每个字段都有一个类型注释(指示是一个字符串,整数,列表等),还可以根据需要执行长度(字符串的长度,列表中的iterm数)。通过字段标签取代字段名
CompactProtocol: 语义上等同于BinaryProtocol,相同信息打包只有34字节。会将字段类型和标签号打包到单个字节中,并使用可变长度证书来实现。将数字1337编码成2个字节,每个字节的最高为表示是否还有更多字节
Thrift
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}
- Protocol Buffers:与Thrift的CompactProtocol相似,可以将相同信息打包到33字节中
Protocol Buffers
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}
字段是否必需?对字段如何编码没有影响(二进制数据中没有任何字段指示是否需要字段)
字段标签和模式演变
- 字段标记不能改变(否则导致现有的编码数据无效),字段名可以改变
- 向前兼容:添加新的字段到架构,给每个字段新的标签号吗。就的代码读取新写入的数据,如果标签号码不能识别,简单忽略
- 向后兼容:添加的每个字段必须是可选的或具有默认值的,否则之前的代码会检查失败
- 删除字段:只能删除可选字段;不能再次使用相同的号码标签
数据类型和模式演变
- 数据类型可以被改变:int32 升级 int64,新代码可以读取旧代码写入的数据(补0);但是旧代码不能解析新数据(int32 读取 int64 会被截断)
- Protobuf 一个细节:没有列表或数组类型,只有 repeated,因此可以把可选字段改为重复字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表。读取新数据的旧代码只能那个看到列表的最后一个元素
- Thrift 不能把更改为列表参数,但优点是可以嵌套列表
Avro
Avro是作为Hadoop的子项目在2009年开始的,因为Thrift不适合Hadoop的用例。也使用模式来指定正在编码的数据的结构。 它有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON),更易于机器读取。
Avro 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"}
]
}
- 没有标签号吗,仅32个字节长,是所有编码中最近凑的。并且编码只是连在一起的值,不能识别字段和数据类型
- 必须按照顺序遍历字段才能解码
- 编解码必须使用完全相同的模式
Writer模式和Reader模式
- Avro的关键思想是Writer模式和Reader模式不必是相同的 - 他们只需要兼容
- 数据读取的时候,会对比 Writer模式 和 Reader模式 的字段,然后就知道怎么读了
模式演变规则
- 为了保持兼容性,只能添加或删除具有默认值的字段
- 如果要添加一个没有默认值的字段,新的阅读器将无法读取旧作者写的数据,所以会破坏向后兼容性。如果要删除没有默认值的字段,旧的阅读器将无法读取新作者写入的数据,因此会打破兼容性
- Avro不包含任何标签号码,因此对动态生成的模式更友善。因为使用Thrift或者PB需要手动写字段标签。而Avro在数据库发生变化时,可以直接生成新的Avro模式,导出数据,自动兼容
模式的优点
- Protocol Buffers,Thrift和Avro都使用模式来描述二进制编码格式,比XML和JSON简单,也更支持更详细的验证规则
- 基于模式的二进制编码相对于JSON,XML和CSV等文本数据格式的优点:
- 它们可以比各种“二进制JSON”变体更紧凑,因为它们可以省略编码数据中的字段名称
- 模式是一种有价值的文档形式,因为模式是解码所必需的,所以可以确定它是最新的(而手动维护的文档可能很容易偏离现实)
- 维护一个模式的数据库允许您在部署任何内容之前检查模式更改的向前和向后兼容性
- 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查
数据流的类型
数据库中的数据流
- 一般来说,会有多个进程访问数据库,可能会有某些进程运行较新代码、某些运行较旧的代码。因此数据库也经常需要向前兼容
- 假设增加字段,那么较新的代码会写入把该值写入数据库。而旧版本的代码将读取记录,理想的行为是旧代码保持领域完整
- 用旧代码读取并重新写入数据库时,有可能会导致数据丢失
在不同时间写入不同的值
架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录
归档存储
- 建立数据库快照,比如备份或者加载到数据仓库:即使有不同时代的模式版本的混合,但通常使用最新模式进行编码
- 由于数据转储是一次写入的,以后不变,所以 Avro 对象容器文件等格式非常适合
服务中的数据流:REST与RPC
Web服务
当服务使用HTTP作为底层通信协议时,可称之为Web服务
REST
- 它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商
- 与SOAP相比,REST已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关
- 根据REST原则设计的API称为RESTful
SOAP - SOAP是用于制作网络API请求的基于XML的协议
- SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务
- 尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题
RPC
RPC的缺陷
- 本地函数调用是可预测的,并且成功或失败仅取决于受您控制的参数。而网络请求是不可预知的
- 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。无法得知远程服务的响应发生了什么。
- 如果响应丢失而出发请求充实,会导致该操作被多次执行,需要引入幂等操作
- 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
- 客户端和服务端可以用不同的编程语言实现,RPC 框架必须把数据类型做翻译,可能会出问题
消息传递中的数据流
消息代理
RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka等消息队列
- 消息代理通常不会执行任何特定的数据模型,消息知识包含一些元数据的字节序列,可以用任何编码格式
分布式的Actor框架
- Actor模型是单个进程中并发的编程模型
- 逻辑被封装在actor中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)
- 每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。
- 不保证消息传送:在某些错误情况下,消息将丢失。
- 由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度
分布式Actor框架
- 在分布式Actor框架中,此编程模型用于跨多个节点伸缩应用程序。
- 不管发送方和接收方是在同一个节点上还是在不同的节点上,都使用相同的消息传递机制。
- 如果它们在不同的节点上,则该消息被透明地编码成字节序列,通过网络发送,并在另一侧解码
位置透明
- 位置透明在actor模型中比在RPC中效果更好,因为actor模型已经假定消息可能会丢失,即使在单个进程中也是如此。
- 尽管网络上的延迟可能比同一个进程中的延迟更高,但是在使用actor模型时,本地和远程通信之间的基本不匹配是较少的