《演进式架构》学习笔记(二)

(此书写得有点杂,但部分内容还可以,以此几篇记录下笔记。)

四、架构耦合

演进式架构注重适当的耦合,即如何确定哪些架构维度间应该相互耦合来以最小的开销和成本最大程度地获益。

模块化

平台不同,代码复用机制也不同,但它们都支持将相关代码组成模块。模块化描述了相关代码的逻辑分组。
可以以不同的物理方式封装模块。组件就是模块的物理封装。模块意味着逻辑分组,而组件意味着物理划分。

是其中一类组件,它往往和调用代码在相同的内存地址内运行,通过编程语言的函数调用机制进行通信。库常用作编译时的依赖。由于大多数复杂应用是由各式各样的组件构成的,因此在应用程序架构中存在很多和库相关的问题。
另一类组件被称为服务,倾向于在自己的地址空间中运行,通过低级网络协议(比如 TCP/IP)、更高级的网络协议(比如简单对象访问协议,SOAP),或表述性状态转移(REST)进行通信。服务相关问题往往在集成架构中出现,因为它造成了运行时的依赖。

架构的量子和粒度

组件级的耦合并不是联接软件的唯一方式。许多业务概念在语义上联接系统的各个部分,这便产生了功能内聚

正如物理学所定义的,量子是物理实体相互作用时所涉及的最小单位。架构量子则是具有高功能内聚并可以独立部署的组件,它包括了支持系统正常工作的所有结构性元素。
在单体架构中,量子就是整个应用程序,每个部分都高度耦合,因此开发人员必须对其进行整体部署。

微服务架构在架构元素之间定义了物理限界上下文,封装了所有可能变化的部分。这种架构就是为了增量变更而设计的。在微服务架构中,限界上下文作为量子边界,包含了服务所依赖的组件,比如数据库服务器。它还包含一些架构组件,例如搜索引擎、报表工具及任何有助于交付功能的组件。

架构师都应该显式定义架构量子的大小。小的架构量子意味着更快的变更速度,因为其影响范围更小。通常小组件比大组件更易于使用。量子的大小决定了架构中进行增量变更的可能性(量子越小,可能性越大)。

构建演进式架构的关键之一在于决定自然组件的粒度以及它们之间的耦合,以此来适应那些通过软件架构支持的能力。

不同类型架构的演进能力

软件架构之所以存在,部分原因是为了实现跨特定维度的某种演进——便于变更是架构模式的原因之一。架构模式不同,架构量子大小也不同,这影响着架构的演进能力。

需要注意的是,虽然架构模式对于成功演进至关重要,但是它并不是唯一的决定性因素。必须结合架构模式固有的特征和系统定义的附加特征才能完整定义演进性的各个维度。

“大泥团”架构

某个无法识别架构的混乱系统,俗称“大泥团反模式”。这些系统高度耦合,当发生变更时会产生连锁副作用。开发人员创建了高度耦合且模块化很差的类。

如图某个大泥团架构中类的耦合情况,图中每个节点代表一个类,每条线(向内或向外)代表耦合,线的粗细程度表示连接的数量:
p-9

站在演进能力的角度来看,这个架构表现极差。

  • 增量变更:对这种架构难以做任何变更。相关的代码散布于系统各个角落,这意味着修改其中一个组件将意外地破坏其他组件。修复这些破损会导致更多的破损发生,从而产生无尽的连锁反应。
  • 通过适应度函数引导变更:由于没有明确定义分区,我们很难为这种架构构建适应度函数。为了构建保护功能,开发人员必须确定需要保护的部分,但是在这种架构中,除了低级的函数或类之外不存在任何结构。
  • 适当的耦合:这种架构是不当耦合的典型。构建这样的软件没有任何架构优势。

在这样的糟糕状态下,变更困难且成本高。本质上,由于系统各部分间高度耦合,架构量子就是整个系统本身,没有哪个部分可以轻易改变,因为牵一发而动全身。

单体架构

单体架构的大量代码通常高度耦合。

1. 非结构化的单体架构

这种架构模式包含几种不同的变体,其中包括实质上由相互独立的类互相协调而构成的系统:

p-10

不同的模块各自处理不同的任务,通过共用的类实现通用功能。在这种架构中,由于缺乏一致的总体结构而阻碍了变更。

  • 增量变更:巨大的架构量子阻碍了增量变更,因为高度耦合要求部署大块应用。组件之间存在高度耦合,这导致很难单独部署某个组件,因为需要变更其他组件。
  • 通过适应度函数引导性变更:为单体架构构建适应度函数很难,但并非不可能。因为这种架构模式存在了很长时间,可以用随之发展而来的很多工具和测试实践来构建适应度函数。然而,常见的引导性变更对象通常会成为单体架构的致命弱点,例如性能和伸缩性。虽然开发人员很容易理解单体架构,但难以构建良好的伸缩性和性能,这很大程度上源于它固有的耦合。
  • 适当的耦合:单体架构除了简单的类之外几乎没有内部结构,其耦合程度类似于大泥团架构。因此代码某处的变更可能对其中某个较远的部分产生意想不到的副作用。

