微服务之集成(四)

1. 寻找理想的集成技术

微服务之间通信的方式的选择非常多样化,但哪个是正确的呢?SOAP ? XML-RPC ? REST ?  Protocol Buffers?后面会逐一讨论。

首先,我们要考虑的是,我们到底希望从这些技术中得到什么。

1.1 避免破坏性修改

有时候,对某个服务做的一些修改会导致该服务的消费方也随之发生改变。但是,我们希望选用的技术可以尽量避免这种情况的发生。

1.2 保证API的技术无关性

保证微服务之间的通信方式的技术无关性是非常重要的。这就意味着,不应该选择哪种对微服务的具体实现技术有限制的集成方式。

1.3 使你的服务易于消费方使用

消费方应该很容易的使用我们的服务。理想情况下,消费方应该可以使用任何技术来实现,从另一方面来说,提供一个客户端库也可以简化消费方的使用。但是通常这种库与其他我们想要得到的东西不可兼得。例如,使用客户端库对于消费方来说很方便,但是会造成耦合的增加。

1.4 隐藏内部实现细节

我们不希望消费方与服务的内部实现细节绑定在一起,因为这会增加耦合。所以,所有倾向于暴露内部实现细节的技术都不应该被采用。

2.为用户创建接口

3.共享数据库

目前业界最常见的集成形式应该就是数据库集成了。使用这种方式时,如果其他服务想要从一个服务获取信息,可以直接访问数据库。如果想要修改,也可以直接在数据库中修改。

这种方式看起来非常简单,而且可能是最快的集成方式,这也是它这么流行的原因。

但是它有一些缺点。

如图,使用数据库集成来访问和修改数据信息

缺点一,首先,这使得外部系统能够查看内部实现细节,并与其绑定在一起。存储在数据库中的数据结构对所有人来说都是平等的,所有服务都可以完全访问数据库。如果我决定为了更好的表示数据或者增加可维护性而修改表结构的话,我的消费方就无法进行工作。

数据库是一个很大的共享API,但同时也非常不稳定。为了不影响其他服务,我必须非常小心的避免修改与其他服务相关的表结构。这种情况下,通常需要做大量的回归测试来保证功能的正确性。

缺点二,其次,消费方与特定的技术选择绑定在了一起。可能现在来看,使用关系型数据库做存储是合理的,所以消费方会使用一个合适的驱动(很有可能是与具体数据库相关的)来与之一起工作。说不定一段时间之后我们会意识到,使用非关系型数据库才是更好的选择。

如果消费方和客户服务非常紧密的绑定在了一起,那么能轻易替换这个数据库吗?答案肯定是不能。所以,正如前面所提过的,隐藏实现细节非常重要,因为它可以让我们的服务拥有一定的自治性,从而可以轻易的修改其内部实现。这关系到松耦合。

缺点三,最后,我们考虑一下行为。肯定会有一部分逻辑负责对客户进行修改。那么这个逻辑应该放在什么地方呢?如果消费方直接操作数据库,那么它们都需要对这些逻辑负责。对数据库进行操作的相似逻辑可能会出现在很多服务中。就是说,如果仓库,注册用户界面,呼叫中心都需要编辑客户的信息,那么当修复一个bug的时候,你需要修改三个不同的地方,并且对这些修改分别做部署。这关系到内聚性。

我们知道,关于好的微服务的核心原则就是高内聚和松耦合。但是,使用数据库集成使得这两者都很难实现。服务之间很容易通过数据库集成来共享数据,但是无法共享行为。内部表示暴露给了我们的消费方,而且很难做到无破坏性的修改,进而不可避免的导致不敢做任何修改,所以无论如何都要避免这种情况。

在后面的部分中,我们会介绍服务之间不同风格的集成方式,这些方式都可以保证服务的内部实现得以隐藏。

4. 同步和异步

在介绍具体的技术选择之前,我们先讨论一下服务之间如何协作的问题。服务之间的通信应该是同步还是异步呢?这个基础选择的不同,会引导我们使用不同的实现。

