微服务设计实现时的十大常见反模式和陷阱

数据驱动迁移反模式(Data-Driven Migration)

1.png


如上图所示,此种反模式的问题在于微服务的粒度没有最终肯定以前就作了数据迁移,如此当不断的调整服务粒度时,那么数据库就免不了频繁迁移,带来极大的成本。更好的方式以下图所示:数据库

2.png


即先分离功能,数据库先保持以前的单体,等到服务粒度最终肯定以后,再分离数据库。编程

超时反模式(The Timeout)

微服务架构是由一系列分离的服务组成的,这些服务之间经过一些远程协议进行互相之间的通讯。其中牵扯到了服务的可用性和响应性问题。以下图所示:json

3.png

 

  • 可用性:服务消费方可以链接服务方,并能够向其发送请求。
  • 响应性:服务方可以在消费方指望时间内给予请求响应。


为了防止服务的不可用和没法响应,一般的作法就是设置一个调用超时。此种作法表面上看是没问题的,可是试想一下以下情景:发起一个购买100个商品的请求,请求成功返回一个确认号。若是当请求超时可是请求在服务端已经成功执行了,此时这个交易实际是完成的,可是消费方没有拿到确认号,若是重试请求,那么服务方须要一个复杂的机制判断这是否一次重复提交。

一种解决此问题的方案是设置一个较长的超时时间,如一个服务的一般响应耗时须要2s,最大耗时须要5s,那么超时时间能够设置为10s。但这样的问题就是若是服务不可用,全部消费方都得等待10s,这个是很是损耗性能的。

解决超时反模式的方案就是使用“断路器模式”。就相似于房屋中的电源断路器,当断路器关闭,电流能够经过,当断路器打开,那么电流中断一直到断路器关闭。断路器模式就是说当检测到服务方没法响应时就打开,后续的请求都会被拒绝掉。一旦服务方可响应了,那么断路器关闭,恢复请求。其工做模式以下图所示:后端

4.png


断路器会持续地监测远程服务,确保其是可响应的。只要服务可响应,那么断路器会一直关闭,容许请求经过。若是服务忽然不可响应,那么断路器打开,拒绝后续的请求。然后续若是断路器又检测到服务恢复了,那么断路器会自动关闭,请求也就恢复了。此种方案与超时时间相比,最大的优点就是一旦服务不可响应,那么断路器模式可让请求马上返回而不是须要等待必定的时间。

Hystrix的Netflix是此种断路器模式的一种开源实现。此外,Akka中也包含了一个断路器实现:Akka CircuitBreaker类。
 安全

共享反模式(“I Was Taught to Share”)

微服务被广泛认为是一种不共享任何东西的架构。但实际上只能是尽量地少共享,毕竟在某些层面代码被多个服务共享也能带来必定好处。例如,与单独部署一套安全服务(认证和受权)其余全部服务都经过远程访问此服务相比,把安全相关的功能封装成jar包(security.jar),而后其余服务都集成此jar包,就可以避免每次都要发起对安全服务的访问,从而提升性能和可靠性。但后面的方案带来的问题就是依赖噩梦:每个服务都依赖多个自定义的jar包。如此不只打破了服务之间的边界上下文,同时也引入了诸如整体可靠性、变动控制、易测试性、部署等问题。

在一个使用面向对象编程语言的单体应用中,使用abstract类和接口实现代码复用和共享是一个良好的实践。但当从单体切换到微服务架构时,对于不少自定义的共享类和工具类(日期、字符串、计算)的处理要考虑到微服务间共享的东西越少越有利于保持服务间的边界上下文,从而更利于快速测试和部署。如下是几种推荐的方式,也是解决“共享反模式”的方案:性能优化

共享项目

5.png


将共享的代码做为一个项目在编译期与各个服务集成。此种方式便于变动和开发软件,可是最大的问题在于很难发觉哪个共享模块被修改以及修改的缘由,也没法肯定本身的服务是否须要这些变动。尤为是在服务发布前期发现某一个共享模块发生了变更的话须要再一次的测试才能走后续流程。架构