尽管这种架构的演进能力略好于大泥团架构,但是这种架构很容易退化,因为几乎没有结构限制来防止其退化。

2.分层架构

其他单体架构以更加结构化的方式创造出了分层架构,其中一个变体如:

p-11

每层代表一种技术能力,使得开发者能够轻易地置换技术架构功能。分层架
构的主要设计准则是将不同的技术能力分隔到不同的层,每层职责各异。这种架构的主要优点是关注点独立且分离。每一层相对于其他层都是独立的,但能通过明确定义的接口互相访问。这使得对某一层的变更不会影响其他层,同时将相似的代码组织到一起,为该层的专业化和分离提供了空间。

无论哪种单体架构,架构量子本身就是应用,包括一些独立组件,例如数据库服务器。量子较大的系统是难以演进的。

  • 增量变更:开发人员发现变更这种架构很容易,特别是在将变更隔离到现有层的情况下。跨不同层的变更则会带来协调上的挑战,特别是在组织人员结构和架构分层类似的情况下(这反映了“康威定律”)。例如,某个团队能在不打扰其他团队的情况下替换整个持久层框架,因为他们可以在明确定义的接口背后完成这项工作。但是,当业务要求变更 ShipToCustomer(送货服务)时,该变更则会影响所有层,于是协调在所难免。
  • 通过适应度函数引导性变更:在一个更加结构化的单体应用中编写适应度函数更为容易,因为这种架构的结构更明显。同时,将关注点分离到不同层使得开发人员能对更多部分进行隔离测试,便于构建适应度函数。
  • 适当的耦合:单体架构的一个优点是易于理解。了解设计模式等概念的开发人员能轻易将这些知识应用于分层架构中。这种易理解性很大程度上是因为开发者能轻松地访问所有代码。分层架构使得由层定义的技术架构划分更易于演进。例如,一个设计(并实现)良好的分层架构能让我们很容易地替换掉数据库、业务规则或其他任何层,并将副作用减至最小。

无论有意或无意,单体架构往往都高度耦合。当开发人员使用分层架构来分离关注点时(例如使用持久层去简化数据访问),该层通常会表现出内部高度耦合和外部低耦合。在层内,各组件为相同的目标合作,因此它们趋向于高度耦合。相反,开发人员通常会更仔细地定义各层之间的接口,在各层之间创建更低的耦合。

3. 模块化的单体架构

架构师们所赞赏的微服务的许多的优点也能在单体架构中实现,例如隔离性、独立性和小变更单元等,但前提是开发人员极其严格地处理耦合。需要注意的是,这个原则必须拓展到技术架构之外,囊括其他维度(特别是数据)。现代工具让代码易于复用,这使得开发人员很难在容易产生耦合的环境中实现适当的耦合。

大部分现代编程语言都支持构建严格的可见性和连接规则。如果架构师和开发人员运用这些规则构建一个模块化的单体应用,那么构建出的架构会更具可塑性。

  • 增量变更:由于开发人员能够执行模块化,因此在此类架构中很容易进行增量变更。尽管在逻辑上功能被划分为不同的模块,但如果难以单独部署包含模块的组件,那么架构量子依然会很大。在模块化单体架构中,组件的可部署程度决定了增量变更的速度。
  • 通过适应度函数进行引导性变更:测试、度量及其他适应度函数在这种架构中更容易设计和执行,因为合理划分了组件,使得测试模拟和其他依赖于隔离层的测试技术更容易实现。
  • 适当的耦合:一个设计良好的模块化单体架构是适当耦合的好例子。每个组件在功能上是内聚的,组件之间的接口设计良好且耦合度低。

p-12

在开始一个新项目时,单体架构,特别是分层架构是普遍的选择,因为它的结构容易理解。但是由于性能下降、代码库过大和其他一系列因素,很多单体最终被取代而走到生命尽头。当前微服务架构是单体架构常见的迁移目标,但相比于单体架构,它在很多方面都更复杂,例如服务、数据粒度、运维、协调、事务等。如果开发团队难以构建最简单的架构,那么转向更复杂的架构又如何能解决问题吗?

如果无法构建单体应用,为什么你认为微服务能解决问题呢? ——Simon Brown

在重建昂贵的架构之前,提升现有架构的模块化程度能让架构师获益。如果已经没有可以提升的地方了,那么这便是开始重建更加复杂的架构的好时机。

4. 微内核架构

还有一种流行的单体架构——微内核架构,它通常出现在浏览器和集成开发环境(IDE)中,

p-13

