Design Data-Intensive Applications 读书笔记十二 数据流模型

数据流模型

在章节开头就说过,当你需要将数据传输到一个与你的进程没有共享内存的进程时,例如通过网络或者写入到文件时,你需要将数据编码为二进制序列。之前讨论了不同的编码方法。

我们讨论了前向和后向兼容性,这对于演化性很重要(允许独立升级系统的一部分,不需要同时做出改变)。兼容性就是编码数据的进程和另一个解码数据进程之间的关系。接下来我们来看看进程间数据流动的最常见的几种方式。

通过数据库,调用服务,异步消息传入。

 

经过数据库的数据流

在数据库里,写入进程编码数据,读取进程解码数据。也有只有一个进程存在的情况,这时读取器只是后续的进程,我们可以将存储进数据库看成是个给自己发送信息。后向兼容在这里很重要,否则未来就无法读取先前的写入。

一般而言,同时间有多个不同进程连接数据库很正常,可能是不同的应用或者服务,也可能是同一服务的不同实例。无论如何,当应用改变时,一些进程会运行新代码,同时一些进程会运行旧代码,比如正在逐步更新时,一些实例已经更新,而其他的实例没有。这意味着一个值可能是新代码写入的,然后被旧代码读取,因此前向兼容是数据库需要考虑的。

但是这里有一个额外的障碍,假设你往模式里添加了一个字段,新代码往新字段里写入了值,然后旧版本代码读取了记录,更新,写回去了,在这个场景下,理想的行为是旧代码保持新字段完整,即便它无法解析。

之前的编码格式讨论过支持这种未知字段,但是需要在应用层面仔细考虑,如图4-7

如果你在数据库中解码一个类,然后重新编码这些类,未知的字段可能在事物处理中丢失,这不是一个难题,你只需要意识到这点。

 

不同时间写入的不同值

数据库一般运行在任何时候写入任何值,这意味着在单一数据库里你可能读取到5分钟前写入的值和5年前写入的值。

当你更新新版本的应用时,你可能会在几分钟内更新全部的代码,但是对于数据库而言,5年前的数据依然以原始的编码格式存在,除非你重写写入。这种情况称为 data outlives code,数据比代码长寿。

用新模式重写(合并)旧数据是可行的,但是对于大型数据库来说,操作消耗非常大,绝大多数数据库尽可能避免这种操作。绝大多数关系型数据库允许简单的模式修改,例如添加一列,默认值为null,不需要重写现有的数据。读取旧的行时,数据库会往缺失值的列中填null。

模式演化因此允许整个数据库只有单一的模式,即便底层的记录可能是用不同历史版本模式编码的数据。

 

档案存储

或许你会不时需要数据库快照,比如备份或者是导进数据仓库。这种情况下,数据备份会用最新的模式编码,即便原始数据使用不同历史版本的模式编码的。既然你需要备份数据,你也会重新编码数据的备份。

因为数据转储文件只写入一次,而且以后都不变,因此类似Avro对象这种格式就很合适。这也是将数据编码成适于分析的列存储格式例如Parquet。

 

经过服务的数据流:REST和RPC

如果你想通过网络交流。有几种不同的方式, 最常见的就是两个角色:servers和clients。servers对外提供接口,clients连接接口并发送请求。服务端释放出来的API称为service。

一般工作方式如下:clients向网络服务器请求,通过GET请求来下载HTML,CSS等,通过POST请求向服务端提交数据,API由标准的协议和数据集组成。因为网络浏览器,服务器和网站开发者都遵循一套标准,理论上你可以使用任何浏览器访问任何网站。

从某些方面来说,services类似数据库:他们允许clients提交和查询数据。但是,数据库允许任意的使用查询语句的查询,services释放出应用限定的API,只允许商业逻辑预定义的输入和输出。这个限制提供了一定程度的包装:services可以限制client什么可以做,什么不能做。

