购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第4章
架构分解

10月4日,星期一,10:04

现在Addison和Austen得到了批准,可以向分布式架构迁移并拆分Sysops Squad这个单体应用了,他们现在需要确定开始的最佳方式。

“这应用太大了,完全不知道从哪里下手。和大象那么大!”Addison惊叫道。

“好吧,”Austen说,“怎么吃一只大象呢?”

“哈!我听过这个笑话,Austen。一口一口吃呗!”Addison大笑。

“没错。我们可以用同样的原理对待Sysops Squad应用,”Austen说,“为什么不开始一点点拆解它呢?还记得我说过报表是导致系统卡住的一个原因吗?也许我们可以从那里开始。”

“那应该是个不错的开始,”Addison说,“但是数据怎么办?只是把服务拆开解决不了这个问题。我们需要把数据也分开才行,甚至可以新建一个报表数据库,再把数据喂给它。”

“你说得对,”Austen说,“那知识库功能怎么样?那部分也相对独立,而且可能抽取出来更简单。”

“也对。那调查功能怎么样?那部分应该也比较容易分出来,”Addison说,“问题是,我总觉得我们应该用一些更成体系的方式来解决这个问题,而不是走一步算一步。”

“也许Logan可以给我们一些建议。”Austen说。

Addison和Austen去见了Logan,并讨论了他们想到的几种拆分应用的方式。他们向Logan解释说想从知识库和调查功能开始,但不知道接下来做什么。

“你们提出的方式,”Logan说,“正是叫作大象迁移(Elephant Migration)的反模式。开始的时候,一次吃一口大象看起来是个不错的方式,但大多数情况下,它会因为毫无组织的推进最终搞出一个分布式大泥球来,也有人叫它分布式单体。我不推荐这样做。”

“所以,还有什么别的方式呢?有没有什么模式我们可以利用来拆解应用呢?”Addison问道。

“你们需要从整体上来看这个应用,考虑战术性切分或基于组件分解这两种方式,”Logan说,“这是我知道的最有效的两种方式。”

Addison和Austen看向Logan:“但我们怎么知道应该使用哪个呢?”

架构模块化解释了为什么要拆分单体应用,架构分解则描述了如何拆分单体应用。拆分错综复杂的大型单体应用是一件繁复且耗时的工作,了解开始这样的工作是否可行并知道如何推进是十分重要的。

基于组件分解和战术分叉是两种常见的拆分单体应用的方式。基于组件分解是一种应用各种重构模式来精炼和提取组件(应用的逻辑组成模块)的方法,用渐进可控的方式转化为分布式架构。战术分叉的方式则采用复制现有应用,从服务中削掉不需要的部分,类似于雕塑家用花岗岩或大理石创造出精美的艺术品。

哪种方式最有效率呢?这个问题的答案当然是看情况。决定单体应用拆分方式的主要因素就是当前代码的结构化程度。代码库里的组件和组件边界是清晰划分的,还是大体上就是一团毫无组织的大泥球?

如图4-1所示的流程图,架构分解的第一步就是确定这个代码库到底能不能分解。我们在下一章会详述这个话题。如果架构是可以分解的,下一步是确认源代码是不是一团毫无组织的乱麻,没有定义清晰的组件。如果是这样,战术分叉可能才是正确的方向(详见4.3节)。然而,如果源代码文件用组织的方式结合了功能,组成定义清晰(甚至是松散定义)的组件,那么基于组件分解的方式(详见4.2节)就是个好办法了。

本章介绍这两种方式,在第5章中,我们会详述基于组件分解的每种模式。

4.1 代码库能分解吗