上图所示的微内核架构定义了一个核心系统,核心系统对外提供 API 来通过插件丰富其功能。
在这种架构中架构量子大小有两种:一种来自核心系统,另一种来自插件。架构师通常将核心系统设计成单体应用,并在一些熟知的扩展点为插件创建钩子(hook)。我们通常把插件设计成独立且可单独部署的组件。因此,这种架构支持积极的增量变更,开发人员可以针对可测试性进行设计,更容易定义适应度函数。从技术耦合的角度来看,架构师往往将此类系统设计成低耦合,以保持插件相互独立,从而简化它们。

微内核架构的主要挑战围绕着契约,它是某种形式的语义耦合。为了发挥作用,插件必须和核心系统进行双向信息传递。只要插件不需要互相协调,那么开发人员就可以专注于插件与核心系统间的信息和版本控制。例如,大多数浏览器插件只和浏览器交互,而不和其他插件交互。

通常,微内核架构包含一个注册表来跟踪安装的插件及其所支持的契约。在插件间建立明确的耦合加重了系统各部分间的语义耦合,进而导致架构量子变大。

微内核架构广泛应用于 IDE 工具,它也能应用于各种商业应用。

如果难以通过插件使技术架构演进,那么微内核架构是个不错的选择。由完全独立的插件组成的系统更易于演进,因为插件之间不存在耦合。但依赖彼此协作的插件会增加耦合,进而阻碍系统演进。如果使用彼此交互的插件来设计系统,那么你还应该通过消费者驱动的契约模型构建适应度函数来保护那些集成点。微内核架构的核心系统通常很庞大,但是很稳定,因为大部分的变更应该发生在插件上(除非架构师将应用划分得很差)。因此,增量变更很简单:部署流水线触发对插件的变更并对其进行验证。

架构师通常不会在微内核技术架构中包含数据依赖,因此开发人员和数据库管理员必须单独考虑数据的演进能力。将每个插件视为限界上下文可以提高该架构的演进能力,因为这样可以降低内部耦合。

从架构演进的角度来看,微内核架构的理想特征如下所示。

  • 增量变更:一旦完成了核心系统,大多数行为应来自插件。如果插件都是独立的,那么增量变更会更容易。
  • 通过适应度函数进行引导性变更:通常在这种架构中构建适应度函数很简单,因为核心系统和插件是相对独立的。开发人员分别为核心系统和插件维护两套适应度函数。核心适应度函数守护核心系统的变更,包括伸缩性等部署问题。插件测试通常更简单,因为对领域行为的测试是隔离的。为了便于测试插件,开发人员需要很好地模拟核心系统。
  • 适当的耦合:微内核模式明确定义了这种架构的耦合特征。从耦合的角度来看,构建独立的插件使变更变得不重要。协调相互依赖的插件则更难。开发人员应该通过适应度函数来将相互依赖的组件正确地集成。

此类架构还应包含一些整体适应度函数来确保开发人员维持关键的架构特征。例如,单独的插件可能影响某个系统属性,比如伸缩性。因此,开发人员应该计划构建一套集成测试,把它作为整体适应度函数。当系统中存在相互依赖的插件时,开发人员还应该构建整体适应度函数来确保契约和消息的一致性。

事件驱动架构

事件驱动架构(EDA)通常通过消息队列将几个不相关的系统集成在一起。此类架构常用的实现方式有两种:代理模式中介模式。两种模式的核心能力不同。

1. 代理模式

代理模式的事件驱动架构由如下架构组件构成。

  • 消息队列:消息队列由多种技术实现。
  • 始发事件:启动业务流程的事件。
  • 流程内事件:为了满足业务流程,在事件处理器之间传递的事件。
  • 事件处理器:事件处理器是活跃的架构组件,执行实际的业务流程。当两个处理器间需要协调时,它们通过队列传递消息。

代理模式的事件驱动架构在构建强大的异步系统时存在一些设计挑战。例如,由于缺少集中的中介,很难进行协调和错误处理。由于架构各部分高度分离,开发人员必须通过架构还原业务流程的功能内聚。因此,像事务这样的行为将更难实现。
尽管在实现上存在挑战,但它仍然是极具演进性的架构。开发人员可以通过向现有事件队列添加新的监听器来向系统添加新行为。

  • 增量变更:代理模式的事件驱动架构允许多种形式的增量变更。通常开发人员将服务设计为松散耦合的,便于独立部署。解耦转而使开发人员更容易做出无须中断的架构变更。为代理模式的事件驱动架构构建部署流水线是一项挑战,因为架构的本质是异步通信,但它很难测试
  • 通过适应度函数进行引导性变更:对开发人员来说,在这种架构中编写原子适应度函数很容易,因为事件处理器的个体行为很简单。然而,在这种架构中编写整体适应度函数是必要且复杂的。整个系统的很多行为都依赖于松散服务之间的通信,这导致我们难以测试多层面的工作流。
  • 适当的耦合:代理模式的事件驱动架构所展现的低耦合增强了其进行演进式变更的能力。例如,想为这种架构添加新的行为,只需要将新的监听器添加到现有端点,这样不会影响现有的监听器。在这种架构中,服务和它们所维持的消息契约之间存在耦合,这是功能内聚的一种形式。适应度函数运用消费者驱动的契约等技术来帮助管理集成点,避免其被破坏。

