第4章 集成

 ̄綄美尐妖づ 提交于 2019-12-06 15:13:58
 
在我看来,集成是微服务相关技术中最重要的一个。做得好的话,你的微服务可以保持自 治性,你也可以独立地修改和发布它们;但做得不好的话会带来灾难。希望本章能够帮助 你在微服务之旅中,避免曾经在SOA中遇到的那些问题。
 
4.1寻找理想的集成技术
 
微服务之间通信方式的选择非常多样化,但哪个是正确的呢? SOAP? XML-RPC ? REST? Protocol Buffers ?后面会逐一讨论,但是在此之前需要考虑的是,我们到底希望 从这些技术中得到什么。
 
4.1.1避免破坏性修改
 
有时候,对某个服务做的一些修改会导致该服务的消费方也随之发生改变。后面会讨论如 何处理这种情形,但是我们希望选用的技术可以尽量避免这种情况的发生。比如,如果一 个微服务在一个响应中添加了一个字段,那么已有的消费方不应该受到影响。
4.1.2保证API的技术无关性
 
我很喜欢保持开放的心态,这也正是我喜欢微服务的原因。因此我认为,保证微服务之间 通信方式的技术无关性是非常重要的。这就意味着,不应该选择那种对微服务的具体实现 技术有限制的集成方式。
4.1.3使你的服务易于消费方使用
 
消费方应该能很容易地使用我们的服务。如果消费方使用该服务比登天还难,那么无论该 微服务多漂亮都没有任何意义。所以让我们考虑一下,如何让消费方简便地使用美妙的新 服务。理想情况下,消费方应该可以使用任何技术来实现,从另一方面来说,提供一个客 户端库也可以简化消费方的使用。但是通常这种库与其他我们想要得到的东西不可兼得。 举个例子,使用客户端库对于消费方来说很方便,但是会造成耦合的增加。
 
4.1.4隐藏内部实现细节
我们不希望消费方与服务的内部实现细节绑定在一起,因为这会增加耦合。与细节绑定意 味着,如果想要改变服务内部的一些实现,消费方就需要跟着做出修改。这会增加修改的 成本,而这恰恰是我们想要避免的。这也会导致为了避免消费方的修改而尽量少地对服务 本身进行修改,而这会导致服务内部技术债的增加。所以,所有倾向干暴露内部实现细节 的技术都不应该被采用。
 
4.2为用户创建接口
 
既然现在有了一些有关如何选择服务间集成技术的不错的指导原则,那么就来看看最常用 的技术有哪些,以及哪个最合适。为了帮助思考,让我们从MusicCorp中选择一个真实的 例子。
 
创建客户这个业务,乍一看似乎就是简单的CRUD操作,但对于大多数系统来说并不止这 些。添加新客户可能会触发一个新的流程,比如进行付账设置、发送欢迎邮件等。而且修 改或者删除客户也可能会触发其他的业务流程。
 
知道了这些信息之后,在MusicCorp系统中对客户的处理方式可能就需要有所不同了。
 
4.3共享数据库
 
目前为止,我和同事在业界所见到的最常见的集成形式就是数据库集成。使用这种方式 时,如果其他服务想要从一个服务获取信息,可以直接访问数据库。如果想姜修改,也可 以直接在数据库中修改。这种方式看起来非常简单,而且可能是最快的集成方式,这也正 是它这么流行的原因。
图4-1展示了注册部分的用户界面,它直接使用SQL在数据库中创建用户。还可以看到, 呼叫中心应用程序可以直接运行SQL来查看和编辑数据库中的数据。仓库通过查询数据库 来显示更新后的客户订单信息。这是一种非常普通的模式,但实践起来却困难重重。
 
 
图4-1 :使用数据库集成来访问和修改数据信息
 
首先,这使得外部系统能够查看内部实现细节,并与其绑定在一起。存储在数据库中的数 据结构对所有人来说都是平等的,所有服务都可以完全访问该数据库。如果我决定为了更 好地表示数据或者增加可维护性而修改表结构的话,我的消费方就无法进行工作。数椐库 是一个很大的共享API,但同时也非常不稳定。如果我想改变与之相关的逻辑,比如说帮 助台如何管理客户,这就需要修改数据库。为了不影响其他服务,我必须非常小心地避免 修改与其他服务相关的表结构。这种情况下,通常需要做大量的回归测试来保证功能的正 确性。
 
