大家好,我是架构君,一个会写代码吟诗的架构师。今天说一说微服务架构设计模式,希望能够帮助大家进步!!!
代码语言:txt复制目录
- 什么是微服务模式
- 单体结构的历程
- 单体地狱的银弹-微服务架构
- 扩展立方体和服务
- 微服务架构的好处和弊端
- 优点
- 大型的复杂应用程序可以持续交付和持续部署
- 每个服务都相对较小并容易维护
- 更好的容错性
- 更容易实验和采纳新的技术
- 弊端
- 服务的拆分和定义是一项挑战
- 分布式系统带来的各种复杂性
- 开发者需要思考到底应该在应用的什么阶段使用微服务架构
- 优点
- 服务的拆分策略
- 识别系统操作
- 创建抽象领域模型
- 定义系统操作
- 根据业务能力进行服务拆分
- 从业务能力到服务
- 根据子域进行服务拆分
- 上帝类的处理
- 识别系统操作
什么是微服务模式
随着网络基础设施的高速发展,以及越来越多的个体接入互联网,在考虑构建支持海量请求以及多变业务的软件平台时,微服务架构成为多数人的首选。微服务架构的出现时服务事物发展规律的:当问题足够大,有足够多的的不确定因素时,人们习惯于把大的问题拆分成小的问题。通过分割,抽象和重用小而可靠的功能模块来构建整体方案。但是当这些小的,可重用的部分多来越多的时候,又会出现新的问题。再相似的阶段,人们遇到的问题也是相似的,这个时候人们需要一些共识,需要用一些通用的词汇来描述问题以及解决方案,这也是人们知识的总结,微服务模式就是这样的总结和概括,是一种可以通用的共识,用于描述微服务领域的中的问题及解决方案。
单体结构的历程
在企业发展的初期,应用程序相对较小,所有的代码运行在一个应用程序中有以下好处
- 应用的开发很简单:IDE 和其他开发工具只需要构建这一个单独的应用程序。
- 易于对应用程序进行大规模的更改:可以更改代码和数据库模式,然后构建和部署。测试相对简单直观:开发者只需要写几个端到端的测试,启动应用程序,调用 REST API,然后使用 Selenium 这样的工具测试用户界面。
- 部署简单明了:开发者唯一需要做的,就是把 WAR 文件复制到安装了 Tomcat 的服务器上。
- 横向扩展不费吹灰之力:FTGO 可以运行多个实例,由一个负载均衡器进行调度。
不幸的是,开发人员已经意识到的,单体架构存在着巨大的局限性。每一次开发冲刺(Sprint), 开发团队就会实现更多的功能,显然这会导致代码库膨胀。而且,随着公司的成功,研发团队的规模不断壮大。代码库规模变大的同时,团队的管理成本也不断提高。
- 过度的复杂性吓退开发者 系统本身过于庞大和复杂,以至于任何一个开发者都很难理解它的全部。因此,修复软件中的问题和正确地实现新功能就变得困难且耗时。各种交付截止时间都可能被错过。更糟糕的是,这种极度的复杂性正在形成一个恶性循环:由于代码库太难于理解,因此 开发人员在更改时更容易出错,每一次更改都会让代码库变得更复杂、更难懂
- 开发速度缓慢 巨大的项目把开发人员的 IDE 工具搞得很慢,构建一次应用需要很长时间,更要命的是,因为应用太大,每启动一次都需要很长的时间。因此,从编辑到构建、运行再到测试这个周期花费的时间越来越长,这严重地影响了团队的工作效率。
- 从代码提交到实际部署的周期很长,而且容易出问题 从代码完成到运行在生产环境是一个漫长且费力的过程。一个问题是,众多开发人员都向同一个代码库提交代码更改,这常常使得这个代码库的构建结果处于无法交付的状态。当我们尝试采用功能分支来解决这个问题时,带来的是漫长且痛苦的合并过程。紧接着,一旦团队完成一个冲刺任务,随后迎接他们的将是一个漫长的测试和代码稳定周期。
- 把更改推向生产环境的另一个挑战是运行测试需要很长时间。因为代码库如此复杂,以至于一个更改可能引起的影响是未知的,为了避免牵一发而动全身的后果,即使是一个微小的更改,开发人员也必须在持续集成服务器上运行所有的测试套件。系统的某些部分甚至还需要手工测试。如果测试失败,诊断和修复也需要更多的时间。因此,完成这样的测试往往需要数天甚至更长时间。
- 需要长期依赖某个可能已经过时的技术栈 单体地狱的最终表现,也体现在团队必须长期使用一套相同的技术栈方面。单体架构使得采用新的框架和编程语言变得极其困难。在单体应用上采用新技术或者尝试新技术都是极其昂贵和高风险的,因为这个应用必须被彻底重写。结果就是,开发者被困在了他们一开始选择的这个技术之内。有时候这也就意味着团队必须维护一个正在被废弃或过时的技术所开发的应用程序。
单体地狱的银弹-微服务架构
软件架构其实对功能性需求影响并不大。事实上,在任何架构甚至是一团糟的架构之上,你都可以实现一组用例(应用的功能性需求)
架构的重要性在于它影响了应用的非功能性需求,也称为质量属性或者其他的能力 。随着 FTGO 应用的增长,各种质量属性和问题都浮出水面,最显著的就是影响软件交付速度的可维护性、可扩展性和可测试性。 训练有素的团队可以减缓项目陷入单体地狱的速度。团队成员可以努力维护他们的模块化应用。他们也可以编写全面的自动化测试。但是另一方面,他们无法避免大型团队在单体应用程序上协同工作的问题,也不能解决日益过时的技术栈问题。团队所能做的就是延缓项目陷入单体地狱的速度,但这是不可避免的。为了逃避单体地狱,他们必须迁移到新架构:微服务架构。
今天,针对大型复杂应用的开发,越来越多的共识趋向于考虑使用微服务架构。但微服务到底是什么?不幸的是,微服务这个叫法本身暗示和强调了尺寸 。针对微服务架构有多种定义。有些仅仅是在字面意义上做了定义:服务应该是微小的不超过 100 行代码,等等。另外有些定义要求服务的开发周期必须被限制在两周之内。曾在 Netflix 工作的著名架构师Adrian Cockcroft 把微服务架构定义为面向服务的架构,它们由松耦合和具有边界上下文的元素组成。这个定义不错,但仍旧有些复杂难懂。立方体模型会是更好的定义。
扩展立方体和服务
- X 轴扩展:在多个实例之间实现请求的负载均衡
X 轴扩展是扩展单体应用程序的常用方法。在负载均衡器之后运行应用程序的多个实例。负载均衡器在 N 个相同的实例之间分配请求。这是提高应用程序吞吐量和可用性的好方法
- Z 轴扩展:根据请求的属性路由请求 Z 轴扩展也需要运行单体应用程序的多个实例,但不同于 X 轴扩展,每个实例仅负责数据的一个子集。图 1-5 展示了 Z 轴扩展的工作原理。置于前端的路由器使用请求中的特定属性将请求路由到适当的实例。例如,应用程序可能会使用请求中包含的 userId 来路由请求。在这个例子中,每个应用程序实例负责一部分用户。该路由器使用请求 Authorization头部指定的 userId 来从 N 个相同的应用程序实例中选择一个。对于应用程序需要处理增加的事务和数据量时,Z 轴扩展是一种很好的扩展方式
- Y轴扩展:根据功能把应用拆分为服务 X 轴和 Z 轴扩展有效地提升了应用的吞吐量和可用性,然而这两种方式都没有解决日益增长的开发问题和应用复杂性。为了解决这些问题,我们需要采用 Y 轴扩展,也就是功能性分解。Y 轴扩展把一个单体应用分成了一组服务
服务本质上是一个麻雀虽小但五脏俱全的应用程序,它实现了一组相关的功能,例如订单管理、客户管理等。服务可以在需要的时候借助 X 轴或 Z 轴方式进行扩展。例如,订单服务可以被部署为一组负载均衡的服务实例。
微服务架构的概括性定义是:把应用程序功能性分解为一组服务的架构风格。请注意这个定义中并没有包含任何与规模有关的内容。重要的是,每一个服务都是由一组专注的、内聚的功能职责组成。
微服务架构的好处和弊端
优点
大型的复杂应用程序可以持续交付和持续部署
- 拥有持续交付和持续部署所需要的可测试性。自动化测试是持续交付和持续部署的一个重要环节。因为每一个服务都相对较小,编写和执行自动化测试变得很容易。因此,应用程序的 bug 也就更少。
- 拥有持续交付和持续部署所需要的可部署性。每个服务都可以独立于其他服务进行部署。如果负责服务的开发人员需要部署对该服务的更改,他们不需要与其他开发人员协调就可以进行。因此,将更改频繁部署到生产中要容易得多
- 使开发团队能够自主且松散耦合。你可以将工程组织构建为一个小型(例如,两个比萨 )团队的集合。每个团队全权负责一个或多个相关服务的开发和部署。每个团队可以独立于所有其他团队开发、部署和扩展他们的服务。结果,开发 的速度变得更快
每个服务都相对较小并容易维护
微服务架构的另一个好处在于:相比之下每个服务都比较小。开发者更容易理解服务中的代码。较小规模的代码库不会把 IDE 等开发工具拖慢,这样可以提升开发者的工作效率。服务的启动速度也比大型的单体应用快得多,千万别小看这一点,快速启动的服务会提高效率,加速研发(提高调试、部署等环节的效率)。
更好的容错性
微服务架构也可以实现更好的故障隔离。例如,某个服务中的内存泄漏不会影响其他服务。其他服务仍旧可以正常地响应请求。相比之下,单体架构中的一个故障组件往往会拖垮整个系统
更容易实验和采纳新的技术
原则上,当开发一个新的服务时,开发者可以自由选择适用于这个服务的任何语言和框架。当然,很多公司对此往往有各种限制和规范,但重要的是团队有了选择的权利,而不是被之前选定的技术绑架。更进一步,因为服务都相对比较小,使用更好的编程语言和技术来重写一项服务变得有可能。这也意味着,如果对一项新技术的尝试以失败而告终,我们可以直接丢弃这部分工作而不至于给整个应用带来失败的风险。这跟单体架构是完全不同的,单体架构之下的技术选型会严重限制后期新技术的尝试
弊端
当然,没有一项技术可以被称为“银弹” 。微服务架构也存在一些显著的弊端和问题
微服务架构的主要弊端和问题如下:
服务的拆分和定义是一项挑战
采用微服务架构首当其冲的问题,就是根本没有一个具体的、良好定义的算法可以完成服务的拆分工作。与软件开发一样,服务的拆分和定义更像是一门艺术。更糟糕的是,如果对系统的服务拆分出现了偏差,你很有可能会构建出一个分布式的单体应用:一个包含了一大堆互相之间紧耦合的服务,却又必须部署在一起的所谓分布式系统。这将会把单体架构和微服务架构两者的弊端集于一身。
分布式系统带来的各种复杂性
使用微服务架构的另一个问题是开发人员必须处理创建分布式系统的额外复杂性。服务必须使用进程间通信机制。这比简单的方法调用更复杂。此外,必须设计服务来处理局部故障,并处理远程服务不可用或出现高延迟的各种情况。
开发者需要思考到底应该在应用的什么阶段使用微服务架构
使用微服务架构的另一个问题是决定在应用程序生命周期的哪个阶段开始使用这种架构。在开发应用程序的第一个版本时,你通常不会遇到需要微服务架构才能解决的问题。此外,使用精心设计的分布式架构将减缓开发速度。这对初创公司来说可能是得不偿失的,其中最大的问题通常是在快速发展业务模型和维护一个优雅的应用架构之间的取舍。微服务架构使得项目开始阶段的快速迭代变得非常困难。初创公司几乎肯定应该从单体的应用程序开始 。但是稍后,当问题变为如何处理复杂性时,那就是将应用程序功能性地分解为一组服务的时候了。由于盘根错节的依赖关系,你会发现重构很困难
服务的拆分策略
如何定义一个微服务架构呢?跟所有的软件开发过程一样,一开始我们需要拿到领域专家或者现有应用的需求文档。跟所有的软件开发一样,定义架构也是一项艺术而非技术。本节我们将介绍一种定义应用程序架构的三步式流程
定义其架构的第一步是将应用程序的需求提炼为各种关键请求。但是,不是根据特定的进程间通信技术(如 REST 或消息)来描述这些请求,而是使用更抽象的系统操作这个概念。系统操作(system operation)是应用程序必须处理的请求的一种抽象描述。它既可以是更新数据的命令,也可以是检索数据的查询。每个命令的行为都是根据抽象领域模型定义的,抽象领域模型也是从需求中派生出来的。系统操作是描述服务之间协作方式的架构场景
该流程的第二步是确定如何分解服务。有几种策略可供选择。一种源于业务架构学派的策略是定义与业务能力相对应的服务。另一种策略是围绕领域驱动设计的子域来分解和设计服务。但这些策略的最终结果都是围绕业务概念而非技术概念分解和设计的服务。
定义应用程序架构的第三步是确定每个服务的 API。为此,你将第一步中标识的每个系统操作分配给服务。服务可以完全独立地实现操作。或者,它可能需要与其他服务协作。在这种情况下,你可以确定服务的协作方式,这通常需要服务来支持其他操作
识别系统操作
定义应用程序架构的第一步是定义系统操作。起点是应用程序的需求,包括用户故事及其相关的用户场景(请注意,这些与架构场景不同)。使用图 2-6 中所示的两步式流程识别和定义系统操作。第一步创建由关键类组成的抽象领域模型,这些关键类提供用于描述系统操作的词汇表。第二步确定系统操作,并根据领域模型描述每个系统操作的行为。
创建抽象领域模型
定义系统操作的第一步是为这个应用程序描绘一个抽象的领域模型。注意这个模型比我们最终要实现的简单很多。应用程序本身并不需要一个领域模型,因为我们在稍后会学到,每一个服务都有它自己的领域模型。尽管非常简单,抽象的领域模型仍旧有助于在开始阶段提供帮助,因为它定义了描述系统操作行为的一些词语
创建领域模型会采用一些标准的技术,例如通过与领域专家沟通后,分析用户故事和场景中频繁出现的名词。例如 Place Order 用户故事,我们可以把它分解为多个用户场景,
代码语言:javascript复制Given a consumer
And a restaurant
And a delivery address/time that can be served by that restaurant
And an order total that meets the restaurant's order minimum
When the consumer places an order for the restaurant
Then consumer's credit card is authorized
And an order is created in the PENDING_ACCEPTANCE state
And the order is associated with the consumer
And the order is associated with the restaurant
只听到从山间传来架构君的声音:
向河梁、回头万里,故人长绝。有谁来对上联或下联?
在这个用户场景中的名词,如 Consumer、Order、Restaurant 和 CreditCard,暗示了这些类都是需要的
同样,Accept Order 用户故事也可以分解为多个场景,如下
代码语言:javascript复制此代码由Java架构师必看网-架构君整理
Given an order that is in the PENDING_ACCEPTANCE state
and a courier that is available to deliver the order
When a restaurant accepts an order with a promise to prepare by a particular
time
Then the state of the order is changed to ACCEPTED
And the order's promiseByTime is updated to the promised time
And the courier is assigned to deliver the order
经过分析最终我们可以得出如下的类图结构
每一个类的作用如下:
代码语言:javascript复制■ Consumer:下订单的用户。
■ Order:用户下的订单,它用来描述订单并跟踪状态。
■ OrderLineItem:Order 中的一个条目。
■ DeliveryInfo:送餐的时间和地址。
■ Restaurant:为用户准备生产订单的餐馆,同时也要发起送货。
■ MenuItem:餐馆菜单上的一个条目。
■ Courier:送餐员负责把订单送到用户手里。可跟踪送餐员的可用性和他们的位置。
■ Address:Consumer 或 Restaurant 的地址。
■ Location:Courier 当前的位置,用经纬度表示
定义系统操作
当定义了抽象的领域模型之后,接下来就要识别系统必须处理的各种请求。我们并不讨论具体的用户界面,但是你能够想象在每一个用户场景下,前端的用户界面向后端的业务逻辑发出请求,后端的业务逻辑进行数据的获取和处理
识别系统指令的切入点是分析用户故事和场景中的动词。例如 Place Order 用户故事,它非常明确地告诉我们,这个系统必须提供一个 Create Order 操作。很多用户故事都会直接对应或映射为系统命令。表 2-1 列出了一些关键的系统命令
命令规范定义了命令对应的参数、返回值和领域模型类的行为。行为规范中包括前置条件(即当这个操作被调用时必须满足的条件)和后置条件(即这个操作被调用后必须满足的条件)。例如,以下就是 createOrder() 系统操作的规范。
前置条件对应着 Place Order 用户场景中的 givens,后置条件对应着场景中的Then。当系统操作被调用时,它会检查前置条件,执行操作来完成和满足后置条件。
抽象的领域模型和系统操作能够回答这个应用“做什么”这一问题。这有助于推动应用程序的架构设计。每一个系统操作的行为都通过领域模型的方式来描述。每一个重要的系统操作都对应着架构层面的一个重大场景,是架构中需要详细描述和特别考虑的地方。现在我们来看看如何定义应用程序的微服务架构
系统操作被定义后,下一步就是完成应用服务的识别。如之前提到的,这并不是一个机械化的流程,相反,有多种拆分策略可供选择。每一种都是从一个侧面来解决问题,并且使用它们独有的一些术语。但是殊途同归,这些策略的结果都是一样的:一个包含若干服务的架构,这样的架构是以业务而不是技术概念为中心
根据业务能力进行服务拆分
创建微服务架构的策略之一就是采用业务能力进行服务拆分。业务能力是一个来自于业务架构建模的术语。业务能力是指一些能够为公司(或组织)产生价值的商业活动。特定业务的业务能力取决于这个业务的类型。例如,保险公司业务能力通常包括承保、理赔管理、账务和合规等。在线商店的业务能力包括:订单管理、库存管理和发货
组织的业务能力通常是指这个组织的业务是做什么,它们通常都是稳定的。与之相反,组织采用何种方式来实现它的业务能力,是随着时间不断变化的。这个准则在今天尤其明显,很多新技术在被快速采用,商业流程的自动化程度越来越高。例如,不久之前你还通过把支票交给银行柜员的方式来兑现支票,现在很多 ATM 机都支持直接兑现支票,而今,人们甚至可以使用智能手机拍照的方式来兑现支票。正如你所见,“兑现支票”这个业务能力是稳定不变的,但是这个能力的实现方式正在发生戏剧性的变化
从业务能力到服务
一旦确定了业务能力,就可以为每个能力或相关能力组定义服务。下图显示了 FTGO应用程序从能力到服务的映射。某些顶级能力(如会计记账能力)将映射到服务。在其他情况下,子能力映射到服务
上图显示的服务仅仅是定义架构的第一次尝试。随着我们对应用程序领域的了解越来越多,它们可能会随着时间的推移而变化,特别是架构定义流程中的一个重要步骤是调查服务如何在每个关键架构服务中协作。例如,你可能会发现由于过多的进程间通信而导致特定的分解效率低下,导致你必须把一些服务组合在一起。相反,服务可能会在复杂性方面增长到值得将其拆分为多个服务的程度
根据子域进行服务拆分
领域驱动设计是构建复杂软件的方法论,这些软件通常都以面向对象和领域模型为核心。领域模型以解决具体问题的方式包含了一个领域内的知识。它定义了当前领域相关团队的词汇表,DDD 也称之为通用语言(Ubiquitous language)。领域模型会被紧密地映射到应用的设计和实现环节。在微服务架构的设计层面,DDD 有两个特别重要的概念,子域和限界上下文
领域驱动为每一个子域定义单独的领域模型。子域是领域的一部分,领域是 DDD 中用来描述应用程序问题域的一个术语。识别子域的方式跟识别业务能力一样:分析业务并识别业务的不同专业领域,分析产出的子域定义结果也会跟业务能力非常接近。FTGO 的子域包括:订单获取、订单管理、餐馆管理、送餐和会计。正如你所见:这些子域跟我们之前定义的业务能力非常接近。
DDD 把领域模型的边界称为限界上下文(bounded context)。限界上下文包括实现这个模型的代码集合。当使用微服务架构时,每一个限界上下文对应一个或者一组服务。换一种说法,我们可以通过 DDD 的方式定义子域,并把子域对应为每一个服务,这样就完成了微服务架构的设计工作。图 2-9 展示了子域和服务之间的映射,每一个子域都有属于它们自己的领域模型。
DDD 和微服务架构简直就是天生一对。DDD 的子域和限界上下文的概念,可以很好地跟微服务架构中的服务进行匹配。而且,微服务架构中的自治化团队负责服务开发的概念,也跟 DDD 中每个领域模型都由一个独立团队负责开发的概念吻合。更有趣的是,子域用于它自己的领域模型这个概念,为消除上帝类和优化服务拆分提供了好办法
上帝类的处理
上帝类是在整个应用程序中使用的全局类。上帝类通常为应用程序的许多不同方面实现业务逻辑。它有大量字段映射到具有许多列的数据库表。大多数应用程序至少有一个这样的上帝类。Order 类是 FTGO 应用程序中上帝类的一个很好的例子。这并不奇怪:毕竟 FTGO 的
目的是向客户提供食品订单。系统的大多数部分都涉及订单。如果 FTGO 应用程序具有单个领域模型,则 Order 类将是一个非常大的类。它将具有与应用程序的许多不同部分相对应的状态和行为。下图显示了使用传统建模技术创建的 Order 类的结构
Order 类具有与订单处理、餐馆订单管理、送餐和付款相对应的字段及方法。由于一个模型必须描述来自应用程序的不同部分的状态转换,因此该类还具有复杂的状态模型。在目前情况下,这个类的存在使得将代码分割成服务变得极其困难
一种解决方案是将 Order 类打包到库中并创建一个中央 Order 数据库。处理订单的所有服务都使用此库并访问访问数据库。这种方法的问题在于它违反了微服务架构的一个关键原则,并导致我们特别不愿意看到的紧耦合。例如,对 Order 模式的任何更改都要求其他开发团队同步更新和重新编译他们的代码。
另一种解决方案是将 Order 数据库封装在 Order Service 中,该服务由其他服务调用以检索和更新订单。该设计的问题在于这样的一个 Order Service 将成为一个纯数据服务,成为包含很少或没有业务逻辑的贫血领域模型(anemic domain model)。这两种解决方案都没有吸引力,但幸运的是,DDD 提供了一个好的解决方案。
更好的方法是应用 DDD 并将每个服务视为具有自己的领域模型的单独子域。这意味着FTGO 应用程序中与订单有关的每个服务都有自己的领域模型及其对应的 Order 类的版本。Delivery Service 是多领域模型的一个很好的例子。如图 2-11 所示为 Order,它非常简单:取餐地址、取餐时间、送餐地址和送餐时间。此外,Delivery Service 使用更合适的 Delivery 名称,而不是称之为 Order。Delivery Service 对订单的任何其他属性不感兴趣
Kitchen Service 有一个更简单的订单视图。它的 Order 版本就是一个 Ticket(后 厨 工 单)。如图所 示,Ticket 只包含 status、requestedDeliveryTime、prepareByTime 以及告诉餐馆准备的订单项列表。它不关心消费者、付款、交付等这些与它无关的事情
Order Service 具有最复杂的订单视图,如图所示。即使它有相当多的字段和方法,它仍然比原始版本的那个 Order 上帝类简单得多。
每个领域模型中的 Order 类表示同一 Order 业务实体的不同方面。FTGO 应用程序必须维持不同服务中这些不同对象之间的一致性。例如,一旦 Order Service 授权消费者的信用卡,它必须触发在 Kitchen Service 中创建 Ticket。同样,如果 Kitchen Service 拒绝订单,则必须在 Order Service 中取消订单,并且为客户退款。我们通常会用用分布式事务去处理这些问题,这又是微服务架构的另一个问题了。
参考:克里斯·理查森(Chris Richardson) 微服务架构设计模式