
O’Reilly的电子书《Microservices AntiPatterns and Pitfalls》讲述了在微服务设计实现时十种最常见的反模式和陷阱。本文基于此书,将这十个点列出。书籍地址:https://www.oreilly.com/programming/free/microservices-antipatterns-and-pitfalls.csp,更全的反模式和陷阱可见作者的视频:http://oreil.ly/29GVuDG。 数据驱动迁移反模式(Data-Driven Migration)



超时反模式(The Timeout)

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

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

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

微服务被普遍认为是一种不共享任何东西的架构。但实际上只能是尽可能地少共享,毕竟在某些层面代码被多个服务共享也能带来一定好处。例如,与单独部署一套安全服务(认证和授权)其他所有服务都通过远程访问此服务相比,把安全相关的功能封装成jar包(security.jar),然后其他服务都集成此jar包,就能够避免每次都要发起对安全服务的访问,从而提高性能和可靠性。但后面的方案带来的问题就是依赖噩梦:每一个服务都依赖多个自定义的jar包。如此不仅打破了服务之间的边界上下文,同时也引入了诸如总体可靠性、变更控制、易测试性、部署等问题。 在一个使用面向对象编程语言的单体应用中,使用abstract类和接口实现代码复用和共享是一个良好的实践。但当从单体切换到微服务架构时,对于很多自定义的共享类和工具类(日期、字符串、计算)的处理要考虑到微服务间共享的东西越少越有利于保持服务间的边界上下文,从而更利于快速测试和部署。以下是几种推荐的方式,也是解决“共享反模式”的方案: 共享项目




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

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




沙粒陷阱(Grains of Sand)

微服务实现中最有挑战的问题在于如何拆分service,如何控制服务的粒度,而正确的服务粒度则决定了微服务是否能够成功实现。服务粒度也能够影响到性能、健壮性、可靠性、易测试性、部署等。 “沙粒陷阱”即把服务拆分的太细。其中的一个原因就是很多时候开发者会把一个class与一个服务等同。合理的,应该是一个服务组件(Service component)对应一个服务。一个服务组件具有清晰、简洁的角色、职责,具有一组定义好的操作。其一般通过多个模块(Java Class)实现。如果组件和模块是一对一的关系,那么不仅仅会造成服务粒度过细同时也是一种不好的编程实践:服务的实现都是通过一个Class,那么此Class会非常大并且承担太多的责任,不利于测试和维护。 更进一步的,服务的粒度并不应该受其中实现类的数目影响:有些服务可能只需要一个类就可以实现,而有些服务会需要多个类来实现。 为了避免“沙粒陷阱”,可以通过以下三种测试来判断服务粒度是否合理: 分析服务范围和功能 要明确服务用来干什么?有哪些操作?一般通过使用文档或者语言来描述服务的范围和功能就能够看出来服务是否做的工作太多。如果在描述中使用了“和”(“and”)或者“此外”(“in addition”)之类的词,很有可能就是此服务职责太多。 服务的高内聚是一种良好的实践,其明确一个服务提供的操作之间必须要是有关联的。如对于一个顾客服务,有以下操作:
-
添加顾客
-
更新顾客信息
-
获取顾客信息
-
通知顾客
-
记录顾客评论
-
获取顾客评论
无因的开发者陷阱(Developer Without a Cause)

此陷阱主要讲的是开发者或者架构师在做设计时很多时候是拍脑袋在做,没有任何合理的原因或者原因是错误的,也不会做取舍。而想要解决此问题,不仅仅是架构师,开发者也需要同时了解技术带来的好处以及缺陷,从中做权衡。 了解业务驱动是避免此陷阱的关键一步。每一个开发者和架构师都应该清楚的了解下面这些问题的答案:
-
为什么要使用微服务?
-
最重要的业务驱动是什么?
-
架构中的哪一点是最为重要的?