如果使用同步通信,发起一个远程服务调用后,调用方会阻塞自己并等待整个操作的完成。如果使用异步通信,调用方不需要等待操作完成就可以返回,甚至可能不需要关心这个操作是否完成。

异步通信对于运行时间比较长的任务来说比较有用,否则就需要在客户端和服务器之间开启一个长连接,而这时非常不实际的。

这两种不同的通信模式有着各自的协作风格,即请求/响应或者基于事件对于请求/响应来说,客户端发起一个请求,然后等待响应。这种模式能够与同步通信模式很好的匹配,但异步通信也可以使用这种模式。我们可以发起一个请求,然后注册一个回调,当服务端操作结束之后,会调用该回调。

对于使用基于事件的协作方式来说,情况会颠倒过来。客户端不是发起请求,而是发布一个事件,然后期待其他的协作者接收到该消息,并且知道该怎么做。基于事件的系统天生就是异步的。基于事件的协作方式耦合性很低。也就是说,客户端发布一个事件,但并不需要知道谁或者什么会对此做出响应,这也意味着,你可以在不影响客户端的请求下对该事件添加新的订阅者。

5. 编排与协同

在开始对越来越复杂的逻辑进行建模时,我们需要处理跨服务业务流程的问题,而使用微服务时,这个问题会来的更快。

当我们在MusicCorp中创建用户时,发生了什么:

(1) 在客户的积分账户中创建一条记录

(2) 通过邮政系统发送一个欢迎礼包

(3) 向客户发送欢迎电子邮件

当我们在考虑具体实现时,有两种架构风格可以采用。

第一种,使用编排。我们会依赖于某个中心大脑来指导并驱动整个流程。第二种,使用协同。我们仅仅会告知系统中各个部分各自的职责,而把具体怎么做的细节留给它们自己。

可以想象在编排的解决方案中,会让客户服务未做大脑中心。

在创建时,它会跟积分账户、电子邮件服务及邮政服务通过请求响应的方式进行通信。客户服务本身可以对当前进行到了哪一步进行跟踪。

编排方式的缺点是,客户服务作为中心控制点承担了太多的逻辑,它会成为网状结构中的中心枢纽及很多逻辑的起点。这种方法可能会导致少量的上帝服务,而与其打交道的那些服务通常都会沦为贫血的、基于CRUD的服务。

如果使用协同,可以仅仅从客户服务中使用异步的方式触发一个事件,该事件名可以叫做客户创建。电子邮件服务、邮政服务及积分账户可以简单的订阅这些事件并且做相应的处理。这种方法能显著的消除耦合。

缺点是,看不到图4-2中展示的那种很明显的业务流程视图。

这意味着,需要做一些额外的工作来监控流程,以保证其正确的进行。处理该问题的一种方法是,构建一个与图4-2中展示的业务流程相匹配的监控系统。实际的监控活动是针对每个服务的,但最终需要把监控的结果映射到业务流程中。在这个流程图中我们可以看出系统是如何工作的。

通常来讲,协同的方式可以降低系统的耦合度,并且你能更加灵活的对现有系统进行修改。但是,确实需要额外的工作来对业务流程做跨服务的监控。而且,大多数重量级的编排方案都非常不稳定且修改代价很大。基于这些事实,更推荐使用协同方式。因为在这种方式下,每个服务都足够聪明,并且能够很好的完成自己的任务。

如果想要请求/响应风格的语义,又想避免其在耗时业务上的困境,可以采用异步请求加回调的方式。另一方面,使用异步方式有利于协同方案的实施,从而大大减少服务之间的耦合,这恰恰就是我们为了能独立发布服务而追求的特性。

针对请求/响应方式,可以考虑两种技术:RPCRemote Procedure Call,远程过程调用)和RESTRepresentational State Transfer,表述性状态转移)。

6. 远程过程调用

远程过程调用允许你进行一个本地调用,但事实上结果是由某个远程服务器产生的。RPC的种类繁多,其中一些依赖于接口定义(SOAPThriftprotocol buffers)

