finish chapter 4

This commit is contained in:
Yin Gang 2021-08-01 12:09:18 +08:00
parent e09e685e3e
commit 19b303b4d4

54
ch4.md
View File

@ -355,7 +355,7 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
在部署应用程序的新版本时,也许用不了几分钟就可以将所有的旧版本替换为新版本(至少服务器端应用程序是这样的)。但数据库内容并非如此:对于五年前的数据来说,除非对其进行显式重写,否则它仍然会以原始编码形式存在。这种现象有时被概括为:数据的生命周期超出代码的生命周期。
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn的文档数据库Espresso使用Avro存储允许它使用Avro的模式演变规则【23】。
将数据重写(迁移)到一个新的模式当然是可能的,但是在一个大数据集上执行是一个昂贵的事情,所以大多数数据库如果可能的话就避免它。大多数关系数据库都允许简单的模式更改,例如添加一个默认值为空的新列,而不重写现有数据[^v]读取旧行时,对于磁盘上的编码数据缺少的任何列,数据库将填充空值。 LinkedIn的文档数据库Espresso使用Avro存储允许它使用Avro的模式演变规则【23】。
因此,模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
@ -377,15 +377,15 @@ Avro为静态类型编程语言提供了可选的代码生成功能但是它
当您需要通过网络进行通信的进程时安排该通信的方式有几种。最常见的安排是有两个角色客户端和服务器。服务器通过网络公开API并且客户端可以连接到服务器以向该API发出请求。服务器公开的API被称为服务。
Web以这种方式工作客户Web浏览器向Web服务器发出请求使GET请求下载HTMLCSSJavaScript图像等并向POST请求提交数据到服务器。 API包含一组标准的协议和数据格式HTTPURLSSL/TLSHTML等。由于网络浏览器网络服务器和网站作者大多同意这些标准您可以使用任何网络浏览器访问任何网站至少在理论上
Web以这种方式工作客户Web浏览器向Web服务器发出请求通过GET请求下载HTMLCSSJavaScript图像等并通过POST请求提交数据到服务器。 API包含一组标准的协议和数据格式HTTPURLSSL/TLSHTML等。由于网络浏览器网络服务器和网站作者大多同意这些标准您可以使用任何网络浏览器访问任何网站至少在理论上
Web浏览器不是唯一的客户端类型。例如在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端该技术被称为Ajax 【30】。在这种情况下服务器的响应通常不是用于显示给人的HTML而是用于便于客户端应用程序代码如JSON进一步处理的编码数据。尽管HTTP可能被用作传输协议但顶层实现的API是特定于应用程序的客户端和服务器需要就该API的细节达成一致。
Web浏览器不是唯一的客户端类型。例如在移动设备或桌面计算机上运行的本地应用程序也可以向服务器发出网络请求并且在Web浏览器内运行的客户端JavaScript应用程序可以使用XMLHttpRequest成为HTTP客户端该技术被称为Ajax 【30】。在这种情况下服务器的响应通常不是用于显示给人的HTML而是便于客户端应用程序代码进一步处理的编码数据如JSON。尽管HTTP可能被用作传输协议但顶层实现的API是特定于应用程序的客户端和服务器需要就该API的细节达成一致。
此外服务器本身可以是另一个服务的客户端例如典型的Web应用服务器充当数据库的客户端。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务这样当一个服务需要来自另一个服务的某些功能或数据时就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为**面向服务的体系结构service-oriented architectureSOA**,最近被改进和更名为**微服务架构**【31,32】。
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在[第2章](./ch2.md)中讨论的查询语言进行任意查询但是服务公开了一个特定于应用程序的API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装服务可以对客户可以做什么和不可以做什么施加细粒度的限制。
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在[第2章](./ch2.md)中讨论的查询语言进行任意查询但是服务公开了一个特定于应用程序的API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如每个服务应该由一个团队拥有并且该团队应该能够经常发布新版本的服务而不必与其他团队协调。换句话说我们应该期望服务器和客户端的旧版本和新版本同时运行因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——正是我们所做的本章一直在谈论
面向服务/微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如每个服务应该由一个团队拥有并且该团队应该能够经常发布新版本的服务而不必与其他团队协调。换句话说我们应该期望服务器和客户端的旧版本和新版本同时运行因此服务器和客户端使用的数据编码必须在不同版本的服务API之间兼容——这正是我们在本章所一直在谈论的
#### Web服务
@ -395,15 +395,19 @@ Web浏览器不是唯一的客户端类型。例如在移动设备或桌面
2. 一种服务向同一组织拥有的另一项服务提出请求,这些服务通常位于同一数据中心内,作为面向服务/微型架构的一部分。 (支持这种用例的软件有时被称为 **中间件middleware**
3. 一种服务通过互联网向不同组织所拥有的服务提出请求。这用于不同组织后端系统之间的数据交换。此类别包括由在线服务如信用卡处理系统提供的公共API或用于共享访问用户数据的OAuth。
有两种流行的Web服务方法REST和SOAP。他们在哲学方面几乎是截然相反的往往是各自支持者之间的激烈辩论即使在每个阵营内也有很多争论。 例如,**HATEOAS超媒体作为应用程序状态的引擎** 经常引发讨论【35】。
有两种流行的Web服务方法REST和SOAP。他们在哲学方面几乎是截然相反的往往也是各自支持者之间的激烈辩论的主题[^vi]。
[^vi]: 即使在每个阵营内也有很多争论。 例如,**HATEOAS超媒体作为应用程序状态的引擎** 就经常引发讨论【35】。
REST不是一个协议而是一个基于HTTP原则的设计哲学【34,35】。它强调简单的数据格式使用URL来标识资源并使用HTTP功能进行缓存控制身份验证和内容类型协商。与SOAP相比REST已经越来越受欢迎至少在跨组织服务集成的背景下【36】并经常与微服务相关[31]。根据REST原则设计的API称为RESTful。
相比之下SOAP是用于制作网络API请求的基于XML的协议 尽管首字母缩写词相似SOAP并不是SOA的要求。 SOAP是一种特殊的技术而SOA是构建系统的一般方法。。虽然它最常用于HTTP但其目的是独立于HTTP并避免使用大多数HTTP功能。相反它带有庞大而复杂的多种相关标准Web服务框架称为`WS-*`它们增加了各种功能【37】。
相比之下SOAP是用于制作网络API请求的基于XML的协议[^vii]。虽然它最常用于HTTP但其目的是独立于HTTP并避免使用大多数HTTP功能。相反它带有庞大而复杂的多种相关标准Web服务框架称为`WS-*`它们增加了各种功能【37】。
SOAP Web服务的API使用称为Web服务描述语言WSDL的基于XML的语言来描述。 WSDL支持代码生成客户端可以使用本地类和方法调用编码为XML消息并由框架再次解码访问远程服务。这在静态类型编程语言中非常有用但在动态类型编程语言中很少参阅“[代码生成和动态类型化语言](#代码生成和动态类型化语言)”)
[^vii]: 尽管首字母缩写词相似SOAP并不是SOA的要求。 SOAP是一种特殊的技术而SOA是构建系统的一般方法
由于WSDL的设计不是人类可读的而且由于SOAP消息通常是手动构建的过于复杂所以SOAP的用户在很大程度上依赖于工具支持代码生成和IDE【38】。对于SOAP供应商不支持的编程语言的用户来说与SOAP服务的集成是困难的。
SOAP Web服务的API使用称为Web服务描述语言WSDL的基于XML的语言来描述。 WSDL支持代码生成客户端可以使用本地类和方法调用编码为XML消息并由框架再次解码访问远程服务。这在静态类型编程语言中非常有用但在动态类型编程语言中很少参阅“[代码生成和动态类型的语言](#代码生成和动态类型的语言)”)。
由于WSDL的设计不是人类可读的而且由于SOAP消息通常因为过于复杂而无法手动构建所以SOAP的用户在很大程度上依赖于工具支持代码生成和IDE【38】。对于SOAP供应商不支持的编程语言的用户来说与SOAP服务的集成是困难的。
尽管SOAP及其各种扩展表面上是标准化的但是不同厂商的实现之间的互操作性往往会造成问题【39】。由于所有这些原因尽管许多大型企业仍然使用SOAP但在大多数小公司中已经不再受到青睐。
@ -415,11 +419,11 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
所有这些都是基于 **远程过程调用RPC** 的思想该过程调用自20世纪70年代以来一直存在【42】。 RPC模型试图向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同这种抽象称为位置透明。尽管RPC起初看起来很方便但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同
* 本地函数调用是可预测的,并且成功或失败,这仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
* 本地函数调用是可预测的,并且成功或失败仅取决于受您控制的参数。网络请求是不可预知的:由于网络问题,请求或响应可能会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在您的控制范围之内。网络问题是常见的,所以你必须预测他们,例如通过重试失败的请求。
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过。 (我们将在[第8章](ch8.md)更详细地讨论这个问题。)
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( **幂等idempotence**)机制。本地函数调用没有这个问题。 (在[第十一章](ch11.md)更详细地讨论幂等性)
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:在不到一毫秒的时间内它可能会完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。没关系,如果参数是像数字或字符串这样的基本类型,但是对于较大的对象很快就会变成问题。
* 如果您重试失败的网络请求,可能会发生请求实际上正在通过,只有响应丢失。在这种情况下,重试将导致该操作被执行多次,除非您在协议中引入除重( **幂等idempotence**)机制。本地函数调用没有这个问题。 (在[第11章](ch11.md)更详细地讨论幂等性)
* 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间完成一样的东西。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
客户端和服务可以用不同的编程语言实现所以RPC框架必须将数据类型从一种语言翻译成另一种语言。这可能会捅出大篓子因为不是所有的语言都具有相同的类型 —— 例如回想一下JavaScript的数字大于$2^{53}$的问题(参阅“[JSONXML和二进制变体](#JSONXML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
@ -429,17 +433,19 @@ Web服务仅仅是通过网络进行API请求的一系列技术的最新版本
尽管有这样那样的问题RPC不会消失。在本章提到的所有编码的基础上构建了各种RPC框架例如Thrift和Avro带有RPC支持gRPC是使用Protocol Buffers的RPC实现Finagle也使用ThriftRest.li使用JSON over HTTP。
这种新一代的RPC框架更加明确的是远程请求与本地函数调用不同。例如Finagle和Rest.li 使用futurespromises来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务的情况,并将其结果合并【45】。 gRPC支持流其中一个调用不仅包括一个请求和一个响应包括一系列的请求和响应【46】。
这种新一代的RPC框架更加明确的是远程请求与本地函数调用不同。例如Finagle和Rest.li 使用futurespromises来封装可能失败的异步操作。`Futures`还可以简化需要并行发出多项服务并将其结果合并的情况【45】。 gRPC支持流其中一个调用不仅包括一个请求和一个响应可以是随时间的一系列请求和响应【46】。
其中一些框架还提供服务发现即允许客户端找出在哪个IP地址和端口号上可以找到特定的服务。我们将在“[请求路由](ch6.md#请求路由)”中回到这个主题。
使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是RESTful API还有其他一些显著的优点对于实验和调试只需使用Web浏览器或命令行工具curl无需任何代码生成或软件安装即可向其请求它是受支持的所有的主流编程语言和平台还有大量可用的工具服务器缓存负载平衡器代理防火墙监控调试工具测试工具等的生态系统。由于这些原因REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求通常在同一数据中心内。
使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是RESTful API还有其他一些显著的优点方便实验和调试只需使用Web浏览器或命令行工具curl无需任何代码生成或软件安装即可向其请求能被所有主流的编程语言和平台所支持还有大量可用的工具服务器缓存负载平衡器代理防火墙监控调试工具测试工具等的生态系统。
由于这些原因REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求通常在同一数据中心内。
#### 数据编码与RPC的演化
对于可演化性重要的是可以独立更改和部署RPC客户端和服务器。与通过数据库流动的数据相比如上一节所述我们可以在通过服务进行数据流的情况下做一个简化的假设假定所有的服务器都会先更新其次是所有的客户端。因此您只需要在请求上具有向后兼容性并且对响应具有前向兼容性。
RPC方案的前后向兼容性属性从它使用的编码方式中继承
RPC方案的前后向兼容性属性从它使用的编码方式中继承
* ThriftgRPCProtobuf和Avro RPC可以根据相应编码格式的兼容性规则进行演变。
* 在SOAP中请求和响应是使用XML模式指定的。这些可以演变但有一些微妙的陷阱【47】。
@ -469,27 +475,27 @@ RPC方案的前后向兼容性属性从它使用的编码方式中继承
过去,**消息代理Message Broker**主要是TIBCOIBM WebSphere和webMethods等公司的商业软件的秀场。最近像RabbitMQActiveMQHornetQNATS和Apache Kafka这样的开源实现已经流行起来。我们将在[第11章](ch11.md)中对它们进行更详细的比较。
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给一个或多个消费者或订阅者到那个队列或主题。在同一主题上可以有许多生产者和许多消费者。
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给那个队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在[第11章](ch11.md)中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求/响应数据流类似于RPC
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后兼容的,则您可以灵活地更改发行商和消费者的独立编码,并以任意顺序进行部署。
消息代理通常不会执行任何特定的数据模型 - 消息只是包含一些元数据的字节序列,因此您可以使用任何编码格式。如果编码是向后和向前兼容的,您可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。
如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图4-7](img/fig4-7.png))。
#### 分布式的Actor框架
Actor模型是单个进程中并发的编程模型。逻辑被封装在角色中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个角色通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
Actor模型是单个进程中并发的编程模型。逻辑被封装在actor中而不是直接处理线程以及竞争条件锁定和死锁的相关问题。每个actor通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。
在分布式Actor框架中此编程模型用于跨多个节点伸缩应用程序。不管发送方和接收方是在同一个节点上还是在不同的节点上都使用相同的消息传递机制。如果它们在不同的节点上则该消息被透明地编码成字节序列通过网络发送并在另一侧解码。
位置透明在actor模型中比在RPC中效果更好因为actor模型已经假定消息可能会丢失即使在单个进程中也是如此。尽管网络上的延迟可能比同一个进程中的延迟更高但是在使用参与者模型时,本地和远程通信之间的基本不匹配是较少的。
位置透明在actor模型中比在RPC中效果更好因为actor模型已经假定消息可能会丢失即使在单个进程中也是如此。尽管网络上的延迟可能比同一个进程中的延迟更高但是在使用actor模型时,本地和远程通信之间的基本不匹配是较少的。
分布式的Actor框架实质上是将消息代理和角色编程模型集成到一个框架中。但是,如果要执行基于角色的应用程序的滚动升级,则仍然需要担心向前和向后兼容性问题,因为消息可能会从运行新版本的节点发送到运行旧版本的节点,反之亦然。
分布式的Actor框架实质上是将消息代理和actor编程模型集成到一个框架中。但是如果要执行基于actor的应用程序的滚动升级,则仍然需要担心向前和向后兼容性问题,因为消息可能会从运行新版本的节点发送到运行旧版本的节点,反之亦然。
三个流行的分布式actor框架处理消息编码如下
* 默认情况下Akka使用Java的内置序列化不提供前向或后向兼容性。 但是,你可以用类似缓冲区的东西替代它从而获得滚动升级的能力【50】。
* 默认情况下Akka使用Java的内置序列化不提供前向或后向兼容性。 但是,你可以用类似Prototol Buffers的东西替代它从而获得滚动升级的能力【50】。
* Orleans 默认使用不支持滚动升级部署的自定义数据编码格式; 要部署新版本的应用程序您需要设置一个新的群集将流量从旧群集迁移到新群集然后关闭旧群集【51,52】。 像Akka一样可以使用自定义序列化插件。
* 在Erlang OTP中对记录模式进行更改是非常困难的尽管系统具有许多为高可用性设计的功能。 滚动升级是可能的但需要仔细计划【53】。 一个新的实验性的`maps`数据类型2014年在Erlang R17中引入的类似于JSON的结构可能使得这个数据类型在未来更容易【54】。
@ -498,7 +504,7 @@ Actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
## 本章小结
在本章中,我们研究了将数据结构转换为网络中的字节或磁盘上的字节的几种方法。我们看到了这些编码的细节不仅影响其效率,更重要的是应用程序的体系结构和部署它们的选项。
在本章中,我们研究了将数据结构转换为网络中的字节或磁盘上的字节的几种方法。我们看到了这些编码的细节不仅影响其效率,更重要的是也影响了应用程序的体系结构和部署它们的选项。
特别是,许多服务需要支持滚动升级,其中新版本的服务逐步部署到少数节点,而不是同时部署到所有节点。滚动升级允许在不停机的情况下发布新版本的服务(从而鼓励在罕见的大型版本上频繁发布小型版本),并使部署风险降低(允许在影响大量用户之前检测并回滚有故障的版本)。这些属性对于可演化性,以及对应用程序进行更改的容易性都是非常有利的。
@ -510,7 +516,7 @@ Actor模型是单个进程中并发的编程模型。逻辑被封装在角色中
* JSONXML和CSV等文本格式非常普遍其兼容性取决于您如何使用它们。他们有可选的模式语言这有时是有用的有时是一个障碍。这些格式对于数据类型有些模糊所以你必须小心数字和二进制字符串。
* 像ThriftProtocol Buffers和Avro这样的二进制模式驱动格式允许使用清晰定义的前向和后向兼容性语义进行紧凑高效的编码。这些模式可以用于静态类型语言的文档和代码生成。但是他们有一个缺点就是在数据可读之前需要对数据进行解码。
我们还讨论了数据流的几种模式,说明了数据编码重要的不同场景:
我们还讨论了数据流的几种模式,说明了数据编码重要的不同场景:
* 数据库,写入数据库的进程对数据进行编码,并从数据库读取进程对其进行解码
* RPC和REST API客户端对请求进行编码服务器对请求进行解码并对响应进行编码客户端最终对响应进行解码