数据流的类型
数据在不共享内存的进程传递,就需要编码为字节序列。有多种方式。
兼容性 实际描述了 编码数据的进程 和 解码数据的进程 之间的关系。它对 可演化性(允许你升级系统的部分,而不必全部升级) 非常重要。
数据可以通过多种方式从一个流程流向另一个流程(通常先编码为字节序列,再解码)。谁编码,谁解码?以下是常见场景:
- 数据库
- 服务调用
- 异步消息传递
数据库中的数据流
写入数据 —> 编码数据;读取数据 — > 解码数据。
单进程连接数据库,现在写入的数据,未来才读取。向后兼容 是必须的。
多个进程更常见,可能是不同的应用程序或服务连接一个数据库,也可能是一个服务的几个实例(并行运行,并行的目的是可伸缩性和容错性)。在这个场景下,可能发生,一个数据库中的值被较新的代码写入,被旧代码读取。因此 数据库也需要 向前兼容。
当你向数据库新增了一个字段(即修改了数据库的表结构,或者说修改了数据的模式 schema ),较新的代码写入包含新字段内容的数据到数据库中,旧代码(不知道有新字段)在读取、更新、写回记录到数据库时,理想状态时需保值新字段的值不变。编码格式需要支持,在应用层面也需要小心。
数据的生命周期超出代码的生命周期
代码的版本更新频繁,数据库的数据版本可能变化可能大多并不频繁。
将数据迁移到一个新的模式是昂贵的,大多数数据库尽可能避免它,大多数关系数据库(除了mysql,并非真的必要时,它也经常会重写整个表)允许一些简单的模式变更,如添加一个默认为空的新列,而非重写现有数据。 LinkedIn 的文档数据库 Espresso 使用 Avro 存储,允许它使用 Avro 的模式演变规则。
模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
数据归档
创建数据快照时,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
由于数据转储是一次写入的,而且以后是不可变的,所以 Avro 对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如 Parquet。
服务中的数据流:REST 与 RPC
当你需要通过网络在进程间通信时,有很多方法,最常见的是采用 cs 架构,即 服务端 通过网络暴露 API,客户端通过网络向服务端的 API 发出请求。 服务器公开的 API 被称为 服务 。
Web 是怎么工作的,客户端(即浏览器)想服务器发出请求,通过 GET 请求下载 HTML,CSS,JavaScript,图片等,通过 POST 请求发送数据给服务器。API 包含一组协议和数据格式(HTTP,URLs, SSL/TLS, HTML,等)。浏览器、web服务器,网站作者都同意这些标准。
除了浏览器,还有很多其他客户端,例如,移动端或电脑端的app,浏览器中的客户端 JavaScript 程序可以使用XMLHttpRequest成为客户端。
另外服务器本身也可以作为另一个服务的客户端(如 Web 应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为 面向服务的体系结构(service-oriented architecture,SOA) ,最近被改进和更名为 微服务架构 。
服务类似于数据库,相同点,允许客户端提交和查询数据。不同之处,数据库允许使用特定的查询语言进行任意查询,但服务只公开了一个特定于应用程序的 API (只允许由服务业务逻辑预定义的输入和输出)。这种限制提 供了一定程度的封装:服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容。
Web服务
当服务使用HTTP作为底层通信协议时,可称之为Web服务。不仅在Web上使用,还可以用于不同的环境中。如:
- 客户端应用程序(移动设备上的app,使用Ajax的 JS 应用程序)通过 HTTP 像服务发出请求。通常通过公共互联网进行。
- 一种服务向同组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心,作为面向服务/微型架构的一部分。(支持这种用例的软件有时被称为中间件)。
- 一种服务通过互连网向不同的组织所拥有的服务提出请求。用于不同组织后端系统之前的数据交换。如在线服务提供的公共 API,用户共享用户数据的OAuth 等。
由两种流行的服务方法: REST 和 SOAP。
REST 不是一个协议,是基于HTTP原则的设计哲学。强调简单的数据格式,使用 URL 来标记资源,使用 HTTP 功能来进行缓存控制,身份验证和内容类型协商。根据 REST 原则设计的 API 称为 RESTful 。
SOAP是一种基于XML的用于发起网络API请求的协议。尽管它最常用于HTTP上,但它的目标是独立于HTTP,并避免使用大多数HTTP特性。
远程过程调用(RPC)的问题
RPC模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管RPC起初看起来很方便,但这种方法根本上是有缺陷的。网络请求与本地函数调用非常不同:
- 本地函数可预测,成功或失败取决于参数。网络请求不可预知:网络问题,请求和响应可能丢失,远程计算机可能很慢或不可用,这些问题不在您的控制范围且常见,所以你必须预测并应对它们,比如重试失败的请求。
- 本地函数要么返回结果,要么抛出异常,或永远不返回(进入无限循环或进程崩溃)。网络请求可能由于超时不返回结果,如果你没有收到来自远程服务的响应,可能的原因有服务器出错,网络超时(请求未到达,响应未到达)。
- 调用本地功能,通常需要的时间是大致相同的。网络请求比本地函数调用慢的多,且延迟非常可变:不到一毫秒可以完成的请求,在网络拥塞或远程服务超载时,可能需要几秒钟。
- 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。对于字符串和数字没什么问题,但是对于较大的对象可能就会成为问题。
客户端和服务可以用不同的编程语言实现,所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子,因为不是所有的语言都具有相同的类型 。用单一语言编写的单个进程中不存在此问题。
所有这些因素意味着尝试使远程服务看起来像编程语言中的本地对象一样毫无意义,因为这是一个根本不同的事情。 REST的部分吸引力在于,它并不试图隐藏它是一个网络协议的事实(尽管这似乎并没有阻止人们在REST之上构建RPC库)。
RPC 的当前方向
尽管有这样那样的问题,RPC不会消失。有很多基于编码格式的RPC框架:Thrift和Avro带有RPC支持,gRPC是使用Protocol Buffers的RPC实现,Finagle也使用Thrift,Rest.li 使用 JSON over HTTP 。
这种新一代的RPC框架更加明确的是,远程请求与本地函数调用不同。Finagle和Rest.li 使用futures(promises)来封装可能失败的异步操作。 Futures 还可以简化需要并行发出多项服务的情况,并将其结果合并。gRPC支持流,其中一个调用不仅包括一个请求和一个响应,还包括一系列的请求和响应
其中一些框架还提供服务发现,即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。
使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是,RESTful API还有其他一些显著的优点:
- 实验和调试更友好(只需使用Web浏览器或命令行工具curl,无需任何代码生成或软件安装即可向其请求)
- 它受到所有主流编程语言和平台的支持,并且拥有庞大的工具生态系统(服务器、缓存、负载均衡器、代理、防火墙、监控、调试工具、测试工具等)可供使用。
由于这些原因,REST 似乎是公共 API 的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。
消息传递中的数据流
REST和RPC:其中一个进程通过网络向另一个进程发送请求并期望尽可能快的响应。
数据库:一个进程写入编码数据,另一个进程在将来再次读取。
异步消息传输 是介于 RPC 和 数据库之间的 进程间通信方式。类似 RPC 客户端的请求(通常称为消息)以低延迟传送到另一个进程,类似于数据库,不是通过直接的网络连接发送消息,而是通过称为消息代理(也称为消息队列或面向消息的中间件)的中介来临时存储消息。
VS RPC的优点:
- 接受端不可用或者过载时,充当缓冲区,可靠性++。
- 自动的重传消息到已经崩溃的进程,防止丢失。
- 避免发送方知道接收方的IP和端口(云部署环境中,虚拟机经常创建和销毁,比较有用)
- 允许一条消息发送给多个接受端。
- 发送端和接收端逻辑分离。
消息传递通信通常是单向的,RPC是双向的。发送者通常不期望收到消息的回复。但是回复也是可以实现的,通常是在另外一个单独的通道(channel)上,这种模式叫作 异步 。
消息代理
开源方案: RabbitMQ,ActiveMQ,HornetQ,NATS,Apache Kafka
消息代理的大致逻辑:
一个进程(称为生产者)将消息发送到一个被命名的队列或主题(topic),而消息代理(即经纪人)负责确保该消息被传递给订阅了该队列或主题的一个或多个消费者。
队列或主题(topic)是单向的。但是,消息的消费者可以将回复的消息发布到另一个主题或者原始消息发送者使用的回复队列(就类似与RPC了)。
消息代理通常不强制特定的数据模型——一个消息统称只是一个包含元数据的字节流。
分布式 actor 框架
actor 模型是单进程的并发模型。对比直接操作线程(线程存在的问题竞争条件,锁,死锁),逻辑被封装在 actor 中,每个 actor 代表一个客户和实体,它有一些本地状态(不与其他 actor 共享),通过发送和接受异步消息来与其他 actor 通信。消息传递不具有保障性,可能丢失。
在分布式的 Actor框架中,此模型用于跨多个节点伸缩应用程序。不管发送方和接收方是相同还是不同的节点,都是用相同的消息传递机制。如果它们位于不同的节点,消息则被透明编码为字节序列,通过网络发送,然后在另一侧解码。
在使用Actor模型时,由于该模型已经假设了消息可能会丢失,因此位置透明性效果更好(相比 RPC )。即使在单个进程内部,Actor模型也能处理消息丢失的情况。尽管网络延迟可能比同一进程内部更高,但在使用Actor模型时,本地和远程通信之间的基本不匹配较少,这意味着在设计应用程序时,可以更容易地处理本地和远程通信之间的差异。
分布式的 Actor 框架实质上是将消息代理和 actor 编程模型集成到一个框架中。
三个流行的分布式 actor 框架处理消息编码如下:
- 默认情况下,Akka 使用 Java 的内置序列化,不提供前向或后向兼容性。 但是,你可以用类似 Prototol Buffers 的东西替代它,从而获得滚动升级的能力。
- Orleans 默认使用不支持滚动升级部署的自定义数据编码格式;要部署新版本的应用程序,你需要设置一个新的集群,将流量从旧集群迁移到新集群,然后关闭旧集群。 像 Akka 一样,可以使用自定义序列化插件。
- 在 Erlang OTP 中,对记录模式进行更改是非常困难的(尽管系统具有许多为高可用性设计的功能)。 滚动升级是可能的,但需要仔细计划。 一个新的实验性的
maps
数据类型(2014 年在 Erlang R17 中引入的类似于 JSON 的结构)可能使得这个数据类型在未来更容易。