不同的技术栈可以通过接口定义轻松的生成客户端和服务端的桩代码。

然而,所有这些技术都有一个核心特点,那就是使用本地调用的方式和远程进行交互。

有些RPC实现与特定的网络协议相绑定(比如SOAP名义上使用的就是HTTP),当然不同的实现会使用不同的协议,不同的协议可以提供不同的额外特性。比如TCP能够保证送达,UDP虽然不能保证送达但协议开销较小,所以你可以根据自己的使用场景来选择不同的网络技术。

那些RPC的实现会帮你生成服务端和客户端桩代码,从而让你快速开始编程。这通常是RPC的主要卖点之一:易于使用。

但是有一些RPC的实现确实存在一些问题

6.1 技术的耦合

有一些RPC机制,如Java RMI,与特定的平台紧密绑定,这对于服务端和客户端的技术选型造成了一定的限制。Thriftprotocol buffers对于不同的语言的支持很好,从而在一定程度上减小了这个问题的影响。

从某种程度上来讲,这种技术上的耦合也是暴露内部实现细节的一种方式。

例如,使用RMI不仅把客户端绑定在了JVM上,服务端也是如此。

6.2 本地调用和远程调用并不相同

RPC的核心想法是隐藏远程调用的复杂性。使用本地调用不会引起性能问题,但是RPC会花大量的时间对负荷进行封装和解封装,更别提网络通信的时间。这意味着,要使用不同的思路来设计远程和本地的API。只是简单的把一个本地API改造成为跨服务的远程API往往会带来问题。

你还要考虑网络本身。分布式计算中一个著名的错误观点就是“网络是可靠的”,事实上,网络并不可靠。即使客户端和服务端都正常运行,整个调用也有可能会出错。你应该做出一个假设:有一个恶意的攻击者随时有可能对网络进行破坏,因此网络的出错模式也不止一种。

6.3 脆弱性

有一些很流行的RPC实现可能会造成一些令人讨厌的脆弱性。

这里以Java RMI为例。

如果对规格说明(指服务端定义的接口,比如添加了新的接口)进行了修改,所有的客户端都需要重新生成桩(这里的桩应该是指的客户端的方法实现),无论该客户端是否需要这个新方法。

还有一种形式的脆弱性。如果我们对服务端中某个对象的字段做了修改(比如删除了一个无用的字段),如果客户端没有做相应的修改的话,那么即使它们从来没有使用过这个字段,在使用过程中还是会出问题。所以为了应用的这些修改,需要同时对服务端和客户端进行部署。

这就是任何一个使用二进制桩生成机制的RPC所要面临的挑战:客户端和服务端的部署无法分离。

6.4  RPC很糟糕吗?

当然不是。

如果你决定要选用RPC这种方式的话,需要注意一些问题:不要对远程调用过度抽象,以至于网络因素完全被隐藏起来;确保你可以独立的升级服务端接口而不用强迫客户端升级,所以编写客户端代码时要注意这方面的平衡;在客户端中一定不要隐藏我们是在做网络调用这个事实;在RPC的方式中经常会在客户端使用库,但是这些库如果在结构上组织的不够好,也可能会带来一些问题。

6.5总结:

RPC是请求请求/响应协作方式的一种,相比使用数据库做集成的方式,RPC显然是一个巨大的进步。

7. REST

REST 是受Web启发而产生的一种架构风格。REST风格包含了很多的原则和限制,但是这里我们仅仅专注于,如何在微服务的世界里使用REST更好的解决集成问题。REST是RPC的一种替代方案。

其中最重要的一点是资源的概念。资源,比如Customer,处于服务之内。服务可以根据请求内容创建Customer对象的不同表示形式。也就是说,一个资源的对外显示方式和内部存储方式之间没有任何耦合。

REST本身并没有提到底层应该使用什么协议,尽管事实上最常用HTTP。实际上,也有使用其他协议来实现REST的例子,比如串口或者USB,当然这会引入大量的工作。

7.1 REST和HTTP