在适合代理模式的 EDA 的业务流程中,事件处理器通常是无状态的、解耦的并且管理自身的数据。这使得演进更加容易,因为它的外部耦合更少,例如数据库耦合。

2. 中介模式

另一个常见的 EDA 模式是中介模式,该模式包含一个额外的组件:作为中介的总线。

事务性的协调是中介架构的主要优势。中介能保证流程的正确性,并生成一条单一状态消息发送给受保人。在代理事件驱动架构中,这样的协调更加困难。例如,要生成统一的通知消息,需要协调通知事件处理器或通过某个显式消息队列来处理这种聚合。虽然异步架构在协调和事务行为方面带来了挑战,但是它们的并行规模极佳。

  • 增量变更:和代理模式类似,在中介模式中,服务通常很小并且是独立的。因此,这种架构在进行增量变更时具有和代理版本相同的优势。
  • 通过适应度函数进行引导性变更:开发人员发现,相比代理模式,为中介模式构建适应度函数更容易。两种模式在测试单个事件处理器上大致相同。然而,构建全系统适应度函数会更容易,因为开发人员可以依赖中介进行协调。例如,在保险工作流中,开发人员可以编写测试并能轻易知晓整个过程是否成功,因为中介在协调这一切。
  • 适当的耦合:虽然中介便利了很多测试场景,但也增加了耦合,妨碍了演进。中介包含了重要的领域逻辑,使得架构量子增大,导致了各个服务间的相互耦合。在这种架构中,当有开发人员进行变更时,其他开发人员必须考虑变更对工作流中其他服务产生的副作用、增加的耦合。

从演进的角度来看,由于降低了耦合,代理架构具有明显优势。在中介模式中,中介充当了耦合点,它将所有受影响的服务绑定在一起。在代理模式中,行为可以通过向已有消息队列添加新的处理器来演进,而不影响其他消息队列。(除了通过流量使队列的负载过重的情况,这种情况可以通过多种架构模式或适应度函数来解决)。由于代理模式在本质上是解耦的,因此更易于演进。

这是权衡架构的典型例子。代理模式在演进能力、异步性、伸缩性及其他一些所期望的特征上具有优势,但不擅长协调事务等一些基本任务。

服务导向架构

现有的服务导向架构(SOA)有很多种类,包括一些混合架构。下面介绍一些常见的架构模式。

1. 企业服务总线驱动的 SOA

有种构建 SOA 的特殊方式在几年前流行起来,那便是通过服务总线构建围绕服务和协调的架构,通常称为企业服务总线(ESB)。服务总线充当复杂事件交互的中介,并处理其他典型的集成架构中的各种琐事,例如消息转换、编排等。

虽然企业服务总线架构通常使用和事件驱动架构相同的构件,但服务的组织方式不同。企业服务总线架构基于严格定义的服务分类方法来组织服务。其样式因组织而异,但都是根据复用性、共享概念及服务范围划分服务。

一个典型的 ESB 驱动的 SOA:

p-14

架构的每一层都有特殊的职责。业务服务抽象定义了业务的粗粒度功能,有时业务人员使用标准工具来定义业务服务。

抽象的业务服务必须调用代码来执行行为,这便是企业服务。它由不同的服务团队负责,旨在实现共享。这些团队的目标是构建可复用的服务集成架构,架构师可以通过编排将服务“缝合”在一起形成业务实现。开发人员以高度复用为目标并设计相应的企业服务。

有些服务并不需要高度可用。例如,系统的某个部分可能需要地理位置,但是没有重要到需要投入资源将其构建成完整的企业服务的程度。

基础设施服务是共享的服务,由基础设施团队负责。它处理一些非功能需求,例如监控、日志、认证 / 授权等。
ESB 驱动的 SOA 的标志是消息总线,它负责以下各类任务。

  • 中介和路由:消息总线能够定位服务并与服务通信。通常,消息总线会维护一张注册表,涵盖服务的物理地址、协议以及调用服务所需的其他信息。
  • 流程编排与编制:消息总线将企业服务组合到一起并管理任务,例如服务调用顺序。
  • 消息增强和转换:集成总线的优势之一是能够代表应用处理通信协议及其他信息的转换。例如,支持 HTTP 协议的服务 A(ServiceA)想调用仅支持 RMI/IIOP 协议的服务 B(ServiceB)。
    当需要此类转换时,开发人员可以配置消息总线,在无形之中完成此类消息转换。

ESB 驱动的 SOA 的架构量子很大。它基本包含了整个系统,和单体应用差不多,但由于它是分布式架构,因此更为复杂。在 ESB 驱动的 SOA 中进行演进式变更非常困难,因为在促进服务复用的同时,其服务分类方法会阻碍普通的变更。