service-oriented/microservices架构的主要设计目标就是通过能让服务独立地部署和演化,让应用易于修改和维护。例如,每个服务由一个组负责,这个组应该可以频繁发布版本,不需要与其他组协调。换句话说,我们应该期望新旧版本的服务端和客户端能在统一时间允许,因此服务端和客户端的编码格式必须能够兼容(无论服务端是何版本),这就是之前我们讨论的

 

网络服务

当http使用底层协议通知服务时,这就称为web service,网络服务。这里有些不准确,因为网络服务不仅在网络上使用,而是在不同的场景写都可以使用,例如:

1、运行在用户设备上的客户算应用(比如手机上的原生APP)通过HTTP向服务端发送请求,这些请求通常经过公用网络。

2、一个服务向属于同组织的另一个服务发送请求,经常是处于同一个数据中心,是一个service-oriented/microservices架构的一部分。(支持这种用户场景的软件称为middleware中间件)。

3、一个服务向属于不同组织的服务发送请求。这通常是用于两个组织间的后台数据交换。这类案例包含了在线服务提供的公用API,或者是获取用户信息的OAuth服务。

目前有两个处理网络服务的方式:REST和SOAP。它们的设计思想几乎是相反的,这经常引起它们追随者的争议。

REST不是协议,而是在HTTP之上的一种设计理念。它强调简单数据格式,使用URL来气氛资源,使用HTTP的特性来缓存控制,认证和内容类型转换。REST比SOAP更受欢迎,至少在跨组织的服务整合场景下是如此,并且经常和微服务联系在一起。一个一级REST理念设计的API称为RESTful。

对比SOAP是基于XML的网络请求协议。尽管它一般使用HTTP但是它的目标就是独立于HTTP,避免使用HTTP的特性。最为替代,它带来了相关的复杂的标准,增加了不少特性(网络服务框架,就是WS-*)。

SOAP的api使用了基于XML的语言来描述,称为 Web Services Description Language,WSDL。WSDL允许代码生成,因此客户端能够使用本地类和方法来访问远程服务。这对于静态类型语言来说很有用,对于动态类型就不那么有用了。

因为WSDL语言不是设计为人可以阅读的,所以构建SOAP信息很复杂,用户依赖SOAP工具和代码生成器,IDE。对于不支持SOAP的编程语言的供应商,整合SOAP很困难。即便是SOAP带来的多样的特性在表面上很标展,但是不同供应商之间实现上的差异经常引起问题。因为这些原因,尽管SOAP在大型企业中还在使用,它已经不受绝大多数小一些的企业欢迎了。

Restful API倾向于简单处理,典型的就是更少的代码生成和自动工具。一个格式定义,如OpenAPI,也被成为Swagger,也以用来描述RESTful API和生成文档。

 

远程程序调用(RPC)产生的问题

网络服务只是网络API请求这个技术的最近的一个代表而已,其中的技术有很多问题。Enterprise JavaBeans(EJB)和Java的Remote Method Invocation(RMI)只能用Java。 Distributed Component Object Model( DCOM)只能用微软平台。 Common Object Request Broker Architecture( CORBA)过于复杂,且不提供前向或者后向兼容性。

所有的这些都是建立在 remote procedure call (RPC),远程程序调用这个想法之上的。RPC模式视图想使用同一个进程里的方法一样进行远程网络请求(抽象的说法是 location transparency,位置透明)。尽管RPC看起来很方便,但是这个方法本质上有缺陷。网络请求不同于本地方法调用:

1、本地方法调用时可以预测的,不论成功与否,取决于你控制下的参数。网络请求是不可预测的,请求或者回复都可能因为网络问题而丢失,或者是远程机器非常慢、不可用,类似的问题都超出了你的控制。网络问题非常普遍,你不得不应对它们,比如重试。

2、本地方法可以返回结果,抛出异常或者不返回结果。网络请求有另一种可能的输出:因为超时而不返回结果。这种情况下,你不知道发生了什么:如果你没有从远程端获取返回,你没有方法知道是否请求通过了。

