都在说微服务,那么微服务的反模式和陷阱是什么

前言

网上看到一本关于微服务反模式的电子书,看后感觉内容非常棒,于是我决定分阶段翻译成中文书,翻译的目的也是想帮助想深入了解微服务的朋友,由于英文水平有限,如有翻译不对之处希望多留言指正。

书籍英文目录如下

书籍中文目录如下: 1、数据驱动的迁移反模式1.1、太多的数据迁移1.2、功能分割优先,数据迁移最后2、超时反模式2.1、使用超时2.2、使用断路器模式3、共享反模式3.1、过多依赖3.2、共享代码的技术4.到达报告反模式4.1、微服务报告的问题4.2、Asynchronous Event Pushing5、沙粒陷阱5.1、分析服务的范围和功能5.2、分析数据库事务 5.3、分析服务编排6、无因的开发者陷阱7、随大流陷阱8、其它架构模式9、静态契约陷阱10、通信协议使用的陷阱11、REST陷阱

#### 一、数据驱动的迁移反模式

微服务会创建大量小的、分布式的、单一用途的服务,每个服务拥有自己的数据。这种服务和数据耦合支持一个有界的上下文和一个无共享数据的架构,其中,每个服务及其对应的数据是独立一块,完全独立于所有其他服务。服务只暴露了一个明确的接口(服务契约)。有界的上下文可以允许开发者以最小的依赖快速轻松地开发,测试和部署。

采用数据驱动迁移反模式主要发生在当你从一个单体应用向微服务架构做迁移的时候。我们之所以称之为反模式主要原因是,刚开始我们觉得创建微服务是一个不错的主意,服务和相应的数据都独立成微服务,但这可能会将你带向一个错误的道路上,导致高风险、过剩成本和额外的迁移工作。

单体应用迁移到微服务架构有两个主要目标:

第一个目标是单体应用程序的功能分割成小的,单一用途的服务。

第二个目标是单体应用的数据迁移到每个服务自己独占的小数据库(或独立的服务)。

下图展示了一个典型的迁移,看起来像服务代码和相应的数据同时进行迁移。

上图中有三个服务是从单体应用中划分而来,并且还划分独立的三个数据库,这是一个自然演变的过程,因为在每个服务和数据库之间都使用了最为关键的限界上下文,然而我们遇到的问题也正是基于这一过程将带领我们进入数据迁移的反模式。

1.1 太多的数据迁移

这种迁移路径的主要问题是,我们很难在一次就能够划分清楚每个服务的粒度,从一个更粗粒度的服务开始着手,一步步的进行细化工作,并且要多了解相关业务知识,不断的对服务的粒度进行调整,我们来看图1-1发现最左边的服务粒度太粗了,需要再拆分成二个小的服务,或者你发现左边的二个服务粒度划分的又太细了,需要进行合并。而数据迁移要比源代码迁移更复杂,更容易出错,我们最好只为数据进行一次迁移工作,因为数据迁移是一个高风险的工作。

我们的微服务划分也就是应用代码的迁移和数据的迁移。如图1-2所示。

1.2 功能分割优先,数据迁移最后

此模式主要采用的是一种避免的手段,以迁移服务的功能为第一,同时也需要注意服务和数据之间的限界上下文。我们可以通过合并与拆分的手段对服务进行调整直到满意为止,这时候就可以迁移数据了。

如图1-3所示,左边所有三个服务都已经进行了迁移和拆分,但是所有服务仍然使用的是同一个数据库,如果这是一个临时中间方案还可以作为一个选择,这时候我们就需要更多的了解服务如何使用,以及接受什么类型的请求数据等。

在图1-3中,我们要注意最左边的服务是如何发现粒度太粗而拆分成二个服务的。服务粒度最终确定完成之后,下一步就开始迁移数据了,采用这种方式可以避免重复的数据迁移。

二、超时反模式