当代码库缺乏内在结构的时候会怎样呢?它还能被分解吗?这类软件有一个俗称——大泥球反模式( https:// oreil.ly/7WkHf ),由Brian Foote于1999年在一篇同名文章中提出( http://www.lapu tan.org/mud )。举例来说,如果一个复杂的Web应用,它的事件处理器直接调用数据库,并且没有模块化,就可以称为大泥球架构了。一般来说,架构师不会花很多时间为这类系统创建模式。软件架构涉及内部结构,而这些系统缺乏这种决定性特征。

图4-1:选择分解方式的决策树

不幸的是,如果没有精心治理,许多软件系统会退化成大泥球。任何架构重组工作的第一步都需要架构师确定一个重组计划,而这又需要架构师了解内部结构。架构师必须回答的关键问题是:这个代码库还有救吗?换句话说,它可以参考某种分解模式,还是其他方法更合适?

没有单一的衡量标准可以确定代码库是否具有合理的内部结构——评估由一个或多个架构师来确定。然而,架构师确实有工具来帮助确定代码库的宏观特征,尤其是耦合度量,以帮助评估内部结构。

4.1.1 传入耦合和传出耦合

在1979年Edward Yourdon和Larry Constantine出版的 Structured Design:Fundamentals of a Discipline of Computer Program and Systems Design 一书中定义了很多核心概念,包括传入(afferent)耦合和传出(efferent)耦合。传入(afferent)耦合度量的是一个代码工件(组件、类、函数等)输入连接的数量。传出(efferent)耦合度量的是其对其他工件输出连接的数量。

当系统结构改变时,可以关注这两个度量值的变化。例如,在将一个单体分解成分布式架构时,架构师可以发现公用类,例如Address(地址)。当构建一个单体时,复用如Address的核心概念很常见,并且鼓励程序员这样做,但是当拆分单体时,架构师就必须找出系统里有多少部件用到了这个公用类。

基本上每个平台都有工具允许架构师分析代码的耦合特征,以帮助重构、迁移或者理解代码库。不同的平台有很多工具提供类或组件关系的矩阵视图,如图4-2所示。

在这个例子中,Eclipse插件提供了JDepend的可视化输出,包括每个包的耦合分析,还有下一节将着重介绍的一些聚合指标。

图4-2:Eclipse里JDepend的耦合关系分析视图

4.1.2 抽象性和不稳定性

Robert Martin(软件架构界的一位知名人物)在20世纪90年代后期一本关于C++的书中创造了一些衍生指标,它们可以被应用到任何面向对象的语言中。这些指标(抽象性和不稳定性)度量的是代码库内部特征的平衡度。

抽象性指的是抽象工件(抽象类、接口等)和具象工件(实现类)之间的比例。它表示的是抽象和实现的程度。抽象元素是代码库的特征,帮助程序员更好地理解整体功能。例如,一个代码库里只有一个main()方法和一万行代码,在这项指标里它差不多只有0分,将会十分难以理解。

方程4-1为抽象性公式。

方程4-1:抽象性

在这个方程中, m a 代表代码库中的抽象元素(接口或抽象类),而 m c 代表具象元素。架构师通过计算抽象工件和具象工件总数的比例来得出抽象性。

另一个衍生的指标不稳定性是传出耦合和传入耦合的比例,如方程4-2所示。

方程4-2:不稳定性

在方程中, C e 代表传出耦合, C a 代表传入耦合。

不稳定性指标测定代码库的易变性。高不稳定性的代码库,因其高耦合度,通常更容易随变化而崩溃。考虑这两个场景, C a 都是2。第一个场景中, C e =0,因而不稳定性得到0分。另一个场景里, C e =3,不稳定性得分为3/5。因此,组件的不稳定性得分反映的是当关联组件发生变化时,存在多少潜在的被动修改。一个组件的不稳定性得分趋近于1意味着它是高度不稳定的,趋近于0表示稳定或者僵化:如果这个模块或组件内部大部分是抽象元素则意味着稳定;如果里面大部分是具象元素则意味着僵化。然而,高稳定性的弊端就是缺少复用——如果每个组件都自给自足,那么重复是必然的。

一言概之,如果一个组件的 I 值趋近于1,则可以肯定它是十分不稳定的。然而,一个组件的 I 值趋近于0,则可能是稳定的或僵化的。如果大部分是具象元素,那么就是僵化。

因此,通常来说,重要的是结合 I A 的得分一起来看,而不是单看某一个。这也是为什么我们接下来要考虑主序列。

4.1.3 与主序列的距离

架构师拥有的少数几个衡量架构结构的整体指标之一便是与主序列的距离,它是基于稳定性和抽象性的衍生指标,如方程4-3所示。

方程4-3:与主序列的距离

D =| A + I -1|

在等式中, A =抽象性, I =不稳定性。

与主序列的距离指标设想了抽象性和不稳定性的完美关系,落在这条理想线附近的组件在这两个互斥的考量里维持一个健康的水平。例如,把某个特定的组件在图上画出来,开发人员就能够计算出与主序列的距离,如图4-3所示。

图4-3:特定组件与主序列的标准化距离

开发人员先画出待评估的组件,然后测量它和理想线之间的距离。距离越近,则代表这个组件越均衡。落在远端右上角的组件进入了架构师所谓的无用区域:代码太过抽象变得难以使用。反过来,落在左下角的则是痛苦区域:代码里有太多具象实现,没有足够的抽象,变得脆弱而难以维护,如图4-4所示。

在遇到不熟悉的技术栈、迁移或技术债评估的时候,很多平台有工具来提供这个指标,辅助架构师分析代码库。

与主序列的距离对于想要重构应用的架构师来说有什么帮助呢?与建筑项目一样,挪动一个根基不稳的大型架构会有风险。相似地,如果架构师想要重构一个应用,改善其内部结构有助于实体迁移。

图4-4:无用区域和痛苦区域

这个指标也为内部结构的平衡性提供了一个很好的线索。如果架构师评估代码库,有很多组件落在了无用区域和痛苦区域,试图将其内部结构巩固到可以被修复的程度,可能只会浪费时间。

根据图4-1中的流程图,一旦架构师断定代码库是可分解的,下一步就是决定采用哪种方式来分解应用。接下来描述两种分解应用的方式:基于组件分解和战术分叉。

4.2 基于组件的分解

从过去的经验来看,在将单体应用拆分成微服务这类高度分布式架构的过程中,最大的难点和复杂度来源于糟糕的架构组件定义。这里我们将组件定义为应用的组成部件,在系统中有明确的角色和职责,以及定义完善的操作集合。在大部分应用中,组件通过命名空间或目录结构来声明,并通过组件文件(或源文件)来实现。例如,在图4-5中,目录结构 penultimate/ss/ticket/assign 表示一个叫作Ticket Assign的组件,其命名空间为penultimate.ss.ticket.assign。

图4-5:代码库的目录结构成为组件的命名空间

在把单体应用拆分为分布式架构时,从组件(而不是单独的类)开始创建服务。

在经历多年迁移单体应用到分布式架构的集体工作之后,我们发展出了一套基于组件分解的模式,可以帮助单体应用的迁移,详见第5章。这些模式涵盖了源代码重构,从而形成职责明确的组件,最终可以形成服务,以减少将应用迁移到分布式架构过程中的工作量。

这些基于组件分解的模式,本质上赋能了从单体架构到基于服务的架构的迁移,我们在第2章定义了基于服务的架构这一概念,详细描述请参见 Fundamentals of Software Architecture 一书。基于服务的架构是一种微服务架构风格的混合体,应用被拆分成粗粒度的领域服务,可以独立部署,并包含特定领域的所有业务逻辑。

迁移到基于服务的架构可以作为最终目标,也可以作为走向微服务的其中一步:

● 作为垫脚石,它允许架构师决定哪些领域需要进一步细化成微服务,哪些可以保持粗粒度的领域服务(详见第7章)。

● 基于服务的架构对数据库拆分不做要求,因此架构师可以专注领域和功能的分离,暂时不去理会数据库的分解(详见第6章)。

● 基于服务的架构不需要任何运维自动化或容器化。每个领域服务可以和原始应用使用相同的部署工件(例如EAR文件、WAR文件、Assembly等)。

● 迈向基于服务的架构是纯技术的,这意味着不需要业务干系人的参与,也不需要任何IT部门组织结构的调整,不会改变测试环境和部署环境。

当把单体应用拆分为微服务时,先考虑迁移到基于服务的架构,作为走向微服务的第一步。但如果代码库就是一团毫无组织的大泥球,没什么组件呢?这时我们就需要战术分叉了。

4.3 战术分叉

战术分叉模式是由Fausto De La Torre( https://faustodela tog.wordpress.com )提出的,对于基本上是一团大泥球的架构来说,该模式是一种比较实际的重组方式。

通常来说,当架构师思考如何重组代码库时,他们想的都是提取一部分出来,如图4-6所示。

图4-6:从系统中提取一部分

而换种思考方式,为了从系统中分离一部分,也可以把不需要的部分删除,如图4-7所示。

图4-7:删除不想要的部分是另一种分离系统的方式

在图4-6中,开发人员必须处理应接不暇的耦合关系,它们也正是这类架构的特点。当他们想提取一部分的时候,就会发现因为依赖项的关系,需要从单体里一起带过来的东西层出不穷。在图4-7中,开发人员只需要删除不需要的代码,但保留了依赖项,从而避免了提取过程中无穷无尽的追踪依赖。

提取和删除之间的差异激发了战术分叉模式。在这种分解方法中,系统从单个单体应用开始,如图4-8所示。

图4-8:重组之前,一个单体包含几个部分

该系统包含了几个领域行为(图中用简单的几何形状表示),没有太多内部组织。并且,在这个场景中,想要实现的目标是两个团队从现有的单体中创建两个服务,其中一个包含六边形和正方形领域,另外一个包含圆形领域。

战术分叉的第一步就是克隆整个单体,给每个团队一份代码库的完整备份,如图4-9所示。

每个团队都得到了整个代码库的备份,他们开始删除自己不需要的代码(如图4-7所提到的),而不是提取需要的代码。开发人员通常会觉得这样做更容易一些,尤其是在紧密耦合的代码库里,因为不用考虑提取那些由于高度耦合造成的大量依赖项。相反,在删除策略中,一旦功能被剥离出来,删除任何不影响运行的代码就可以了。

随着模式持续进行,团队开始分离目标部分,如图4-10所示。每个团队持续不断地删除不需要的代码。

图4-9:第一步,克隆单体

图4-10:团队持续重构,删除不需要的代码

作为战术分叉模式的完成态,团队将原有单体应用拆分为两个部分,在每个部分里保留了粗粒度的行为结构,如图4-11所示。

现在重组完成,最终呈现出两个粗粒度的服务。

图4-11:战术分叉的最终状态形成两个服务

权衡分析

对比更加正式的分解方式来说,战术分叉是一个可行的备选项,适合那些几乎没有内部结构的代码库。与所有架构中的实践一样,它有利也有弊。

优点

● 团队可以立即开始工作,基本不需要事先分析。

● 开发人员认为删除代码比提取代码更容易。因为高度耦合,所以从一团乱麻的代码库里提取代码是很困难的,而删除没用的代码只需要通过编译或者简单的测试就可以验证。

缺点

● 最终服务里很可能会残留一大堆单体中的遗留代码。

● 除非开发人员花很多时间,否则新衍生出的服务里的代码并不会比单体里乱糟糟的代码好到哪里去,只是数量上少了一些。

● 在公用代码和公用组件的命名上可能会存在不一致的情况,导致很难识别公用代码并保持其一致性。

这个模式的名字很恰当(所有好模式的名字都应该这样),它提供的是一种战术方式来重组架构,而不是战略方式,允许团队快速将至关重要的系统迁移到新一代中去(尽管它是一种无组织的方式)。

4.4 Sysops Squad的传奇故事:走上分解之路

10月29日,星期五,10:01

现在Addison和Austen对两种方式都有了一些理解,他们在主会议室碰面,用抽象性和不稳定性指标来分析Sysops Squad应用,以此来决定哪种方式更合适。

“看这个,”Addison说,“大部分代码都在主序列附近,只有一小部分游离在外。但我觉得我们可以得出结论,拆分应用是可行的。所以下一步是决定用哪种方式。”

“我很喜欢战术分叉的方式,”Austen说,“它让我想起了著名的雕塑家,当被问及是如何从坚硬的大理石中雕刻出精美的作品时,他们回答说只是从大理石中剔除掉本就不应该在那儿的部分。我感觉Sysops Squad可以成为我的雕塑作品!”

“等一等,米开朗基罗,”Addison说,“之前是体育,现在开始雕塑了?你需要想好业余时间到底要做什么了。我不太喜欢战术分叉的原因是每个服务里都会存在重复的代码和公用功能。我们的大部分问题都与可维护性、可测试性和整体可靠性相关。你能想象同样的变更需要同时在不同的服务里做几遍吗?简直就是噩梦!”

“但是说真的,有多少公用功能呢?”Austen问道。

“我不确定,”Addison说,“但我知道有很多基础设施相关的公用代码,比如日志代码和安全代码。我还知道很多数据库调用是从应用的持久层共用的。”

Austen停下来思考了Addison的观点:“也许你是对的。因为我们已经有明确定义的组件边界了,用基于组件分解的方式虽然慢,但也可行。”

Addison和Austen就组件分解达成了一致,同意这是分解Sysops Squad应用的合理方式。Addison为这个决定写了一份ADR,概述了利弊,以及为什么选择基于组件分解的方式。

ADR:使用基于组件分解的方式进行迁移

背景

我们将会把Sysops Squad这个单体应用拆分为几个可独立部署的服务。我们考虑了两种将其迁移到分布式架构的方式:战术分叉和基于组件分解。

决策

我们将使用基于组件分解的方法,将现有的单体Sysops Squad应用迁移到分布式架构。

该应用有明确的组件边界,适合采用基于组件分解的方法。

这种方法降低了不得不在每个服务中维护重复代码的可能性。

使用战术分叉的方式,我们必须预先定义服务的边界,才能知道需要创建多少个分叉的应用。使用基于组件分解的方法,服务的定义将通过组件分组自然而然地显现出来。

考虑到我们目前的应用程序在可靠性、可用性、可扩展性和工作流方面所面临的问题,使用基于组件分解比使用战术分叉方法提供了一个更安全、更可控的增量迁移。

后果

比起战术分叉,基于组件分解的迁移方式很可能会花费更长的时间。然而,我们认为这么做是利大于弊的。这种方式允许团队里的开发人员紧密合作来识别公共功能、组件边界和领域边界。战术分叉则需要把团队拆成小而独立的团队,工作在每个复制出去的应用程序上,更小的团队意味着更多的团队间协调工作。 Vi/7j/+zMs/FS9zZ1oKbf7So0w4jhehYQJlnlB7k+TrtmA+q9i1AMMwc/YAQSNlt

点击中间区域
呼出菜单
上一章
目录
下一章
×