共享库

6.png


此种方式即将共享的代码做为类库集成到服务中。如此每次共享的库有改动,服务都须要从新打包、测试、重启。但相比起第一种,其有版本标记,可以更好地控制服务的部署和开发,服务开发者能够本身控制什么时候将共享库的改动集成进来。

更进一步的,若是采用此种方案,必定要避免把全部共享的代码都打包进一个jar包中如common.jar。不然会很难肯定什么时候要把库的变更集成到服务中。更好的作法是将共享代码分红几个单独上下文的库,如:security.jar、dateutils.jar、persistence.jar等,如此会比较容易的肯定什么时候去集成共享库的变更。并发

冗余

7.png


此种方案违反DRY原则,在每一服务中都冗余一份共享代码,可以避免依赖共享也可以保持边界上下文。可是一旦共享的代码有变更,那么全部服务都须要改动。所以,此种方案适用于共享模块很是稳定,极小可能变更的状况。app

服务合并

8.png


当多个服务共享的代码变更比较频繁时能够采用此种方案合并成一个服务,如此就避免了多了服务频繁的测试和部署,也避免了依赖共享库。框架

可达性报告反模式(Reach-in Reporting)

微服务中各个服务以及其相应的数据都是包含在一个单独的边界上下文中的,也就是说数据是隔离到多个数据库中的。所以,这也会使得收集微服务的各类数据生成报告变得相对困难。通常来讲有四种方案解决这个问题。其中,前三种都是从各个微服务中拉取数据,是这里所说的反模式,被称做“Reach-in Reporting”。

数据库拉取模式

9.png


报告服务直接从各个服务的数据库中拉取数据从而生成各类报告。此种方式简单迅速,可是会让报告服务和业务服务相互依赖,是一种数据库共享集成风格(经过共享的数据库将多个应用耦合在一块儿)。如此一旦数据库有改动,全部相关服务都要改动,也就打破了微服务中极为重要的边界上下文。

HTTP拉取模式

10.png


与数据库拉取模式相比,此种方式再也不是直接去访问服务的数据库,而是经过HTTP接口去请求服务的数据。此种方式可以保持服务的边界上下文,可是性能比较慢,并且HTTP请求没法很好的承载大数据。

批量拉取模式

11.png


此种方式会有一个单独的报告数据库/数据仓库来存储各个服务的聚合数据。会经过一个批量任务(离线或者基于增量实时)将服务更新的数据导入到报告数据库/数据仓库中。与数据库拉取模式同样,此种方式这也是一种数据库共享集成风格,会打破服务的边界上下文。

异步事件推送模式

12.png


此种方式即解决“Reach-in Reporting”反模式的方案。每一个服务都把本身的发生的事件异步推送到一个数据捕获服务,后续数据捕获服务会将数据解析存储到报告数据库中。此种方式实现起来较复杂,须要在服务和数据捕获服务之间制定一种协议用于异步传输事件数据。但其可以保持服务的边界上下文,同时也能保证数据的时效性。

沙粒陷阱(Grains of Sand)

微服务实现中最有挑战的问题在于如何拆分service,如何控制服务的粒度,而正确的服务粒度则决定了微服务是否可以成功实现。服务粒度也可以影响到性能、健壮性、可靠性、易测试性、部署等。

“沙粒陷阱”即把服务拆分的太细。其中的一个缘由就是不少时候开发者会把一个class与一个服务等同。合理的,应该是一个服务组件(Service component)对应一个服务。一个服务组件具备清晰、简洁的角色、职责,具备一组定义好的操做。其通常经过多个模块(Java Class)实现。若是组件和模块是一对一的关系,那么不只仅会形成服务粒度过细同时也是一种很差的编程实践:服务的实现都是经过一个Class,那么此Class会很是大而且承担太多的责任,不利于测试和维护。

