第四章:集成

集成是微服务相关技术中最重要的一个。做得好的话,你的微服务可以保持自治性,可以独立修改和发布他们,如果做的不好的话,会带来灾难。
4.1寻找理想的集成技术
微服务间的通讯选择性很多,REST、SOAP、RPC、Protocol buffers等。
4.11避免破坏性修改
有些时候对一个微服务的修改会造成该服务消费者的修改,例如:微服务A增加了一个字段,如果处理不好的话,会导致A服务的消费者B服务也必须增加该字段才能保证服务B能够正常调用A服务。
4.12保证API的技术无关性
保证微服务间的技术无关性非常重要,这意味着不应该选择那种对微服务的具体实现技术有限制的集成方式。
4.13使你的服务易于消费方使用
消费方应该很容易的调用我们的服务。理想情况下,消费方可以使用任何技术来实现;
另一方面,提供一个客户端库也可以简化消费方的使用,但是消费方使用客户端库也会造成微服务间的耦合。
4.14隐藏内部实现细节
我们不希望消费方和服务的内部实现细节绑定在一起,因为这会增加耦合。
与细节绑定意味着,如果改动服务内部的一些实现,消费方就需要跟着做出修改,这会增加成本,因此我们要避免。
这也会导致我们为了避免消费方的修改而尽量少的对服务本身进行修改,从而导致服务内部技术债的增加。
因此,所有倾向于暴露内部实现细节的技术都不应该被采用。
4.2为用户创建接口
以MusicCrop为例,创建客户这个业务,包括新客户的创建、付账设置、发送欢迎邮件等接口;
接下来会以这个业务为例说明集成的各种方式。
4.3共享数据库
业界最常见的集成形式就是共享数据。使用这种方式时,如果其他服务想要从一个服务获取信息,可以直接访问数据库。如果想要修改,也可以直接在数据库中修改。这种方式看起来非常简单,而且可能是最快的集成方式,这也正是它流行的原因。
共享数据库的方式有以下缺点:
多个服务同时访问相同共享数据库,会造成外部服务能够查看到内部实现细节,并与其绑定在一起。存储在数据库中的数据对所有人来说都是公平的,所有服务都可以完全访问该数据库。如果我们更改数据库表结构,那么消费方就无法进行工作。共享数据库是一个大的共享API,但同时也非常不稳定。
消费方与特定的技术进行了绑定,这说的是所有服务都与共享数据库进行了绑定。如果一个某个服务使用非关系型数据库更好,那么会为以后这种形式的修改带来很大的困难。这也造成了服务间的耦合。
微服务的设计核心:高内聚和松耦合。因此通过共享数据库的方式很难满足设计要求。
4.4同步和异步
服务间的协作包括两种方式“同步和异步。
如果使用同步通信,发起一个远程服务调用后,调用方会阻塞自己并等到整个操作的完成。
如果使用异步通信,调用方不用等待操作完成就可以返回,甚至不需要关心这个操作是否完成。
同步可以知道事情是否成功执行。
异步通信对于运行时间比较长的任务来说比较有用,否则就需要在客户端和服务端之间开启一个长连接,这是非常不实际的。
当需要低延迟的时候可以使用异步通信。
处理异常通信的技术相对来说比较复杂。
同步通信 即请求/响应:发起一个请求,然后等待响应。
异步通信 即基于事件:发起一个请求,注册一个回调,当服务端操作结束后,会调用该回调。
对于使用基于事件的协作方式来说,客户端不是发起请求,而是发布一个事件,然后期待其他的协作者接收到该消息,并知道该怎么做。
基于事件的系统天生就是异步的;
业务逻辑并非集成中某个核心服务中,而是平均分布在不同的协作者中。
基于事件的协作方式耦合性很低。
4.5编排和协作
对复杂业务逻辑进行建模时,我们需要处理跨服务业务流程的问题,而使用微服务时这个问题会来的更快,我们以MusicCrop中创建用户为例,看看创建用户业务时会发生什么。
MusicCrop的创建用户业务包括三个子业务:
在客户的积分账号中创建一条记录;
通过邮政系统给用户发送一个欢迎礼包;
通过邮件系统发送一条欢迎邮件;
当考虑具体实现时,有两种架构风格可以采用:
一种是使用编排,我们依赖某个中心大脑来驱动整个流程。
编排的话,在创建客户时,会通过请求/响应的方式进行通讯,我们可以对当前进行到具体哪一个进行跟踪。
编排的缺点是客户服务作为中心控制点承担了太多职责,它会成为网状结构的中心枢纽及很多逻辑的起点。例如客户服务中心可能会成为“上帝服务”,而其他三个子服务会成为“贫血服务”即只是简单的CRUD操作。
大多数的重量级的编排方案都非常不稳定,而且修改的代价很大。
一种是使用协同,我们仅仅需要告诉系统中各个部分各自的职责,而把具体怎么做的细节留给它们自己。
使用协同,可以仅仅从客户中心服务使用异步的方式触发一个事件,该事件名可以叫做“创建客户”,例如可以使用订阅或者消息系统,客户服务中心发送一个消息,然后其他三个子服务自己去执行各自的服务。
使用协同可以消除耦合,同时可以对现有系统更加灵活的修改。
协同的缺点是看不到具体的创建客户业务流程处理到哪一个步骤,比较难以跟踪。
同时意味着我们需要做一个些额外的监控工作,以保证其正确的进行。
基于以上两种方式的比较,我更倾向于使用协同的方式,在这种方式下每个服务都足够聪明,并且能够很好的完成自己的任务。
这里有好几个因素需要考虑:同步调用比较简单,而且容易知道整个流程的工作是否正常,如果想用使用请求/响应风格的语义,可以使用异步请求加回调的方式;另一方面,使用异步调用的方式有利于协同方案的实施,从而大大减少服务间的耦合,这恰恰就是我们为了能够独立发布服务而追求的特定。
针对请求/响应可以考虑两种技术RPC和REST。
4.6远程过程调用RPC
远程过程调动允许你进行一个本地调用,但是事实上结果是由某个远程服务器产生的。
RPC的实现会帮你生成服务端和客户端的桩代码,从而让你快速开始编码。基本上不用花时间,我们就可以再服务间进行内容交互了。
然而有一些RPC的实现确实存在一些问题。这些问题通常一开始不明显,但慢慢地就会暴露出来,并且带来的代价要远远大于一开始快速启动的好处。
4.6.1技术的耦合
有一些RPC机制,如Java RMI与特定的平台紧密绑定,这对于服务端和客户端的技术选型造成了一定限制。Thrift和protocol buffers对于不同语言的支持很好,从而在一定程度上减少了这个问题的影响。
从某种程度上来说,这种技术上的耦合也会暴露内部实现细节的一定方式。例如使用Java RMI不仅把客户端绑定在JVM上,服务端也是如此。
4.6.2本地调用和远程调用并不相同
RPC的和兴想法是隐藏远程调用的复杂性。但是很多RPC的实现隐藏的有些过头了,进而会造成一些问题。使用本地调用不会引起性能问题,但是RPC会花大量的时间对负荷进行封装和解封装,更别提网络通信需要的时间。
你需要考虑网络本身的问题。分布式计算中一个非常著名的错误观点就是“网络是可靠的”,事实上网络并不可靠。及时客户端和服务端都正常运行,整个调用也有可能出错,这些错误有可能会很快发生,有可能过一段时间才会显现出来,它们升值有可能损坏你的报文,因此网络的出错模式也不止一种。
4.6.3.脆弱性
有一些很流程的RPC实现可能会造成一些令人讨厌的脆弱性,Java的RMI就是一个很好的例子,考虑一个非常简单的接口,通过该接口可以向客户服务发起一个远程调用,如果对该接口进行了修改,由于是本地的桩代码,你很容易把这个接口当成了本地接口进行了修改,从而造成错误。并且如果想修改一个接口,需要重新生成服务端和客户端两边的桩代码,这种修改会很频繁。
这就是任何一个使用二进制桩代码生成机制的RPC所要面临的挑战:客户端和服务端的部署无法分离。如果使用这种技术。离lock-setp发布就不远了。
实践中,通讯双方使用的数据类型会直接被序列化和反序列化,而如果数据类型中包含了大量的字段,这就会导致不再使用的字段无法被安全的删除掉。
4.6.4RPC很糟糕吗
尽管RPC存在这些缺点,但是可以通过使用thrift或者protocol buffers等技术来避免对客户端和服务端的lock-step发布来消除上面提到的一些问题。
如果你决定使用RPC方式的话,需要注意一些问题:
不要对远程调用过度抽象,以至于网路因素完全被隐藏起来;
确保你可以单独的升级服务端的接口而不用强迫客户端升级,所以在编写客户端代码时要注意这个方面的均衡;
在客户端中一定不要隐藏我们是在做网络调用这个事实,
在RPC的方式中经常会在客户端能使用库,但是这些库如果在结构上组织的不好,也有可能会带来一些问题。
RPC是请求/响应协作方式的一种,相比使用数据库做集成的方式,RPC显然是一个巨大的进步。
4.7REST
REST是受Web启发而产生的一种架构风格,REST风格包含了需要原则和限制,但是这里我们仅仅专注于如果在微服务的世界使用REST更好的解决集成问题。
REST是RPC的一种替代方案。
REST最重要的一点就是资源的概念。服务可以根据请求的内容创建对象的不同表示形式。也就是说一个资源的对外显示方式和内部存储方式之间没有什么耦合。
REST本身并没有提到底层应该使用什么协议,尽管事实上最常用的http。http的一些特定,比如动词put、get等,使得在http上实现REST要简单的多,而如果使用其他协议的话,就需要自己实现这些特定。
4.7.1REST和HTTP
HTTP本身提供了很多功能,这些功能对于实现REST风格非常有用,比如动词put、get等。GET方式使用幂等的方式获得资源。POST创建一个新资源。
HTTP周边也有一个大的生态系统,其中包含很多支撑工具和技术,例如Varnish这要的HTTP缓存代理、mod_proxy这样的负载均衡器。这些工具可以很好帮忙我们处理HTTP流量,并使用聪明的方式对其进行路由。而且这些操作剧本上都对终端用户透明。HTTP还提供了一系列安全控制机制供我们直接使用。包括认证证书。
需要注意的是HTTP也可以用来实现RPC,比如SOAP就是具有HTTP进行路由的。
4.7.2超媒体作为程序状态的引擎
REST引入的用来避免客户端和服务端直接产生耦合的另一个原则是“超媒体作为程序状态的引擎”。
4.7.3JSON、XML还是其他
由于服务端使用标准文本形式的响应,所以客户端可以很灵活的对资源进行使用,而基于HTTP和REST能够提供多种不同的响应形式。例如XML和JSON。
JSON无论从形式上还是使用方法上来说都更加简单。相对于XML来说JSON更加紧凑。
JSON也有一些缺点,XML使用连接来进行超媒体控制,JONS中并没有类似的东西。
当然并不是说只有两种格式,通过HTTP我们可以发送任何格式,甚至于二进制的。
4.7.4留心过多的约定
REST越来越流行,但是有一些工具会为了短期利益而牺牲长期利益。例如有些框架可以很容易的表示数据库对象,并把它们反序列化成进程内的对象,然后直接暴露给外部。这种方式内在的耦合性所带来的痛苦会远远大于从一开始就消除该你啊你之间的耦合所需要的代价。那如何这个问题那?一个有效的方式是先进行外部接口的设计,等到外部接口稳定之后再实现微服务内部的数据持久化,这样可以保证接口是由消费者的需求驱动出来的,从而避免数据春方式对外部接口的影响。
4.7.5基于HTTP的REST的缺点
从易用性的角度来看,基于HTTP的REST无法帮助你生成客户端的桩代码,而RPC可以。
使用HTTP意味着可以使用很多的HTTP客户端。
从个人的角度来看,使用客户端的库可以做的更好,但是使用库会增加复杂度,因为人们会不自觉的回到基于HTTP的RPC的思路上来,然后构建出一些共享库。在客户端和服务端之间共享代码是非常危险的。
还有一个问题就是有些Web框架无法很好的支持所有的HTTP动词,可能很容易处理GET和POST,但是PUT和DELETE就比较麻烦了。推荐使用Jersey这样比较好的REST框架就不存在这样的问题。
性能上也可能会遇到问题,基于HTTP的REST支持不同的格式。比如JSON或者二进制,所以负载相对SOAP来说更加紧凑,当然和像Thrift这样的二进制协议是没办法比的,在低延迟的场景下,每个HTTP请求的封装开销可能是个问题。
虽然HTTP可以用于大流量的通讯场景,但是对于低延迟通讯来说并不是最好的选择,相比之下,有一些构建于TCP或者其他网络技术之上的协议更加高效。比如WebService。
对于服务和服务之间的通讯来说,如果低延迟或者较小的消息尺寸对你来说很重要的话,那么一般来讲HTTP不是一个好主意,你可以选择一个不同的底层协议,类似UDP来满足你的性能要求,很多RPC框架都是可以很好地运行在除了TCP之外的其他网络协议上。
有些RPC的实现支持高级的序列化和反序列化支持,然而对于REST来说,这部分工作就要自己实现了。这部分工作可能会成为服务端和客户端之间的一个耦合点,因为实现一个具有容错性的读取器不是一件容易的事情。
尽管有些缺点,在选择服务间的交互方式时,基于HTTP的REST仍然是一个比较合理的默认选择。
4.8实现基于事件的异步协作
前面讨论了基于请求/响应相关的技术,接下来介绍基于异步事件的通讯。
4.8.1技术选择
主要有两个部分需要考虑:微服务发布事件机制和消费者接收事件机制。
传统来说,像RabbitMQ这样的消息代理能够处理上述两方面的问题。生产者使用API向代理发布事件,代理可以向消费者提供订阅服务,并且在事件发布的时候通知消费者。
不过需要注意的是消息代理仅仅是中间件世界中的一小部分而已。队列本身是很合理、很有用的东西。需要谨记一个原则:尽量让中间件保持简单,而把业务逻辑放在自己的服务中。
另一种方法时使用HTTP来传播事件。ATOM是一个符合REST规范的协议,可以通过他提供资源聚合的发布服务,而且有很多现成的客户端可以用来消费该聚合。另一方面ATOM规范和与之相关的库用起来非常便捷,而且HTTP能够很好的处理伸缩性。但HTTP不擅长处理低延迟的场景,而且使用ATOM的话,用户还需要自己追踪消息是否送达及管理轮询等工作。
4.8.2异步架构的复杂性
事件驱动机制的系统看起来耦合非常低,而且伸缩性很好,但是这样的编程风格会带来一定的复杂性,这种复杂性并不仅仅包括对消息的发布订阅操作。例如一个非常耗时的异步请求/响应,需要考虑响应返回时需要怎么处理,该响应是否回到发送请求的那个节点?如果是的话,节点服务停止了怎么办?如果不是的话,是否需要把信息实现存储到某个比其他地方,以便于做相应的处理?如果API设计的好的话娿,短生命周期的异步操作还是比较容易管理的,但尽管如此,对于习惯了进程间同步使用的程序员来说,使用异步模式也需要思维上的转换。
需要考虑:
限制重试次数;
所有失败的消息需要存储在一个失败队列中。
需要界面展现失败队列中的消息;
· 事件驱动架构和异步编程会带来一定的复杂性,所以通常需要谨慎的选择这种技术。如果使用该技术,需要确保各个流程有很好的监控机制,并考虑使用关联ID,这种机制可以帮助你对跨进程的请求进行跟踪。
4.9服务及状态机
不管选用REST还是SOAP的RCP机制,服务即状态机的概念都很强大,服务应该根据限界上下文进行划分。我们的客户微服务应该拥有与这个上下文中行为相关的所有逻辑。
当消费者想要对客户做修改时,它会向客户服务发送一个合适的请求。客户服务根据自己的逻辑决定是否接受该请求。客户服务控制了所有与客户生命周期相关的事件。
我们要避免简单的对CRUD进行封装的贫血服务,如果出现了在客户服务之外与其进行相关的修改的情况,那么你就失去了内聚性。
把关键领域的生命周期显示建模出来非常有用。我们不但可以再位移的一个地方处理状态冲突,而且可以在这些状态变化的基础上封装一些行为。
可以认为基于HTTP的REST相比其他集成技术更合理,但是不管你选择什么技术,都要记住上面的原则。
4.10响应式扩展
响应式扩展提供了一种机制,在此之上,你可以把多个调用的结果组装起来并在此基础上执行操作。调用本身可以是阻塞或者非阻塞的。响应式扩展改变了传统的流程,以往我们会获取一些数据,然后基于此进行操作,现在可以做的是简单的对操作的结果进行观察,结果会根据相关数据的改变自动更新。
很多响应式扩展实现都在分布式系统中,因为调用的细节被屏蔽了,所以事情也更容易处理,我们可以简单地对下游服务调用的结果进行观察,而不需要关心它是阻塞还是非阻塞的,唯一需要做的就是等待结果并作出响应。其优点在于,我可以把多个不同的调用组合起来,这样就可以更容易的对下游服务的并发调用做处理。
4.11微服务世界中的DRY和代码重用的危险
微服务系统中应该避免系统行为和知识的重复。如果有相同的代码干同样的事情,那么代码就会变大的越来越大,从而降低可维护性。
使用DRY可以得到重复性比较好的代码。如果随意创建可用的代码库,在微服务中进行服用,对微服务系统来说可能比较危险。
我们需要避免消费者和提供者之前的代码耦合,否则对微服务任何小的改动都会引起消费者的改动。而 代码库就有可能引入这种耦合。
跨服务公用代码库很有可能会引入耦合。但使用类似日志库这样的公共代码就没有什么问题。
经验是:在微服务内部不用违反DRY,但在跨服务的情况下可以适当范围DRY。服务之间引入大量的耦合会比重复代码带来更糟糕的问题。
4.12按引用访问
如何传递领域实体的相关信息是一个值得讨论的问题?可以遵守一个原则:即A服务应该是关于A服务的唯一可靠来源。
使用资源的URI,例如客户端订单的通知邮件,在邮件中可以发送关于订单的URI,而不是具体的订单信息,防止在发送邮件后,订单发生了改变而导致订单不是最新的数据。
当然在使用引用时也需要做一些取舍。如果总是从客户服务 去查询给定客户的相关信息,那么客户服务的负载会变大,如果在获得资助的同事,可以得到资源的有效性时限信息的话,就可以进行相应的换粗,从而减少服务的负载,HTTP在缓存控制方面提供了很多现场的支持。
4.13版本管理
每次提及微服务的是偶,需要考虑如果进行版本管理。
4.13.1尽可能推迟
减少破坏性修改影响的最好办法就是尽量不要做这样的修改。比如数据库集成很容易引入破坏性的修改。使用REST就是很好的办法,因为对于内部的实现的修改不太容易引起服务接口的变化。
另一种延迟破坏性修改的关键是鼓励客户端的正确行为,避免过早的将客户端和服务端紧密绑定起来。
客户端尽可能灵活的消费服务响应应该符合Postel法则,该法则认为,系统中每个模块都应该“宽进严出”,即对自己发送的东西要严格,对接收的东西则要宽容。在请求/响应的场景下,该原则可以帮助我们在服务发送改变时,减少消费方的修改。
4.13.2及早发现破坏性的修改
及早发现对消费者产生【破坏的修改非常重要,因为即使使用最好的技术,也很难避免破坏性修改的出现。
最好的方式是使用消费者驱动的契约来及早定位这些问题。
4.13.3使用语义化的版本管理
如果一个客户端能够仅仅通过查询服务的版本号,就知道他是否能够与之进行对接,那就是最好的。“语义化版本管理”就是一种能够支持这样方式的规则说明。
"语义话版本管理"的每个版本号都遵守这样的格式:major.minor.patch。其中major的改变意味着其中包含向后不兼容的修改;minor的改变意味着有新功能的增加,但应该是向后兼容的;patch的改变代表对已有功能的缺陷修复。
4.13.4不同的接口共存
如果已经做了所有可以做的事情来避免对接口的修改,那么下一步的任务就是限制其影响。我们不想强迫客户端跟随服务端一起升级,因为系统微服务可以独立于彼此进行发布。我们使用过的一种成功的方式就是:在同一个服务商使用新接口和老接口同时存在。所以在发布一个破坏性修改是,可以部署一个同时包含新老接口的版本。
4.13.5同时使用多个版本的服务
另一种经常被提起的版本管理方法时同时运行不同版本的服务,然后把老用于路由到老版本的服务,而新服务可以看到新版本的服务。
缺点是如果要修复一个内部bug,需要修复两个版本,并做两次部署。而且可能需要在代码库中拉取分支,这个也会引起很多问题。其次吧用户路由到正确的服务中也是一件比较复杂的事情。最后不同版本的服务的持久化问题,不同版本的服务如果都存储到同一个数据库中,并且他们对不同的服务均课件,这可能引入更多的复杂性。
短期内同时使用两个不同版本的服务时合理的,尤其是当你做蓝绿部署或者金丝雀发布时。在这些情况下,不同版本的服务可能共同存在几分钟或者几小时,而且一般只会有两个版本。
4.14用户界面
4.15与第三方软件集成
第一个你的组织对软件的需求几乎不可能完全有内部满足。
第二个事都自己组织开发的话效率非常低。
如果考虑一个软件是否自己开发:如果某个软件非常棒,并且它是你的战略性资产的话,那就自己构建,如果不是这么特别的话,那就购买。
4.15.1缺乏控制
类似使用CMS或者SAAS这样的产品时,如果与之进行集成对其进行扩展,因为大部分的技术都不受你的控制。这些都需要依赖于厂家所做的决定。
4.15.2定制化
很多企业的工具都声称可以做深度的定制化。一定要小心。这些定制化会非常昂贵。
4.15.3意大利面式的集成
另一个挑战是如何与工具进行集成,服务间的集成是一件非常重要的事情。
4.15.4在自己可控的平台进行定制化
可以使用定制化软件所提供的API进行集成。
4.15.5绞杀者模式
绞杀者模式可以捕获并拦截对老系统的调用。
收藏文章数量从多到少与“把书读薄”是一个道理
原文地址:https://www.cnblogs.com/use-D/p/9912456.html