读书笔记《演进式架构》
《演进式架构》
英文版原名:Building Evolutionary Architectures
第1章 软件架构
演进式架构(Evolutionary Architecture)
演进式架构是支持跨多个维度进行引导性增量变更的架构。
An evolutionary architecture supports guided, incremental change across multiple dimensions.
http://evolutionaryarchitecture.com/ :
- Incremental: Evolutionary architectures are built one part at a time, with many different increments. Speed to the next increment is key.
- Fitness Functions: Every system at different points of their life need to optimise to be "fit" for its environment. Evolutionary architectures make it explicit what "fit" means with as much automation as possible.
- Multiple Dimensions: Evolutionary Architectures must support both technical and domain changes
从定义看出演进式架构的三个关键:增量变更、引导性变更、多个架构维度。
增量变更
增量变更描述了软件架构的两个方面:如何增量地构建软件和如何部署软件。在开发阶段,允许小的增量变更的架构更易于演进,因为对于开发者来说,变更范围相对更小。对部署而言,增量变更指业务功能的模块化和解耦水平,以及它们是如何映射到架构中去的。
增量变更的成功需要一些持续交付实践的配合。
引导性变更
适应度函数
架构适应度函数允许在组织需求和业务功能的上下文中制定决策,并为明晰且可测试的决策奠定了基础。演进式架构并不是毫无约束或不负责任的软件开发方式。相反,它可以在高速变迁的业务、严谨的系统需求和架构特征间找到平衡。适应度函数驱动架构设计决策,并引导架构变更适应业务和技术环境的变化。
**架构的适应度函数Architectural Fitness Functions **
要理解“架构适应度函数”这个词语应该还是英文原文(来源)更合适一些:
Evolutionary architecture allows different parts of the system to evolve in the ways most sensible to solve a problem. However, we don’t want the system to evolve in a way that harms some architectural concern. For example, improving performance with caching might accidentally harm security in the process. How can we build guidelines within the architecture to support change but guard specific attributes? Evolutionary computing defines a fitness function as an objectively quantifiable function used to summarise how close a given design solution is to achieving the set aims. When defining an evolutionary algorithm, the designer seeks a “better” algorithm; the fitness function defines what “better” means in this context.
多个架构维度
软件架构师往往关注技术架构,但那只是软件项目的维度之一。如果架构师想构建可演进的架构,就必须考虑系统中所有会受变化影响的部分。
为了构建可以不断演进的软件系统,架构师不能只考虑技术架构。
架构师确定了可审计性、数据、安全性、性能、合法性和伸缩性是该应用的关键架构特征。随着业务需求不断变化,每个架构特征都通过适应度函数来保护其完整性。
为了构建可以不断演进的软件系统,架构师不能只考虑技术架构。
康威定律
在设计的最初阶段,人们首先需要高瞻远瞩地思考如何将职责划分为不同的模式。团队分解问题的方式会左右他们之后的选择,这便是康威定律。在设计系统时,组织所交付的方案结构将不可避免地与其沟通结构一致。
康威在论文里提到:“每当新的团队组建,其他团队的职责范围会缩小,能够有效执行的可选设计方案也会随之变少。”换句话说,人们很难改变其职责范围外的事情。软件架构师需要时刻关注团队的分工模式,从而使架构目标和团队结构保持一致。
小结
演进式架构主要由三方面构成:增量变化、适应度函数和适当的耦合。
书之外的一些参考资料
网上其他读书笔记参考
- 【读书笔记】Building Evolutionary Architectures
- 懒人吃书:Building Evolutionary Architectures
- building-evolutionary-architectures.md
视频(www.bilibili.com):作者的一些英文演讲视频,先收藏了,:)
- Building Evolutionary Architectures – Rebecca Parsons
- Building Evolutionary Architectures • Patrick Kua
- Evolutionary Architecture & Microservices • Rebecca Parsons
- Neal Ford - Evolutionary Software Architectures
PDF:
如何理解架构适应度函数
(以下为个人理解)
这一章引入了一个令人难以理解的术语——适应度函数,但是这个比较奇怪的“适应度”却贯穿了整本书,导致我再翻了大半本书之后发现还是没有明白它在说什么,所以离开书去找了一些其他的资料来先尝试理解这个概念。最后还是通过书的官网(http://evolutionaryarchitecture.com/ffkatas/index.html)的英文对“架构适应度函数(Architectural Fitness Functions)”稍有了理解,虽然英语没学好,但有时候还真是要看英文原版才能理解一些东西。其实可以稍微简单点理解,架构适应度函数就是一个可衡量架构设计的目标是否达成的工具,或者说是一种验收标准,由于架构存在各种各样的特征(上面说的“多个架构维度”),所以对于一个架构设计是否达成目标就要综合多方面的因素来进行完整的评估,以制订这样一种工具来验证我们的架构设计、演化是否满足要求。
第2章 适应度函数
什么是适应度函数
我们对架构的适应度函数的定义如下:
架构的适应度函数为某些架构特征提供了客观的完整性评估。适应度函数能够保护系统所需的各种架构特征。由于业务驱动、技术能力及其他诸多不同因素,系统和组织对架构的具体需求会有很大区别。有些系统要求很高的安全性,有些要求可观的吞吐量或低延迟,还有一些要求更好的故障恢复能力。这些对系统的考量形成了架构师所关心的架构特征。从概念上来讲,适应度函数体现了系统架构特征的保护机制。
最终,所谓“适应度函数引导演进式架构”,指的是通过单独的适应度函数评估单个架构选择,同时通过全系统适应度函数确定变更的影响。适应度函数共同指出架构中对我们重要的部分,使我们能够在软件开发过程中做出各种关键又令人烦恼的权衡。
适应度函数将许多已有的概念统一为一个整体机制,让架构师可以统一思考许多现有的(往往是临时的)“非功能性需求”测试。收集重要的架构阈值和需求作为适应度函数,使得以前模糊又主观的评价标准变得更加具体。我们利用了大量现有机制来构建适应度函数,包括传统的测试、监控等工具。当然,并非所有测试都是适应度函数,只有当测试有助于验证架构问题的完整性时,它才是适应度函数。
适应度函数分类
- 原子适应度函数与整体适应度函数:
原子适应度函数针对单一的上下文执行,用来校验架构的某一维度;
整体适应度函数在共享的上下文中运行,综合检验架构的多个维度,比如安全性和伸缩性。开发人员设计整体适应度函数来保证原子级特性能够正常地协同工作。 - 触发式适应度函数与持续式适应度函数
触发式适应度函数基于特定的事件执行,比如开发人员执行单元测试、部署流水线执行单元测试。也就是说基于构建等事件触发通过自动化测试来检测是否满足架构要求。
持续式测试不是按计划执行,而是持续不断地验证架构的某些方面,比如事务处理速度。也就是说基于监控等手段来检测是否满足架构要求。 - 静态适应度函数与动态适应度函数
静态适应度函数的结果是固定的,比如单元测试的二进制结果——成功或失败。该类型囊括了预定义期望值的适应度函数,比如二进制、数字区间、集合包含等。这类适应度函数通常会用到各种衡量指标。例如,架构师会为代码中的方法定义可接受的平均圈复杂度,并通过嵌在部署流水线的度量工具对其进行检查。
动态适应度函数依赖基于额外上下文变化的因素。某些值会视具体情况而定,比如在大规模运行的情况下,大多数架构师会采用较低的性能指标。例如,某公司可能基于伸缩性将性能指标设置为特定范围内的浮动值——在较大的规模下允许较低的性能。 - 自动适应度函数与手动适应度函数
自动化执行适应度函数测试
或者某些情况下不得不通过手动的方式来执行适应度函数 - 临时适应度函数
虽然大多数适应度函数在变更发生时被触发,但架构师可能想通过时间组件评估适应度。 - 预设式高于应急式
虽然在项目初期,架构师会定义大多数适应度函数,因为它们阐明了架构的特征,但有些适应度函数在系统开发阶段才显现。架构师无法在开始时就知晓架构的所有重要部分(第6章将讨论经典的未知的未知问题),因此必须随着系统发展不断确定适应度函数。 - 针对特定领域的适应度函数
某些架构有着特定的关注点,比如特殊的安全或监管需求。例如,一家处理跨国转账的公司可能设计特定的持续式整体适应度函数,模仿Simian Army(第3章将介绍)对安全性进行压力测试。许多问题域都包含能引导架构师确定重要架构特征的因素。架构师和开发人员应该捕捉这些因素,并将其作为适应度函数来保证那些重要的架构特征不会随着时间出现磨损。
尽早确定适应度函数
将适应度函数的执行结果可视化至明显的公共区域,能使开发人员记得在日常编码中考虑它们,保持关键部分和相关适应度函数的活力。
对适应度函数进行分类有助于确定设计决策的优先级。如果一个设计决策对某个关键适应度函数有特定影响,那么应该花费更多时间和精力进行探针试验(时间可控的试验性编码工作)来校验设计的架构。有些团队采取基于集合的开发方式,它是精益和敏捷流程中的开发实践,用于同时设计多个解决方案。它以构建多套方案为代价来换取未来决策的可选方案。
审查适应度函数
适应度函数审查以会议的形式进行,会上主要业务和技术利益相关者会一起讨论如何修改适应度函数以满足设计目标。例如,当市场份额或用户数量显著增长时,或者引入新的功能或业务能力时,又或者大规模检修现有系统时,都必须审查适应度函数。
适应度函数审查大致涉及如下几点。· 审查已有的适应度函数。· 审查当前适应度函数的相关性。· 确定每个适应度函数的规模或大小的变化。· 确定是否有更好的方法测量或测试系统的适应度函数。· 发现系统可能需要支持的新的适应度函数。
第3章 实施增量变更
可测试性
通过自动化和工具来构建和发布软件的机制是演进式架构的前提。
将项目的架构关注点(包括演进能力)转换为适应度函数能带来很多好处:
- 适应度函数的结果客观且可量化
- 捕获所有关注点作为适应度函数,创造了一致的执行机制
- 适应度函数列表便于开发人员设计部署流水线
组合不同类型的适应度函数
❑ 原子适应度函数+触发式适应度函数
在软件开发过程中运行的单元测试和功能性测试就属于此类适应度函数。开发人员运行它们来验证变更,与此同时,自动化机制(例如部署流水线)通过持续集成保证验证的及时性。通过单元测试验证应用架构完整性的某些方面是此类适应度函数的常见例子,例如通过单元测试验证代码的循环依赖或圈复杂度。
❑ 整体适应度函数+触发式适应度函数
整体触发式适应度函数会作为集成测试的一部分在部署流水线中运行。开发人员设计它们专门来测试系统的各个方面如何按照定义好的方式进行交互。例如,开发人员可能想知道更严格的安全控制对系统伸缩性的影响。架构师设计它们来测试代码的一些集成特征,因为失败的测试能指出某些架构缺陷。正如所有触发式测试一样,开发人员通常在开发过程中、在部署流水线中或持续集成环境中同时执行这些适应度函数。通常这些测试和衡量会有明确的结果。
❑ 原子适应度函数+持续式适应度函数
持续测试作为架构的一部分运行,开发人员围绕这些测试开展设计工作。例如,架构师可能关心是否所有REST端点都使用了适当的动词、合理地处理错误并正确地支持元数据,为此构建了可持续运行的工具来调用这些REST端点(和普通客户端一样)并验证调用结果。这些适应度函数的原子范围表明它们只测试架构的某个方面,持续性则表现在这些测试作为系统整体的一部分运行。
❑ 整体适应度函数+持续式适应度函数
全系统持续式适应度函数会不停地测试系统的多个部分。基本上,这种机制代表系统的一个代理(或客户端),它不断地评估架构和运维的质量。这类适应度函数在现实世界中的一个突出例子是Netflix的Chaos Monkey。Netflix在设计分布式架构系统时,在亚马逊云上运行其系统设计。但是工程师担心可能发生奇怪的事情,因为在云上他们无法直接控制系统的运行,比如高延迟、可用性和弹性等。为了消除忧虑,他们创建了Chaos Monkey,并最终演化为完整的开源项目Simian Army。Chaos Monkey“潜入”亚马逊数据中心,意想不到的事情发生了,比如延迟上升、可靠性降低,其他混乱也随之发生。由于Chaos Monkey的存在,每个团队构建的服务都必须具有回弹性。Conformity Monkey是前面提到的RESTful验证工具,它按照架构师定义的最佳实践来检查每个服务。
需要注意的是,Chaos Monkey并不是按计划运行的测试工具,它在Netflix的体系中持续运行。它不仅促使开发人员构建强大的系统,还不断地测试系统的有效性。将这种持续不断的验证内置于架构中,使得Netflix成为了世界上最强大的系统之一。Simian Army是整体持续式适应度函数的一个典范。它同时针对架构的多个部分运行,很好地保持了架构特征(如回弹性、伸缩性等)。
假设驱动开发和数据驱动开发
在《精益企业》这本书中,Barry O'Reilly介绍了假设驱动开发的现代化过程。在这个过程中,团队应该利用科学手段,而不是收集正式的需求然后花费时间和资源将功能构建到系统中。一旦团队创建出应用的最小可行产品(无论是新产品还是维护现有产品),他们便能在构思新功能时建立假设,而不是需求。假设驱动开发的假设是根据假设来检验的,什么试验可以确定结果以及用什么验证假设意味着应用开发的走向。
假设驱动开发需要协调很多动态的部分,包括演进式架构、现代DevOps、变更需求收集和同时运行多版本应用的能力。基于服务的架构(比如微服务)通常通过服务间的智能路由实现不同版本服务的并行。例如,某个用户可能通过某个特定的服务组访问应用,而另一个请求可能访问相同服务中完全不同的一组实例。如果多数服务都包含多个运行实例(例如为了伸缩性),那么让一部分实例运行增强过的功能,并将部分用户路由到这些功能会变得很简单。
为了产生可观的结果,试验应该进行足够长的时间。通常最好找到某种可衡量的方式来确定更好的结果,而不是通过弹出窗口等形式的调查来打扰客户。例如,某个假设的工作流能否让客户用更少的键盘输入和点击完成任务?在不打扰用户的情况下将其纳入开发和设计的反馈环中,可以构建出更实用的软件。
第4章 架构耦合
很多架构师认为耦合是必然之恶,但是如果不依赖其他组件(并与之耦合)又很难构建复杂的软件。演进式架构注重适当的耦合,即如何确定哪些架构维度间应该相互耦合以最小的开销和成本最大程度地获益。
4.2 架构的量子和粒度
物理学:量子是物理实体相互作用时所涉及的最小单位。
架构量子则是具有高功能内聚并可以独立部署的的组件,它包括了支持系统正常工作的所有结构性元素。
在单体架构中,量子就是整个应用程序,每个部分都高度耦合,因此开发人员必须对其进行整体部署。
相比之下,微服务架构在架构元素之间定义了物理界限上下文,封装了所有可能变化的部分。这种架构就是为了增量变更而设计的。在微服务架构中,界限上下文作为量子边界,包含了服务所依赖的组件,比如数据库服务器。它还包括一些架构组件,例如搜索引擎、报表工具及任何有助于交付功能的组件。
4.3 不同类型的架构演进能力
4.3.1大泥团架构
❑ 增量变更对这种架构难以做任何变更。相关的代码散布于系统各个角落,这意味着修改其中一个组件将意外地破坏其他组件。修复这些破损会导致更多的破损发生,从而产生无尽的连锁反应。❑ 通过适应度函数引导变更由于没有明确定义分区,我们很难为这种架构构建适应度函数。为了构建保护功能,开发人员必须确定需要保护的部分,但是在这种架构中,除了低级的函数或类之外不存在任何结构。❑ 适当的耦合这种架构是不当耦合的典型。构建这样的软件没有任何架构优势。
在这样的糟糕状态下,变更困难且成本高。本质上,由于系统各部分间高度耦合,架构量子就是整个系统本身,没有哪个部分可以轻易改变,因为牵一发而动全身。
4.3.2 单体架构
1.非结构化的单体架构
由相互独立的类互相协调而构成的系统
❑ 增量变更巨大的架构量子阻碍了增量变更,因为高度耦合要求部署大块应用。组件之间存在高度耦合,这导致很难单独部署某个组件,因为需要变更其他组件。❑ 通过适应度函数引导性变更为单体架构构建适应度函数很难,但并非不可能。因为这种架构模式存在了很长时间,可以用随之发展而来的很多工具和测试实践来构建适应度函数。然而,常见的引导性变更对象通常会成为单体架构的致命弱点,例如性能和伸缩性。虽然开发人员很容易理解单体架构,但难以构建良好的伸缩性和性能,这很大程度上源于它固有的耦合。❑ 适当的耦合单体架构除了简单的类之外几乎没有内部结构,其耦合程度类似于大泥团架构。因此代码某处的变更可能对其中某个较远的部分产生意想不到的副作用。
2.分层架构
内部高度耦合和外部低耦合
❑ 增量变更开发人员发现变更这种架构很容易,特别是在将变更隔离到现有层的情况下。跨不同层的变更则会带来协调上的挑战,特别是在组织人员结构和架构分层类似的情况下(这反映了“康威定律”)。例如,某个团队能在不打扰其他团队的情况下替换整个持久层框架,因为他们可以在明确定义的接口背后完成这项工作。但是,当业务要求变更ShipToCustomer(送货服务)时,该变更则会影响所有层,于是协调在所难免。❑ 通过适应度函数引导性变更开发人员发现,在一个更加结构化的单体应用中编写适应度函数更为容易,因为这种架构的结构更明显。同时,将关注点分离到不同层使得开发人员能对更多部分进行隔离测试,便于构建适应度函数。❑ 适当的耦合单体架构的一个优点是易于理解。了解设计模式等概念的开发人员能轻易将这些知识应用于分层架构中。这种易理解性很大程度上是因为开发者能轻松地访问所有代码。分层架构使得由层定义的技术架构划分更易于演进。例如,一个设计(并实现)良好的分层架构能让我们很容易地替换掉数据库、业务规则或其他任何层,并将副作用减至最小。
3.模块化的单体架构
模块化的单体架构中包含功能的逻辑分组及模块间明确定义的边界
❑ 增量变更由于开发人员能够执行模块化,因此在此类架构中很容易进行增量变更。尽管在逻辑上功能被划分为不同的模块,但如果难以单独部署包含模块的组件,那么架构量子依然会很大。在模块化单体架构中,组件的可部署程度决定了增量变更的速度。❑ 通过适应度函数进行引导性变更测试、度量及其他适应度函数在这种架构中更容易设计和执行,因为合理划分了组件,使得测试模拟和其他依赖于隔离层的测试技术更容易实现。❑ 适当的耦合一个设计良好的模块化单体架构是适当耦合的好例子。每个组件在功能上是内聚的,组件之间的接口设计良好且耦合度低。
4.微内核架构
通常出现在浏览器和集成开发环境(IDE)
微内核架构定义了一个核心系统,核心系统对外提供API来通过插件丰富其功能。在这种架构中架构量子大小有两种:一种来自核心系统,另一种来自插件。架构师通常将核心系统设计成单体应用,并在一些熟知的扩展点为插件创建钩子(hook)。我们通常把插件设计成独立且可单独部署的组件。因此,这种架构支持积极的增量变更,开发人员可以针对可测试性进行设计,更容易定义适应度函数。从技术耦合的角度来看,架构师往往将此类系统设计成低耦合,以保持插件相互独立,从而简化它们。
微内核架构的主要挑战围绕着契约,它是某种形式的语义耦合。为了发挥作用,插件必须和核心系统进行双向信息传递。只要插件不需要互相协调,那么开发人员就可以专注于插件与核心系统间的信息和版本控制。例如,大多数浏览器插件只和浏览器交互,而不和其他插件交互。
对于更复杂的微内核系统,插件间的通信无法避免,例如Eclipse Java IDE。除了与文本文件交互之外,Eclipse的核心不支持任何特定语言。所有复杂行为都是通过插件间的通信实现的。例如,在调试过程中编译器和调试器必须紧密协调。因为插件不应依赖其他插件工作,所以核心系统必须处理插件间的通信,这使得协调契约和版本控制等基本任务变得复杂。这个级别的隔离能让系统几乎无状态,虽然很理想,但通常不太可能实现。例如,在Eclipse中,插件运行通常依赖于其他插件,这导致围绕插件架构量子层面的传递依赖于管理。
通常,微内核架构包含一个注册表来跟踪安装的插件及其所支持的契约。在插件间建立明确的耦合加重了系统各部分间的语义耦合,进而导致架构量子变大。
微内核架构广泛应用于IDE工具,它也能应用于各种商业应用。例如,设想一家保险公司,其理赔的标准业务规则应用于整个公司,但是每个州可能有特别的法规。将该系统构建成微内核系统使得开发人员可以按需支持新的州,并在不影响其他州的情况下升级各州的行为,这得益于插件与生俱来的隔离性。
如果难以通过插件使技术架构演进,那么微内核架构是个不错的选择。由完全独立的插件组成的系统更易于演进,因为插件之间不存在耦合。但依赖彼此协作的插件会增加耦合,进而阻碍系统演进。如果使用彼此交互的插件来设计系统,那么你还应该通过消费者驱动的契约模型构建适应度函数来保护那些集成点。微内核架构的核心系统通常很庞大,但是很稳定,因为大部分的变更应该发生在插件上(除非架构师将应用划分得很差)。因此,增量变更很简单:部署流水线触发对插件的变更并对其进行验证。
架构师通常不会在微内核技术架构中包含数据依赖,因此开发人员和数据库管理员必须单独考虑数据的演进能力。将每个插件视为限界上下文可以提高该架构的演进能力,因为这样可以降低内部耦合。例如,如果所有的插件将同一个数据库用作核心系统,那么开发人员必须注意插件在数据级别发生耦合的情况。如果各个插件完全独立,那么这样的数据耦合是不会发生的。
从架构演进的角度来看,微内核架构的理想特征如下所示。
❑ 增量变更一旦完成了核心系统,大多数行为应来自插件。如果插件都是独立的,那么增量变更会更容易。
❑ 通过适应度函数进行引导性变更通常在这种架构中构建适应度函数很简单,因为核心系统和插件是相对独立的。开发人员分别为核心系统和插件维护两套适应度函数。核心适应度函数守护核心系统的变更,包括伸缩性等部署问题。插件测试通常更简单,因为对领域行为的测试是隔离的。为了便于测试插件,开发人员需要很好地模拟核心系统。
❑ 适当的耦合微内核模式明确定义了这种架构的耦合特征。从耦合的角度来看,构建独立的插件使变更变得不重要。协调相互依赖的插件则更难。开发人员应该通过适应度函数来将相互依赖的组件正确地集成。
此类架构还应包含一些整体适应度函数来确保开发人员维持关键的架构特征。例如,单独的插件可能影响某个系统属性,比如伸缩性。因此,开发人员应该计划构建一套集成测试,把它作为整体适应度函数。当系统中存在相互依赖的插件时,开发人员还应该构建整体适应度函数来确保契约和消息的一致性。
4.3.3 事件驱动架构
代理模式
代理模式的事件驱动架构在构建强大的异步系统时存在一些设计挑战。例如,由于缺少集中的中介,很难进行协调和错误处理。由于架构各部分高度分离,开发人员必须通过架构还原业务流程的功能内聚。因此,像事务这样的行为将更难实现。
中介模式
事务性的协调是中介架构的主要优势。中介能保证流程的正确性,并生成一条单一状态消息发送给受保人。
4.3.4 服务导向架构
1.企业服务总线驱动的SOA
ESB驱动的SOA的标志是消息总线,它负责以下各类任务。❑ 中介和路由消息总线能够定位服务并与服务通信。通常,消息总线会维护一张注册表,涵盖服务的物理地址、协议以及调用服务所需的其他信息。❑ 流程编排与编制消息总线将企业服务组合到一起并管理任务,例如服务调用顺序。❑ 消息增强和转换集成总线的优势之一是能够代表应用处理通信协议及其他信息的转换。例如,支持HTTP协议的服务A(ServiceA)想调用仅支持RMI/IIOP协议的服务B(ServiceB)。当需要此类转换时,开发人员可以配置消息总线,在无形之中完成此类消息转换。ESB驱动的SOA的架构量子很大。它基本包含了整个系统,和单体应用差不多,但由于它是分布式架构,因此更为复杂。在ESB驱动的SOA中进行演进式变更非常困难,因为在促进服务复用的同时,其服务分类方法会阻碍普通的变更。例如,在某个SOA中,商品结账(CatalogCheckout)这一领域概念被打散到了整个技术架构中。仅变更商品结账就需要架构各部分进行协调,这些部分往往由不同的团队负责,从而会产生协调摩擦。
❑ 增量变更虽然对复用和隔离资源有完善的技术服务分类方法,但这种架构却严重地阻碍了对业务领域进行最常规的变更。大多数SOA团队都是按照架构划分的,这导致进行常规的变更需要大量的协调工作。而且ESB驱动的SOA也是难以操作的。通常它由多个物理部署单元组成,这给协调和自动化带来了挑战。没人会为了敏捷性和操作的易用性而采用企业服务总线。❑ 通过适应度函数进行引导性变更在ESB驱动的SOA中,通常很难进行测试。各部分都不完整,都是某个巨大工作流的一个环节,通常无法单独测试它们。例如,某个为了复用而设计的企业服务,测试其核心行为通常很困难,因为它可能只是各个工作流的一部分。为其构建原子适应度函数几乎不可能,这导致需要大规模的整体适应度函数进行端到端测试来完成大部分验证工作。
❑ 适当的耦合从潜在的企业级复用来看,这种奢侈的分类方法是合理的。如果开发人员可以准确地提炼出每个工作流中可复用的精华,那么最终他们就能够一劳永逸地构建出企业的所有行为,而将来的应用开发就变成了连接现有服务。构建ESB驱动的SOA的目标不是为了系统各部分能够独立演进,所以在这方面它表现得非常糟糕。针对分类复用的设计,损害了它在架构级别进行演进变更的能力。
2.微服务架构
将持续交付的工程实践和限界上下文的逻辑划分相结合,便形成了微服务的思想基础以及架构量子概念。
4.3.5 “无服务”架构
BaaS(后端即服务)
FaaS(功能即服务)
4.4 控制架构量子大小
架构量子的大小很大程度上决定了开发人员进行演进式变更的难易程度。
架构演进的结构限制取决于开发人员处理耦合和功能内聚的水平。如果开发人员构建出的模块化组件系统具有明确定义的集成点,那么演进会更容易。
架构量子越小,架构的演进能力越强。
JDepend——一个用于分析依赖的开源工具。
第5章 演进式数据
5.1 演进式数据库设计
数据库的演进式设计指开发人员能够根据需求的不断变化来构建数据库结构并使其演进。
开发人员必须以和代码变更相同的方式处理数据库结构的变更,它们必须是经过检验的、版本化的和增量的。
❑ 经过检验的
为了保证稳定性,DBA和开发人员应该严格测试数据库模式的变更。如果开发人员使用了数据映射工具,例如对象关系映射器(ORM),那么为了使映射关系和数据库模式保持同步,他们应该考虑为此添加适应度函数。
❑ 版本化的
开发人员和DBA应该对数据库模式和那些使用它的代码一同进行版本控制。源代码和数据库模式是共生的,缺一不可。人为分离这两种必然耦合的事物的工程实践将导致低效。
❑ 增量的
和代码变更一样,变更数据库模式应该是渐进的,即随着系统的演进而增量地进行。现代工程实践往往使用自动化的迁移工具,从而避免手动更新数据库模式。
很多数据库重构技术通过在重构的过程中创建过渡阶段来避免时间问题。使用该模式,开发人员会设置开始状态和结束状态,它们会在转变过程中分别维持新、旧两种状态。这个过渡状态允许向下兼容,同时还给企业内的其他系统足够的时间来跟上变化。
5.2 不当的数据耦合
数据库厂商在世界各地的企业中拥有一支秘密军队,他们效忠于数据库厂商而不是其雇主。在这种情况下,DBA会忽略那些不是来自数据库厂商的工具和开发组件。其结果便是工程实践的创新水平停滞不前。
事务是耦合的一种特殊形式,因为事务性行为不会在传统的、以技术架构为中心的工具中出现。架构师能够轻松地通过各种工具确定类的传入和传出耦合。但是,确定事务上下文范围要困难得多。正如数据库模式的耦合会妨碍演进一样,事务性耦合以具体的方式将各个组成部分绑定在一起,使得难以演进。
出于各种各样的原因,事务出现在业务系统中。第一,业务分析师青睐“事务”这个概念(一种在某些情况下暂停世界的简单操作),而忽略技术上的挑战。在复杂系统中很难进行全局协调,而事务就是某种形式的全局协调。第二,我们通常能从事务边界中看出在实现层面上业务概念是如何耦合在一起的。第三,DBA可能管理事务上下文,这导致在技术架构中拆分数据来耦合将难以协调。
在尝试将事务性很强的系统转换为不当的架构模式(如微服务)时,开发者会将事务视为耦合点,从而带来了繁重的解耦负担。
架构师在考虑架构变更时应将事务边界考虑在内。
不要仅按字面意思来理解微服务,对每个服务而言,小并不是必需的,能捕获有用的限界上下文才是关键。
第6章 构建可演进的架构
- 识别受演进影响的架构维度
- 为每个维度定义适应度函数
- 使用部署流水线自动化适应度函数
虽然有些适应度函数会在项目初期自然显现,但还有一些适应度函数在架构经受压力时才会显现。架构师尤其需要注意那些非功能性需求被破坏的情况,并通过适应度函数更新架构来避免可能出现的问题。
第7章 演进式架构的陷阱和反模式
项目中有两种错误的工程实践——陷阱和反模式。很多开发人员使用行话“反模式”来表示“不好的模式”,但其真实含义更加微妙。软件的反模式包含两层含义。首先,反模式是一种实践,开始看起来不错,但结果证明是错的。其次,大多数反模式都有更好的替代方案。很多反模式只有在事后才被架构师注意到,因此很难避免。陷阱表面上像是个好主意,但很快便显露出缺点。本章将详细介绍它们。
7.1 技术架构
7.1.1 反模式:供应商为王
与其沦为供应商为王反模式的受害者,我们不如将供应商产品视为集成点。开发人员可以在集成点间构建防腐层,从而避免架构受到供应商工具变更的影响。
7.1.2 陷阱:抽象泄漏
底层抽象破坏会导致意外的灾难,即原始抽象泄漏,它是技术栈日渐复杂带来的副作用之一。如果最底层的某个抽象出现错误会怎样呢?例如,看似无害的数据库调用带来的意外副作用。由于层层抽象的存在,错误将蜿蜒至技术栈顶层,或许会沿着路径转移,最终在UI上表现为深度嵌套的错误信息。随着技术栈愈发复杂,调试和取证分析变得更加困难。
始终保持对当前抽象层以下至少一个抽象层的完全理解
7.1.3 反模式:最后10%的陷阱
客户需求的10%虽有实现的可能,但是极其困难。因为工具、框架或编程语言都没有提供相应功能。
这些工具(通过一些非正常的方式实现需求)仍然无法完全解决问题,我们称其为“最后10%的陷阱”,即所有项目都存在缺憾。
7.1.4 反模式:代码复用和滥用
微服务避免代码复用,遵循重复优于耦合的理念。
7.1.6 陷阱:简历驱动开发
简历驱动开发的陷阱——为了用这些知识丰富自己的简历而选择框架和库。不要为了架构而构建架构,构建架构是为了解决问题。
7.2 增量变更
7.2.1 反模式:管理不当
十年前,操作系统是非常昂贵的商品。数据库服务器、应用服务器和运行应用的整个基础设施也是如此。为了应对这些现实世界的压力,架构师通过设计架构来最大程度地共享资源。当时涌现了很多架构模式,例如SOA。在那样的环境中,一种常见的管理模式应运而生,它将最大化共享资源作为节省成本的措施。很多工具的商业动机都是从这种趋势中发展而来,例如应用服务器。然而从开发的角度来看,由于无意的耦合,在同一台主机上打包多个资源并不可取。无论共享资源间隔离得多么好,资源竞争终将出现。
当开发人员能够不费成本(金钱或时间)地创建虚拟机和容器资源时,看重单一解决方案的管理模式就变得不适用了。微服务领域出现了一种更好的方式。微服务架构的一个常见特征就是支持异构的环境,各个服务团队可以选择适合的技术栈来实现他们的服务,而不用按照企业标准进行统一。这与传统方式截然相反,因此当传统企业的架构师听到这个建议时会退缩。然而,大多数微服务项目的目标并不是武断地选择不同的技术,而是根据具体问题选择适当的技术。
7.2.3 陷阱:发布过慢
持续交付追求数据驱动的结果,从指标数据中学习如何优化项目。开发人员必须衡量事物从而了解如何优化。生产周期是持续交付的一个关键指标,和交付周期相关。交付周期是指从一个想法开始到它在软件中实现所耗费的时间。然而,交付周期中包含很多主观活动,例如估算、排列优先级等,使其成为了一个糟糕的工程指标。因此持续交付跟踪生产周期,即启动和完成单位工作所用的时间,这里指软件开发。生产周期从开发人员着手开发某个新功能起开始计时,当该功能在生产环境中运行时停止计时。其目标是衡量工程效率,持续交付的关键目标之一便是缩短生产周期。
7.3 业务问题
7.3.1 陷阱:产品定制
❑ 为每个客户定制
在这个场景中,销售人员在紧迫的时间内承诺实现特定版本的功能,迫使开发人员使用版本控制分支或标签技术来跟踪版本。
❑ 永久的功能开关
第3章介绍过功能开关,有时将它战略性地用于构建永久的定制功能。开发人员可以使用功能开关来为不同客户创建不同的版本,或创建“免费”版产品,让用户付费解锁高级功能。
❑ 产品驱动定制化
有些产品甚至可以通过UI来完成定制。在这种情况下,定制功能是应用的永久部分,需要和其他所有产品功能受到同样的维护。
7.3.2 反模式:报表
很多微服务架构通过分离行为来解决报表问题,而微服务的隔离有利于分离但不利于整合。通常构建这类架构时,架构师使用事件流或消息队列来向领域“记录系统”数据库填充数据,每个记录系统嵌在服务架构量子中,使用最终一致性而不是事务行为。一些报表服务也会监听事件流,向针对报表优化过的非规范化数据库中填充数据。
消除因混合领域和报表引起的不当耦合,使得每个团队可以专注于更加具体且简单的任务。
7.3.3 陷阱:规划视野
人们在规划和文档上投入的时间和精力越多,就越可能保护其中的内容,即便有证据表明它们不准确或过时了。
谨防长期规划,因为它会迫使架构师做出的决策不可逆转,同时找到方法来保证多个可选方案。将大型项目分解成更小的早期可交付物,以测试架构选型和开发基础设施的可行性。在通过最终用户反馈验证所用技术确实适用他们试图解决的问题之前,架构师应该避免在实际构建软件之前采用需要大量前期投入的技术,例如大型许可和支持合同。
第8章 实践演进式架构
8.1 组织因素
8.1.1 全功能团队
以领域为中心的团队应该是全功能的,这意味着每个项目角色都由该项目组成员承担。以领域为中心的团队,其目标是消除运营摩擦。换句话说,团队拥有负责设计、实现和部署其服务的所有角色,其中还包括传统上单独的角色,例如运维。
全功能团队的目标之一便是消除协调摩擦。传统的团队彼此独立,开发人员通常需要等DBA做出变更或等运维人员提供资源。同一个团队包含各种角色能消除不同团队协调所产生的偶然摩擦。
由于资源受限,很多公司无法如愿地组建全功能团队。在这种情况下,项目间可以尝试共享受限的资源。例如,与其为每个服务配备一名运维工程师,或许他们可以在几个不同的团队间轮转。
通过围绕领域组建架构和团队,现在可以由同一个团队处理常见的变更单元,从而减少了团队间的摩擦。
8.1.2 围绕业务能力组织团队
围绕领域组织团队即意味着围绕业务能力组织团队。
按照业务能力而非职能来组织团队。
8.1.3 产品高于项目
围绕产品而非项目来组织团队工作,是很多公司切换团队工作重点的一种机制。
通常,开发人员和他们运行的代码的间接层越多,他们与代码之间的联系就越少。有时这会导致在不同团队间产生对立心态,这并不奇怪,因为很多组织结构催生了员工间的冲突。
通过将软件视为产品,公司能在三个方面实现转变。第一,与项目的生命周期不同,产品的生命更长久。全功能团队(通常基于康威逆定律)与产品保持联系。第二,每个产品都有一个负责人,他会主张在体系中使用该产品,并管理其需求。第三,由于是全功能团队,团队拥有产品所需的各种角色,例如业务分析师、开发人员、质量保障人员、DBA、运维人员等。
亚马逊的“两个比萨”团队:亚马逊以其产品团队的组织方式而闻名,他们称之为“两个比萨团队”。其理论是,两个大的比萨就够任何一个团队吃了。这种划分方式背后的动机更多是为了沟通方便而不是控制团队大小,因为在更大的团队中,成员必须和更多的人沟通。每个团队都是全功能团队,并且都奉行着“谁构建,谁运行”的理念,这意味着每个团队全权负责其服务,包括运维工作。
构建全功能团队可以防止不同团队间的相互指责,并让团队产生主人翁意识,激励团队成员做到最好。
8.1.4 应对外部变化
在消费者驱动的契约中,信息消费者构建一组测试来封装他们对服务提供者的需求,并将这些测试移交给服务提供者,服务提供者承诺始终让这些测试通过。因为这些测试覆盖了服务消费者所需的信息,所以服务提供者能以不破坏这些适应度函数的方式演进。在图8-1所示的场景中,服务提供者除了运行自己的测试组件外,还代表所有消费者运行这些契约测试。我们将采用这类适应度函数简单称为工程安全网。当可以轻松构建适应度函数来维护集成协议的一致性时,就不应该手动处理这类杂事。
8.1.5 团队成员间的连接数
人与人之间的连接数=n*(n-1)/2
当团队人数达到20时,他们必须管理190个连接;当组员增长到50个时,连接数则增长为惊人的1225。因此,构建小型团队是为了减少沟通连接。并且,为了消除不同团队间协作所产生的人为摩擦,这些小型团队需要是全功能团队。每个团队无须了解其他团队所做的事情,除非团队之间存在集成点。即便如此,也应该使用适应度函数保证集成点的完整性。
尽量减少开发团队之间的连接数。
8.2 团队的耦合特征
8.2.1 文化
文化(名词):特定人群和社会的想法、习俗及社会行为。
如果团队不习惯改变,那么架构师可以引入实践来优先应对这一点。例如,当团队考虑采用某个新的库或框架时,架构师可以让团队通过快速试验来进行明确的评估,看看新的库或框架会引入多少额外的耦合。工程师是否可以轻松地在该库或框架之外编写和测试代码,又或者新的库或框架是否需要配备额外的运行环境,而拖慢开发周期?
事不过三,三则重构:第一次做某件事时,你尽管去做。第二次做类似的事时,你会对重复犹豫,但无论如何你还是重复这件事。第三次再处理类似的事时,你应该将其重构。
8.2.2 试验文化
8.3 首席财务官和预算
在演进式架构中,架构师追求合适的量子大小和对应成本之间的最佳点。每个公司情况各异。例如,市场迅猛发展,公司可能需要更快的变更速度,因此需要更小的架构量子。记住,新一代架构的出现速度与生产周期成正比,架构量子越小,生产周期越短。
8.5从何开始
8.5.1 容易实现的目标
如果组织需要早期成功来证明这种方法,架构师可以选择最简单的问题来凸显演进式架构方法。通常,这是系统中在很大程度上已经解耦的一部分,而且最好不在任何依赖的关键路径上。团队可以通过增强模块性和降低耦合来展示演进式架构的其他方面,如适应度函数和增量变更。构建更好的隔离可以使测试和适应度函数更具针对性。更好地隔离可部署单元使得构建部署流水线更容易,并为构建更强大的测试提供了平台。
这种“最简单者优先”的方法将风险降到了最低,但可能牺牲价值,除非团队有幸找到既容易解决、价值也高的问题。
8.6 演进式架构的未来
8.6.2 生成式测试
在很多函数式编程社区中,生成测试是广受欢迎的常见实践。传统单元测试包含对每个测试用例结果正确与否的判断。然而,通过生成测试,开发人员运行大量测试并抓取结果,然后对结果进行统计分析来查找反常的行为。例如最常用的边界值检查,传统单元测试检查已知的数字临界点(负数、不断增加的数字等),但无法覆盖意外的少数情况。生成测试检查每一个可能的数值并报告失败的少数情况。