其次,消费方与特定的技术选择绑定在了一起。可能现在来看,使用关系型数据库做存储 是合理的,所以我的消费方会使用一个合适的驱动(很有可能是与具体数据库相关的)来 与之一起工作。说不定一段时间之后我们会意识到,使用非关系型数据库才是更好的选 择。如果消费方和客户服务非常紧密地绑定在了一起,那么能够轻易替换这个数据库吗? 正如前面所讨论的,隐藏实现细节非常重要,因为它让我们的服务拥有一定的自治性,从而可以轻易地修改其内部实现。再见,松耦合。
最后,让我们考虑一下行为。肯定会有一部分逻辑负责对客户进行修改。那么这个逻辑应 该放在什么地方呢?如果消费方直接操作数据库,那么它们都需要对这些逻辑负责。对数 据库进行操作的相似逻辑可能会出现在很多服务中。如果仓库、注册用户界面、呼叫中心 都需要编辑客户的信息,那么当修复一个bug的时候,你需要修改三个不同的地方,并且 对这些修改分别做部署。再见,内聚性。
 
还记得前面提到过的关干好的微服务的核心原则吗?没错,就是高内聚和低耦合。但是使 用数据库集成使得这两者都很难实现。服务之间很容易通过数据库集成来共享数据,但是 无法共享行为。内部表示暴露给了我们的消费方,而且很难做到无破坏性的修改,进而不 可避免地导致不敢做任何修改,所以无论如何都要避免这种情况。
 
4.4同步与异步
 
在介绍具体的技术选择之前,让我们先就服务如何协作这个问题做一些讨论。服务之间 的通信应该是同步的还是异步的呢?这个基础性的选择会不可避免地引导我们使用不同 的实现。
 
这两种不同的通信模式有着各自的协作风格,即请求/响应或者基于事件。对于请求/响 应来说,客户端发起一个请求,然后等待响应。这种模式能够与同步通信模式很好地匹 配,但异步通信也可以使用这种模式。我可以发起一个请求,然后注册一个回调,当服务 端操作结束之后,会调用该回调。
 
对于使用基干事件的协作方式来说,情况会颠倒过来。客户端不是发起请求,而是发布一 个事件,然后期待其他的协作者接收到该消息,并且知道该怎么做。我们从来不会告知任 何人去做任何事情。基于事件的系统天生就是异步的。整个系统都很聪明,也就是说,业 务逻辑并非集中存在于某个核心大脑,而是平均地分布在不同的协作者中。基于事件的协 作方式耦合性很低。客户端发布一个事件,但并不需要知道谁或者什么会对此做出响应, 这也意味着,你可以在不影响客户端的情况下对该事件添加新的订阅者。
 
哪些因素会影响对这两种风格的选择呢? 一个重要的因素是这种风格能否很好地解决复杂 问题,比如如何处理跨服务边界的流程,_而且这种流程有可能会运行很长时间。
4.5编排与协同
 
在开始对越来越复杂的逻辑进行建模时,我们需要处理跨服务业务流程的问题,而使用微 服务时这个问题会来得更快。让我们来看看在MiisicCorp中创逮用户时发生了什么:
(1)在客户的积分账户中创建一条记录
 
(2)通过邮政系统发送一个欢迎礼包
 
(3)向客户发送欢迎电子邮件
在图4-2中可以很容易地使用流程图对这个概念进行建模。
图4-2:创建新客户的流程
 
 
当考虑具体实现时,有两种架构风格可以采用。使用编排(orchestmtion)的话,我们 会依赖于某个中心大脑来指导并驱动整个流程,就像管弦乐队中的指挥一样。使用协同 (choreography)的话,我们仅仅会告知系统中各个部分各自的职责,而把具体怎么做的细 节留给它们自己,就像芭蕾舞中每个舞者都有自己的方式,同时也会响应周围其他人。
 