3、如果你在失败后重试,可能发生的情况是,请求通过了但是返回因为网络问题丢失了。这种情况下,重试造成的问题是动作可能执行多次,除非你在协议中加入去重(幂等性)处理。本地方法调用不会产生这个问题。

4、每次调用本地方法,耗时几乎是相同的。一个网络请求比本地方法慢多了,并且它的延迟变化很大:好的时候,几毫秒就能执行完;网络拥挤或者服务器过载的时候,几秒钟才能处理完同样的事情。

5、调用本地方法时,可以很容易的将它的指针传递给本地的类。当你执行网络请求的时候,所有的参数需要编程成二进制序列,然后通过网络传递。如果参数是些基本类型(数字,字符等)没什么问题,但是使用对象会有问题。

6、客户端和服务端可能是用不同的语言编写的,所有RPC需要将一种语言的数据类型转换为另一种语言的数据类型;结果可能很难看,因为不是所有的原因的数据类型都相同。类似的问题不会出现在用但一语言编写的一个进程中。

所有的这些因素意味着没法将远程服务看成是内置的一个对象,因为它们本质上不同。部分REST请求也没有隐藏它们是网络协议的事实。

 

RPC当前的方向

尽管有这些问题,RPC并没有消失。不同RPC框架已经在之前提到的编码格式上建立起来了:Thrift和Avro已经支持RPC,gRPC就是使用 Protocol Buffers的一个RPC实现, Finagle 使用Thrift, Rest.li 使用HTTP协议之上的JSON。

新一代的RPC框架很明确的表示远程请求不同于本地方法调用。例如, Finagle和Rest.li使用futures(promises)来打包可能失败的异步动作。 Futures简化了需要往多个服务并行请求的场景,并且合并结果。gRPC支持流,调用不是只有一个请求和一个返回组成,而是一系列的请求和返回。

一些框架提供service discovery,服务发现,运行客户端寻找有特定服务的ip和端口。

使用二进制格式自定义RPC协议比原生的(如果REST之上的JSON)性能好。但是一个RESTful api有另一个显著的好处:易于实验和测试,你可以简单的使用浏览器或者是curl工具,不需要任何其他的代码或者安装软件,它支持所有主流编程语言和平台,并且生态里有大量的工具可用。

因为这些原因,REST似乎确定了公用api的风格。RPC框架的主要关注点就是相同组织的服务间的请求,一般是在相同数据中心里。

 

数据编码和RPC演化

对于演化性,很重要的一点就是RPC客户端和服务端可以独立地改变和部署。对比经过数据库的数据流,我们可以对服务间的数据流做一个简单、合理的设想:服务端先更新,然后客户端后更新。因此,你只需要在请求上保证后向兼容,返回上保证前向兼容。

一个RPC模式的前向兼容和后向兼容继承自它所使用的编码:

1、Thrift, gRPC (Protocol Buffers), 和 Avro RPC,根据对应的编码格式的兼容性进行演化。

2、SOAP中,请求和回复固定为XML模式,可以演化,但是有潜在陷阱。

2、RESTful API最常用JSON用作回复, JSON或者URI-encoded/form-encoded格式用作请求参数。添加可选的请求参数和返回中添加新字段是兼容性要容纳的改变。

RPC经常用于跨组织的通信,所以服务的提供者没法控制客户端,不能强制要求升级。因此,需要长时间地维护兼容性,或者是永久。如果需要作出破坏兼容性的改动,服务提供者会一起停止多个版本的服务的API。

如何控制API版本还没有共识。对于RESTful api,一般的方法是在URL中使用版本号或者是在HTTP的Accept头中使用版本号。对于使用API key来确定特定客户端的服务来说,一个选择就是在服务端存储客户端请求的API的版本号,允许版本选择器经过独立的管理接口更新。

 

消息传递数据流

