微服务设计读书笔记 (第四章 集成)

​ 在我看来,集成是微服务相关技术中最重要的一个。做得好的话,你的微服务可以保持自治性,你也可以独立地修改和发布它们;但做得不好的话会带来灾难。

寻找理想的集成技术

微服务之间通信方式的选择非常多样化,但哪个是正确的呢?

  • 避免破坏性修改

​ 有时候,对某个服务做的一些修改会导致该服务的消费方也随之发生改变。后面会讨论如何处理这种情形,但是我们希望选用的技术可以尽量避免这种情况的发生。比如,如果一个微服务在一个响应中添加了一个字段,那么已有的消费方不应该受到影响。

  • 保证APl的技术无关性

​ 我很喜欢保持开放的心态,这也正是我喜欢微服务的原因。因此我认为,保证微服务之间通信方式的技术无关性是非常重要的。这就意味着,不应该选择那种对微服务的具体实现技术有限制的集成方式。

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

​ 消费方应该能很容易地使用我们的服务。如果消费方使用该服务比登天还难,那么无论该微服务多漂亮都没有任何意义。所以让我们考虑一下,如何让消费方简便地使用美妙的新服务。理想情况下,消费方应该可以使用任何技术来实现,从另一方面来说,提供一个客户端库也可以简化消费方的使用。但是通常这种库与其他我们想要得到的东西不可兼得。举个例子,使用客户端库对于消费方来说很方便,但是会造成耦合的增加。

  • 隐藏内部实现细节

​ 我们不希望消费方与服务的内部实现细节绑定在一起,因为这会增加耦合。与细节绑定意味着,如果想要改变服务内部的一些实现,消费方就需要跟着做出修改。这会增加修改的成本,而这恰恰是我们想要避免的。这也会导致为了避免消费方的修改而尽量少地对服务本身进行修改,而这会导致服务内部技术债的增加。所以,所有倾向于暴露内部实现细节的技术都不应该被采用。

数据库共享

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

编排与协同

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

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

​ 使用编排(orchestration)的话,我们会依赖于某个中心大脑来指导并驱动整个流程。编排方式的缺点是,会使某个服务作为中心控制点承担了太多职责,它会成为网状结构的中心枢纽及很多逻辑的起点。我见过这个方法会导致少量的“上帝”服务,而与其打交道的那些服务通常都会沦为贫血的、基于CRUD的服务。

​ 使用协同,可以仅仅从客户服务中使用异步的方式触发一个事件。如果其他的服务也关心客户创建这件事情,它们简单地订阅该事件即可。缺点是,看不到那种很明显的业务流程视图。通常来讲,我认为使用协同的方式可以降低系统的耦合度,并且你能更加灵活地对现有系统进行修改。

​ 同步调用比较简单,而且很容易知道整个流程的工作是否正常。如果想要请求/响应风格的语义,又想避免其在耗时业务上的困境,可以采用异步请求加回调的方式。另一方面,使用异步方式有利于协同方案的实施,从而大大减少服务间的耦合,这恰恰就是我们为了能独立发布服务而追求的特性。

远程过程调用

​ 远程过程调用允许你进行一个本地调用,但事实上结果是由某个远程服务器产生的。RPC的种类繁多,其中一些依赖于接口定义(SOAP、Thrift、protocolbuffers等)。不同的技术栈可以通过接口定义轻松地生成客户端和服务端的桩代码。

  • 技术上的耦合

​ 从某种程度上来讲,这种技术上的耦合也是暴露内部实现细节的一种方式。举个例子,使用java RMI不仅把客户端绑定在了JVM上,服务端也是如此。

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

​ RPC的核心想法是隐藏远程调用的复杂性。但是很多RPC的实现隐藏得有些过头了,进而会造成一些问题。使用本地调用不会引起性能问题,但是RPC会花大量的时间对负荷进行封装和解封装,更别提网络通信所需要的时间。

  • 脆弱性

​ 有一些很流行的RPC实现可能会造成一些令人讨厌的脆弱性,Java的RMI就是一个很好的例子。

  • RPC很糟糕吗

​ 尽管存在这些缺点,我也不会说RPC很糟糕。然而我见过一些常见的实现确实会导致这里列出的问题,比如RMI,我会尽量避免使用它。当然RPC中也包含很多其他不同的实现。更现代的一些RPC机制,比如protocol buffers或者Thrift,会通过避免对客户端和服务端的lock-step发布来消除上面提到的一些问题。

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

Rest

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

​ 对于服务和服务之间的通信来说,如果低延迟或者较小的消息尺寸对你来说很重要的话,那么一般来讲HTTP不是一个好主意。

实现基于事件的异步协作方式

  • 技术选择

主要有两个部分需要考虑:微服务发布事件机制和消费者接收事件机制。

  • 异步架构的复杂性

​ 这些异步的东西看起来挺有趣的,对吧?事件驱动的系统看起来耦合非常低,而且伸缩性很好。但是这种编程风格也会带来一定的复杂性,这种复杂性并不仅仅包括对消息的发布订阅操作。

服务即状态机

​ 前面提到过服务应该根据限界上下文进行划分。我们的客户微服务应该拥有与这个上下文中行为相关的所有逻辑。

​ 当消费者想要对客户做修改时,它会向客户服务发送一个合适的请求。客户服务根据自己的逻辑决定是否接受该请求。客户服务控制了所有与客户生命周期相关的事件。我们想要避免简单地对CRUD进行封装的贫血服务。如果出现了在客户服务之外与其进行相关的修改的情况,那么你就失去了内聚性。