微服务是一种分布式的架构,它所有的组件(也就是服务)会被部署为单独的应用程序,并通过某种远程访问协议进行通讯。分布式应用的挑战之一就是如何管理远程服务的可用性和它们的响应。虽然服务可用性和服务响应都涉及到服务的通信,但它们是两个完全不同的东西。服务可用性是服务消费者连接服务并能够发送请求的能力,服务响应则关注服务的响应时间。

如图2-1的所示,如果此时服务消费者无法连接到服务提供者的时候,通过会在毫秒级的时间里得到通知和反馈,这时候服务消费者可以选择是直接返回错误信息还是进行重试,但是如果服务提供者接收了请求却不进行响应该怎么办,在这种情况下服务消费者可以选择无限期等待或者设置超时时间,使用超时时间看起来是个好办法,但是它会导致超时反模式。

2.1 使用超时

你可能感觉非常困惑,难道设置一个超时时间不是一件好事吗?在大部分的情况下超时时间的错误设置都会带来问题。比如当你上网购物的时候,你提交了订单,服务一直在处理没有返回,你在超时的时候再提交订单,显然服务器需要更复杂的逻辑来处理重复提交订单的问题。

那么超时时间设置多少合适呢?

第一种是基于数据库的超时来计算服务的超时时间。

第二种是计算负载下最长的处理时间,把它乘以2作为超时时间。

在图2-2中,通常的情况下平均响应时间是2秒,在高并发的情况下最长时间是5秒,因为可以使用加倍技术服务的超时时间设置为10秒。

图2-2的解决方案似乎看起来很完美,它使每一个服务消费者必须等待10秒,其实只是为了判断服务没有响应。在大多数情况下,用户在等待提交按钮或放弃和关闭屏幕之前不会等待超过2到3秒。那就必须要有更好的办法来解决。

2.2 使用断路器模式

与上面超时的方法相比,使用断路器的方式更为稳妥,这种设计模式就像家里的电器的保险丝一样,当负载过大,或者电路发生故障或异常时,电流会不断升高,为防止升高的电流有可能损坏电路中的某些重要器件或贵重器件,烧毁电路甚至造成火灾。保险丝会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,从而起到保护电路安全运行的作用。

图2-3说明了断路器模式是如何工作的,当服务保持响应时,断路器将关闭,允许通过请求。如果远程服务突然变得不能响应,断路器就会打开,从而阻止请求通过,直到服务再次响应。当然这并不像你家中的保险丝,断路器本身可以持续监测服务。

断路器模式相比设置超时的优点是,使用者可以立即知道服务已变得不响应,而不必等待超时,使用者将在毫秒内服务不响应,而不是等待10秒获得相同的信息。

另外断路器可以通过几种方式进行监控,最简单的方法是对远程服务进行简单的心跳检查,这种方式只是告诉断路器服务是活的,但是要想获取服务存活的详细信息,就需要定期(比如10秒)获取一次服务的详细信息,还有一种方式是实时用户监控,这种方式可以动态调整,一旦达到阈值,断路器可以进入半开放状态,可以设置一定数量的请求是通过(说1的10)。

三、共享反模式

微服务是一种无共享的架构,我更倾向于叫它为“尽量不共享”模式(share-as-little-as-possible), 因为总有一些代码会在微服务之间共享。比如不提供一个身份验证的微服务,而是将身份验证的代码打包成一个jar文件:security.jar,其它服务都能使用。如果安全检查是服务级别的功能,每个服务接收到请求都会检查安全性,这种方式可以很好的提高性能。

然后如果太过频繁的使用最终会出现依赖噩梦,如图3-1所示,其中每个服务都依赖于多个自定义共享库。

这种共享级别不仅破坏了每个服务的限界上下文,而且还引入了几个问题,包括整体可靠性、更改控制、可测试性和部署能力。

3.1 过多依赖

在面向对象的软件开发过程中,经常会遇到共享的问题,特别是从单一分层结构迁移到微服务结构时,图3-2展示抽象类和共享,它们最终在多数单块分层体系结构中共享。

创建抽象类和接口是面向对象编程的最重要做法,那我们如何来处理数百个服务共享的代码?

微服务架构的主要目标就是共享要尽可能的少,这有助于维护服务的限界上下文,使我们能够快速的测试和布署。服务之间依赖越强,服务隔离也就越困难,因此也就越难单独进行测试和布署。

3.2 共享代码的技术

要避免这个反模式的最好办法就是代码不共享,但是实际工作中总会有一些代码需要进行共享,那这些共享代码应该放到哪里呢?

图3-3给了四个最基本的技术:

共享项目

共享库

复制

服务合并

四、到达报告反模式

有四种方式可以处理微服务架构中的报告。

database pull model

HTTP pull model

batch pull model

event-based push model

前三种模式是从服务的数据库中拉取数据,所以这个反模式就叫"rearch-in reporting"。既然前三种会出现这中反模式,我们就先看看为什么它们会带来麻烦。

4.1 微服务报告的问题

主要是二个方面的问题:

如何及时获取最新数据

保持服务与数据之间的限界上下文

在微服务架构体系中第一种是使用数据库拉取模型,使用者直接从服务的数据库拉取数据,如图4-1所示:

其实获取数据最快、最容易的方法是直接访问数据。虽然这在以前看似乎是个好主意,但它导致了服务之间的明显依赖关系。而上图会带来数据库的非独立性。

避免数据的耦合的另一种技术称为HTTP拉取模型。使用此模型不需要直接访问每个服务的数据库,使用者只需要对每个服务发出一个REST HTTP调用就可以访问其数据。如图4-2所示。

这种方式的优点是根据限界上下文划分出了不同服务,但是这种方式又太慢,无法满足复杂的以及数据量较大数据获取需求。

第三种是批量拉取模式,这种方式是独立出一个报表数据库或者数据仓库,通过批处理作业将不同服务数据库的数据拉取这个新独立的数据库中,如图4-3所示。

这种模型的问题在于依然是强依赖数据库,如果拉取服务的数据库进行了更新,那么这个批量数据拉取过程也必将修改。

最后一种是异步事件模型,也是推荐使用的模型,如图4-4所示

五、沙粒陷阱

架构师和开发人员在采用微服务架构的时候最大的挑战之一就是服务粒度的问题。微服务的服务粒度多大合适?服务粒度至关重要,它会影响应用的性能、健壮性、可靠性、可测性、设置发布模型。

当服务的粒度太小的时候就会遇到沙粒陷阱。微服务的微并不意味着服务越小越好,但是多小是小?

这种陷阱的主要原因之一是开发人员常常将服务与类混淆,往往一个类就是一个服务,在这种情况下会很容易遇到沙粒陷阱。

服务应该被看成是一个服务组件,服务组件应该有一个清晰简明的角色和责任定义,并有一组明确的操作。由开发人员决定服务组件应该如何实现以及服务需要多少个实现类。

如图5-1所示,服务组件是通过一个或多个模块的实现(比如,java类)。模型和服务组件如果是一对一的关系会使服务的粒度过细而后期难以维护,而通过一个类实现的服务往往类太大,承担太多的责任,也使它们难以维护和测试。

当然微服务的粒度并不是靠服务实现的类的数量所决定的,有些服务很简单,只需一个简单的类就可以实现,而有些确需要更多的类。既然类的数量不能用来决定微服务的粒度,那么用什么标准来衡量微服务的粒度是合适的呢?

主要有三种方式:

服务的范围(scope)和功能(functionality)

数据库事务的需求

服务编排的级别。

5.1 分析服务的范围和功能

确定服务粒度级别是否正确的第一种方法是分析服务的范围和功能。服务是做什么的?它的操作是什么?

比如一个顾客服务(customer service)有下面的操作:

add_customer

update_customer

get_customer

notify_customer

recordcustomercomments

getcustomercomments

在这个例子中前三个操作是相关的,它们都是用来管理和维护顾客信息的,但是后面三个并不是和CRUD操作相关的。在分析这个服务的完整性的时候,我们就比较清晰了,这个服务可以被分成三个服务:顾客信息服务、顾客通知服务和顾客评论服务。

图5-1 正是一种由粗粒度服务向细粒度服务逐步演进的过程。

Sam Newman提供了一个很好的可操作的方法,开始不妨将服务划分成粗粒度的服务,随着对服务了解更多,再进一步划分成更小粒度的服务。

5.2 分析数据库事务

数据库事务更正式的叫做 ACID 事务 (atomicity, consistency, isolation, and durability)。ACID事务封装多个数据库更新为一个工作单元,工作单元要不整体完成,要不就出现错误而回滚。

因为微服务架构中服务是分布式的独立的应用,再两个或者多个服务之间维护 ACID 事务就极度困难,所以微服务架构中经常会依赖 BASE (basic availability, soft state, and eventual consistency)。尽管如此,你还是再特定的服务中要使用 ACID 事务。当你需要在 ACID vs. BASE 事务中做艰难的决定的时候,可能你的服务划分的就太细了。

当发现不能使用最终一致性时,你通常就会把服务从细粒度调整为粗粒度的服务,如图5-2所示。

5.3 分析服务编排

第三个衡量方式是分析服务编排。服务编排是指服务之间的通讯,通常也指内部服务通讯。

远程调用服务是需要花时间的,它会降低应用整体的性能。再者,它也会影响服务的健壮性和可靠性。

如果你发现完成一个逻辑请求需要调用太多的服务时,服务的划分可能粒度就太小了,对于单个业务请求,你调用的远程调用越多,其中一个远程调用失败或超时的可能性就越大。

如果你发现需要与太多的服务进行通信以完成单个业务请求,那么你的的服务可能粒度过细了。在分析服务编排水平,你通常会从细粒度的服务迁移到更粗,如图5-4所示。

通过整合服务、合并到更粗粒度可以提升应用的整体性能,提高应用的健壮性和可靠性。你还可以移除服务之间的依赖,可以更好的控制、测试和发布。

当然你可能会说调用多个服务可以并行的执行,提高整体应用的的响应时间,比如 reactive 架构的异步编程方式, 其实关键还是要权衡利弊, 确保对用户的及时响应以及系统整体的可靠性。

都在说微服务,那么微服务的反模式和陷井是什么(一)http://www.jianshu.com/p/3986239138fe

六、无因的开发者陷阱

名字来自詹姆斯·迪恩演的电影《无因的反叛》(Rebel Without a Cause),一个问题青年因为错误的原因做了错误的决定。

很多架构师和开发者在微服务的开发中权衡利弊, 比如服务粒度和运维工具,但是基于错误的原因,做了错误的决定。

6.1 做出错误的决定

图6-1说明了一种情况是通过测试发现服务划分的太细了,因此非常影响性能,主要是由于服务划分的太细导致增加了通信工作量也在一定程度上对稳定性造成一定影响。在这种情况下,开发人员或架构师决定将这些服务整合到一个更粗粒度的服务中,以解决性能和可靠性问题。

这个方案看起来似乎合情合理,但是之后的布署、更改控制和测试都会受到影响。

再看图6-2,这种场景是左边的服务太粗了,影响了服务的测试和布署,于是进行了拆分,减少了每个服务的范围。

通过以上二个场景我们可以看出,如果服务太细我们就需要考虑将服务合并,如果服务太粗,我们又会考虑将服务进行拆分。太细的话会增加通信成本和容易造成可靠性不稳定,太粗的话又容易导致不容易测试和上线布署,所以这就要看我们如何来权衡利弊。

6.2 了解业务驱动

了解业务驱动对于合理设计微服务至关重要,每一个架构师或者开发者都应该先回答以下三个问题:

我们为什么要设计微服务架构?

主要的业务驱动是什么?

最重要的架构特点是什么?

使用可布署、性能、健壮性和可扩展作为主要的架构特性,我们其实最先需要考虑的是如何利用业务来进行服务的整合和拆分。

