FTGO,即Food to GO公司,作者虚构出来的一家公司。图1-1是它的架构图。
过渡的复杂性会吓退开发者
系统本身过于庞大和复杂,以至于任何一个开发者都很难理解它的全部。因此修复,软件中的问题和正确的实现新功能就变得困难且耗时。
更糟糕的是,这种极度的复杂性正在形成一个恶性循环:由于代码库太难于理解,因此开发人员在更改时更容易出错,每一次更改都会让代码库变得更加复杂、更难懂。
开发速度缓慢
因为要跟这些极度复杂的系统打交道,开发人员发现他们日常的开发工作变慢了。这个巨大的项目把开发人员的IDE工具搞得很慢,构建一次应用需要很长的时间,更要命的是,每启动一次都需要很长的时间。这严重的影响的团队的工作效率。
从代码提交到实际部署的时间很长,而且容易出问题
一个月完成几次更新,对于开发人员来说简直就是神话,持续部署更是遥不可及的梦想。
众多开发人员都向一个代码库提交代码更改,这常常使得这个代码库的构建结果出于一个不可交付的状态。当采用分支来解决这个问题时,带来的是漫长且痛苦的合并过程。紧接着,一旦团队完成了一个冲刺任务,随后迎接他们的将是一个漫长的测试和代码稳定周期。
把更改推向生产环境的另一个挑战是运行测试需要很长时间。
难以部署
对应用进行横向扩展时也有挑战。因为在有些情况下,应用的不同模块对资源的需求是相互冲突的,有的模块需要内存,有的模块需要CPU。
交付可靠的单体应用是一项挑战
单体应用的另一个问题是缺乏可靠性,这个问题导致了频繁的系统故障和宕机。系统不可靠的一个原因是应用程序体积庞大而无法进行全面和彻底的测试。缺乏可靠的测试你意味着代码中的错误会进入生产环境。更糟糕的是,该应用程序缺乏故障隔离,因为所有模块都在一个进程中运行。
需要长期以来某个可能已经过时的技术栈
单体地狱的最终表现,也体现在团队必须长期使用一套相同的技术栈方面。单体架构使得采用新的框架和变成语言变得极其苦难。
略~
略~
一方面,训练有素的团队可以减缓项目陷入单体地狱的速度。但是另一方面,他们无法避免大型团队在单体应用程序上协同工作的问题,也不能解决日益过时的技术栈问题。团队所能做的是延缓项目陷入单体地狱的速度,但这是不可避免的。为了逃避单体地狱,他们必须迁移到新架构:微服务架构。
X轴扩展:在多个实例之间实现请求的负载均衡
X轴扩展是扩展单体应用的常用方法。图1-4展示了X轴扩展的工作原理。在负载均衡器之后运行应用程序的多个实例。负载均衡器在N个相同的实例之间分配请求。这是提高应用程序吞吐量和可用性的好方法。
Z轴扩展:根据请求的属性路由请求
Z轴扩展也需要运行单体应用程序的多个实例,但不同于X轴扩展,每个实例仅负责数据的一个子集。图1-5展示了Z轴扩展的工作原理。
Y轴扩展:根据功能把应用拆分为服务
X轴和Z轴扩展有效的提升了应用的吞吐量和可用性,然而这两种方式都没有解决日益增长的开发问题和应用程序复杂性。为了解决这些问题,我们需要采用Y轴扩展,也就是功能性分解。Y轴扩展把一个单体应用分成了一组服务,如图1-6所示。
服务本质上是一个麻雀虽小五脏俱全的应用程序,它实现了一组相关的功能,例如订单管理等。服务可以再需要的时候借助X轴或Z轴方式进行扩展。
我对微服务架构的概括性定义是:把应用程序功能性分解为一组服务的架构风格。
模块化是开发大型、复杂应用程序的基础。
微服务架构使用服务作为模块的单元。服务的API为它自身构筑了一个不可逾越的边界,你无法越过API去访问服务内部的类。因此模块化的服务更容易随着时间的推移而不断演化。微服务架构也带来其他的好处,例如服务可以独立进行部署和扩展。
微服务架构的一个关键特性是每一个服务之间都是松耦合的,它们仅通过API进行通信。实现这种松耦合的方式之一,是每个服务都有自己的私有数据库。
略~
略~
略~
一方面,将微服务架构描述为一种功能分解是有用的。另一方面,它留下了几个未解决的问题。包括:微服务架构如何与更广泛的软件构架概念相结合?什么是服务?服务的规模有多重要?
为了回答这些问题,我们需要退后一步,看看软件架构的定义。软件的架构是一种抽象的结构,它由软件的各个组成部分和这些部分之间的依赖关系构成。
计算机系统的软件架构是构建这个系统所需的一组结构,包括软件元素、它们之间的关系以及两者的属性。
这显然是一个非常抽象的定义。但其是指是应用程序的架构是将软件分解为元素和这些元素之间的关系。由于以下两个原因,分解很重要:
除了这四个视图以外,4+1中的1指场景,它负责把视图串联在一起。每个场景负责描述在一个视图中的多个架构元素如何协作,以完成一个请求。例如,在逻辑视图中的场景,展现的是类如何协作的。同样,在进程视图中的场景,展现了进程是如何协作的。
为什么架构如此重要
应用程序有两个层面的需求。第一类是功能性需求,这些需求决定一个应用程序做什么。这些通常都包括在用例或者用户故事中。应用的架构其实跟这些功能性需求没有什么关系。功能性需求可以通过任意的架构实现,甚至是非常糟糕的大泥球架构。
架构的重要性在于,它帮助应用程序满足了第二类需求:非功能性需求,也称之为你质量属性需求,或者简称为”能力“。这些非功能性需求决定了一个应用程序在运行时的质量,比如可扩展性和可靠性。
分层架构风格
架构的典型例子是分层架构。分层架构将元素按照”层“的方式组织。每个层都有明确定义的职责。分层架构还限制了层之间的依赖关系。每一层只能依赖于紧邻其下方的层或其下面的任何层。
流行的三层架构是应用于逻辑视图的分层架构,它将应用程序的类组织到一下层中:
分层架构是架构风格的一个很好的例子,但它却是有一些明显的弊端:
此外分层架构错误的表示了精心设计的应用程序中的依赖关系。业务逻辑通常定义数据访问方法的接口或接口库。数据持久化层则定义了实现存储库接口的DAO类。换句话说,依赖关系与分层架构所描述的相反。
关于六边形架构
六边形架构选择以业务逻辑未中心的方式组织逻辑视图。此架构的一个关键特性和优点是业务逻辑不依赖于适配器。相反,各种适配器都依赖业务逻辑。
六边形架构风格的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来。业务逻辑不依赖于表示层逻辑或数据表示层。
由于这种分离,单独测试业务逻辑就容易的多。另一个好处是它更准确的反映了现在应用程序的架构。可以通过多个适配器调用业务逻辑,每个适配器实现特定的API或用户界面。业务逻辑还可以调用多个适配器,每个适配器调用不同的外部系统。
微服务架构强加的一个关键约束是服务松耦合。因此,服务之间的协作方式存在一定限制。
什么是服务
服务是一个单一的、可独立部署的软件组件,它实现了一些有用的功能。
服务的API封装了其内部实现。与单体架构不同,开发人员无法绕过服务的API直接访问服务背部的方法或数据。因此,微服务架构强制实现了应用程序的模块化。
什么是松耦合
微服务架构的最核心的特性是服务之间的松耦合性。服务之间的交互采用API完成,这样做就封装了服务的实现细节。这允许服务在不影响客户端的情况下,对实现方式做出修改。松耦合服务是改善开发效率、提升可维护和可测试性的关键。小的、松耦合的服务更容易被理解、修改和测试。
我们通过API来实现松耦合服务之间的协调调用,这样就避免了外界对服务的数据库的直接访问和调用。服务自身的持久化数据就如同类的私有属性一样,时候不对外的。保证数据的私有属性是实现松耦合的前提之一。
共享类库的角色
开发人员经常把一些通用的功能打包到库或模块中,以便多个应用程序可以重用它而无须复制代码。从表面上看,它似乎是减少服务中代码重复的好方法。但是你需要确保不会意外地在服务之间引入耦合。
例如,想象一下多个服务需要更新Order业务对象的场景。一种选择是将该功能打包为可供多个服务使用的库。一方面,使用库可以消除代码重复。另一方面,如果业务需求的变更影响了Order业务对象,开发者需要同时重建和重新部署所有使用了共享库的服务。
你应该努力使用共享库来实现不太可能改变的功能。
服务大小并不重要
微服务这个术语的一个问题是会将你的关注点错误的聚集到微上。它暗示服务应该非常微小。实际上,大小不是一个重要的考虑因素。
更好的目标是将精心设计的服务定义为能够由小团队开发的服务,并且交付时间最短,与其他团队的协作最少。理论上,团队可能只负责单一服务,因此服务绝不是微小的。
世界上并没有一个机械化的流程可以遵循,然后指望这个流程输出一个合理的架构。我们只能介绍一个大概的方法,现实世界中,这是一个不断迭代和持续创新的过程。
应用程序是用来处理客户端请求的,因此定义其架构的第一步是将应用程序的需求提炼为各种关键请求。系统操作是应用程序必须处理的请求的一种抽象描述。它既可以是更新数据的命令,也可以是检索数据的查询。系统操作是描述服务之间协作方式的架构场景。
该流程的第二步是确定如何分解服务。有几种策略可供选择,一种源于业务架构学派的策略是定义与业务能力相对应的服务。另一种策略是围绕领域驱动设计的子域来分解和设计服务。d但这些策略的最终结果都是围绕业务概念而非技术概念分解和设计服务。
定义应用程序架构的第三部是确定每个服务的API。为此,你将第一步中标识的每个系统操作分配给服务。服务完全可以独立地实现操作。或者,它可能需要与其他服务协作。在这种情况下,你可以确定服务的协作方式,这通常需要服务来支持其他操作。你还需要确定选用哪种进程间通信机制来实现每个服务的API。
定义应用程序架构的第一步是定义系统操作。起点是应用程序的需求,包括用户故事及其相关的用户场景。使用图2-6所示的两步式流程识别和定义系统操作。
第一步创建有关键类组成的抽象领域模型,这些关键类提供用户描述系统操作的词汇表。第二步确定系统操作,并根据领域模型描述每个系统操作的行为。
领域模型主要源自用户故事中提及的名词,系统操作主要来自于用户故事中提及的动词。
创建抽象领域模型
略~
定义系统操作
系统操作可分为以下两种:
命令规范定义了命令对应的参数、返回值和领域模型类的行为。行为规范中包括前置条件(即当这个操作被调用时必须满足的条件)和后置条件(即这个操作被调用后必须满足的条件)。例如,以下就是createOrder()系统操作的规范。
业务能力是指一些公司(或组织)产生价值的商业活动。
业务能力定义了一个组织的工作
组织的业务能力通常是指这个组织的业务是做什么,它们通常都是稳定的。与之相反,组织采用何种方式来实现它的业务能力,是随着时间不断变化的。
识别业务能力
一个组织有哪些业务能力,是通过对组织的目标、结构和商业流程的分析得来的。业务能力规范包括多个元素,比如输入和输出、服务等级协议(SLA)。
业务能力通常集中在特定的业务对象上。
从业务能力到服务
一旦确定了业务能力,就可以为每个能力或相关能力组定义服务。图2-8显示了FTGO应用程序从能力到服务的映射。
决定将哪个级别的能力层次结构映射到服务是一个非常主观的判断。
围绕能力组织服务的一个关键好处是,因为它们是稳定的,所以最终的架构也将相对稳定。架构的各个组件可能会随着业务的具体实现方式的变化而变化,但架构仍保持你不变。
领域驱动设计为每一个子域定义了单独的领域模型。子域是领域的一部分,领域是DDD中用来描述应用程序问题的一个术语。识别子域的方式跟识别业务能力一样:分析业务并识别业务的不同专业领域,分析产出的子域定义结果也会跟业务能力非常接近。
DDD把领域模型的边界成为限界上细纹。限界上下文包括实现这个模型的代码集合。当使用微服务架构时,每一个限界上线文对应一个或一组服务。换一种说法,我们可以通过DDD的方式定义子域,并把子域对应为一个服务,这样就完成了微服务架构的设计工作。图2-9展示子域和服务之间的映射,每一个子域都有属于它们自己的领域模型。
DDD和微服务架构简直就是天生一对。DDD的子域和限界上下文的概念,可以很好地跟微服务架构中的服务进行匹配。而且,微服务架构中的自治化团队负责服务开发的概念也跟DDD中每个领域模型都由一个独立团队负责开发的概念吻合。更有趣的是,子域用于它自己的领域模型这个概念,为消除上帝类和优化服务拆分提供了好办法。
单一职责原则(SRP)
改变一个类应该只有一个理由。
类所承载的每一个职责都是对它进行修改的潜在原因。如果一个类承载了多个职责,并且相互之间的修改是独立的,那么这个类就会变得非常不稳定。遵照SRP原则,你所定义的每一个类都应该只有一个职责,因此也就只有一个理由对它进行修改。
闭包原则(CCP)
在包中包含的所有类应该是对同一个变化的一个集合,也就是说,如果对包做出修改,需要调整的类应该都在这个包之内。
在微服务架构下采用CCP原则,这样我们就能把根据同样原因进行变化的服务放在一个组件内。这样做可以控制服务的数量,当需求发生变化时,变更和部署也更加容易。理想情况下,一个变更只会影响一个团队和一个服务。CCP是解决分布式单体这种可怕反模式的法宝。
网络延迟
网络延迟是分布式系统中一直存在的问题。你可能会发现,对服务的特定分解会导致两个服务之间的大量往返调用。有时,你可以通过实施批量处理API在一次往返中获取多个对象,从而将延迟减少到可接受的数量。但在其他情况下,解决方案是把多个相关的服务组合在一起,用变成语言的函数调用替换昂贵的进程间通信。
同步进程间通信导致可用性降低
另一个需要考虑的问题是如何处理进程间通信而不降低系统的可用性。例如,实现creatOrder()操作最常见的方式是让Order Service使用REST同步调用其他服务。这样做的弊端是REST这样的协议会降低Order Service的可用性。
在服务之间维持数据的一致性
另一个挑战是如何在某些系统操作需要更新多个服务中的数据时,仍旧维护服务之间的数据一致性。
传统的解决方案是使用基于两阶段提交的分布式事务管理机制。对于现今的应用程序而言,这不是一个好的选择,你必须使用一种非常不同的方法来处理事务管理,这就是Saga。Saga是一系列使用消息协作的本地事务。Saga的一个限制是它们是最终一致的。
获取一致的数据视图
分解的另一个障碍是无法跨多个数据库获得真正一致的数据视图。在单体应用程序中,ACID事务的属性保证查询将返回数据库的一致视图。相反,在微服务架构中,即使每个服务的数据库是一致的,你也无法获得全局一致的数据视图。
上帝类阻碍了拆分
分解的另一个障碍是存在所谓的上帝类。上帝类是在整个应用程序中使用的全局类。上帝类通常为应用程序的许多不同方面实现业务逻辑。
存在服务API操作有一下两个原因:
定义服务API的起点是将每个系统操作映射到服务。之后确定服务是否需要与其他服务协作以实现系统操作。如果需要协作,我们将确定其他服务必须提供哪些API才能支持协作。