一直以来,由于软件架构涉及范围广且内涵不断变化,开发人员不断尝试给它一个简洁的定义。Ralph Johnson 就将其定义为“重要的东西(无论那是什么)”。架构师的工作就是理解和权衡那些“重要的东西”(无论它们是什么)。
为了给出解决方案,架构师工作的第一步是理解业务需求,也即领域需求。这些需求是使用软件来解决问题的动机,但终究只是架构师在构建架构时需要考虑的因素之一。架构师还必须考虑其他很多因素,其中一些比较明确(比如清楚地写在性能服务水平协议里),还有一些则隐含在商业活动中不言自明(比如公司正着手并购重组,软件架构显然也要有变动)。所以对于软件架构师来说,架构水平体现了他们在权衡业务需求和其他重要因素后找到最佳方案的能力。软件架构涵盖了所有这些架构因素,如图 1-1 所示。
如图 1-1 所示,业务需求与其他架构关注点(由架构师定义)并存。这包括各种外部因素,这些因素可以改变构建软件系统的决策过程。表 1-1 列举了一些例子。
可访问性 | 可问责性 | 准确性 | 适应性 | 可管理性 |
可负担性 | 敏捷性 | 可审计性 | 自治性 | 可用性 |
兼容性 | 可组合性 | 可配置性 | 正确性 | 可信度 |
可定制性 | 可调试性 | 可降级性 | 可确定性 | 可演示性 |
可信赖性 | 可部署性 | 可发现性 | 分布性 | 耐久性 |
有效性 | 高效性 | 可用性 | 可扩展性 | 故障透明性 |
容错性 | 保真性 | 灵活性 | 可检查性 | 可安装性 |
完整性 | 互通性 | 可学习性 | 可维护性 | 可管理性 |
移动性 | 可修改性 | 模块性 | 可操作性 | 正交性 |
可移植性 | 精确性 | 可预测性 | 过程能力 | 可生产性 |
可证性 | 可恢复性 | 相关性 | 可靠性 | 可重复性 |
重现性 | 弹性 | 响应性 | 可复用性 | 稳健性 |
安全 | 伸缩性 | 无缝性 | 自我维持性 | 可服务性 |
安全性 | 简单性 | 稳定性 | 标准合规性 | 可生存性 |
可持续性 | 可裁剪性 | 可测试性 | 及时性 | 可追溯性 |
在构建软件时,架构师必须明确哪些特征最重要。然而,许多因素是互相矛盾的。比如,让软件具备高性能的同时还要实现极大的伸缩性就很困难,因为实现这两者需要谨慎地平衡架构、运维及其他诸多因素。因此,在为架构设计做必要分析的同时,又要处理好各个因素之间不可避免的冲突,架构师在权衡每个架构设计方案的利弊时,常常需要做出非常艰难的 折中 。近年来,软件开发核心工程实践的持续发展给我们提供了条件,使我们得以重新思考架构随时间的推移要如何变化,以及当这样的演进发生时,如何保护重要的架构特征。本书将这些部分联系起来,以一种新的方式思考 架构 和 时间 。
我们想为软件架构添加一个新的标准“特征”—— 演进能力 。
无论我们怎么努力,软件依然变得越来越难以改变。由于各种原因,软件的组成部分不容易变更,而且随着时间推移变得愈发脆弱和难以操作。软件项目的变更通常是由于对功能或范围做了重新评估而导致的,但是还有一些变化是架构师和长期规划者无法控制的。尽管架构师喜欢为未来做战略性规划,但不断变化的软件开发环境使这一切变得困难重重。既然变化是必然的,那么我们就只能因势利导地来利用它。
生物世界中,环境因自然因素和人为因素而不断变化。例如,20 世纪 30 年代初,澳大利亚的甘蔗受到甲虫危害,导致甘蔗作物严重减产,利润大减。1935 年 6 月,作为应对措施,当时的甘蔗实验站管理总局引入了甘蔗蟾蜍来捕食甲虫,这种蟾蜍原本只产于中美洲和南美洲。短暂喂养后,1935 年 7 月和 8 月在昆士兰州北部投放了甘蔗蟾蜍。因为它们的皮肤有剧毒,并且在当地没有天敌,很快这种蟾蜍就泛滥成灾了。如今,澳大利亚的甘蔗蟾蜍大约有 2 亿只。这件事告诉我们:向高度动态的(生态)系统中引入变化,可能会产生无法预料的结果。
软件开发体系由所有的工具、框架、库以及最佳实践(软件开发领域的技术积累)构成。和生态系统一样,软件开发体系实现了平衡,开发人员能够理解这个体系并为其添砖加瓦。然而,这种平衡是动态的,随着新事物不断出现,平衡不断被打破和重建。想象一个脚踏独轮车,手里还拿着盒子的人。他是 动态的 ,因为他需要不断调整来保持挺立;他又是 平衡的 ,因为他保持着身体平衡。在软件开发体系中,每一项创新或新实践都可能打破现状,迫使系统重新建立平衡。就好比我们不断地将更多的盒子抛向骑独轮车的人,迫使他不断寻求新的平衡。
架构师在很多方面都和这个倒霉的独轮车手相似,不断地平衡以适应环境变化。持续交付这项工程实践使得这个平衡过程有了结构性的转变,它将过去孤立的功能(例如运维)合并到了软件开发的生命周期中,这让我们对 变化 的含义有了新的认识。企业级架构师不能再依赖静态的五年计划了,因为整个软件开发体系在不断变化,任何一个长期计划都可能变得毫无意义。
即便对经验丰富的实践者来说,颠覆性的创新也是难以预测的。比如,像 Docker 这样的容器化技术的崛起就是一个不可预知的行业转变。但我们仍然可以通过一系列小的演进找到一些蛛丝马迹。以前,操作系统、应用服务器和其他基础设施都是商品,需要斥巨资购买使用许可。当时许多架构设计着重于高效利用共享资源。渐渐地,Linux 变得足以支撑企业及应用,使得购买操作系统的 费用 降为零。接下来,通过 Puppet 和 Chef 等工具自动配置服务器的 DevOps 实践使得 Linux 运维工作 也不再需要成本。一旦开发环境免费并得到广泛应用,势必朝着更加通用和便携的方向发展,于是 Docker 应运而生。但如果没有之前所有的演进过程,容器化就不会发生。
我们所使用的编程平台也在持续演进。新的编程语言提供了更好的应用编程接口(API),提高了对新问题的灵活性和适用性。新的编程语言还提供了不同的范式和概念。例如,引入 Java 替代 C++,降低了编写网络代码的难度并改善了内存管理。回顾过去的 20 年,很多语言一直在持续改进它们的 API,与此同时,新的编程语言往往用于解决新的问题。编程语言的演变如图 1-2 所示。
无论是在软件开发的哪个方面,比如编程平台、编程语言、运维环境、持久化技术等,我们都知道改变会持续发生。虽然无法预测技术或领域格局何时会改变,或哪些变化会持续下去,但我们清楚改变是不可避免的。因此,我们应该在构建系统的过程中对这一点保持清醒的认识。
如果整个体系持续地以出乎意料的方式发生变化,预测变化就变得不可能了,那么用什么来 替代 固定计划呢?企业架构师和开发人员必须学会适应变化。做长期计划有一个隐含的原因是财务上的考虑,因为以前软件变更的成本很高。但是,现代工程实践通过自动化和其他先进实践(例如 DevOps)降低了软件变更的成本。
多年来,一些聪明的开发人员发现系统的某些部分相对而言更难修改。这便是将 软件架构 定义为“将来难以变更的部分”的原因,这个省事的定义简单地区分了软件系统中真的难以修改的部分和可以轻松修改的部分。但这个定义依然不免走进了盲区,因为开发者预先假设“变更是困难的”,这变成了一个自证式的预言。
几年前,一些富有创新精神的架构师用新的视角审视了“将来难以变更”的问题。使架构具有可变性会怎样呢?换句话说,如果 易于改变 是架构的基本原则,那么变更将不再困难。反过来,使架构具备演进能力会导致一组全新的行为出现,进而再次打破整个体系的平衡。
即使环境不改变,架构特征出现磨损该怎么办?架构师设计出架构,将其置于纷乱的现实世界,基于架构执行各项事务。架构师要如何保护他们定义的重要部分呢?
有一种不幸的退化叫作架构 比特衰减 ,它在很多组织中均有发生。架构师选择特定的架构模式来满足业务需求及让系统具备某些能力,但这些特征常常意外地随着时间推移而退化。例如,架构师构建了一个包含顶部展现层、底部持久层和一些中间层的分层架构。负责报表功能的开发人员出于性能上的考虑经常会要求绕过中间层,直接从展现层访问持久层。架构师通过分层来隔离变化。而后开发人员绕过这些层,不仅增加了耦合,还使得分层变得毫无价值。
定义了那些重要的架构特征后,架构师如何 保护 这些特征不磨损呢?答案是添加 演进能力 作为新的架构特征,使其在系统演进时保护其他特征。比如,架构师追求架构设计的高伸缩性,但不希望在系统演进时削弱该特征。因此, 演进能力 是一种元特征和保护其他所有架构特征的架构封装器。
本书将阐明演进式架构的另一个作用,即它是一种为架构的重要特征提供保护的机制。我们探寻 持续架构 背后的理念。 持续架构 指构建架构的过程没有最终状态,它会随着软件开发体系的不断变化而演进,并保护重要的架构特征。我们不会尝试定义整个软件架构,因为已经存在很多定义了。我们通过引入 时间 和 变化 作为头等架构元素来扩展当前的定义。
我们对演进式架构的定义如下。
演进式架构支持跨多个维度的引导性增量变更。
增量变更描述了软件架构的两个方面:如何增量地构建软件和如何部署软件。
在开发阶段,允许小的增量变更的架构更易于演进,因为对于开发者来说,变更范围相对更小。对部署而言,增量变更指业务功能的模块化和解耦水平,以及它们是如何映射到架构中去的。示例如下。
假设有一个小工具商家 PenultimateWidgets,该商家采用微服务架构和一些现代工程实践运营着一个产品目录页面。该页面的一个功能是让用户给出小工具的星级评分。PenultimateWidgets 的其他一些业务也需要评分,比如客服代表、物流服务商评价等,所以这些业务共享星级评分服务。某天,该服务的开发团队发布了新版本,新版本允许用户给出半星评价(一个小而重要的升级)。其他需要评分的服务不需要强制升级,而可以逐渐地在合适的时候迁移到新版本。PenultimateWidgets 的 DevOps 实践采用了架构级别的监控,不仅监控各个服务,还监控服务与服务之间的路由。当运营人员观察到在给定时间内没有请求路由至特定服务时,他们自动将该服务从体系中移除。
这是一个在架构级别进行增量变更的例子:如有需要,原服务和新服务可以并存。团队可以在适当(不太忙或必要)的时候完成迁移,当所有迁移都完成时,旧版本的服务将会作为垃圾被自动回收。
增量变更的成功需要一些持续交付实践的配合。并不是任何情况都需要所有这些实践,但通常它们会一起发生。第 3 章将讨论实现增量变更的方法。
一旦架构师选择了重要的架构特征,他们会把变更引导进入架构,以保护这些重要特征。为此,我们借用演化计算中的一个概念: 适应度函数 。该函数是一种目标函数,用于计算潜在的解决方案与既定目标的差距。在演化计算中,适应度函数决定一个算法是否在持续提升。换句话说,随着每个算法变体的产生,基于设计者对算法“适应度”的定义,适应度函数决定每个变体的“适应程度”。
对于演进式架构,随着架构的演进,我们有着类似的需求。我们需要评估机制,来评估变化对架构重要特征的影响,并防止这些特征随着时间的推移而退化。适应度函数的隐喻涵盖多种机制,包括度量、测试和其他检验工具。我们采用这些机制来确保架构不会以不良方式变更。当架构师确定了需要保护的架构特征时,他们会定义一个或多个适应度函数来提供保护。
以往,架构往往要划出一部分作为管理活动,最近架构师才接受了通过架构实现变更的思想。架构适应度函数允许在组织需求和业务功能的上下文中制定决策,并为明晰且可测试的决策奠定了基础。演进式架构并不是毫无约束或不负责任的软件开发方式。相反,它可以在高速变迁的业务、严谨的系统需求和架构特征间找到平衡。适应度函数驱动架构设计决策,并引导架构变更适应业务和技术环境的变化。
第 2 章将详细介绍使用 适应度函数 指导架构演进。
不存在单独的系统。世界是一个整体。如何划分系统边界取决于讨论的主题。
——Donella H. Meadows
古希腊的物理学通过固定点分析宇宙,最终发展成了经典力学。但是到了 20 世纪初,随着仪器越来越精密,现象越来越复杂,人们逐渐从经典力学转向相对论。科学家意识到之前被视为孤立的现象其实是相互影响的。自 20 世纪 90 年代起,受到启发的架构师越来越多地将软件架构视作多维的。持续交付也将运维纳入了软件架构的范畴。软件架构师往往关注技术架构,但那只是软件项目的维度之一。如果架构师想构建可演进的架构,就必须考虑系统中所有会受变化影响的部分。正如物理学所讲的,万物都是相关联的,架构师深知软件项目是多维的。
为了构建可以不断演进的软件系统,架构师不能只考虑技术架构。例如,如果项目包含一个关系型数据库,那么数据库实体之间的结构和关系也会随着时间的推移而不断变化。另外,架构师也不希望系统在演进过程中暴露安全漏洞。这些例子展示了架构的不同 维度 ,它们通常在架构中以正交方式交织在一起。部分维度在常见的 架构关注点 范围之内(见表 1-1),但是架构 维度 的意义其实更广泛,它囊括了很多传统意义上技术架构范畴之外的东西。每个项目都有许多维度,架构师在考虑架构演进时必须要想到。下面是一些影响现代软件架构演进能力的常见维度。
架构中的实现部分:框架、依赖的库和实现语言。
数据库模式、表格布局、优化计划等。通常由数据库管理员(DBA)处理这类架构。
定义安全策略、指导方针和指定工具来帮助发现缺陷。
关注架构如何映射到现有的物理或虚拟的技术设施中,包括服务器、机器集群、交换机、云等。
以上每个视角构成一个架构 维度 ——为了支持特定视角而有意进行的划分。这里架构维度的概念涵盖了传统的架构特征和其他有助于构建软件系统的因素。随着问题和周遭环境的改变,我们想保护架构不磨损,每个维度都提供给我们一个审视架构的视角。
从概念上划分架构的方法有很多,比如 IEEE 的软件架构定义中的 4+1 视图模型。它关注不同角色的不同视角,将整个系统划分成了 逻辑视图 、 开发视图 、 进程视图 和 物理视图 。在著名的《软件系统架构》一书中,作者给出了软件架构的视点目录,它涵盖了更多角色。类似地,Simon Brown 的 C4 建模符号也从概念上帮助组织区分关注点。不过,本书没有尝试创建任何维度分类,而是识别那些现有项目中的维度。从实用角度来看,不论如何对关注点进行分类,架构师都需要保证这些维度不磨损。不同的项目有不同的关注点,这导致每个项目都有特定的维度。对于新项目,以上任何技术都能提供有用的见解,但是对于现有的项目,我们必须处理眼前的实际情况。
按照架构的维度思考,通过评估重要维度对变化的响应,架构师可以分析不同架构的演进能力。随着系统与互相冲突的问题(伸缩性、安全性、分布式、事务性等)关联得越来越紧密,架构师必须跟踪更多的维度。只有结合所有这些重要维度,思考系统将如何演进,才能构建出可以不断演进的系统。
项目的整个架构范围由软件需求和其他维度构成。当架构和整个体系随着时间的推移一起演进时,我们可以使用适应度函数来保护架构特征,如图 1-3 所示。
在图 1-3 中,架构师确定了 可审计性 、 数据 、 安全性 、 性能 、 合法性 和 伸缩性 是该应用的关键架构特征。随着业务需求不断变化,每个架构特征都通过适应度函数来保护其完整性。
我们强调架构整体的重要性,但也应意识到,技术架构模式及相关议题也是架构演进的很大一部分,比如耦合和内聚。第 4 章将讨论技术架构耦合对演进能力的影响,第 5 章将讨论数据耦合的影响。
耦合不只和软件项目的结构元素有关。近来,很多软件公司认识到了团队结构对架构的影响。我们将讨论软件中的各类耦合因素,但是团队结构的影响出现得如此早且频繁,我们需要马上就此展开讨论。
1968 年 4 月,梅尔文 • 康威在《哈佛商业评论》上发表了一篇名为“How Do Committees Invent?”的论文。在这篇论文中,康威提出:社会结构,特别是人与人之间的沟通途径,将不可避免地影响最终的产品设计。
康威描述道,在设计的最初阶段,人们首先需要高瞻远瞩地思考如何将职责划分为不同的模式。团队分解问题的方式会左右他们之后的选择,这便是 康威定律 。
在设计系统时,组织所交付的方案结构将不可避免地与其沟通结构一致。**
——梅尔文 • 康威
正如康威所描述的,当技术人员将问题分解成更小的块,使其更易于委派时,就会产生协调问题。很多组织为了解决协调问题,会设置正式的沟通结构或是建立森严的等级制度,但这样的解决方案往往是僵化的。比如,按照技术职能(用户界面、业务逻辑等)划分的团队,他们在处理常见的跨职能问题时协调的开销就会增加。那些从创业团队跳槽到跨国公司工作的人,很可能会经历两种截然不同的文化:前者非常灵活且适应性很强,而后者的沟通结构往往是僵化的。康威定律一个很好的例子就是修改两个服务间的契约。如果一方想修改服务,而这需要另外一方的同意和配合,那么这件事就可能变得很难。
在论文中康威特别提醒软件架构师,不要只关注软件架构和设计,还应关注团队之间委派、分配和协调工作的方式。
在很多组织中,团队是根据职能来划分的。示例如下。
负责用户界面(UI)技术(如 HTML、移动端和桌面端)。
专门构建后端服务(有时还包括 API 层)。
专门构建存储和逻辑服务。
在这样的组织中,管理层从人力资源的角度简单地按照职能划分团队,没有充分考虑工程效率。虽然每个团队都有其擅长的领域(比如构建一个视图,增加一个后端 API 或服务,或者开发一个新的存储机制),但是当需要发布新的业务功能或特性时,三个团队都要参与其中。各个团队通常都会针对眼前的任务优化效率,而不是针对那些更抽象的战略业务目标(特别是有工期压力时)。这会导致各团队往往专注于交付各自的组件,而不关注端到端的特性价值,导致这些组件可能无法高效协作。
在这样的团队编制下,由于每个团队都在不同的时间忙于自己的组件,因此那些依赖所有团队的特性需要花费更长的时间。例如,修改目录页这样常见的业务变更涉及 UI、业务规则和数据库模式的变更。如果每个团队都各自为战,那么他们必须协调时间表,这将增加实现该特性所需的时间。这个例子很好地解释了团队结构对架构和演进能力的影响。
康威在论文里提到:“每当新的团队组建,其他团队的职责范围会缩小,能够有效执行的可选设计方案也会随之变少。”换句话说,人们很难改变其职责范围外的事情。软件架构师需要时刻关注团队的分工模式,从而使架构目标和团队结构保持一致。
很多构建架构(如微服务)的公司围绕服务边界构建团队,而不是按孤立的技术架构来划分。在 ThoughtWorks 技术雷达中,我们称之为“康威逆定律”。以这种方式组织团队是理想的,因为团队结构会影响软件开发的很多维度,并且会反映问题的大小和范围。比如,在构建微服务架构时,企业通常会构建与架构相仿的团队结构,通过打破功能筒仓使每个团队都有人能考虑到业务的各个角度和技术的方方面面。
构建与目标系统架构相仿的团队结构,这样项目会更容易实现。
本书以 PenultimateWidgets 公司为例。它是排行倒数第二的小工具经销商,还是一家大型在线小工具(各种小商品)商家。这家公司正在更新其大部分 IT 基础设施,包括一些想暂时保留一段时间的遗留系统和正在迭代开发中的新战略系统。本书将重点介绍 PenultimateWidgets 所遇到的问题以及采取的解决方案。
他们的架构师首先观察了软件开发团队。旧的单体应用采用了分层架构,将展现、业务逻辑、持久化和运维分离开来。而他们的团队设置也和架构如出一辙:所有前端开发人员在一起工作,后端开发人员和数据库管理员也各自工作,运维则外包给了第三方。
当开发人员开始在架构(基于具有细粒度服务的微服务架构)上工作时,协调成本急剧增加。因为服务是围绕领域(例如 用户结算 )而不是技术架构来构建的,所以在变更单个领域时将需要各团队间进行大量的协调。
于是,PenultimateWidgets 采取了康威逆定律,建立了和服务范围相匹配的跨职能团队:每个服务团队包括服务负责人、几位开发人员、一位业务分析师、一位DBA、一位质量保障人员和一位运维人员。
本书多处提到了团队的影响,并会通过示例说明它带来的结果。
对于演进式架构,一个常见的问题是关于其名称的:为什么叫作演进式架构而不是别的?比如 增量式 、 持续式 、 敏捷式 、 响应式 或 应急式 ,等等。这些术语都不够准确达意。这里所说的演进式架构包含了两个关键特征:增量和引导。
持续式、 敏捷式 和 应急式 都表达了随时间不断变化的概念,这确实是演进式架构的关键特征,但是这些术语都没能准确地表达出架构将如何变化,或者说期望的架构最终是什么样子的。虽然这些术语都隐含着环境变化,但是都没能涵盖架构应有的样子。而在演进式架构的定义中, 引导 的含义反映了我们想实现的架构,即我们的最终目标。
相比 可适应 这个词,我们倾向于 演进式 ,因为我们对经历根本性演变的架构感兴趣,而不是那些经历了修修补补后变得越来越难以理解、复杂问题频发的架构。 适应 意味着解决方案不求优雅或长久,只要能找到方法使系统运作就行。为了构建能实实在在演进的架构,架构师必须支持真正的变化,而不是权宜之计的考虑。回到我们的生物学隐喻,演进是这样一个过程:建立一个适用的并能在其所处的不断变化的环境中持续运行的系统。系统可能存在个别的适应性,但是架构师应该关心整体可演进的系统。
演进式架构主要由三方面构成:增量变化、适应度函数和适当的耦合。本书余下的部分将分别讨论它们,然后再将它们结合起来,帮助我们构建和维护支持不断变化的架构。