更进一步的,服务的粒度并不该该受其中实现类的数目影响:有些服务可能只须要一个类就能够实现,而有些服务会须要多个类来实现。

为了不“沙粒陷阱”,能够经过如下三种测试来判断服务粒度是否合理:

分析服务范围和功能

要明确服务用来干什么?有哪些操做?通常经过使用文档或者语言来描述服务的范围和功能就可以看出来服务是否作的工做太多。若是在描述中使用了“和”(“and”)或者“此外”(“in addition”)之类的词,颇有可能就是此服务职责太多。

服务的高内聚是一种良好的实践,其明确一个服务提供的操做之间必需要是有关联的。如对于一个顾客服务,有如下操做:

  • 添加顾客
  • 更新顾客信息
  • 获取顾客信息
  • 通知顾客
  • 记录顾客评论
  • 获取顾客评论


其中的前三个操做都是对顾客的CRUD操做,是相关联的。然后三者则无关。为了实现服务的高内聚,合理的应该是把此服务拆分红三个服务:顾客维护、顾客通知、顾客评论。

如此,以粗粒度的服务开始,而后逐渐拆分红细粒度的服务有利于对微服务的拆分。

分析数据库事务

传统的关系型数据库都提供了ACID事务特性用于把多个更新操做打包成一个总体提交,要么都成功,要么都失败。而在微服务中,因为服务都是一个个分离的应用,很难实现ACID,通常实现BASE事务(basic availability、soft state、eventual consistence)便可。可是没法避免的,仍然会有一些场景是须要ACID的。所以,当你不断的须要在BASE和ACID事务作判断和取舍的时候,颇有可能就是服务粒度过细。

若是业务场景没法接受最终一致性,那么最好就是将服务粒度粗化一些,把多个更新操做放到一个服务中。

分析服务编排

这里主要说的是服务之间的互相通讯。因为对服务的调用都是一次远程调用,所以服务编排会很是大的影响微应用整体的性能。此外,它也会影响系统总体的健壮性和可靠性,越多的远程调用,那么越高的概率会有失败或者超时的请求出现。

若是发现完成一次业务逻辑须要调用太多的远程服务,就说明服务的粒度可能太细了。这时候就须要将服务粗化。而合并细粒度服务还可以提升性能,提高整体的健壮性和可靠性。同时也减小了多个服务间的依赖,更利于测试和部署。

此外,使用响应式编程技术异步并行调用远程服务也是一种提高性能和可靠性的方案。

无因的开发者陷阱(Developer Without a Cause)

此陷阱主要讲的是开发者或者架构师在作设计时不少时候是拍脑壳在作,没有任何合理的缘由或者缘由是错误的,也不会作取舍。而想要解决此问题,不只仅是架构师,开发者也须要同时了解技术带来的好处以及缺陷,从中作权衡。

了解业务驱动是避免此陷阱的关键一步。每个开发者和架构师都应该清楚的了解下面这些问题的答案:

  • 为何要使用微服务?
  • 最重要的业务驱动是什么?
  • 架构中的哪一点是最为重要的?


假如易部署性、性能、健壮性、可扩展性是系统最看重的特性,那么对于不一样的业务侧重点,微服务的粒度需求也是不一样的。细粒度的服务可以达到更好的易测试性和易部署性,而粗粒度的服务则有更好的性能、健壮性以及可靠性。
 

追随流行陷阱(Jump on the Bandwagon)

微服务是目前很是流行的架构理念,愈来愈多的公司也都在紧跟这个潮流纷纷转型微服务架构,而无论到底本身是否真的须要。为了不此陷阱,须要首先了解微服务的优势和缺点。

