微软AzureCAT模式和实践团队发布了9个新的设计模式。这9个设计模式在设计和实现微服务时很有用。
下图展示了这些模式如何运用在一个微服务架构中:
9种设计模式如下:
1. 外交官模式(Ambassador)可以用于语言无关的方式处理常见的客户端连接任务,如监视,日志记录,路由和安全性(如TLS)。
场景
基于云的应用需要一些特色功能,比如熔断,路由,监测以及网络化配置更新。通常来说更新遗留应用或给已经存在的代码库添加新的特性是很困难的或者是不可能的。因为这些代码要么已经不再被开发团队维护,要么修改起来很不容易。
网路请求需要大量稳定的关于连接,授权,和认证的配置。如果这些请求在多个应用之间使用,并且这些应用还是由多种语言和框架构建的,那么这些请求必须要针对每一个实例来配置。此外,网络和安全相关功能可能需要一个支撑团队来管理。在巨大的代码库情况下,这个团队来更新他们不熟悉的应用代码是很有风险的。
解决方案
将客户端框架和库放到一个外部的进程里面去,这个进程可以作为应用与外部服务之间的一个代理。该代理部署在与应用相同的主机环境中,允许路由控制,弹性,安全特性和避免任何与主机相关的访问限制。也可以使用外交官模式来标准化及扩展监测功能。代理可以监控性能指标比如延迟或者资源利用率,并且监控发生在和应用一样的主机环境中。示意图如下:
代理独立于应用来管理某些特色功能。更新和修改外交官(代理)不会打乱遗留应用的功能。这种模式允许由不同的专门团队来实现和维护已经被移到外交官(代理)中去的安全,网络或者认证等相关特性。
外交官服务可以以挎斗的形式部署,与消费应用或服务有相同的生命周期。另外,如果外交官服务被同一个主机的多个不同的进程分享,那么也可以被部署为守护进程或者Windows服务。如果消费服务被容器化,外交官服务应该在相同的主机中被创建为一个独立的容器。
考虑点
代理增加了一些延迟开销。考虑以客户端库的形式直接被应用调用是不是更好的方式。
考虑通用化特性带来的可能后果。比如,外交官服务处理重试,除非所有的操作都是幂等性的,否则是不安全的。
考虑能够允许客户端传递某些上下文给代理以及传回到客户端的机制。
考虑如何打包和部署代理。
考虑所有的客户端是使用单一共享的实例还是每个客户端一个实例。
什么时候使用这种模式?
适用:
需要使用多样的语言或者框架来构建客户端连接相关特性的共同集合时候。
需要将横向客户端连接相关的关注点移交给到那些基础设施开发人员或其它专门团队中去。
需要在遗留应用或一个很难修改的应用中支持云或集群的连接需求。
不适用:
当网络请求的延迟非常重要时。代理会引入某些开销,即使很少,在某些情况下也会影响应用。
当使用单一语言消费客户端连接相关特性时候。在这种情况下,客户端库作为一个包分发给开发团队可能是一个更好的选择。
当连接相关特性无法被通用化以及需要深层次地与客户端应用集成时候。
例子
2. 防腐层(Anti-corruption layer)介于新应用和遗留应用之间,用于确保新应用的设计不受遗留应用的限制。
场景
很多应用依赖其它系统的数据或功能。新的特性需要具有调用遗留系统的能力。特别是对于那些渐进式的迁移。
一般来说那些需要被迁移的遗留系统都有质量问题,比如过度复杂的数据结构或者过时的API接口。遗留系统使用的特性和技术与很多现代的系统差别很大。与遗留系统交互,新的应用可能需要支持过时的基础架构,协议,数据模型,API,或者其它的不想放在现代应用里面的特性。
维护新系统和遗留系统之间的访问,会强制新系统和一些遗留系统的API或其它语意部分粘连。当这些遗留系统的特性有质量问题时候,需要一个干净设计的现代应用来支持这些“腐败”。
解决方案
通过在遗留系统和现代系统之间设置一个防腐层来隔离它们。该层负责两个系统之间的通信转换,从而使得遗留系统继续保持不变,同时新应用能够避免对设计和技术方式的妥协。
防腐层包含了所有的两个系统之间转换逻辑。该层可以是一个在应用内部的组件,也可以是一个独立的服务。
考虑点
可能会增加两系统间通信延迟。
防腐层增加了额外的服务,需要管理和维护。
需要考虑防腐层如何扩展。
考虑是否需要更多的防腐层。
在与其它应用或服务有关系情况下,考虑如何管理这些防腐层。如何与监控,发布和配置流程等集成?
确保维护和监控事务和数据的一致性。
考虑防腐层是否需要处理所有的遗留和新系统之间的通信还是仅仅一部分。
考虑防腐层是否是永久的,或者当所有的遗留系统功能完成迁移时,这些防腐层是否最终要被移除。
什么时候使用?
适用:
渐进式迁移,需要维护遗留系统和新系统之间的集成。
新系统和遗留系统之间有不同的语意环境,但是还必须有交互。
不适用:
新系统和遗留系统没有明显的语意不同。
3. 后端服务前端(Backends for Frontends)为不同类型的客户端(如桌面和移动设备)创建单独的后端服务。这样,单个后端服务就不需要处理各种客户端类型的冲突请求。这种模式可以通过分离对特定客户端的关注来帮助保持每个微服务的简单性。
场景
应用刚开始只是针对桌面网络UI。通常,需要并行地开发一个后台服务来提供该UI所需要的特性。随着应用的用户数量不断增长,需要开发移动应用,移动应用必须可以和该后台服务交互。该后台服务就变成了一个通用的后台服务,既满足桌面应用也满足移动应用。
但是,移动设备和桌面浏览器区别是很大的,包括屏幕尺寸,性能以及对显示的限制。因此,对移动应用的后台服务的需求是有别于桌面网络UI的。
这些区别导致了对后台服务的竞争性需求。一般,不同的前端是由单独的团队负责,这就导致了在开发过程中,后台服务成为了瓶颈。需求变更的冲突以及需要满足所有前端的要求往往会导致需要花费更多的精力在单一部署的资源上。
当某个前端团队要求后台服务更改时,这些更改在被集成到后台服务中之前,必须通过其它的前端团队的验证。
解决方案
每种前端创建一个对应的后台服务。通过合理地调整每个后台服务的行为和性能来最佳地匹配前端的需求,而不用担心影响其它的前端体验。
因为每个后台服务专注于一个前端界面,该服务能够为这个前端界面来优化。因此,该服务将会比较小,相对不复杂,以及可能比那些要满足所有前端需求的通用的后台服务更快。每个界面团队有控制他们自己的后台服务自主权,并且不需要依赖集中化的后台服务开发团队。这给予界面团队很大的灵活性去选择语言,发布节奏,工作优先级,以及集成到后台服务里的特性。
需要考虑的点
考虑有多少个后台服务需要部署。
如果不同的界面(比如移动客户端)有相同的请求,考虑是否有每个界面实现一个后台服务的必要,或者是否单一后台服务就足够了。
当实现这种模式时,很大可能造成不同服务之间的代码重复问题。
面向前端的后台服务应该只包括客户端特定的逻辑和行为。通用的业务逻辑和其它全局特性应该由应用的其它服务来管理。
想想该模式如何反应在开发团队的职责中。
考虑实现该模式要花费多少时间。当继续支持现存的通用后台服务时,构建新的后台服务是否导致技术债。
什么时候使用?
适合:
维护共享的和通用的后台服务需要很大的开发上的开销。
需要根据特定的客户端优化后台服务。
通用后台服务需要定制化来适应不同的界面。
用另一种语言更适合开发某种前端的后台服务的时候。
不适合:
当界面对后台服务产生相同或者相似的请求时候。
当只有一种前端与后台服务交互的时候。
4. 舱壁模式(Bulkhead)隔离了每个工作负载或服务的关键资源,如连接池、内存和CPU。使用舱壁避免了单个工作负载(或服务)消耗掉所有资源,从而导致其他服务出现故障的场景。这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。
场景
基于云的应用可能含有多种服务,每个服务有一个或多个消费者。某个服务中发生太多的失败或者有太多的负载将会影响其它所有消费该服务的消费者。
此外,消费者可能会同时发送请求给多个服务,每个请求都会使用到资源。当消费者发送某个请求到配置错误的或无响应的服务上时,客户端请求使用的资源可能不会被及时的释放。随着持续发送请求到该服务,这些资源可能会被耗尽。
同样的资源耗尽问题会影响有多个消费者的服务。从某个客户端来的大量的请求也可能会耗尽服务上的可用资源。其它的消费者就无法消费服务,从而引起级联故障。
解决方案
根据消费者负载和可用性需求,将服务实例分成不同的组。这种设计能帮助隔离故障,允许在故障期间,为一些消费者维持服务的功能性。
消费者也能够分区资源来保证请求某个服务所用的资源不会影响到其它请求服务所使用的资源。比如,为每个服务,给调用不同服务的消费者分配一个连接池。如果某个服务开始故障了,故障只会影响这个服务所拥有的连接池。该消费者可以继续使用其它的服务。
该模式的好处:
从级联故障中隔离消费者和服务。消费者或者服务发生的问题能够被隔离在它自己的船壁中,从而防止整个系统故障。
倘若发生服务故障,允许保存一些功能。该应用的其它服务和特性会继续工作。
允许去部署那些为不同的消费应用提供不同质量的服务。高优先级的消费者池能够被配置去使用高优先级的服务。
下图展示了通过连接池来调用单独的服务的船壁结构。如果服务A故障或者有其它一些问题,服务A的连接池会被隔离,只有使用服务A的线程池的工作负载会被影响。使用服务B和C的工作负载不会被影响并且能够继续工作而不被中断。
下图展示了多个客户端调用单个服务。每个客户端被分配一个独立的服务实例。客户端1产生太多的请求,从而导致服务实例崩溃。因为每个服务实例是隔离的,其它的客户端能够继续发送请求。
考虑点
围绕着业务和技术上的需求来定义分区。
当划分服务或者消费者到船壁中时,既要考虑隔离级别也要考虑开销,性能和可管理性。
考虑船壁模式与重试,断路器以及限流模式的结合来提供更多的复杂巧妙的故障处理。
当划分消费者到船壁中,考虑使用进程,线程池,以及信号量。
当划分服务到船壁中,考虑部署服务到独立的虚拟机,容器或者进程中。容器能在资源隔离性和开销之间提供较好的平衡。
使用异步消息通信的服务能够从不同队列集中隔离出来。每个队列能够有专属的实例集,用来处理队列中的消息,或者单个实例组使用某个算法来做出列和分发处理。
确定船壁的颗粒度水平。
监控每个分区的性能和SLA。
什么时候使用?
适合:
隔离那些被用来消费后台服务集的资源,特别是,如果应用能够提供某种程度的功能,即使当其中某个服务不响应的时候。
从普通的消费者中隔离重要的消费者。
防止应用发生级联故障。
不适合:
项目中不能接受资源的低利用率。
没有必要增加复杂性的时候。
5. 网关聚合(Gateway Aggregation)将对多个单独微服务的请求聚合成单个请求,从而减少消费者和服务之间过多的请求。
场景
要完成某个单一任务,客户端可能需要产生多个请求到不同的后台服务上。依赖多个服务来完成一件任务的应用必须在每个请求上消耗资源。当任何新的特性或者服务加到应用中时,需要增加额外的请求、资源需求和网络调用。客户端和后台服务之间的频繁的通信能够有害地影响应用的性能和规模。由于应用是围绕着很多小的服务来构建,天然地存在大量的跨服务调用,所以微服务架构使这种问题更突出。
在下图中,客户端发送请求到每个服务(1,2,3)。每个服务处理请求并且返回响应到应用(4,5,6)。在通常有高延迟的网状网路中,以这种方式使用独自的请求是效率低的以及可能会导致连接中断或未完成的请求。当每个请求可以被并行的处理时,应用必须为每个请求发送,等待以及处理数据,所有的都有独立的连接,这增加了故障发生几率。
解决方案
使用网关来减少客户端和服务之间的通信量。网关接收客户端请求,分发这些请求到不同的后台服务上去,然后聚合结果并返回这些结果给请求的客户端。
这种模式能够减少应用对于后台服务的请求数量,以及在高延迟网络中能提高应用的性能。
在下图中,应用发送一个请求到网关(1)。该请求包含了一系列额外的请求。网关分拆这些请求并且发送请求到相关服务(2)来处理每个请求。每个服务返回一个响应给网关(3)。网关合并这些来自于每个服务的响应到一个响应并发送该响应到应用(4)。应用产生单个请求,从网关仅仅接收到单个响应。
考虑点
网关不应该在后台服务之间引入服务耦合。
网关应该靠近后台服务来尽可能的减少延迟。
网关服务可能会引入单点故障。确保网关的设计满足应用的可用性要求。
网关可能会引入瓶颈。确保网关有足够的性能来处理负载并且能够被扩展来满足预期的增长。
针对网关做负载测试,确保网关没有引入服务的级联故障。
使用某些技术实现弹性设计,比如:船壁模式,断路器,重试以及超时机制。
如果一个或多个服务调用耗时太长,此时超时限制以及返回部分数据是可以接受的。需要考虑应用如何处理这种情况。
使用异步I/O来确保后台服务延迟不会在应用中引起性能问题。
使用相关ID追踪每个独立调用来实现分布式追踪。
监控请求指标和响应的大小。
考虑返回缓存的数据作为处理故障的一个故障转移策略。
考虑在网关之前放置一个聚合服务,而不是在网关中集成聚合功能。在网关中,请求聚合相对于其它服务可能会有不同的资源需求,这可能会影响网关路由和卸载的功能。
什么时候使用?
适合:
客户端需要和不同种类的后台服务通信来完成某个操作。
客户端可能使用高延迟的网络,比如网状网络。
不适合:
想减少客户端和单一服务之间多个操作之间的请求数量。这种场景下,最好是使用在服务中增加一个批处理操作的方式。
客户端和应用位于后台服务附近并且延迟不是一个重要的因素。
6. 网关卸载(Gateway Offloading)能使每个微服务卸载共享服务功能(共同的功能抽到API网关来做),比如在API网关使用SSL认证。
场景
一些特性在多个服务之间被共同地使用,并且这些特性需要配置,管理和维护。每个应用部署都有一个共享的或特定的服务会增加管理上的开销以及部署上的错误。任何针对共享特性的更新必须在所有的共享了该特性的服务之间都部署。
正确地处理安全问题(token验证,加密,SSL认证管理)以及其它的复杂任务会需要团队成员具有高度专业化的技能。比如,一个应用需要的一个证书必须被配置和部署到所有的应用实例上。对于每次新的部署,必须管理证书确保不过期。在每个应用部署中,任何共同的证书要过期时都必须更新,测试以及验证。
其它的一些共同服务例如认证,授权,日志,监控或限流很难在大量部署服务之间实现和管理。为了减少开销和故障发生的可能性,最好是合并这种类型的功能。
解决方案
卸载(转移)一些特性到API网关中去,特别是一些交叉点例如证书管理,认证,SSL协议终止,监控,协议转换或限流。
下图展示了API网关终止到达的SSL连接。网关以原始请求的名义(未加密)从任一在API网关上游的HTTP服务器上请求数据。
该模式的好处:
通过移除对分布式和维护支撑资源的需求,使服务的开发简化。相对简单的配置有利于更容易的管理和扩展,并且使服务升级更简单。
允许专职团队去实现那些需要特殊专业知识的特性,比如安全。这使得核心团队可以专注在应用功能上,而将这些特殊并且交叉的任务留给相关专家处理。
提供一些关于请求及响应的日志和监控的一致性。即使如果某个服务没有被正确地仪表化,网关也能够被配置并且确保监控和日志的最低水平。
考虑点
确保API网关是高可用的和对故障的弹性。通过运行多个API网关实例来避免单点故障。
确保网关是为了应用和端点容量和伸缩需求而设计。确保网关不会成为应用的瓶颈并且能够被高效的扩展。
只卸载(转移)那些被整个应用使用的特性,比如安全或者数据传输。
业务逻辑绝不能被卸载(转移)到API网关中去。
如果需要追踪事务,考虑为日志记录产生相关ID。
什么时候使用?
适合:
部署应用时有相同的考虑点,比如SSL证书或者加密。
某个特性在应用部署中是通用的,并且可能有不同的资源需求,比如内存资源,存储能力或网络连接。
希望将某些职责比如网络安全,限流或其它网络边界关注点交给特定的团队负责。
不适合:
如果引入了服务间的耦合。
7. 网关路由(Gateway Routing)使用单个端点,将请求路由到多种多样的微服务上,从而消费者无需管理很多分离的端点。
场景
当客户端需要消费多个服务时,为每个服务设置一个单独的端点以及每个端点都有客户端消息将会很有挑战性。比如,一个电子商务应用可能会提供很多服务例如搜索,评论,购物车,付款以及历史订单。每个服务有不同的API与客户端交互,并且为了连接到服务,客户端必须知道每个端点。如果某个服务发生更改或者更新了,客户端必须也要更新。如果某个服务被重构成2个或更多个独立的服务,服务和客户端代码都必须改动。
解决方案
在一组应用、服务或部署前面放置一个网关。使用应用7层路由来路由请求到恰当的实例上。
利用该模式,客户端应用仅需要知道单个端点并与之通信。如果某个服务被合并了或被分解了,客户端不一定需要更新。可以继续向网关发送请求,仅仅是路由发生改变。
网关也能够从客户端抽象后台服务,从而使得当位于网关之后的后台服务发生改变时,客户端调用请求始终保持简单。客户端请求能够被路由到任意需要用来处理预期客户端行为的单个服务或多个服务上去。可以新增,分解,以及重组网关后的后台服务而无需更改客户端。
通过管理如何将更新推出给用户,该模式也有助于部署。当部署某个服务的新版本时,能够与现存的版本并行地部署新版本。通过路由来控制哪个版本的服务被呈现给客户端,这样可以弹性地使用不同的发布策略,无论增量式,并行式,或者完整地推出更新。在新服务部署后,发现的任何问题都能够通过更改网关的配置来迅速地恢复到原先版本,从而不会影响到客户端。
考虑点
网关服务可能会引入单点故障。确保网关的设计满足可用性需求。在实现时,考虑弹性和容错能力。
网关服务可能会引入瓶颈。确保网关有足够的性能来处理负载以及能够很容易地线性扩展。
通过针对网关的负载测试来确保没有引入服务的级联故障。
网关路由是7层。网关路由能够根据IP,端口,头,或URL。
什么时候使用?
适合:
客户端需要消费很多不同的能够通过网关访问的服务。
希望通过单个端点来简化客户端应用。
需要从外部可寻址的端点路由请求到内部虚拟端点上,比如暴露VM上端口给虚拟IP地址集群。
不适合:
当应用很简单并且只使用一个或两个服务时候。
8. 挎斗模式(Sidecar)将应用程序的辅助组件部署为单独的容器或进程以提供隔离和封装。
场景
应用和服务经常需要某些相关功能,比如监控、日志、配置以及网络服务。这些外围的任务能够作为独立的组成部分或服务被实现。
如果它们被紧密的集成到应用中,那么这些外围任务能够和应用运行在相同的进程中,对于共享资源的利用比较有效率。但是,这也意味着他们没有很好地被隔离,并且在这些组件中任何一个发生中断都能够影响其它的组件或整个系统。还有,通常需要使用和父应用相同的语言来实现这些组件。从而使得组件和应用具有很紧密的相互依赖关系。
如果应用被分解成多个服务,那么每个服务可以使用不同的语言和技术来构建。尽管这增加了灵活性,同时也意味着每个组件有它自己的依赖以及需要特定语言的库来访问底层的平台和任何与父应用共享的资源。此外,以独立服务来部署这些特性会增加应用的延迟。为这些特定语言的接口管理代码和依赖也会增加相当大的复杂性,特别是对于托管,部署和管理。
解决方案
并置一个一组内聚的任务与主应用一起,但是要将他们放在他们自己的进程或容器中,为平台服务提供一个同种的跨语言的接口。
挎斗服务对于应用来说不是必要的部分,但是挎斗服务可以连接到应用上。挎斗服务随着父应用而变动。挎斗服务支持以进程或服务形式和主应用一起部署。挎斗被附加到一个“摩托车”上并且每个“摩托车”都有自己的挎斗。在相同方式下,一个挎斗服务共享它父应用的生命周期。对于每个应用的实例,都会部署一个挎斗服务的实例来为其提供服务。
使用挎斗模式的好处:
在运行环境和编程语言方面,挎斗独立于它的主要的应用,因此不需要为每种语言都开发一种挎斗。
挎斗能够如同主应用访问相同的资源。比如,挎斗能够监控被挎斗服务和主应用使用的系统资源。
由于它靠近主应用,因此当互相通信时,没有明显的延迟。
即使那些没有提供可扩展机制的应用,也可以使用挎斗来扩展功能,挎斗在相同主机上有自己独立的进程或作为主应用的子容器方式附加到主应用。
挎斗模式经常与容器一起使用并且一般被称为挎斗容器或附属容器(sidekick container)。
考虑点
考虑用来部署服务,进程或容器的部署和打包格式。容器特别适合挎斗模式。
当设计挎斗服务时,谨慎地决定进程间通信机制。试着使用语言无关或框架无关的技术除非性能需求使此技术不现实。
在将功能封装进挎斗之前,考虑作为单独的服务或更传统的守护进程更合适。
考虑功能是否可以以库的方式实现或使用传统的扩展机制。特定语言的库可能会有更深级别的集成和更少的网络延迟。
什么时候使用?
适合:
主应用使用异种的语言集和框架。挎斗服务中的组件能够被使用不同框架和不同语言的应用消费。
远程团队或不同的组织拥有某个组件。
某个组件或特性必须被并置在和应用同样的主机中。
需要某个服务共享主应用整个生命周期,但是能够被独立地更新。
对于特别的资源或组件,需要细颗粒度的控制。例如,可能想去限制某个特定组件的内存使用。可以部署该组件为一个挎斗,从而单独地管理主应用的内存使用。
不适合:
当进程间通信需要被优化。父应用和挎斗服务之间的通信会有一些开销,在调用中会有显著的延迟。对于频繁通信的接口,这种性能的牺牲不能够被接受。
对于一些小的应用,为每个实例部署挎斗服务的资源消耗相对于隔离的优势是不划算的。
当服务相对于主应用需要差异化或独立地扩展时。如果如此,将特性部署为独立的服务更好。
9. 绞杀者模式(Strangler)通过用新服务逐渐地替换特定的功能片段来支持渐进式迁移。
场景
随着系统年限的增加,开发工具,服务托管技术,甚至构建系统依赖的架构都能变得日益过时。随着新的特性和功能的加入,这些应用的复杂性会急剧的增加,使得维护或增加新特性到应用中变得非常困难。
完全地替换掉一个复杂系统将会是一个巨大的工程。通常需要渐进式地迁移到新系统,同时保持旧系统去处理那些还没有被迁移的特性。然而,运行一个应用的两个独立的版本意味着客户端需要知道特定的特性的位置。每当某个特性或服务被迁移时,需要更新客户端来指向新的位置。
解决方案
渐进式地用新应用和服务替换特定功能片段。创建一个立面层来拦截针对后台遗留系统的请求。该层路由请求要么到遗留系统,要么到新服务。现存的特性能够逐渐地迁移到新系统,而且消费者能够继续使用相同的接口,注意不到已发生的任何迁移。
该模式有助于最小化迁移的风险,并且随着时间的推移分摊开发工作量。因为立面层安全地路由用户到正确的应用上,以任何节奏给新系统增加功能都是可以的,同时保证了遗留应用继续工作。久而久之,随着特性都被迁移到新的系统,遗留系统最终被“绞杀”且不再需要。一旦整个过程完成后,遗留系统就能够被安全的移除。
考虑点
考虑如何处理新系统和遗留系统都可能会使用到的服务和数据存储。确保两者都能够并行地访问这些资源。
以某种方式组织新应用和服务使它们在将来的绞杀者迁移中能够容易地被拦截和替换。
在某些时候,当迁移完成时,绞杀者层将会要么消失,要么演化到某个遗留客户端适配器中。
确保立面层紧跟着迁移。
确保立面层不会变成单点故障或性能瓶颈。
什么时候使用?
适合:
当渐进式迁移后台应用到新的架构中去的时候。
不适合:
当对于后台系统的请求不能够被拦截时。
对于比较小的系统,当整个替换的复杂性很低的时候。
微服务的目标是通过将应用分解成小的自治的服务,这些服务能够被独立部署,来加速应用发布的速度。微服务架构也带来了很多挑战,上面这些模式能够帮助我们应对这些挑战。
参考文章: