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

第3章
架构模块化

9月21日,星期二,09:33

虽然他们曾经在这个会议室里开过上百次会,但今天的氛围不一样,很不一样。人都来了,但没人私下聊天。只有沉默,死一般的沉默,仿佛可以用刀切断一样。对于今天会议的议题来说,这个老套的形容再合适不过了。

业务领导和发生故障的Sysops Squad工单系统发起人前来与系统架构师Addison及Austen碰面,以表达他们对IT部门无法解决与工单系统相关的数不清的问题和担忧和沮丧。“如果没有能用的系统,”他们说过,“我们可能就无法继续这条业务线了。”

当这个气氛紧张的会议结束时,业务发起人一个接一个地悄然离场,留下Addison和Austen独自在会议室里。

“这个会糟透了,”Addison说,“我简直不敢相信他们真的把这破系统的锅全甩到我们头上。”

“是啊,我知道,”Austen说,“尤其是提到可能要砍掉产品支持业务线那部分。我们会被分到其他项目中,也可能更惨,直接让我们走人。虽然我也想把所有时间都花在球场或者冬季滑雪上,但我可不能失业。”

“我也不能,”Addison说,“并且,我很喜欢现在的开发团队,不忍心看它就这么散了。”

“我也是,”Austen说,“我还是相信拆分系统可以解决大部分问题。”

“我同意,”Addison说,“但我们要如何说服业务负责人投入更多的钱和时间来做架构重构呢?你也看到了刚才会上他们是怎么抱怨花了多少钱给我们到处打补丁,却把流程里的问题搞得更多了。”

“你是对的,”Austen说,“在这个节骨眼上,他们不可能同意花大价钱和大把时间做架构迁移。”

“但如果我们都同意必须把系统拆分才能继续用它,到底要如何说服业务负责人给我们预算和时间来重组Sysops Squad系统呢?”Addison问道。

“难倒我了,”Austen说,“我们看看Logan有没有空一起讨论一下这个问题吧。”

Addison看了看线上,发现Logan正好有空。Addison发消息说他们想拆分现有的单体应用,但不知道如何说服业务负责人。Addison在消息中解释他们陷入困境了,需要人指点迷津。Logan同意来他们的会议室碰头聊一下。

“你怎么确信拆分Sysops Squad系统可以解决所有问题?”Logan问道。

“因为,”Austen说,“我们已经试过不停地给代码打补丁了,好像没什么用,还是有太多的问题。”

“你完全没在回答我的问题,”Logan说,“让我换个方式问,你用什么来保证拆分系统能解决问题,而不仅仅是花更多钱和浪费更多宝贵的时间?”

“好吧,”Austen说,“说实话,我们没法保证。”

“那你是怎么知道拆分系统是正确的方向的?”Logan问道。

“我已经告诉过你了,”Austen说道,“因为其他的方式都不行!”

“抱歉,”Logan说,“但是你也知道业务负责人可不听你这一套。用这个理由你是得不到预算的。”

“所以,怎么说才能让业务负责人信服呢?”Addison问道。

“好吧,”Logan说,“想做好这个量级的系统的业务案例,首先必须了解架构模块化的好处,把这些好处和当前系统遇到的问题对应起来,然后再分析和证明拆分系统带来的利弊。”

今天的业务面临变革的洪流,市场演变似乎一直在以惊人的速度发展。业务驱动因素(例如兼并和收购)、商业活动的竞争加剧、消费者需求飞升、新增的创新(比如通过机器学习和人工智能实现的自动化)都需要底层计算机系统的变化。在很多案例中,这些计算机系统的变化都需要改变底层架构来支持。

然而正在持续快速变化的并不是只有业务——计算机系统赖以生存的技术环境也在变化。容器化、基于云的基础设施迁移、DevOps的采用,甚至持续部署流水线的进一步提升都对这些计算机系统的底层架构产生了影响。

在如今的世界,想要在软件架构层面控制所有持续且快速的变化是非常困难的。软件架构是系统的基础结构,因此通常认为应该是相对稳定的,不应该经常变化,如同摩天大楼的地基一样。然而不同于建筑物的架构,软件架构必须持续变化才能适应当今的业务和技术环境产生的新需求。

考虑当今商业活动中越来越多的兼并和收购。当一家公司收购了另外一家,带来的不仅仅是物理资产(例如员工、楼宇、库存等),也包括更多客户。作为兼并和收购的结果,用户量的增长是其中任何一家公司的现有系统可以通过伸缩来支撑的吗?可伸缩性(scalability)是兼并和收购中的重要部分,同敏捷性和可扩展性(extensibility)一样,都是架构需要考虑的一部分。

大型单体系统(单一部署)通常无法提供可以支持大部分兼并收购这种程度的可伸缩性、敏捷性和可扩展性。额外的机器资源容量(线程、内存和CPU)很快就会用光。为了说明这一点,考虑图3-1所示的玻璃水杯。玻璃杯代表的是服务器(或虚拟机),水代表应用程序。当单体应用逐渐扩大去处理客户需求和用户量的增长(无论来源于兼并、收购还是公司成长),它们开始消耗越来越多的资源。当杯子里的水越来越多(代表单体越来越大),杯子就要满了。加一个杯子(代表额外的服务器或虚拟机)于事无补,因为新的杯子需要盛的水和之前的一样多。

图3-1:一满杯水代表大型单体接近其容量极限

架构模块化的一个方面就是把大型单体应用拆分成更小且隔离的部分,为未来提供增长和伸缩的空间,同时更容易应对持续且快速的变化。反过来,这些能力也可以帮助企业实现战略目标。

在水杯例子中,通过增加一个空玻璃杯,并且把水(应用)分成两份,可以将一半的水倒入新杯中,提供了额外50%的容量,如图3-2所示。水杯比喻是给业务干系人和CxO这类高管解释架构模块化(拆分单体应用)的一个绝佳比喻,他们才是最终要为这个架构重构付钱的人。

图3-2:两个半杯水代表了拆分后的应用有大量可供增长的空间

增加可伸缩性只是架构模块化的好处之一。另外一个重要的好处是敏捷性,即快速响应变化的能力。2020年1月,David Benjamin和David Komlos发表在 Forbes https://oreil.ly/2im3v )上的一篇文章指出:

能够按需进行大胆果断的路线修正,并有效且紧迫地执行,是成败的关键。

企业想在当今世界里存活,业务必须敏捷。然而,即使业务干系人可能做到当机立断并迅速转型,公司的技术部门却不一定能跟上这些新指令来改变现状。让技术能和业务齐头并进(或者反过来说,不拖业务后腿)需要一定程度上的架构敏捷性。

3.1 模块化的驱动因素

除非有明确的业务驱使,否则架构师不需要把系统大卸八块。拆分应用最主要的业务因素包括上市速度(有时也叫上市时间),在商业活动中达到一定程度的竞争优势。

上市速度通过架构敏捷性来实现。敏捷性是一个复合架构特征,由很多其他架构特征组成,包括可维护性、可测试性和可部署性。

竞争优势通过上市速度、可伸缩性以及应用整体可用性和容错性达成。一家公司做得越好,发展得越大,越需要更多的可伸缩性来支持日益增加的用户活动。容错性,即应用在发生错误时仍然可以继续运行的能力,是保证即使应用的某一部分出错、其他部分仍然可以正常工作的重要能力,最小化终端用户受到的影响。图3-3说明了模块化的技术及受其影响的业务驱动因素(框起来的部分)之间的关系。

在今天快节奏且变幻莫测的市场中,业务必须敏捷才能生存,这意味着底层架构也得随之敏捷。如图3-3所示,在如今的商业活动中,5个关键的架构特征支撑了敏捷性、上市速度,最终获得竞争优势,它们是可用性(容错性)、可伸缩性、可部署性、可测试性和可维护性。

图3-3:模块化的驱动因素及其之间的关系

需要注意的是,架构模块化并不一定意味着分布式架构。可维护性、可测试性和可部署性在单体架构里同样可以实现,例如模块化单体甚至微内核架构。这些架构风格都基于其组件的组织形式提供了一定程度上的模块化。例如,在模块化单体中,用结构良好的领域组织组件构成了俗称的领域划分架构(见 Fundamentals of Software Architecture 一书的第8章)。在微内核架构中,功能被分层为独立的插件组件,缩小了测试和部署的范围。

3.1.1 可维护性