考虑一下对这个流程来说,编排的解决方案会是什么样子的。可能最简单的方式就是让客 户服务作为中心大脑。在创建时它会跟积分账户、电子邮件服务及邮政服务通过请求/响 应的方式进行通信,如图4-3所示。客户服务本身可以对当前进行到了哪一步进行跟踪。 它会检查客户账户是否创建成功、电子邮件是否发送出去及邮包是否寄出。图4-2中的流 程图可以直接转换成为代码。甚至有工具可以帮你实现,比如一个合适的规则引擎。也有 一些商业工具可以完成这些工作,它们通常被称作商业流程建模软件。假如使用的是同步 的请求/响应模式,我们甚至能知道每一步是否都成功了。
 
编排方式的缺点是,客户服务作为中心控制点承担了太多职责,它会成为网状结构的中心 枢纽及很多逻辑的起点。我见过这个方法会导致少量的“上帝”服务,而与其打交道的那 些服务通常都会沦为贫血的、基于CRUD的服务。
 
如果使用协同,可以仅仅从客户服务中使用异步的方式触发一个事件,该事件名可以叫作 “客户创建”。电子邮件服务、邮政服务及积分账户可以简单地订阅这些事件并且做相应处理,如图4-4所示。这种方法能够显著地消除耦合。如果其他的服务也关心客户创建这件 事情,它们简单地订阅该事件即可。缺点是,看不到如图4-2中展示的那种很明显的业务 流程视图。
 
总结:如果是各个步骤之间有依赖关系时,应该使用编排模式,否则使用协同模式。
图4-3:通过编排处理客户创建
图4-4:通过协同处理客户创建事件
 
这意味着,需要做一些额外的工作来监控流程,以保证其正确地进行。举个例子,如果积 分账户存在的bug导致账户没有创建成功,程序是否能够捕捉到这个问题?处理该问题的 一种方法是,构建一个与图4-2中展示的业务流程相匹配的监控系统。实际的监控活动是 针对每个服务的,但最终需要把监控的结果映射到业务流程中。在这个流程图中我们可以 看出系统是如何工作的。
 
通常来讲,我认为使用协同的方式可以降低系统的耦合度,并且你能更加灵活地对现有系 统进行修改。但是,确实需要额外的工作来对业务流程做跨服务的监控。我还发现大多数 重量级的编排方案都非常不稳定且修改代价很大。基干这些事实,我倾向干使用协同方 式,在这种方式下每个服务都足够聪明,并且能够很好地完成自己的任务。
 
这里有好几个因素需要考虑。同步调用比较简单,而且很容易知道整个流程的i作是否正 常。如果想要请求/响应风格的语义,又想避免其在耗时业务上的困境,可以采用异步请 求加回调的方式。另一方面,使用异步方式有利于协同方案的实施,从而大大减少服务间 的耦合,这恰恰就是我们为了能独立发布服务而追求的特性。
 
当然我们也可以选择混用不同的方式。然而不同的技术适用于不同的方式,因此需要了解 不同技术的实现细节,从而更好地做出选择。
 
针对请求/响应方式,可以考虑两种技术•. RPC (Remote Procedure Call,远程过程调用) 和 REST (Representational State Transfer,表述性状态转移)。
4.6远程过程调用
 