优势:

  • 易部署:容易部署是微服务的一个很大的优势。毕竟相比起一个庞大的单体应用,一个小而且职责单一的微服务的部署很是简单而且带来的风险也会小不少。而持续部署技术则进一步放大了这个优势。
  • 易测试:职责单1、共享依赖少使得测试一个微服务是很容易的。而基于微服务作回归测试与单体大应用相比也是很容易的。 控制变动:每一个服务的范围和边界上下文使得很容易控制服务的功能变更。
  • 模块化:微服务就是一个高度模块化的架构风格。这种风格也是一种敏捷方式的表达,可以很快的响应变化。一个系统模块化程度越高,就越容易测试、部署和发布变动。一个服务粒度划分合理的微服务系统是全部架构中模块化程度最高的架构形式。
  • 可扩展性:因为每个服务都是一个职责单一的细粒度服务,所以此种架构风格是全部架构分隔中可扩展性最高的。其很是容易扩展某一个或者某几个功能从而知足总体系统的需求。而得益于服务的容器化特性以及各类运维监控工具,服务也可以自动化进行启动和关闭。


缺点:

  • 组织变更:微服务须要组织在不少层面进行变更。研发团队须要包含UI、后端开发、规则处理、数据库处理建模等多种职位,从而使得一个小的团队可以具备实现微服务的全部技术栈。同时,传统的单体、分层应用架构的软件发布流程也须要更新为自动化、高效的部署流水线。
  • 性能:因为服务都是隔离的,所以发起对服务的远程调用确定是会影响性能的。服务编排、运行环境都是影响性能的很大因素。了解远程调用的延迟、须要与多少服务通讯都是与性能相关的须要掌握的信息。
  • 可靠性:和性能同样。服务的远程调用越多,那么失败的概率就越高,整体的可靠性就会越低。
  • DevOps:随着微服务架构而来的是成千上百的服务。手动管理这么多的服务是很不现实的。这就对于自动化运维部署、协做提出了很高的挑战。须要依赖很是多的操做工具和实践,是一个很是复杂的工做。目前差很少有12种类型的操做工具(监控工具、服务注册、发现工具、部署工具等)和框架在微服务架构中被使用,其中每一种又包含了不少具体的工具和产品供选择。对于这些工具和框架的选择通常都会须要将近数月的研究、测试、权衡分析才能作出最适合的技术选型。


了解了微服务的优缺点后,下一步则须要根据实际的业务来分析微服务是否是解决这些问题的最佳方案。能够采起如下问题:

  • 业务和技术的目标是什么?
  • 使用微服务是为了完成什么?
  • 目前和可预知的痛点是什么?
  • 应用的最关键的技术特性是什么?(性能、易部署性、易测试性、可扩展性)


回答这些问题再结合微服务的优缺点可以让你明确如今是不是使用微服务的适当时机。

除了微服务之外,还有其余7种比较广泛使用的架构供选择:

  • 基于服务的架构(Service-Based)
  • 面向服务的架构(Service-Oriented)
  • 分层架构(Layered)
  • 微内核架构(Microkernel)
  • 基于空间的架构(Space-Based)
  • 事件驱动架构(Event-Driven)
  • 流水线架构(Pipeline)

 

静态合约陷阱(The Static Contract)

微服务的消费方和服务提供方之间会有一个合约/协议用来规定输入输出数据的格式、操做名称等等。通常状况下这个合约是不变的。可是若是没有使用版本号来管理服务接口,那么就会进入“静态合约”陷阱。

给合约打上版本标记不只仅可以避免巨大的变更(服务提供方修改合约使得全部消费方也都得修改),还可以提供向后兼容性。这里有两种技术能够实现合约的版本号:

在头部信息附加版本号

13.png


如图,此种方式即在远程访问协议的头部添加版本信息。而若是远程协议使用的是REST,那么还可使用vendor mime type(vnd)来指定合约的版本号。以下:

POST /trade/buy
Accept: application/vnd.svc.trade.v2+json


服务接受到请求,可以经过正则等手段简单解析出其中的合约版本号再根据版本号作相应的处理。