微服务世界中的DRY和代码重用的危险

​ 开发人员对DRY这个缩写非常熟悉,即Don't Repeat Yourself。虽然从字面上看DRY仅仅是避免重复代码,但其更精确的含义是避免系统行为和知识的重复。

​ 使用DRY可以得到重用性比较好的代码。把重复代码抽取出来,然后就可以在多个地方进行调用。比如说可以创建一个随处可用的共享库。但是这个方法在微服务的架构中可能是危险的。

​ 我们想要避免微服务和消费者之间的过度耦合,否则对微服务任何小的改动都会引起消费方的改动。而共享代码就有可能会导致这种耦合。

​ 跨服务共用代码很有可能会引入耦合。但使用像日志库这样的公共代码就没什么问题,因为它们对外是不可见的。

​ 在微服务内部不要违反DRY,但在跨服务的情况下可以适当违反DRY。服务之间引入大量的耦合会比重复代码带来更糟糕的问题。

​ 很多团队坚持在最开始的时候为服务开发一个客户端库。原因在于,这样不仅能简化对服务的使用,还能避免不同消费者之间存在重复的与服务交互的代码。

​ 如果你想要使用客户端库,一定要保证其中只包含处理底层传输协议的代码,比如服务发现和故障处理等。千万不要把与目标服务相关的逻辑放到客户端库中。想清楚你是否要坚持使用客户端库,或者你是否允许别人使用不同的技术栈来对底层API进行调用。最后,确保由客户端来负责何时进行客户端库的升级,这样才能保证每个服务可以独立于其他服务进行发布!

版本管理

​ 每次提及微服务的时候,都会有人问我如何做版本管理。大家担心服务的接口难免发生改变,那么如何管理这些改变呢?

  • 尽可能推迟

​ 减小破坏性修改影响的最好办法就是尽量不要做这样的修改。另一个延迟破坏性修改的关键是鼓励客户端的正确行为,避免过早地将客户端和服务端紧密绑定起来。

  • 及早发现破坏性修改

​ 及早发现会对消费者产生破坏的修改非常重要,因为即使使用最好的技术,也难以避免破坏性修改的出现。

  • 使用语义化的版本管理

​ 如果一个客户端能够仅仅通过查看服务的版本号,就知道它是否能够与之进行集成,那就太棒了!语义化版本管理(http://semver.org/)就是一种能够支持这种方式的规格说明。语义化版本管理的每一个版本号都遵循这样的格式:MAJOR.MINOR.PATCH。其中MAJOR的改变意味着其中包含向后不兼容的修改;MINOR的改变意味着有新功能的增加,但应该是向后兼容的;最后,PATCH的改变代表对已有功能的缺陷修复。

  • 不同的接口共存

​ 如果已经做了可以做的所有事情来避免对接口的修改(但还是无法避免),那么下一步的任务就是限制其影响。我们不想强迫客户端跟随服务端一起升级,因为希望微服务可以独立于彼此进行发布。我用过的一种比较成功的方法是,在同一个服务上使新接口和老接口同时存在。所以在发布一个破坏性修改时,可以部署一个同时包含新老接口的版本。

​ 这其实就是一个扩展/收缩模式的实例,它允许我们对破坏性修改进行平滑的过度。首先扩张服务的能力,对新老两种方式都进行支持。然后等到老的消费者都采用了新的方式,再通过收缩API去掉旧的功能。

  • 同时使用多个版本的服务

​ 另一种经常被提起的版本管理的方法是,同时运行不同版本的服务,然后把老用户路由到老版本的服务,而新用户可以看到新版本的服务。

​ 短期内同时使用两个版本的服务是合理的,尤其是当你做蓝绿部署或者金丝雀发布时(第7章会详细讨论这些模式)。在这些情况下,不同版本的服务可能只会共存几分钟或者几个小时,而且一般只会有两个版本。升级消费者到新版本的时间越长,就越应该考虑在同一个微服务中暴露两套API的做法。

与第三方软件集成

​ 我的客户经常纠结这样的问题:“应该自己做,还是买?”一般来讲,我和同事的建议是,对于一般规模的组织来说,如果某个软件非常特殊,并且它是你的战略性资产的话,那就自己构建;如果不是这么特别的话,那就购买。

  • 缺乏控制

​ 尽量把集成和定制化的工作放在自己能够控制的部分。

  • 定制化

​ 多企业购买的工具都声称可以为你做深度定制化。一定要小心!这些工具链的定制化往往会比从头做起还要昂贵!如果你决定购买一个产品,但是它提供的能力不完全适合你,也许改变组织的工作方式会比对软件进行定制化更加合理。

  • 在自己可控的平台进行定制化

​ COTS和SAAS产品当然是有用的,但不适用于重头开始构建系统的场景(或者说这么做不合理)。那么如何解决这些挑战呢?关键是把事情移到自己可控的部分做。核心思想是,任何定制化都只在自己可控的平台上进行,并限制工具的消费者的数量。

小结

​ 前面了解了很多不同的集成选择,我也谈了什么样的选择能够最大程度地保证微服务之间的低耦合:

· 无论如何避免数据库集成

· 理解REST和RPC之间的取舍,但总是使用REST作为请求/响应模式的起点

· 相比编排,优先选择协同

· 避免破坏性修改、理解Postel法则、使用容错性读取器

· 将用户界面视为一个组合层

posted @ 2020-10-03 09:43  清晨时光  阅读(206)  评论(0编辑  收藏  举报