ESB 驱动的 SOA 没有展现出任何演进式架构的特性,所以它在演进性的各个方面得分都不高就不足为奇了。

  • 增量变更:虽然对复用和隔离资源有完善的技术服务分类方法,但这种架构却严重地阻碍了对业务领域进行最常规的变更。大多数 SOA 团队都是按照架构划分的,这导致进行常规的变更需要大量的协调工作。而且 ESB 驱动的 SOA 也是难以操作的。通常它由多个物理部署单元组成,这给协调和自动化带来了挑战。没人会为了敏捷性和操作的易用性而采用企业服务总线。
  • 通过适应度函数进行引导性变更:在 ESB 驱动的 SOA 中,通常很难进行测试。各部分都不完整,都是某个巨大工作流的一个环节,通常无法单独测试它们。例如,某个为了复用而设计的企业服务,测试其核心行为通常很困难,因为它可能只是各个工作流的一部分。为其构建原子适应度函数几乎不可能,这导致需要大规模的整体适应度函数进行端到端测试来完成大部分验证工作。
  • 适当的耦合:从潜在的企业级复用来看,这种奢侈的分类方法是合理的。如果开发人员可以准确地提炼出每个工作流中可复用的精华,那么最终他们就能够一劳永逸地构建出企业的所有行为,而将来的应用开发就变成了连接现有服务。构建 ESB 驱动的 SOA 的目标不是为了系统各部分能够独立演进,所以在这方面它表现得非常糟糕。针对分类复用的设计,损害了它在架构级别进行演进变更的能力。

软件架构并不是在真空环境中构建的,它们始终反映其所处的环境。例如,当 SOA 还是流行的架构样式时,企业不会使用开源的操作系统,所有基础设施都需要付费获得使用许可,且非常昂贵。
虽然可能由于某些原因架构师选择了 ESB 驱动的 SOA,例如处理集成繁重的环境、规模、服务分类或其他合理的原因,但决不是为了演进能力,因为 ESB 驱动的 SOA 并不适合演进。

2. 微服务架构

将持续交付的工程实践和限界上下文的逻辑划分相结合,便形成了微服务的思想基础以及架构量子概念。

在分层架构中,关注点在技术层面,或者说在应用各部分的工作方式,例如持久性、UI、业务规则等。大部分软件架构都关注这些技术维度。然而,还有另一种视角。

p-15

从领域的视角来看,分层架构不具有演进性。在高度耦合的架构中,由于各部分
之间的高耦合度,开发人员很难对其进行变更。然而,在大部分项目中,通常会围绕领域概念进行变更。

相反,设想一个主要通过领域维度进行划分的架构:

p-16

每个服务都围绕 DDD 的领域概念定义,并将技术架构和所依赖的其他组件(例如数据库)封装到限界上下文中,构建了高度解耦的架构。每个服务包含其限界上下文的所有部分,并通过消息(例如 REST 或消息队列)和其他限界上下文进行通信。因此,服务不需要知道另一个服务的实现细节(例如数据库模式),从而避免了不当的耦合。该架构的运作目标是用一个服务取代另一个服务而不影响其他服务。

微服务架构通常遵循以下七个原则:

  • 围绕业务领域建模:微服务设计的重点是基于业务领域,而不是基于技术架构。因此,架构量子反映了限界上下文。一些开发人员错误地认为限界上下文代表某个单独的实体,例如客户。相反,它代表某个业务上下文或工作流,例如商品结账。微服务的目标是创建有用的限界上下文,而不是让开发人员构建更小的服务。
  • 隐藏实现细节:微服务的技术架构封装在基于业务领域的服务边界中。每个领域形成一个物理限界上下文。服务间通过传递消息或资源来集成,而不是通过暴露实现细节集成,例如数据库模式。
  • 自动化文化:微服务架构支持持续交付,它使用部署流水线严格地测试代码,并将一些任务自动化,例如服务器准备和部署。在高速变化的环境中,自动化测试能发挥巨大作用。
  • 高度去中心化:微服务形成了一种无共享架构,其目标是尽可能地减少耦合。通常重复好于耦合。
  • 独立部署:开发人员和运维人员希望可以独立部署每个服务(包括基础设施),反映了服务间的物理限界上下文。微服务架构的一个明显的优点是开发人员可以在不影响其他服务的情况下部署某个服务。而且,开发人员通常会自动化所有的部署和运维任务,例如并行的测试和持续交付。
  • 隔离失败:开发人员会在微服务上下文中和服务间的协调中隔离失败。每个服务都应该处理合理的错误场景并在可能的情况下将其恢复。很多 DevOps 的最佳实践通常在这种架构中出现,例如熔断器模式、舱壁模式等。很多微服务架构遵循着响应式宣言(reactive manifesto),它是一系列运作和协调原则,遵循这些原则可以构建出更加强大的系统。
  • 高度可观察:开发人员不能期望人工监控成百上千个服务(一个开发人员无法观察多个 SSH 终端会话)。因此,在微服务架构中监控和日志成了首要问题。如果运维人员无法监控某个服务,那么它相当于不存在了。