可维护性是指添加、变更或者删除功能的难易程度,也包含系统内部的变更,比如打补丁、服务框架升级、第三方库升级等。与大多数复合架构特征一样,很难客观地定义可维护性。Alexander von Zitzewitz[hello2morrow( http://www.hello2morrow.com )的架构师和创始人]写过一篇关于定义应用的可维护性级别的客观指标的文章( https://oreil.ly/TbFjN )。尽管von Zitzewitz的可维护性指标相当复杂,引入了很多因素,但它的原始形式如下:

这里的 ML 指的是系统整体的可维护性水平(0%~100%), k 是系统内部所有逻辑组件的数量, c i 指的是给定组件的耦合水平,特指传入耦合水平。这个等式想表达的是组件间传入耦合水平越高,代码库的整体可维护性越低。

先把复杂的数学问题放一边,基于组件(应用架构的基础构件)的应用的可维护性度量指标有如下这些:

组件耦合度

组件间联系的程度和方式。

组件内聚度

组件内操作相互关联的程度和方式。

循环复杂度

组件内部间接和嵌套的程度。

组件大小

组件内聚合代码语句的数量。

技术划分与领域划分

按技术用途或领域目的划分的组件。

在架构中,我们定义组件为执行某些业务或者基础设施功能的应用程序的基础构件,通常通过包结构(Java)、命名空间(C#)或在目录结构下物理分组的文件来声明。例如,Order History(订单历史)组件可以通过命名空间app.business.order.history下的一系列类文件来实现。

大型单体架构通常可维护性都很低,因为通过技术将功能分层、组件之间紧密耦合,以及从领域角度来看组件缺乏功能内聚。比如,考虑在传统单体分层架构中增加一个新需求,给客户的愿望清单(一个以后可能会购买的商品列表)里的商品增加过期日期。注意,在图3-4中,这个新需求的变化范围一直扩大到应用级别,因为这个变更波及应用的所有分层。

图3-4:在分层单体架构中,变更是在应用级别的

取决于团队结构,在单体分层架构里实现给愿望清单里的商品增加过期日期这样一个简单的变更,可能需要至少三个团队的协作:

● UI团队的一员可能需要在界面上增加新的过期字段。

● 后端团队的一员需要添加与过期日期相关的业务规则,更改接口契约以增加新的过期字段。

● 数据库团队的一员需要更改表结构,以在愿望清单表中增加新的过期列。

因为愿望清单领域分布在整个架构中,增加了维护一个特定的领域或者子领域的难度。模块化架构则相反,将领域和子领域划分为更小且可以单独部署的软件单元,从而使修改一个领域或子领域更容易了。注意,在如图3-5所示的基于服务的分布式架构中,新需求的变更范围是领域级别的,在特定的领域服务中,使隔离需要更改的特定部署单元变得更容易。

更进一步的架构模块化(比如微服务架构)如图3-6所示,新需求的变更范围限制在功能级别,将变更隔离到负责愿望清单功能的特定服务。

图3-5:在基于服务的架构中,变更是领域级别的

图3-6:在微服务架构中,变更是功能级别的

这三种不同程度的模块化表明,随着架构模块化水平的提高,可维护性也随之提高,从而更容易添加、更改或删除功能。

3.1.2 可测试性

可测试性定义为测试的难易度(通常用自动化测试实现)以及测试的完整性。可测试性是架构敏捷度不可或缺的一部分。分层架构这类的大型单体架构通常只支持很低程度的可测试性(以及敏捷性),这归因于在庞大的部署单元里难以进行完全针对所有功能的回归测试。即使一个单体应用有整套的回归测试,改一行简单的代码要跑数百个甚至上千个单元测试也很让人抓狂。不仅仅是跑所有的测试非常耗时,可怜的程序员很可能被一堆失败的测试卡住,即使这些失败的测试和他修改的代码毫无关系。

架构模块化——将应用拆分成更小的部署单元——显著降低了服务发生变更后的测试范围。可以用相对容易的方式进行更完整的测试。模块化不仅让测试集变得更小、更有针对性,同时也降低了维护单元测试的难度。

架构模块化通常来说可以提高可测试性,但它也可以导致和单体、单一部署应用一样的问题。例如,考虑一个应用程序,它被拆分为三个较小的独立部署单元(服务),如图3-7所示。

修改服务A的测试范围仅在服务内部,因为服务B和C与服务A并不耦合。然而当这些服务间发生通信,如图3-7的下方所示,可测试性迅速下降,因为测试范围从服务A变成了现在的包含服务B和服务C,因此影响到了测试的难易度以及完整性。

图3-7:测试范围因为服务间通信而扩大

3.1.3 可部署性

可部署性不仅仅关乎部署的难易度,还与部署频率以及部署总体风险有关。为了支持敏捷性并快速响应变化,应用程序必须支持所有这三个因素。每两周部署一次(或者更久)不仅提高了总体部署风险(因为合并了多个变更),还造成了不必要的延迟,因为新功能和问题修复在那之前已经准备好上线了。当然,部署频率也要考虑客户(或用户)对变化接受的速度。

单体架构通常可部署性更低,因为在部署应用的过程中卷入了更多步骤(例如代码定版、模拟部署等),一旦新功能或者问题修复上线也可能引发新的问题,而且部署间隔过长(数周到数月),这些都让本来就风险重重的部署雪上加霜。在一定程度上模块化的应用程序,因为有独立的软件部署单元、更少的部署步骤、更低的部署风险,因而可以比大单体部署得更加频繁。

与可测试性一样,当服务切分得过小,完成一个业务事务需要频繁和其他服务通信时,可部署性会受到负面影响。部署风险增高,因为害怕部署一个简单的变化而破坏其他服务。引用软件架构师Matt Stine( https://www.mattstine.com )关于微服务编排( https://oreil.ly/e9EGN )的一篇文章里的一句话:

如果你的微服务必须按照特定顺序作为完整的集合部署,那么不如让它们回归单体吧,这样你还可以免去一些痛苦。

这种场景通常被称为“分布式大泥球”,几乎无法体现架构模块化的任何好处。

3.1.4 可伸缩性

可伸缩性(scalability)定义为当用户量日积月累逐年增长时,系统是否能保持响应。与可伸缩性相关的弹性(elasticity)定义为系统在用户负载激增和不定期高峰流量期间保持响应的能力。图3-8展示了可伸缩性和弹性的区别。

图3-8:可伸缩性和弹性是不同的

这两个架构特征都包含响应并发请求(或系统用户)数量的功能,但是对它们的处理却不尽相同,无论是从架构还是实现方式的角度来说。可伸缩性通常发生在一个较长的时间段里,作为公司正常增长的支撑功能,而弹性是对用户数量激增的即时响应。

音乐会订票系统可以更好地解释两者之间的区别。在重要音乐会活动之间的间隙,基本上没有并发用户流量。然而,当人气音乐会的票一上架,并发用户量瞬间增加。在数秒内,系统的并发用户可以从20人激增到3000人。为了保持响应,系统必须有能力处理高峰用户量,并且还要能够瞬时启动更多的服务来处理流量激增。弹性依赖于服务的平均启动时间(MTTS),架构上通过更小、更细分的服务来实现。有一个合理的架构方案,MTTS(并由此延伸到弹性)可以用设计时(design-time)技术(比如小且轻量的平台)以及运行时环境来管理。

尽管可伸缩性和弹性都会随着服务的细分得到提升,但弹性更像粒度的作用(部署单元的大小),而可伸缩性更像模块化的作用(将应用拆分为多个部署单元)。图3-9展示了传统的分层架构、基于服务的架构以及微服务架构,这几种架构风格针对可伸缩性和弹性的评分(更多关于这些架构风格及其相应的评分可以参考 Fundamentals of Software Architecture 一书)。注意,一星代表的是这种架构风格难以支撑这项能力,五星代表的是这项能力是这种架构风格的主要特点,可以很好地支持。

图3-9:可伸缩性和弹性随模块化程度提升而提高

需要注意的是,可伸缩性和弹性在单体分层架构里的评分相对较低。大型单体分层架构的伸缩困难且昂贵,因为应用内部所有的功能必须一起伸缩(应用级别可伸缩性和很差的MTTS)。在基于云的基础设施里这可能变得更加昂贵。然而,在基于服务的架构里,虽然可伸缩性提升了,但弹性却没什么改善。这是因为基于服务的架构中的领域服务切分相对较粗,通常整个领域都在一个部署单元里(例如订单处理或仓库管理),通常来说要响应即时弹性需求,它的平均启动时间(MTTS)还是太长,因为其体量过大(领域级别的可伸缩性和中等的MTTS)。而到了微服务这里,可伸缩性和弹性都达到了最大,因为每个独立部署的服务都精炼、目的单一、划分良好(功能级别的可伸缩性和优越的MTTS)。

与可测试性以及可部署性一样,完成单一业务流程的服务间通信越多,对可伸缩性和弹性产生的负面影响越大。因此,当需要更高的可伸缩性和弹性水平时,最好尽可能减少服务间的同步通信。

3.1.5 可用性/容错性

与很多架构特征一样,容错性的定义各式各样。在架构模块化的上下文里,我们定义容错性为当系统的一些部分出现故障时,其他部分可以保持响应的能力。例如,在零售应用的支付流程里,如果出现了一个致命错误(例如内存不足的情况),系统用户应该还能够搜索商品和创建订单,即使支付流程是不可用的。

所有的单体系统都苦于极低的容错性。虽然在单体系统里,如果整个应用有多实例负载均衡,容错性就可以得到一定程度上的提升,然而这项技术又贵效果又不好。如果问题是由程序问题引起的,则所有实例里都存在这个问题,因此可能会把所有实例都拖垮。

想达到领域级别或功能级别容错性的系统,架构模块化是必经之路。通过将系统拆分为多个部署单元,灾难性故障将会被限制在该部署单元内,从而使得系统的其他部分可以继续工作。但这里有个警告:如果其他服务同步依赖于这个故障服务,就没有容错性可言了。这也是为什么在分布式系统中,服务之间的异步通信对于保持良好的容错水平至关重要。

3.2 Sysops Squad的传奇故事:创建业务案例

9月30日,星期四,12:01

带着对架构模块化更深的理解,以及相应的拆分系统的驱动因素,Addison和Austen碰头讨论Sysops Squad的问题,尝试把它们和模块化的驱动因素进行匹配,构建一个坚实的业务理由来说服业务负责人。

“我们挨个过一遍遇到的问题,看看能不能和模块化的驱动因素对应上,”Addison说,“这样,我们就可以给业务负责人展示拆分系统确实能解决我们遇到的问题。”

“好主意,”Austen说,“那就从会上讨论的第一个问题开始——变更。我们无法有效地在现有单体系统上做变更。还有,做变更太花时间了,测试这些变更也是一大痛点。”

“而且开发人员一直抱怨说代码库太庞大了,很难找到正确的地方去做变更或修复bug。”Addison说道。

“OK,”Austen说,“这里很清楚了,整体可维护性是一个关键问题。”

“对,”Addison说,“所以,拆分这个应用不仅可以解耦代码,还能把功能剥离到独立部署的服务中,方便开发人员修改东西。”

“可测试性是另外一个关键特性,但我们的自动化单元测试已经覆盖它了。”Austen说道。

“实际上并没有,”Addison回答道,“看看这个。”

Addison演示给Austen看,有超过30%的测试要么被注释掉了,要么已经作废了,而对系统至关重要的工作流部分却没有测试用例。Addison也解释说开发人员一直抱怨说无论大大小小改了什么都得把整套测试跑一遍,这样不仅花时间,他们还得经常去修复和自己的改动无关的问题。这也是即使做最简单的修改也旷日持久的原因之一。

“可测试性和测试的难易度有关,也和测试的完整性相关,”Addison说,“而我们两个都没有。通过拆分应用,我们可以很大程度上减少变更所需的测试范围,把相关联的自动化测试组织在一起,得到更好的测试完整性,从而减少bug。”

“可部署性也是一样的道理,”Addison继续说,“因为我们有一个单体应用,即使一个很小的故障修复,就得部署整个系统。因为部署风险很高,Parker坚持说生产系统一个月才能上线一次。但Parker不理解的是,这样做的话我们每次上线都堆积了一堆变更,很多变更甚至都没有交叉测试过。”

“我同意,”Austen说,“而且,每次上线前的模拟部署和代码定版也花费了很多宝贵的时间——我们根本就没时间。然而,我们说的这些都和架构问题没关系,都是部署流水线的问题。”

“我不这么认为,”Addison说,“必然和架构有关系。你好好想想,Austen。如果我们把系统拆成可以独立部署的服务,特定服务的修改就会限定在那个服务内部。比如说,我们要在工单分派流程里进行修改。如果这个流程在独立的服务里,不仅测试范围会缩小,部署风险也会降低很多。这意味着我们可以用更少的步骤进行更频繁的部署,并显著地减少bug。”

“我理解你的意思了,”Austen说,“尽管我同意你的看法,但我还坚持认为在某个点上,我们也得改变当前的部署流水线。”

讨论完将Sysops Squad应用拆分为分布式架构可以解决变更问题以后,Addison和Austen开始讨论业务负责人的其他关注点。

“OK,”Addison说,“会议上业务负责人关注的另外一件大事是总体客户满意度。系统有时候用不了,好像一天之中总有时候会崩溃,我们已经遇到太多丢单和派单的问题了,也难怪客户已经开始退出保修计划了。”

“等一等,”Austen说,“我这里有一些最新的数据可以显示总是让系统崩溃的并不是核心工单功能,而是客户调查功能和报表。”

“这可是个好消息,”Addison说,“这样的话,把这些系统功能拆成独立的服务,我们就可以隔离故障,让核心的工单功能正常工作了。这本身就是个极好的理由!”

“正是如此,”Austen说,“所以,我们都同意通过提升容错性从而提高整体可用性,可以解决系统对客户来说不总是可用的问题,因为他们只用到系统的工单部分。”

“但是系统卡住了又怎么办呢?”Addison问道,“拆分系统怎么解决这个问题呢?”

“碰巧我刚让Sysops Squad开发团队的Sydney对于这个问题做了一些分析,”Austen说,“结果是两个问题共同影响的。首先,无论何时只要我们有超过25个客户同时提工单,系统就会卡住。但都没有下面这个问题严重,如果他们在白天客户提问题工单的时候运行运营报告,系统会直接卡死。”

“也就是说,”Addison说,“这里既有可伸缩性问题,也有数据库负载问题。”

“没错!”Austen说,“我们可以加上这一点——通过拆分应用程序和单体数据库,可以将报告拆分到独立的系统中,并为面向客户的工单功能提供额外的可伸缩性。”

他们对这个业务案例表示满意,应该可以对业务负责人讲了,同时他们也相信这是保存这条业务线的正确做法。Addison为这次拆分系统的决定创建了一个架构决策记录(ADR),也为业务负责人创建一个对应的业务案例报告。

ADR:将Sysops Squad系统迁移到分布式架构

背景

Sysops Squad目前是一个单体问题工单应用,支持和问题工单相关的不同的业务功能,包括客户注册、工单录入和处理、运营和分析报告、账单和支付流程以及各种维护管理功能。当前应用在可伸缩性、可用性和可维护性上有无数问题。

决策

我们将把现有单体Sysops Squad应用迁移到分布式架构,可以达到以下目的:

● 对于外部客户,核心工单功能的可用性将得到提升,因此提供更好的容错性。

● 为客户和工单数量的增长提供更好的可伸缩性,解决应用频繁卡死问题。

● 分离报告功能及其给数据库带来的负载,解决应用频繁卡死问题。

● 对比现在的单体应用,可以让团队更快地开发新功能和修复问题,因此提升整体敏捷性。

● 减少由变更带来的bug数量,因此提供更好的可测试性。

● 让新功能和故障修复可以用更高的频率部署(每周甚至每天),因此提供更好的可部署性。

后果

迁移所带来的工作量会造成新功能延迟,因为大部分开发人员需要在架构迁移上工作。

迁移工作量会产生额外的成本(成本估计待定)。

直到改完现有的部署流水线,发布工程师必须管理上线和监控多个部署单元。

迁移工作需要我们同时拆分现在的单一数据库。

Addison和Austen会见了Sysops Squad问题工单系统的业务负责人,以清晰简洁的方式展示了他们的案例。业务负责人对报告表示满意并同意他们的做法,Addison和Austen被告知可以开始他们的迁移工作了。 jXASVndHjntx9e61GbtuTyQwJIvm5L9tgroPdKsKLkbDFgd41Jjd5QtZtUvmF0Wd

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