编码与演化
应用程序总是增增改改。修改程序大多数情况下也在修改存储的数据。
- 关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即ALTER语句),但是在任何时间点都有且仅有一个正确的模式。
- 读时模式(schema-on-read)(或 无模式(schemaless))数据库不会强制一个模式,因此数据库可以包含在不同时间写入的新老数据格式的混合。
当数据格式(format)或模式(schema)发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
- 对于 服务端(server-side) 应用程序,可能需要执行 滚动升级 (rolling upgrade) (也称为 阶段发布(staged rollout) ),一次将新版本部署到少数几个节点,检查新版本是否运行正常,然后逐渐部完所有的节点。这样无需中断服务即可部署新版本,为频繁发布提供了可行性,从而带来更好的可演化性
- 对于 客户端(client-side) 应用程序,升不升级就要看用户的心情了。用户可能相当长一段时间里都不会去升级软件
这意味着,新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持双向兼容性:
- 向后兼容 (backward compatibility)
新代码可以读旧数据 - 向前兼容 (forward compatibility)
旧代码可以读新数据
编码数据的格式
程序通常(至少)使用两种形式的数据:
- 在内存中,数据保存在对象,结构体,列表,数组,哈希表,树等中。 这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
- 如果要将数据写入文件,或通过网络发送,则必须将其 编码(encode) 为某种自包含的字节序列(例如,JSON文档)。 由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同
语言特定的格式
许多编程语言都内建了将内存对象编码为字节序列的支持。例如,Java有java.io.Serializable ,Ruby有Marshal,Python有pickle
这些编码库非常方便,可以用很少的额外代码实现内存对象的保存与恢复。但是它们也有一些深层次的问题,除非临时使用,采用语言内置编码通常是一个坏主意。
- 这类编码通常与特定的编程语言深度绑定,其他语言很难读取这种数据
- 为了恢复相同对象类型的数据,解码过程需要实例化任意类的能力,这通常是安全问题的一个来源
- 在这些库中,数据版本控制通常是事后才考虑的。因为它们旨在快速简便地对数据进行编码,所以往往忽略了前向后向兼容性带来的麻烦问题。
- 效率(编码或解码所花费的CPU时间,以及编码结构的大小)往往也是事后才考虑的
JSON,XML和二进制变体
跨语言的编码:JSON,XML和CSV,属于文本格式,因此具有人类可读性。
- 数值编码有歧义:XML 和 CSV 不能区分数字和字符串。JSON 不能区分整数和浮点数。
- 处理大数值困难。
- JSON 和 XML 对 unicode(人类可读的文本)有很好的支持,但是不支持二进制。通过 base64 绕过这个限制。
- XML 和 JSON 都有可选的模式支持。
- CSV 没有模式,行列的含义完全由应用程序指定。格式模糊
二进制编码
当数据很多的时候,数据格式的选择会有很大影响。
JSON比XML简洁,但与二进制格式相比还是太占空间。现在有很多二进制格式的 JSON(MessagePack,BSON,BJSON,UBJSON,BISON和Smile等)。
JSON 字符串是:
{
"userName": "Martin",
"favoriteNumber": 1337,
"interests": ["daydreaming", "hacking"]
}
使用MessagePack编码的记录
Thrift与Protocol Buffers
● Protocol Buffers最初是在Google开发的,Thrift最初是在Facebook开发的,并且在2007~2008年都是开源的,都是二进制编码库。
● Thrift和Protocol Buffers都需要一个模式来编码任何数据
Avro 😱
● Apache Avro 是另一种二进制编码格式。
● Avro 有两种模式语言:一种(Avro IDL)用于人工编辑,一种(基于JSON)更易于机器读取
数据流的类型
数据在流程之间流动的一些常见的方式:
● 通过数据库
● 通过服务调用
● 通过异步消息传递
数据库中的数据流
● 如果只有一个进程访问数据库,向后兼容性显然是必要的。
● 一般来说,会有多个进程访问数据库,可能会有某些进程运行较新代码、某些运行较旧的代码。因此数据库也经常需要向前兼容。
● 假设增加字段,那么较新的代码会写入把该值吸入数据库。而旧版本的代码将读取记录,理想的行为是旧代码保持领域完整。
● 用旧代码读取并重新写入数据库时,有可能会导致数据丢失。
当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。
在不同的时间写入不同的值
- 单一的数据库中,可能有一些值是五毫秒前写的,而一些值是五年前写的。
- 架构演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用模式的各种历史版本编码的记录
归档存储
- 建立数据库快照,比如备份或者加载到数据仓库:即使有不同时代的模式版本的混合,但通常使用最新模式进行编码。
- 由于数据转储是一次写入的,以后不变,所以 Avro 对象容器文件等格式非常适合。
- 也是很好的机会,把数据编码成面向分析的列式格式。
服务中的数据流:REST与RPC
- 网络通信方式:常见安排是客户端+服务器
- Web 服务:通过 GET 和 POST 请求
- 服务端可以是另一个服务的客户端:微服务架构。
- 微服务架构允许某个团队能够经常发布新版本服务,期望服务的新旧版本同时运行。
Web服务
- 当服务使用HTTP作为底层通信协议时,可称之为Web服务。
- 有两种流行的Web服务方法:REST和SOAP。
REST
- REST不是一个协议,而是一个基于HTTP原则的设计哲学。
- 它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商。
- 与SOAP相比,REST已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关。
- 根据REST原则设计的API称为RESTful。
SOAP
- SOAP是用于制作网络API请求的基于XML的协议。
- 它最常用于HTTP,但其目的是独立于HTTP,并避免使用大多数HTTP功能。
- SOAP Web服务的API使用称为Web服务描述语言(WSDL)的基于XML的语言来描述。 WSDL支持代码生成,客户端可以使用本地类和方法调用(编码为XML消息并由框架再次解码)访问远程服务。
- 尽管SOAP及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题。
- 尽管许多大型企业仍然使用SOAP,但在大多数小公司中已经不再受到青睐。
远程过程调用(RPC)的问题
- 本地函数调用是可预测的,并且成功或失败仅取决于受您控制的参数。而网络请求是不可预知的。
- 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。无法得知远程服务的响应发生了什么。
- 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( 幂等(idempotence))机制。本地函数调用没有这个问题。
- 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求慢得多,不可预知。
- 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
- 客户端和服务端可以用不同的编程语言实现,RPC 框架必须把数据类型做翻译,可能会出问题。
消息传递中的数据流
与直接RPC相比,使用消息代理(消息队列)有几个优点:
● 如果收件人不可用或过载,可以充当缓冲区,从而提高系统的可靠性。
● 它可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失。
● 避免发件人需要知道收件人的IP地址和端口号(这在虚拟机经常出入的云部署中特别有用)。
● 它允许将一条消息发送给多个收件人。
● 将发件人与收件人逻辑分离(发件人只是发布邮件,不关心使用者)。
与 PRC 相比,差异在于
● 消息传递通常是单向的:发送者通常不期望收到其消息的回复。
● 通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。
消息代理
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上,或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流,类似于RPC)
分布式的Actor框架
网友评论