微服务的主要目标是通过物理限界上下文来隔离领域及理解问题领域。因此,它的架构量子就是服务,这使得它成为了演进式架构的优秀示例。

  • 增量变更:在微服务架构中,从各方面来看进行增量变更都很容易。每个服务围绕领域概念形成了限界上下文,使得变更只会影响服务所处的上下文。微服务架构强烈依赖于持续交付的自动化实践,利用部署流水线和现代 DevOps 实践。
  • 通过适应度函数进行引导性变更:开发人员可以很容易地为微服务架构构建原子适应度函数和整体适应度函数。每个服务有着明确定义的边界,开发人员可以在服务组件内进行各种级别的测试。服务间需要通过集成来相互协作,这些集成点也需要测试。幸运的是,随着微服务的发展,先进的测试技术也不断涌现。
  • 适当的耦合:微服务的耦合通常有两种:集成和服务模板。很明显,集成耦合的服务间需要互相调用来传递信息。另一种耦合,服务模板,用于防止有害的重复。如果各种设施能够在微服务内部管理并保持一致,开发人员和运维人员就能从中受益。例如,每个服务都需要包含监控、日志、认证 / 授权和其他一些基本能力。如果将它们交给各个服务团队负责,那么保证兼容性及生命周期管理(例如升级)将会非常困难。通过在服务模板中定义适当的技术架构耦合点,并让基础设施团队管理这些耦合,就能使各个服务团队免于这些苦恼。领域团队只需要扩展这些模板并编写自己的业务行为。基础设施升级后,在下一
    次部署流水线执行时服务模板将自动采用新行为。

微服务架构中的物理限界上下文正好与架构量子概念吻合,架构量子是一种具有高功能内聚性并在物理上解耦的可部署组件。
严格按照领域限界上下文进行服务划分是微服务架构的一个关键原则。微服务将技术架构内嵌到领域中,遵循 DDD 的限界上下文原则,对各个服务进行物理隔离,使得微服务在技术上成为无共享架构。每个服务在物理上都是分离的,可以轻松地替换和演进。由于每个微服务都在其限界上下文中内嵌了技术架构,它们都能以必要的方式演进。因此,微服务演进性的维度与其服务的数量相当,开发人员可以单独处理每个服务,因为每个服务都是高度解耦的。

架构师通常将微服务称为“无共享”架构。这种架构的主要优势是在技术架构层面完全解耦。但是对耦合不满的人通常会提到“不当的耦合”。毕竟,一个没有耦合的软件系统也强不到哪里去。这里的“无共享”实际上是指“没有混乱的耦合点”。

持续交付和 DevOps 的发展为软件开发的动态平衡增添了新的因素。如今,我们可以将主机的定义用于版本控制并将自动化运用到极致。部署流水线并行启动多个测试环境来支持安全的持续部署。由于大部分的软件栈都是开源的,所以软件使用许可等问题不再影响架构。社区对软件开发领域出现的新能力做出反应,产生了更加以领域为中心的架构样式。

在微服务架构中,领域中封装了技术架构和其他架构,使得跨领域维度的演进更容易。关于架构,没有所谓“正确”的观点,它只是反映了开发人员构建项目的目标。如果仅关注技术架构,那么跨该维度的变更将会更容易。然而,一旦忽略了领域视角,那么跨领域维度的演进将和大泥团架构差不多。

系统的各部分如何在无意间互相耦合,这是在架构层面影响应用程序演进能力的一个主要因素。例如,在分层架构中,架构师有意地将某些层耦合在一起。然而,无意地将领域维度耦合到了一起,导致在领域维度难以演进。这是由于架构师围绕技术架构分层来设计,而不是围绕领域。因此,演进式架构的一个重要方面就是跨维度的适当耦合。

3. 基于服务的架构