微服务是目前非常流行的架构理念,越来越多的公司也都在紧跟这个潮流纷纷转型微服务架构,而不管到底自己是否真的需要。为了避免此陷阱,需要首先了解微服务的优点和缺点。 优点:
-
易部署:容易部署是微服务的一个很大的优点。毕竟相比起一个庞大的单体应用,一个小并且职责单一的微服务的部署非常简单并且带来的风险也会小很多。而持续部署技术则进一步放大了这个优点。
-
易测试:职责单一、共享依赖少使得测试一个微服务是很容易的。而基于微服务做回归测试与单体大应用相比也是很容易的。 控制变更:每个服务的范围和边界上下文使得很容易控制服务的功能变动。
-
模块化:微服务就是一个高度模块化的架构风格。这种风格也是一种敏捷方式的表达,能够很快的响应变化。一个系统模块化程度越高,就越容易测试、部署和发布变更。一个服务粒度划分合理的微服务系统是所有架构中模块化程度最高的架构形式。
-
可扩展性:由于每一个服务都是一个职责单一的细粒度服务,因此此种架构风格是所有架构分隔中可扩展性最高的。其非常容易扩展某一个或者某几个功能从而满足整体系统的需求。而得益于服务的容器化特性以及各种运维监控工具,服务也能够自动化进行启动和关闭。
-
组织变动:微服务需要组织在很多层面进行变动。研发团队需要包含UI、后端开发、规则处理、数据库处理建模等多种职位,从而使得一个小的团队能够具有实现微服务的所有技术栈。同时,传统的单体、分层应用架构的软件发布流程也需要更新为自动化、高效的部署流水线。
-
性能:由于服务都是隔离的,因此发起对服务的远程调用肯定是会影响性能的。服务编排、运行环境都是影响性能的很大因素。了解远程调用的延迟、需要与多少服务通信都是与性能相关的需要掌握的信息。
-
可靠性:和性能一样。服务的远程调用越多,那么失败的几率就越高,总体的可靠性就会越低。
-
DevOps:随着微服务架构而来的是成千上百的服务。手动管理这么多的服务是很不现实的。这就对于自动化运维部署、协作提出了很高的挑战。需要依赖非常多的操作工具和实践,是一个非常复杂的工作。目前差不多有12种类型的操作工具(监控工具、服务注册、发现工具、部署工具等)和框架在微服务架构中被使用,其中每一种又包含了很多具体的工具和产品供选择。对于这些工具和框架的选择一般都会需要将近数月的研究、测试、权衡分析才能做出最适合的技术选型。
-
业务和技术的目标是什么?
-
使用微服务是为了完成什么?
-
目前和可预知的痛点是什么?
-
应用的最关键的技术特性是什么?(性能、易部署性、易测试性、可扩展性)
-
基于服务的架构(Service-Based)
-
面向服务的架构(Service-Oriented)
-
分层架构(Layered)
-
微内核架构(Microkernel)
-
基于空间的架构(Space-Based)
-
事件驱动架构(Event-Driven)
-
流水线架构(Pipeline)

微服务的消费方和服务提供方之间会有一个合约/协议用来规定输入输出数据的格式、操作名称等等。一般情况下这个合约是不变的。但是如果没有使用版本号来管理服务接口,那么就会进入“静态合约”陷阱。 给合约打上版本标记不仅仅能够避免巨大的变动(服务提供方修改合约使得所有消费方也都得修改),还能够提供向后兼容性。这里有两种技术可以实现合约的版本号: 在头部信息附加版本号

POST /trade/buyAccept: 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);
在合约本身中附加版本号


微服务架构中,各个服务都是独立的个体,也就意味着所有客户端或者API层和服务之间的通信都是一次远程调用。如果对这些远程调用的耗时没有什么概念,那么就陷入了“Are We There Yet”陷阱。合理的做法需要去测试远程访问的平均延迟、长尾延迟(95%、99%、99.%之外的请求延迟)等指标。而很多时候即使有很好的平均延迟,但是较差的长尾延迟会造成非常大的破坏。 在生产环境或者准生产环境测试有助于去了解应用的真实性能。例如,一个业务请求需要调用四个服务,假设一个服务调用的延迟是100毫秒,那么加上业务请求本身的延迟,完成此次业务请求共需要500毫秒的延迟。这和单单从代码上去看得出的结论是不一样的。 了解目前所用协议的平均延迟是一方面,另一方面则需要对比其他远程协议的延迟,从而在合适的地方使用合适的协议。如:JMS、AMQP、MSMQ。

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之前会一直保存在队列中)。 因此对于服务消费者需要合并多个远程请求到一个事务中的场景可以选择事务消息。 原文链接:http://www.rowkey.me/blog/2018/06/02/microservice-pitfall/