若是使用消息队列,那么能够将版本号放置在属性部分(Property section)。JMS的一个例子以下:

String msg = createJSON("acct","12345","sedol","2046251","shares","1000");
jsmContext.createProducer()
  .setProperty("version",2)
  .send(queue,msg);

 

在合约自己中附加版本号

14.png


此种方式版本号独立于远程访问协议,与头部信息版本号相比,这也是其最大的优势。但与此同时,其缺点比较多。首先要从请求信息主体中解析版本号,会出现不少解析的问题。其次,合约的模式可能会很是复杂,使得很难作数据转换。最后,服务还要引入对模式的验证逻辑。

咱们到了吗陷阱(Are We There Yet)

微服务架构中,各个服务都是独立的个体,也就意味着全部客户端或者API层和服务之间的通讯都是一次远程调用。若是对这些远程调用的耗时没有什么概念,那么就陷入了“Are We There Yet”陷阱。合理的作法须要去测试远程访问的平均延迟、长尾延迟(95%、99%、99.%以外的请求延迟)等指标。而不少时候即便有很好的平均延迟,可是较差的长尾延迟会形成很是大的破坏。

在生产环境或者准生产环境测试有助于去了解应用的真实性能。例如,一个业务请求须要调用四个服务,假设一个服务调用的延迟是100毫秒,那么加上业务请求自己的延迟,完成这次业务请求共须要500毫秒的延迟。这和单单从代码上去看得出的结论是不同的。

了解目前所用协议的平均延迟是一方面,另外一方面则须要对比其余远程协议的延迟,从而在合适的地方使用合适的协议。如:JMS、AMQP、MSMQ。

15.png


如图,AMQP协议的性能是最好的。那么结合业务场景,就能够选择REST做为客户端与服务间的通讯协议,AMQP作为服务之间的通讯协议以提升应用的性能。

固然,性能并不是在选择远程协议时惟一考虑的因素。下一节中就会考虑利用消息队列的一些额外功能。

REST使用陷阱(Give It a Rest)

REST如今是微服务中用的最多的通讯协议。流行的开发框架如DropWizard、Spring Boot都提供了REST支持。可是若是只选择REST这一种协议,不去考虑其余诸如消息队列的优点,那么就陷入了“REST使用”陷阱。毕竟异步通讯、广播、合并请求事务这些需求,REST是很难实现的。

消息队列标准目前包括平台特定和平台无关两种。前者包括Java平台中的JMS和C#平台的MSMQ,后者则是AMQP。对于平台特定的消息标准JMS,其规范了API,所以切换broker实现(ActiveMQ、HornetQ)时无需修改API,但因为底层通讯协议是不一样的,集成的客户端或者服务端jar包须要随着修改。对于平台无关的消息标准,其规范了协议实现标准,并无规范API。使得不一样平台之间均可以互相通讯,而无论实际产品是什么。如一个使用了RabbitMQ的客户端能够很容易地与一个StormMQ通讯(假设使用的协议相同)。也就是其独立于平台的特性使得RabbitMQ成为微服务架构中最流行的消息队列。

异步请求

异步通讯是消息队列适用的场景之一。服务消费者发起请求后无需等待服务方响应可以提升整体的性能,同时调用方无需担忧调用超时,也就无需使用断路器,从而提升了系统的可靠性。

广播

将消息广播给多个service是消息队列的又一个适用场景。一个消息生产者向多个消息接受者发送消息,无需知道谁在接受消息以及如何处理它。

事务请求

消息系统提供了对事务消息的支持:若是多个消息被发送到了在一个交易上下文的多个队列或者主题中时,那么直到消息发送者commit,服务才会真正的接受到相应的全部消息(在commit以前会一直保存在队列中)。

所以对于服务消费者须要合并多个远程请求到一个事务中的场景能够选择事务消息。

推荐一个交流学习群:705127209 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多:

相关文章
相关标签/搜索