另一种常用于迁移的架构是基于服务的架构,它和微服务相似,但有三个明显的区别,分别是服务粒度数据库范围集成中间件。基于服务的架构同样以领域为中心,但当开发人员将现有应用重建为更具演进性的架构时,它能解决开发人员所面临的一些挑战。

  • 更大的服务粒度:这种架构中的服务往往更大,相较于纯粹围绕领域概念的服务,其服务粒度更像一个“单体应用”。虽然它仍以领域为中心,但是更大的服务导致变更单元(开发、部署、耦合以及其他一系列因素)也更大,增加了变更的难度。当架构师评估一个单体应用时,他们通常会观察围绕常见领域概念的粗粒度进行划分。基于服务的架构在运维隔离上和微服务的目标相同,但是更难实现。由于服务变大,开发人员必须考虑更多的耦合点,并且更大的代码段本身就更为复杂。理想情况下,架构应该支持和微服务一样的部署流水线和小的变更单元。当开发人员修改某个服务时,它应该触发部署流水线来重建相关服务,包括应用。
  • 数据库作用域:无论服务的分解程度如何,基于服务的架构往往都使用单体数据库。在很多应用中,将多年(甚至十多年)难以管理的数据库模式重建为原子大小的微服务是不可行甚至不可能的。虽然在某些情况下无法分解数据可能造成不便,但在某些问题域中是不可能的。
    事务完整性很强的系统不太适合微服务,因为在服务间进行事务行为协调的成本太高了。由于对数据库的要求更宽松,有着复杂事务需求的系统更适合基于服务的架构。
    虽然数据库保持单一整体,但依赖于数据库的组件可能变得更为细化。因此,尽管服务和基础数据之间的映射可能改变,但这所需的重建较少。
  • 集成中间件:微服务和基于服务的架构之间的第三个区别涉及通过中介(如服务总线)来进行外部协调。开发人员在构建全新的微服务应用时不用担心老的集成点,然而很多环境中仍然有大量遗留系统在做着有价值的工作。集成总线(如企业服务总线)擅长将不同协议和消息格式的各种服务整合到一起。如果架构师发现开发环境中集成架构的优先级最高,可以使用集成总线添加和变更相关服务。
    使用集成总线是一个典型的架构折中。使用集成总线,开发人员能够以更少的代码来将应用集成到一起,并能使用总线来模仿服务间的事务协调。然而使用总线增加了组件间的架构耦合,在不与其他团队协调的情况下,开发人员无法独立完成变更。适应度函数可以降低一些协调成本,但是开发者带来的耦合越高,系统演进就越难。

演进评估:

  • 增量变更:在这种架构中进行增量变更相对可行,因为每个服务都是以领域为中心的。软件项目中的大多数变更都是围绕领域发生的,在变更单元和部署量子之间保持了一致性。由于服务通常会更大,所以无法像微服务那样敏捷,但还是保留了微服务的很多优点。
  • 通过适应度函数引导变更:开发人员发现,在基于服务的架构中构建适应度函数通常比在微服务中构建更难,这是由于更高的耦合(通常是数据库耦合)和更大的限界上下文。高耦合的代码通常使编写测试更困难,同时数据耦合度的上升也会导致一系列问题。基于服务的架构中创建的限界上下文越大,系统内部的耦合点就越多,使得测试和其他一些诊断变得更复杂。
  • 适当的耦合:耦合通常是开发人员选择基于服务的架构而不是微服务架构的原因,例如,分解数据库模式的难度太大、重建单体应用时面临的高度耦合问题等。构建以领域为中心的服务有助于确保适当的耦合,同时服务模板有助于构建适当的技术架构耦合。

基于服务的架构内在的演进能力肯定比 ESB 驱动的 SOA 架构要好。开发人员偏离限界上下文的程度决定了架构量子的大小和破坏性耦合的数量。
基于服务的架构在纯粹的微服务思想和很多项目的实现之间做出了很好的折中。通过放宽对服务大小、数据库独立性和偶然但有用的耦合的限制,该架构解决了微服务中最另人头疼的问题,同时还保留了许多长处。

“无服务”架构

“无服务”架构是软件开发动态平衡中最近出现的变化,它的两大含义都适用于演进式架构。

BaaS(后端即服务)是那些明显或从根本上依赖于第三方应用或云端服务的应用。其简单示例如图:

p-17

开发人员编写少量代码甚至无须编写代码。架构由相连的服务组成,包
括认证、数据传输和其他集成架构组件。这种架构之所以吸引人是因为组织编写的代码越少,他们需要维护的代码就越少。然而,重度集成的架构有其自身的挑战。

另一类无服务架构是 FaaS(功能即服务),它对基础设施要求不高(至少是在开发人员看来),为每个请求提供基础设施,自动处理水平扩展,还承担环境准备和其他一系列管理职责。在 FaaS 中,功能由服务提供商所定义的事件类型触发。

通常假设 FaaS 功能是无状态的,因而调用者需要处理状态。

  • 增量变更:在无服务架构中,增量变更需要重新部署代码,基础设施相关的所有问题都在“无服务”的抽象背后。这种架构非常适用于部署流水线,在开发人员开展变更时使用部署流水线进行测试和增量的部署。
  • 通过适应度函数引导变更:在此类架构中,为了使集成点保持一致,适应度函数至关重要。因为服务间的协调是关键,开发人员需要编写更高比例的整体适应度函数,并在多个集成点上下文中运行它们,以保第三方 API 不变。架构师频繁地在集成点之间构建防腐层,以避免“供应商为王”反模式。
  • 适当的耦合:从演进式架构的角度来看,FaaS 吸引人的原因是它消除了考虑因素中多个不同的维度,例如技术架构、运维和安全问题等。虽然这种架构可能易于演进,但在实际考虑方面受到了严重的限制,并将大量复杂问题转嫁给了调用方。例如,虽然 FaaS 能处理弹性的水平扩展,但是调用方必须处理所有的事务行为和其他复杂的协调工作。在传统应用中,通常由后端处理事务协调。然而,如果 BaaS 不支持该行为,那么协调工作就必然会转嫁给用户接口(服务请求者)。