远程过程调用允仵你进行一个本地调用,但事实上结果是由某个远程服务器产生的。RPC 的种类繁多,其中一些依赖于接口定义(SOAP、Thrift、protocol buffers等〉。不同的技术 找可以通过接口定义轻松地生成客户端和服务端的桩代码。举个例子,我可以让一个Java 服务暴露一个 SOAP 接口,然后使用 WSDL (Web Service Definition Language, Web 服务 描述语言)定义的接口生成.NET客户端的代码。其他的技术,比如JavaRMI,会导致服 务端和客户端之间更紧的_合,这种方式要求双方都要使用相同的技术栈,但是不需要额 外的共享接口定义。然而所有的这些技术都有一个核心特点,那就是使用本地调用的方式 和远程进行交互。
 
有很多技术本质上是二进制的,比如JavaRMI、Thrift、protocol buffers等,而SOAP使用 XML作为消息格式。有些RPC实现与特定的网络协议相绑定(比如SOAP名义上使用的 就是HTTP),当然不同的实现会使用不同的协议,不同的协议可以提供不同的额外特性。 比如TCP能够保证送达,UDP虽然不能保证送达但协议开销较小,所以你可以根据自己 的使用场景来选择不同的网络技术。
 
那些RPC的实现会帮你生成服务端和客户端的桩代码,从而让你快速开始编码。基本不用 花时间,我就可以在服务之间进行内容交互了。这通常也是RPC的主要卖点之一:易干使 用。从理论上来说,这种可以只使用普通的方法调用而忽略其他细节的做法简直是给程序 员的巨大福利。
 
然而有一些RPC的实现确实存在一些问题。这些问题通常一幵始不明显,但慢慢地就会暴 露出来,并且其带来的代价要远远大于一开始快速启动的好处。
 
4.6.1技术的耦合
有一些RPC机制,如JavaRMI,与特定的平台紧密绑定,这对于服务端和客户端的技术 选型造成了一定限制。Thrift和protocol buffers对于不同语言的支持很好,从而在一定 程度上减小了这个问题的影响。但还是要注意,有时候RPC技术对于互操作性有一定的 限制。
 
从某种程度上来讲,这种技术上的耦合也是暴露内部实现细节的一种方式。举个例子,使 用RMI不仅把客户端绑定在了 JVM上,服务端也是如此。
 
4.6.2本地调用和远程调用并不相同
RPC的核心想法是隐藏远程调用的复杂性。但是很多RPC的实现隐藏得有些过头了,进 而会造成一些问题。使用本地调用不会引起性能问题,但是RPC会花大量的时间对负荷进 行封装和解封装,更别提网络通信所需要的时间。这意味着,要使用不同的思路来设计远 程和本地的API。简单地把一个本地的API改造成为跨服务的远程API往往会带来问题。 最糟的情况是,开发人员会在不知道该调用是远程调用的情况下对其进行使用。
 
你还需要考虑网络本身。分布式计算中一个非常著名的错误观点就是“网络是可靠的” (https://blogs.oracle.com/jag/resource/Fallacies.html),事实上网络并不可靠。即使客户端和 服务端都正常运行,整个调用也有可能会出错。这些错误有可能会很快发生,也有可能会 过一段时间才会显现出来,它们甚至有可能会损坏你的报文。你应该做出一个假设:有一 些恶意的攻击者随时有可能对网络进行破坏,因此网络的出错模式也不止一种。服务端 可能会返回一个错误信息,或者是请求本身就是有问题的。你能够区分出不同的故障模 式吗?如果可以,分别如何处理?(也就是对于远程调用而言,请求本身是错误的或者服务出错,这两种情况的区分与处理对远程调用而言是不透明的)如果仅仅是因为远程服务刚刚启动,所以响应才会有点 慢,该怎么办?在第11章中讨论弹性时,会就这些话题做更多讨论。
 
4.6.3 脆弱
有一些很流行的RPC实现可能会造成一些令人讨厌的脆弱性,java的RMI就是一个很好 的例子。考虑一个非常简单的Java接口,通过该接口可以向客户服务发起一个远程调用。 示例4-1中声明了向远端提供的接口。然后Java RMI会针对这些方法生成客户端和服务端 的桩代码。
示例4-1 :使用Java RMT定义一个服务的接口
 
import java.rmi.Remote; 
import java.rni.RenoteException;
 
public interface CustomerRenote extends Remote {
 
public Customer findCustomer(String id) throws RenoteException;
 
public Customer createCustoner(Strlng firstnane, String surname, String emallAddress) throws RenoteException;
 
}
 
在这个接口中,findCustomer接受名(first name)、姓(surname)及电子邮件地址(email address)作为参数。如果我决定只需要电子邮件地址(email address)就可以创建客户对 象的话,该怎么办呢?很容易添加一个新的方法,如下所示:
public Customer createCustomer(String emailAddress) throws RemoteException;
这里存在一个问题,因为对规格说明进行了修改,所以所有的客户端都需要重新生成桩, 无论该客户端是否需要这个新方法。对每一个具体的点来说,这种修改还是可控的,但事 实上这样的修改会非常普遍。RPC接口最后通常都会包含很多与对象进行交互或者创建对 象的方法。造成这种后果的一部分原因就是,没有意识到现在是在做远程调用,而非本地 调用。 ,
还有一种形式的脆弱性。让我们来看看Customer对象是什么样子:
public class Customer implements Serializable { 
private String firstNane;
 private String surname; 
private String emailAddress; 
private String age;
 
}
 
如果最后发现Customer对象中的年龄字段完全没有任何消费者使用,你可能想要去掉这 个字段。但如果单单从服务端的实现中删除年龄,而客户端没有做相应修改的话,那么即 使它们从来没有用过这个字段,客户端中和Customer对象反序列化相关的代码还是会出问 题。所以为了应用这些修改,需要同时对服务端和客户端进行部署。这就是任何一个使用 二进制桩生成机制的RPC所要面临的挑战:客户端和服务器的部署无法分离。如果使用这 种技术,离lock-step发布就不远了。
 
类似地,不删除字段而是调整Customer的结构也会遇到类似的问题。一个可能的例子是, 把名(firstNane)和姓(surname)封装到一个新的类型中来简化代码。有一个办法可以
 
避免这种问题,即使用一个字典类型作为参数进行传递。但如果真这么做的话,就会失去 自动生成桩的好处,因为你还是要手动去匹配和提取这些字段。
 
在实践中,通信双方使用的数据类型会直接被序列化和反序列化。而这个数据类型中会包 含大量的字段,这就导致不再使用的字段无法被安全删除。
 
 
4.6.4 RPC很糟糕吗
 
尽管存在这些缺点,我也不会说RPC很糟糕。然而我见过一些常见的实现确实会导致这里 列出的问题,比如RMI,我会尽量避免使用它。当然RPC中也包含很多其他不同的实现。 更现代的一些RPC机制,比如protocol buffers或者Thrift,会通过避免对客户端和服务端 的lock-step发布来消除上面提到的一些问题。
 
如果你决定要选用RPC这种方式的话,需要注意一些问题:不要对远程调用过度抽象,以 至于网络因素完全被隐藏起来;确保你可以独立地升级服务端的接口而不用强迫客户端升 级,所以在编写客户端代码时要注意这方面的平衡;在客户端中一定不要隐藏我们是在做 网络调用这个事实;在RPC的方式中经常会在客户端使用库,但是这些库如果在结构上组 织得不够好,也可能会带来一些问题,后面会对此做更详细的讨论。
 
RPC是请求/响应协作方式中的一种,相比使用数据库做集成的方式,RPC显然是一个巨 大的进步。但是我们还有其他的选择。
 
4.7 REST
 
REST是受Web启发而产生的一种架构风格。REST风格包含了很多原则和限制,但是 这里我们仅仅专注于,如何在微服务的世界里使用REST更好地解决集成问题。REST是 RPC的一种替代方案。
 
其中最重要的一点是资源的概念。资源,比如说Customer,处于服务之内。服务可以根据 请求内容创建Custome「对象的不同表示形式。也就是说,一个资源的对外显示方式和内部 存储方式之间没有什么耦合。举个例子,客户端可能会请求一个Custome「的JSON表示形 式,而Customer在内部的存储方式可以完全不同。一旦客户端得到了该Customer的表示, 就可以发出请求对其进行修改,而服务端可以选择应答与否。
•REST风格包含的内容很多,上面仅仅给出了简单的介绍。我强烈建议你看一看 Richardson 的成熟度模型(http://martinfowler.com/articles/richardsonMaturityModel.html), 其中有对REST不同风格的比较。
 
REST本身并没有提到底层应该使用什么协议,尽管事实上最常用HTTP。我以前也见 过使用其他协议来实现REST的例子,比如串口或者USB,当然这会引入大量的工作。 HTTP的一些特性,比如动词,使得在HTTP之上实现REST要简单得多,而如果使用其 他协议的话,就需要自己实现这些特性。
4.7.1 REST 和 HTTP
认证到客户端证书,HTTP生态系统提供了大量的工具来简化安全性处理,第9章会就泫 话题做更多讨论。即便如此,你需要正确地使用HTTP才能得到这些好处。如果用得不 好,它就会像其他技术一样变得既不安全又难以扩展。如果用得好,你会得到很多好处。
 
需要注意的是,HTTP也可以用来实现RPC。比如SOAP就是基干HTTP进行路由的,但 不幸的是它只用到HTTP很少的特性,而动词和HTTP的错误码都被忽略了。很多时候, 似乎那些已有的并且很好理解的标准和技术会被忽略,然后新推出的标准又只能使用全新 的技术来实现,而这些新技术的提供者也就是制定那些新标准的公司!
 
4.7.2超媒体作为程序状态的引擎
 
REST引人的用来避免客户端和服务端之间产生耦合的另一个原则是“HATEOAS” (Hypermedia As The Engine Of Application State,超媒体作为程序状态的引擎。天哪,它真 的需要一个缩写吗?)。这个概念很长也很有趣,所以让我们详细看一下。
 
超媒体的概念是:有一块内容,该内容包含了指向其他内容的链接,而这些内容的格式可 以不同(如文本、图像、声音等)。这个概念你应该很熟悉,因为你可以在任何一个网页 上看到超媒体控制形式的链接,当你点击链接时可以看到相关的内容。HATEOAS背后的 想法是,客户端应该与服务端通过那些指向其他资源的链接进行交互,而这些交互有可能 造成状态转移。它不需要知道Custome「在服务端的URI,相反客户端根据链接导航到它想 要的东西。
这个概念有点奇怪,所以让我们退后一步,考虑一下人类和网页这个超媒体之间是如何交 互的。
 
使用超媒体控制时,我们希望电子用户2也能达到同样的聪明程度。首先看一看MusicCorp 可能会用到的超媒体控制都有哪些。在示例4-2中我们对表示专辑目录项的资源进行访问。 在专辑的信息中可以看到一系列超媒体控制。
 
示例4-2:专辑信息中的超媒体控制  
<album>
<name>Give Blood</name>
<link rel="/artist" href="/artist/theBlakes” />  ①
<description>
Awesome, short, brutish, funny and loud. Must buy!
</descripti.on>
<link rel="/instantpurchase" href="/instantPurchase/1234" />  ②
</album>
 
①这个超媒体控制告诉我们去哪里找作者的信息。
②如果我想要买这张专辑,应该知道如何购买。
 
这个文档中存在两个超媒体控制。读取该文档的客户端需要知道,应该从关系(即示例中 的rel)为artist的那个链接中获取作者的信息,而文档中的instantpurchase也是协议的 一部分,访问该链接即可购买该专辑。就像人类需要理解如何识别购物网站上的购物车一 样,客户端也需要理解该API的语义。
 
作为一个客户端,我不需要知道购买专辑的UR1,只需要访问专辑资源,找到其购买链 接,然后访问它即可。购买链接的位置可能会改变,UR1也可能会变,该站点甚至可以发 送给我额外的信息,但是作为客户端不用在意这些。这就使得客户端和服务端之间实现了 松耦合。
 
这样底层细节就被很好地隐藏起来r。我们可以随意改变链接的展现形式,只要客户端仍 然能够通过特定的协议找到它即可。类似地,购物车也可以从一个很简单的链接变成一个 复杂一些的JavasScript控件。你也可以随意向文档中添加新的链接,从而给客户端提供对 资源状态的额外操作。除非我们改变某个链接的语义或者删除该链接,否则客户端不会受 到影响。
 
使用这些链接来对客户端和服务端进行解耦,从长期来看有着很显著的好处,因为你不需 要一再调整客户端代码来匹配服务端的修改。通过使用这些链接,客户端能够自行获取相 关API,这对于实现新客户端来说非常方便。
 
这种方式的一个缺点是,客户端和服务端之间的通信次数会比较多,因为客户端需要不断 地发现链接、请求、再发现链接,直到找到自己想要进行的那个操作,所以这里终究还是 需要做一些取舍。我建议,你一开始先让客户端去自行遍历和发现它想要的链接,然后如 果有必要的I舌再想办法优化。别忘了前面我们提到了很多跟HTTP相关的工具可以帮助我 们。很多文献都讨论过过早优化的坏处,所以这里我就不花时间讨论这个话题了。需要注 意的是,这里的很多方法都可以用来创建分布式超文本系统,但并不是所有的方法都适用 于所有的场景!有时候你会发现自己需要的就是一个好一些的老式RPC而已。
 
我个人很喜欢让客户端自行遍历和发现API这种形式。自行发现和解耦的好处非常之大, 然而很显然并非所有人都买账,因为我身边很多人都不是这么做的。我认为主要原因是这 么做需要一'定的投入,但是回报的时间往往比较长。
 
 
 
 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!