场景一:迁移到微服务主要是想达到快速上线和布署

在这种场景下服务的可布署能力相对要大于性能、稳定性因素,所以要拆分服务的时候可以考虑稍微细粒度一些。

场景二:迁移到微服务主要是想提高系统的性能和健壮性

这种场景是从单一应用程序迁移到微服务架构的一个常见因素,很多公司都是业务驱动,那么就需要考虑服务的可靠性和健壮性,因此倾向于更粗粒度的服务,而不是细粒度的服务。

我经常使用的一种方式借助白板和团队成员一起讨论,如图6-3所示,每当有服务粒度的划分问题的时候,我们经常在白板上做草稿讨论清楚。

七、赶潮流陷阱

你必须拥抱微服务,因为这是目前的趋势,而且其他人和公司目前都已经这么做了。

我们盲目的使用微服务,却还没有仔细分析业务需求、组织架构和技术环境,这就是随大流陷阱。

避免这个陷阱的方式充分理解微服务的好处和短处,俗话说,知己知彼,百战不殆。

7.1 微服务的优点和缺点

优点:

发布:易于发布

测试:易于测试

改变控制:更容易的改变一个服务的功能

模块

规模可扩展

缺点:

Team组织改变

性能

可靠性降低

运维难度加大

7.2 其他架构模型

微服务的架构很好,但是不是唯一的架构模式,比如下面还有一些其它的架构模式:

Service-Based Architecture

Service-Oriented Architecture

Layered Architecture

Microkernel Architecture

Space-Based Architecture

Event-Driven Architecture

Pipeline Architecture

当然你并不一定只使用唯一的一种架构模式,你可能在系统中混用这些架构模式。

下面有一些架构的参考资料:Software Architecture Fundamentals: Understanding the BasicsSoftware Architecture Fundamentals: Beyond the BasicsSoftware Architecture Fundamentals: Service-Based ArchitectureSoftware Architecture PatternsMicroservices vs. Service-Oriented Architecture

八、静态契约陷阱

在微服务的消费者和提供者之间提供了一种契约,契约主要包括输入和输出数据、以及操作的名称,契约通常是以XML、JSON或者JAVA对象等来表示。但是这些契约永远不会改变吗?

这里举个例子来说明因为服务契约版本控制而发生的问题:假如你有一个服务是由三个不同的客户端访问(client1、client2和client3),这时client1想更改服务契约,你要检查client2和client3能否适应这个改变,并且client2和client3告诉我适应不了这个改变,需要数周时间才能调整完成。这时候我需要告诉client1,client2和client3需要数周时间才能适应调整完成,但是client1却不能等待这么长时间。

可以在你的服务契约中提供版本控制,实现向后兼容。现在我们可以根据client1的请求做一些灵活的控制,我们可以创建一个新版本的契约,比如v1.1,client2和client3依然使用v1版本,这样client1就可以立刻作为契约更改,而不必纠结于client2和client3需要适应调整的时间。

有两种实现方式:在header中加入版本号,或者在契约自身scheme中加入版本号。

8.1 header版本控制

契约版本的第一种办法是把版本号放在远程访问协议头,如图8-1所示,远程访问协议包括REST, SOAP, AMQP, JMS, MSMQ等等。

例如在使用REST的时候,可以使用MIME类型来指定协议版本,如下代码:

通过在header的MIME类型中指定契约版本号,服务端就可以通过header的MINE类型解析得到相应的版本号。

8.2 Schema版本控制

第二种版本控制方式是在契约自身中进行,不需要在header中指定版本号,如图8-2所示。

请先看如下json格式数据:

该模式直接将版本号定义在契约的输入数据中,这种模式最大的优点是与协议无关,只与数据格式有关。

这种方式也有一些不足就是每一次都需要对消息数据进行解析,提取出版本号进行校验,而且数据格式也有可能会改变也不太容易做到自动映射到JAVA对象中。

最好的赞赏

就是你的关注

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180202G03VQS00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券