虽然无服务架构有很多吸引人的特性,但也存在限制。团队需要构建的大部分内容都很快捷,但有时构建完整的方案会令人很苦恼。

架构师选择架构前,必须针对需要解决的现实问题评估架构。确保架构与问题领域匹配。不要尝试强行使用不合适的架构。

控制架构量子大小

架构量子的大小很大程度上决定了开发人员进行演进式变更的难易程度。大的架构量子很难演进,例如单体架构和 ESB 驱动的 SOA 架构,因为每次变更都需要进行协调。低耦合的架构为轻松的演进提供了更多途径,例如代理模式的 EDA 和微服务。

架构演进的结构限制取决于开发人员处理耦合和功能内聚的水平。如果开发人员构建出的模块化组件系统具有明确定义的集成点,那么演进会更容易。例如,如果开发人员构建了一个单体应用,但是致力于良好的模块化和组件分离,那么该架构将更易于演进,因为解耦使得它的架构量子更小。

架构量子越小,架构的演进能力越强。


五、演进式数据

当提到 DBA 时,我们指的是那些设计数据架构、编写代码访问数据并在应用中使用数据的人;编写在数据库中执行的代码,维护数据库并优化其性能的人;在故障发生时,确保数据库正常备份和恢复的人。通常 DBA 和开发人员是应用的核心构建者,他们应该紧密合作。

演进式数据库设计

数据库的演进式设计指开发人员能够根据需求的不断变化来构建数据库结构并使其演进。数据库模式是抽象的,和类的层次结构类似。当现实世界发生变化,这些变化需要反映在开发人员和 DBA 所建立的抽象当中。否则,抽象将逐渐脱离与现实世界的同步。

数据库模式演进

数据库设计演进的关键在于数据库模式代码演进。持续交付使得传统数据孤岛能够适应现代软件项目的持续反馈环。 开发人员必须以和代码变更相同的方式处理数据库结构的变更,它们必须是经过检验的、版本化的和增量的。

  • 经过检验的:为了保证稳定性,DBA 和开发人员应该严格测试数据库模式的变更。如果开发人员使用了数据映射工具,例如对象关系映射器(ORM),那么为了使映射关系和数据库模式保持同步,他们应该考虑为此添加适应度函数。
  • 版本化的:开发人员和 DBA 应该对数据库模式和那些使用它的代码一同进行版本控制。源代码和数据库模式是共生的,缺一不可。人为分离这两种必然耦合的事物的工程实践将导致低效。
  • 增量的:和代码变更一样,变更数据库模式应该是渐进的,即随着系统的演进而增量地进行。现代工程实践往往使用自动化的迁移工具,从而避免手动更新数据库模式。

利用数据库迁移工具使得开发人员(或 DBA)能够对数据库进行小的增量变更,这一过程将作为部署流水线的一部分自动完成。

共享数据库集成

它以关系型数据库为数据共享机制,如

p-18

使用数据库作为集成点僵化了所有共享项目的数据库模式。

常用扩展 / 收缩重构模式来化解这种耦合。很多数据库重构技术通过在重构的过程中创建过渡阶段来避免时间问题

p-19

使用该模式,开发人员会设置开始状态和结束状态,它们会在转变过程中分别维持新、旧两种状态。这个过渡状态允许向下兼容,同时还给企业内的其他系统足够的时间来跟上变化。

不当的数据耦合

数据和数据库是大多数现代软件架构中不可或缺的部分,如果开发人员忽略这一关键因素,在尝试演进架构时将会遭遇挫折。

数据模式是宝贵的,因为它永久有效。

通常 DBA 通过添加另一张连接表来扩展数据库模式定义。与其冒着破坏现有系统的风险更改数据库模式,他们往往添加一张新的数据表,并通过关系型数据库原语将其与原始数据表关联起来。虽然这样做短期内有效,但是它混淆了真实的根本抽象,因为在现实世界中,一个实体是通过多个事物表现的。随着时间推移,那些几乎不懂重构数据库模式的 DBA 通过拜占庭分组和聚束策略构建出了日渐僵化的模式。

遗留的数据库模式和数据具有价值,但它们也妨碍了系统的演进能力。架构师、DBA 和业务代表需要展开坦诚的对话,讨论哪个对组织更有价值,是永久地保存遗留数据还是进行演进式变更的能力。我们应识别真正有用的数据并将其保留下来,将旧数据作为参考但不将其纳入演进式开发的主流。

拒绝重构数据库模式或删除旧数据会使架构耦合到过去,这将导致重构难以进行。