这节里,我们简单看看异步消息传递系统,它介于RPC和数据库之间。它类似RPC,客户端请求(称为message,消息)快速发送至另一个进程。它与数据库类似,消息不是直接通过网络连接发送,而是经过一个中间部件称之为message broker消息代理( message queue 或者message-oriented middleware,消息队列,消息中间件),它能临时存储消息。

对比直接使用RPC,消息代理有几个好处:

1、如果接受者不可用或者超载,可以用作缓冲区,提高可用性。

2、自动重新发送丢失的消息,防止消息丢失。

3、发送者不需要知道接受者的IP和端口号(这在虚拟机来来往往的云环境中很有用)。

4、允许一个消息发送至多个接收者。

5、发送者和接受者逻辑上解耦(发送者只发送消息,不需要关心谁消耗它们)。

但是,与RPC不同的是,消息传递是单向的:发送者一般不期望接受到消息的回复。可能有进程发送回复,但是那是另一个频道完成的事。交流模式是异步的:发送者不会等待消息传输,只是发送,然后就忘了。

 

消息代理

过去消息代理被商业软件统治,如 TIBCO, IBM WebSphere, 和 webMethods。最近,有了开源的实现,如 RabbitMQ, ActiveMQ, Hor‐netQ, NATS,和Apache Kafka。

交易过程中的具体的语义依据实现和配置而变化,一般而言,消息代理如下使用:一个进程发送消息至一个队列或者主题,然后代理确保消息输送至一个或多个消费者,会有很多生产者和消费者作用于同一个主题。

一个主题提供单向数据流。但是一个消费者可以自己向另外一个主题发送消息,或者是一个被原始信息发送者消费的回复队列(支持请求/回复数据流,类似RPC)。

消息代理不会强制任何额外的数据格式,一个信息只是一系列的有着元数据的字节序列,所以你可以使用任意编码格式。如果编码是前向和后向兼容的,你就有最大的灵活性来独立地改变发布者和消费者,以任意顺序部署它们。

如果消费者向另一个主题重新发布了消息,你可能需要仔细保护未知的字段,防止之前讨论的数据库场景下出现的问题。

 

分布式角色框架

actor model(角色模型),是在单一进程中用于并发的编程模型。不是直接处理线程(相关问题是竞态条件,加锁和死锁),逻辑包含在角色中。每个角色代表客户端或者实体,它们可能有一些本地状态,通过发送和接收异步消息来和其他角色交流。消息传输没法保证在特定错误场景中,消息不会丢失。因为每个角色一次只处理一个消息,不需要担心线程,而且每个角色可以用框架定时执行。

在分布式角色框架中,编程模型用于在多节点情况下扩展应用。不论发送者和接受者是在相同节点或者是在不同节点,用的都是相同的消息传递机制。如果是在不同的节点,消息会被编码成字节序列,通过网络发送,在另一端解锁。

角色模型的位置透明性比RPC更好,因为角色模型已经假设即便是在单一进程中,消息也会丢失。尽管网络延迟比在同一进程内延迟要高,使用角色模型的本地和远程通信在本质上没有多少不同。

分布式角色框架本质上整合了消息代理和角色模型。但是如果你要逐步更新以角色为基础的应用,还是要考虑兼容性,一个新版本的节点可能向旧版本的节点发送消息,反之亦然。

三个主流的分布式角色框架如下处理消息编码:

1、Akka使用Java内置的序列化方法,不提供前向和后向兼容性。但是可以用类似Protocol Buffers来代替,因此获得逐步升级的能力。

2、Orleans使用自定义的数据编码格式,不支持逐步升级和部署,为例部署新版本应用,你需要建立新的集群,转移旧集群至新集群,然后关掉旧的,类似Akka可以使用自定义的序列化插件。

3、在Erlang OTP很难对记录的模式做出改变(尽管系统有很多针对可用性的特性);逐步升级是可行的,但是需要仔细规划。一个实验中的新数据类型maps未来可能会简化这项工作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值