事实上,REST架构风格声明了一组对所有资源的标准方法,而HTTP恰好也定义了一组方法可供使用。

从概念上来说,对于一个Customer资源,访问接口只有一个,但是可以通过HTTP协议的不同动词对其进行不同的操作。

HTTP周边也有一个大的生态系统,其中包含很多的支撑工具和技术。比如类似Varnish这样的HTTP缓存代理、mod proxy这样的负载均衡、大量针对HTTP的监控工具等。这些组件可以帮助我们很好的处理HTTP流量,并使用聪明的方式对其进行路由,而且这些操作基本上都对终端客户透明。HTTP还提供了一系列安全控制机制供我们直接使用。从基本认证到客户端证书,HTTP生态系统提供了大量的工具来简化安全性处理。

需要注意的是,HTTP也可以用来实现RPC。比如SOAP就是基于HTTP进行路由的,但不幸的是它只用到HTTP很少的特性。

7.2 超媒体作为程序状态的引擎

REST引入的用来避免客户端和服务端之间产生耦合的另一个原则是“HATEOAS”(Hypermedia As The Engine of Application State,超媒体作为程序状态的引擎。)

超媒体的概念是:有一块内容,该内容包含了指向其他内容的链接,而这些内容的格式可以不同(如文本,图像,声音等)。

HATEOAS背后的想法是,客户端应该与服务端通过那些指向其他资源的链接进行交互,而这些交互有可能造成状态转移。

这就类似于网页下的带有超链接的图标,例如,你想加入购物车,你只需要去点击图标,图标下会有一个超链接来完成加入的动作。

7.3 JSON、XML还是其他

由于服务端使用标准文本形式的响应,所以客户端可以很灵活的对资源进行使用,而基于HTTP的REST能够提供多种不同的响应形式。例如XML,或者更流行的JSON。

7.4 留心过多的约定

由于REST越来越流行,帮助我们构建RESTFul Web 服务的框架也随之流行起来。

我们很容易把存储的数据直接暴露给消费者,那么如何避免这个问题呢?(这种方式内在耦合性所带来的痛苦会远远大于一开始就消除概念之间的耦合所需要的代价。)

一个有效的模式是先设计外部接口,等到外部接口稳定之后在实现微服务内部的数据持久化。

这样做可以保证服务的接口是由消费者的需求驱动出来的,从而避免数据存储方式对外部接口的影响。其缺点是推迟了数据存储部分的集成。

7.5 基于HTTP的REST的缺点

从易用性的角度来看,基于HTTP的REST无法帮助你生成客户端的桩代码,而RPC可以

另外,性能上也可能会遇到问题。基于HTTP的REST支持不同的格式,比如JSON或者二进制,所以负载相对SOAP来说更紧凑,当然和像Thrift这样的二进制协议是没法比的。在要求低延迟的场景下,每个HTTP请求的封装开销可能是个问题。

虽然HTTP可以用于大流量的通信场景但对于低延迟通信来说并不是最好的选择。相比之下,有一些构建与TCPTransmission Control Protocol,传输控制协议)或者其他网络技术之上的协议更加高效。比如WebSockets,在初始的HTTP握手之后,客户端和服务器之间就仅仅通过TCP连接了。对于向浏览器传输数据这个场景而言,WebSockets更加高效。

对于服务和服务之间的通信来说,如果低延迟或者较小的消息尺寸对你来说是很重要的话,那么一般来讲HTTP不是一个好主意。你可能需要选择一个不同的底层协议,比如UDPUser Datagram Protocol, 用户数据报协议)来满足你的性能要求。很多RPC框架都可以很好的运行在除了TCP之外的其他网络协议上。

有些RPC的实现支持高级的序列化和反序列化机制,然而对于REST而言,这部分工作就要自己做了。

尽管有这些缺点,在选择服务之间的交互方式时,基于HTTP的REST仍然是一个比较合理的默认选择。

 

未完,待续...

posted @ 2019-09-10 00:29 Vincent-yuan 阅读(...) 评论(...) 编辑 收藏