架构决策
对架构师的主要期望之一就是做出架构决策。架构决策通常涉及应用程序或系统的结构,但也可能涉及技术决策,尤其是当这些技术决策影响架构特征时。无论在什么情况下,一个好的架构决策是能够指导开发团队做出正确技术选择的架构决策。做出架构决策需要收集足够多的相关信息,证明决策的合理性,记录决策并将决策有效地传达给正确的利益相关者。
架构决策是一门艺术。不出意外的话,架构师在进行决策时会遇到几种架构反模式。程序员 Andrew Koenig 将反模式定义为:那些一开始看起来似乎是一个好主意,但却会带来麻烦的模式。反模式的另一个定义是:能够产生负面结果的可重复过程。在架构决策过程中,可能会(并且通常会)出现的三种主要架构反模式是
“掩盖你的资产(Covering Your Assets)”
“土拨鼠日( Groundhog Day)”
“电子邮件驱动架构( Email-Driven Architecture)”
这三种反模式通常是一个循序渐进的过程:克服“掩盖你的资产”会导致“土拨鼠日”,而克服“土拨鼠日”则导致“电子邮件驱动架构”。做出有效而准确的架构决策需要架构师克服所有这三种反模式。
尝试做出架构决策时会出现的第一个反模式是掩盖你的资产。当架构师由于担心做出错误选择而避免或推迟做出架构决策时,就会发生这种反模式。
有两种方法可以克服这种反模式。第一种方法是等到最后的责任时刻( last responsible moment)再完成重要的架构决策。最后的责任时刻意味着一直等到你有足够的信息来证明和验证你的决策,但不能等待太久,以避免拖延开发团队或陷入分析瘫痪( Analysis Paralysis)反模式。第二种方法是持续地与开发团队协作,以确保你做出的决策能够按预期实现。这一点至关重要,因为作为一名架构师,你无法掌握特定技术的所有细节以及所有与之相关的问题。通过与开发团队紧密协作,架构师可以在出现问题时及时快速地改变架构决策。
为了说明这一点,假设架构师决定,对于所有与产品相关的参考数据(产品描述、重量和尺寸),依赖这些数据的所有服务实例都需要对它们进行复制并以只读的形式存放在自己的缓存当中,数据的主副本归商品目录服务所有。复制缓存意味着,如果产品信息有任何更改(或者有新产品被添加),商品目录服务将更新其缓存,然后需要通过某个复制(在内存中进行)缓存产品,将产品数据复制到需要该数据的所有其他服务当中。做出此决策的理由是减少服务之间的耦合,并有效共享数据,从而无须进行服务间调用。但是,执行此架构决策的开发团队发现,由于某些服务的一些可扩展性需求,该决策所需要的进程内存比可用内存更大。通过与开发团队紧密协作,架构师可以快速了解问题并调整架构决策来适应这些情况。
一旦架构师克服了掩盖你的资产反模式并开始做出决策,就会出现第二个反模式:土拨鼠日反模式。当人们不知道为什么要做出某个决策时,就会发生土拨鼠日反模式,因此它不断地被反复讨论。土拨鼠日反模式是从比尔·默里( Bill Murray)的电影《土拨鼠日》中得名的,影片中每一天都是 2 月 2 日。
发生土拨鼠日反模式的原因是架构师做出了某个架构决策,但无法为该决策提供理由(或无法提供一个完整的理由)。在论证架构决策时,为决策提供技术和业务辩护是非常重要的。例如,架构师可以决定将单体应用分解为多个单独的服务,将应用程序的功能解耦,从而使应用的每个部分使用较少的虚拟机资源,并且可以进行单独维护和部署。虽然这是技术合理性的,但是缺少业务合理性。换句话说,企业为什么要在这种架构重构上花钱?本例中,一个好的业务合理性可能是:更快地交付新的业务功能,从而缩短产品上市时间;另一个可能是减少开发和发布新功能的成本。
ps:经常有人在讨论我想在我们家尝试DDD,但是得不到研发团队支持。我想拆分微服务,但是产品经理不理解balabala......你当然可以为你的“技术冒险”找一个理由,但最好的方式是解决问题出发。
论证决策对业务的价值对于任何架构决策都至关重要。这也是一个首先确定是否应该做出架构决策的试金石。如果某个架构决策不能带来任何业务价值,那么它可能不是一个好的决策,应该重新考虑。
四个最常见的业务合理性包括成本、上市时间、用户满意度和战略定位。当着眼于这些常见的业务合理性时,考虑对业务利益相关者来说紧要的事情就变得很重要。如果业务利益相关者较少关注成本,而更关注上市时间,此时仅依据节省成本来证明特定的决策可能是不正确的。
一旦架构师做出了一系列决策并充分证明了这些架构决策的合理性,就会出现第三个架构反模式:电子邮件驱动架构反模式。电子邮件驱动架构反模式是指因遗弃、忘记或者甚至不知道已经做出的架构决策而无法实现该架构决策。这个反模式讲的就是要有效地沟通架构决策。电子邮件是一种很好的交流工具,但它并不是一个好的文档存储库系统。
提高架构决策沟通有效性的方法有很多,通过它们可以很好地避免电子邮件驱动架构反模式。沟通架构决策的第一条规则就是不要在电子邮件正文中包含架构决策,因为这会为该决策创建多个系统记录。很多时候,重要的细节(包括合理性)都不会包含在电子邮件当中,因而会重新陷入土拨鼠日反模式。而且,如果该架构决策被更改或取代,如何让人们收到修订后的决策呢?更好的方法是仅在电子邮件正文中提及决策的性质和上下文,并提供架构决策和相应详细信息的单个系统记录链接(无论它是一个 wiki 页面还是文件系统中的文档)。
有效的架构决策沟通的第二条规则是只通知真正关心架构决策的人。一种有效的方法是编写如下的电子邮件:
“嗨,Sandra,我对服务间的通信做了一个重要的决策,它会直接影响到你。请使用以下链接查看这个决策……”
请注意第一句话中的措辞:“对服务间的通信做了一个重要的决策。”这里交代了决策的上下文,而不是决策本身。第一句话的第二部分更为重要:“它会直接影响到你。”如果某个架构决策不会直接影响这个人,为什么要就这个架构决策打扰人家呢?这是一个很好的试金石,用来确定应该将架构决策直接通知给哪些利益相关者(包括开发人员)。第二句话提供了架构决策的链接,因而保证架构决策仅存储在一个地方,是决策的单一系统记录。
许多架构师认为,如果架构决策涉及任何特定技术,那它就不是一个架构决策,而是技术决策。事实上并非总是如此。如果架构师因某技术可以支持特定的架构特征(如性能或可扩展性)而决定使用该技术,那么这就是架构决策。
Release It!( Pragmatic Bookshelf 出版社)的作者,著名的软件架构师 Michael Nygard, 通过创造术语“具备架构意义”,解决了架构师应该负责哪些决策(以及什么是架构决策)的问题。Michael 认为,具备架构意义的决策是那些会影响结构、非功能特性、依赖项、接口或构建技术的决策。
结构是指影响架构模式或风格的决策。例如,在一组微服务之间共享数据的决策。该决策影响微服务的界限上下文,并因此影响应用程序的结构。
非功能特性是对正在开发或维护的应用程序或系统很重要的架构特征。如果使用某个技术会影响性能,而性能是该应用程序的一个重要方面,那么使用该技术就是一个架构决策。
依赖项是指系统内组件或服务之间的耦合点,影响整体的可扩展性、模块化、敏捷性、可测试性、可靠性等。
接口是指访问并编制服务和组件的方式,实现方式通常有网关、集成集线器、服务总线、API 代理。接口通常涉及合约的定义,包括合约的版本控制和弃用策略。接口会影响第三方如何使用该系统,因此具备架构意义。
构建技术指的是关于平台、框架、工具甚至流程的决策,尽管这些决策本质上是技术性的,但可能会影响架构的某些方面。
记录架构决策的最有效方法之一是进行架构决策记录( ADR )。ADR 最开始由 Michael Nygard 在博客中进行宣传,随后在 ThoughtWorks 技术雷达中标为“采纳”。一条 ADR 是个描述特定架构决策的短文本文件(通常为 1~2 页)。虽然可以使用纯文本编写ADR,但通常用某种文本文档格式(如 AsciiDoc 或 Markdown)来编写。当然,也可以使用 wiki 页面模板编写 ADR。
也有一些管理 ADR 的工具。Nat Pryce—Growing Object-Oriented Software Guided by Tests( Addison-Wesley 出版社)的合著者— 编写了一个 ADR 开源工具,叫作 ADR- tools。ADR-tools 提供了一组命令行接口来管理 ADR,包括编号方案、位置以及取代逻辑。来自德国的软件工程师 Micha Kops 写了一篇关于如何使用 ADR-tools 的文章,提供了一些使用 ADR-tools 来管理架构决策记录的优秀示例。
ADR 的基本结构包括五个主要部分:标题、状态、背景、决策和后果。我们通常会在基本结构中再添加两个内容:合规性和备注。提供模板是为了保持一致和简洁,在这个基本结构(如图 19-1 所示)之上可以添加任何其他必要的内容。例如,如有必要,可以添加一个替代节,用来对其他替代解决方案进行分析
图 19-1:基本的 ADR 结构
标题
ADR 的标题通常按顺序编号,是一句简短的描述架构决策的话。例如,在订单服务和付款服务之间使用异步消息传递的决策标题可能是:“42. 在订单服务和付款服务之间使用异步消息传递。”标题应具有足够的描述性,可以消除决策性质和背景的歧义,但同时又要简短明了。
状态
ADR 的状态分记为已提议、已接受或已取代。已提议意味着该决策必须由一个更高级别的决策者或某种架构管理机构(例如架构审查委员会)批准。已接受表示该决策已被批准并且可以执行。已取代表示该决策已被更改并被另一个 ADR 取代。已取代始终假定该 ADR 之前已被接受。换句话说,已提议的 ADR 永远不会被另一个 ADR 取代,而是会继续进行修改直到被接受。
已取代是一种保留决策历史的非常有用的方式,能够记录当时做出的决策是什么、为什么做出这样的决策以及为何做出更改。通常,当 ADR 被标为已取代时,会标注上取代它的决策。同样,取代另一个 ADR 的决策也应当用其取代的 ADR 进行标记。例如,假设对于先前已被接受的 ADR 42(“在订单服务和付款服务之间使用异步消息传递”),由
于后来对付款服务的实现和地址进行了修改,因此现在必须使用 REST( ADR 68 )。此时,ADR 42 和 68 的状态如下所示:
ADR 状态的另一个重要意义在于,它迫使架构师与老板或首席架构师进行必要的对话, 以讨论他们可以自行批准架构决策的标准,或者是否必须通过更高级别架构师、架构审查委员会或其他架构管理机构的批准。
成本、跨团队影响和安全性这三个标准能够为此类对话开个好头。成本可包括软件的购买或许可的费用、额外的硬件成本以及实施架构决策所需的工作。可以通过将预估的实施架构决策的小时数乘以公司的标准全时等效( Full-Time Equivalency,FTE)费率来估算工作成本。项目所有者或项目经理通常了解 FTE 的数量。如果架构决策的成本超过一定限额,则必须将它设置为“已提议”状态并经由其他人批准。如果架构决策会对其他团队或系统产生影响,或存在任何安全隐患,则该决策不能由架构师自行批准,必须由更高级别的管理机构或首席架构师批准。
一旦建立了标准及相应的限制并达成一致(例如,“若成本超过 5000 欧元,则必须由架构审查委员会批准”),则应进行充分记录,以便所有创建 ADR 的架构师都知道什么时候可以自行批准架构决策,什么时候不可以。
背景
ADR 的背景部分指明了决策背后的作用力。换句话说,“什么情况迫使我做出这个决策?”ADR 的这一部分让架构师描述具体情况或问题,并简要阐述可能的替代方案。如果需要架构师详细记录每个替代方案的分析,则可以将“其他替代方案”部分添加到ADR 中,而不是将该分析添加到背景当中。
背景部分还是一种记录架构的方法。描述背景的同时,架构师也在描述架构。这是一种以清晰简明的方式记录架构特定部分的有效方法。继续上一节中的例子,背景可能是:“订单服务必须将信息传递给付款服务才能完成当前订单的付款。这里的信息传递可以使用 REST 或异步消息传递来完成。”请注意,这条简洁的声明不仅说明了方案,还说明了替代方案。
决策
ADR 的决策部分包含架构决策及该决策的合理性分析。Michael Nygard 介绍了一种陈述架构决策的好方法,即使用非常正面、指挥性的表达而不是消极的表达。例如,在服务之间使用异步消息传递的决策可表达为“我们将在服务之间使用异步消息传递”,与“我认为服务之间的异步消息传递是最好的选择”相比,这是一种更好的陈述决策的方式。请注意,第二种表述没有说清楚决策是什么,甚至没有说明是否已有决策,仅仅只是陈述了架构师的看法。
ADR 决策部分最强大的一个地方可能是它让架构师将重点更多地放在为什么( why)而不是如何( how)上。理解为什么要做出某个决策比了解其工作原理重要得多。大多数架构师和开发人员可以通过查看上下文图来确定事物的工作方式,但看不出为什么要做出某个决策。知道做出决策的原因以及相应的依据有助于更好地理解问题的背景,并避免重构到其他解决方案,进而导致问题。
为了说明这一点,假设几年前原本的架构决策是使用 Google 的远程过程调用( gRPC) 作为两种服务之间的通信方式。几年后,另一位架构师在不理解为何做出该决策的情况下,选择取代该决策,并使用消息传递以便更好地分离服务。但是,这样的重构使得延迟突然急剧增加,进而导致上游系统发生超时。如果了解使用 gRPC 的初衷是显著减少延迟(以紧密耦合的服务为代价),就能从一开始防止这样的重构发生。
后果
后果是 ADR 另一个非常强大的部分。它记录了架构决策的总体影响。架构师做出的每个架构决策都会产生某种影响,无论好坏。必须明确架构决策的影响迫使架构师去思考这些影响是否超过了决策带来的收益。
后果的另一个好的用法是记录与架构决策相关的权衡分析。这些权衡可以是基于成本的,也可以是与其他架构特征之间的权衡。例如,假设一个决策说:使用异步(即发即弃)消息传递来将评论发布到网站上。制定该决策的合理性在于,它可将评论请求的响应时间从 3100 毫秒降低到 25 毫秒,因为用户不再需要等待评论的发布完成(仅需要将消息发送到队列中)。尽管这是一个很好的理由,但考虑到与异步请求相关的错误处理的复杂性(“如果有人发表带有一些不良词汇的评论该怎么办?”),其他人可能会认为这是一个馊主意,从而对决策提出质疑。对此决策提出质疑的人不知道,这个问题已经在业务利益相关者和其他架构师中间得到了讨论,并且是权衡之后的决定,相比较于等同步完成之后再告诉用户评论已发布,在处理复杂错误的代价下提高响应能力更为重要。通过利用 ADR,可以在“后果”部分中加入权衡分析,从而全面了解架构决策的背景
(以及相关权衡),从而避免出现这类情况。
合规性
合规性不是 ADR 的标准部分之一,但我们强烈建议添加。合规性迫使架构师考虑如何从合规性角度度量和治理架构决策。架构师必须决定决策的合规性检查是手动进行,还是可以使用适应度函数来自动进行检查。如果可以使用适应度函数进行自动化检查,那么架构师可以在本部分中指明如何编写这个适应度函数,以及是否需要对代码库进行修改,以便对此架构决策的合规性进行度量。
例如,在图 19-2 所示的传统 n 层分层架构中,有这样一个架构决策:“在业务层中,业务对象所使用的所有共享对象都将被放在共享服务层中,以隔离和保留共享功能。”
图 19-2:架构决策示例
可以使用 Java 中的 ArchUnit 或 C #中的 NetArchTest,来自动度量和治理此架构决策。例如使用 Java 中的 ArchUnit,自动化的适应度函数测试可能如下所示:
请注意,此自动化的适应度函数需要添加新的用户故事来创建新的 Java 注解(@Shared- Service),并将该注解添加到所有共享类当中。合规性部分还会指定相关测试是什么、可以在哪里找到、如何执行以及何时进行。
备注
备注是另一个不属于标准 ADR 的部分,但我们强烈建议添加。它包含了有关该 ADR 的各种元数据,例如:
原作者
通过时间
通过人
取代时间
最近一次修改时间
修改人
最近一次修改
即使使用版本控制系统(例如 Git)来存储 ADR,除了库所支持的内容外,其他元信息也很有用,因此无论如何存储 ADR 以及在何处存储,我们都建议添加此部分。
架构师创建 ADR 后,必须将其存储在某处。无论在哪里存储 ADR,每个架构决策都应具有自己的文件或 wiki 页面。一些架构师喜欢将 ADR 与源代码一起保留在 Git 存储库中, 这样还可以对 ADR 进行版本控制和跟踪。但是,对于大型组织,出于以下几个原因,我们不能这样做。首先,每个需要查看架构决策的人都有可能无法访问 Git 存储库。其次, 对于应用程序的 Git 代码库之外的上下文(例如,集成架构决策、企业架构决策或在应用程序之间共享的决策)而言,这不是好的存储 ADR 的地方。由于这些原因,我们建议将ADR 存储在 wiki(使用 wiki 模板)中或共享文件服务器上的共享目录中,可通过 wiki 或其他文档软件轻松访问。图 19-3 是一个此类目录结构(或 wiki 页面导航结构)的示例。
图 19-3:用于存储 ADR 的目录结构示例
application 目录中包含了与某些应用程序上下文相关的架构决策,该目录可细分为更多的目录。common 目录用于存放适用于所有应用程序的架构决策,例如“所有与框架相关的类都将包含一个注解( Java 中为 @Framework)或属性( C #中为 [Framework]),以将该类标识为基础框架代码的一部分。application 目录下的子目录与特定应用或系统上下文相对应,包含特定于该应用或系统的架构决策(即本例中的 ATP 和 PSTD 应用)。integration 目录包含与应用、系统或服务之间通信相关的 ADR。企业架构 ADR 包含在 enterprise 目录中,表明这是影响所有系统和应用的全局架构决策。举一个企业架构 ADR 的例子:“所有对系统数据库的访问,只能由拥有该数据库的系统进行”,这条 ADR 是为了防止多个系统共享数据库。
当在 wiki(我们的建议)中存储 ADR 时,前面描述的结构同样适用,每个目录结构都表示一个导航页面。每个 ADR 都被表示为导航页面( Application、Integration 或Enterprise)中的单个 wiki 页面。
本节所示的目录或页面名称仅作为建议。只要能够在团队中保持一致,每个公司都可以选择适合其情况的任何名称。
记录软件架构一直是一个难题。尽管现在出现了一些绘制架构的标准(例如,软件架构师 Simon Brown 的 C4 模型或 The Open Group ArchiMate 标准),但对于记录软件架构却还没有标准。这就是 ADR 可以帮忙的地方。
ADR 是一个记录软件架构的有效手段。ADR 的“背景”部分可以极好地描述系统中的某个特定部分,也就是需要做出架构决策的部分。“背景”部分还提供了描述替代方案的机会。更重要的是,“决策”部分描述了做出特定决策的原因,而这也是迄今为止软件架构文档的最佳形式。通过描述架构决策的其他方面(比如舍弃可扩展性、选择性能
的原因分析),“后果”部分使得软件架构文档更加完善。
很少有人喜欢标准。多数时候,标准似乎被用于控制人及人们如何做事,除此之外别无他用。用 ADR 描述标准可以改变这种不良做法。例如,ADR 的“背景”部分描述了强制执行特定标准的场合。ADR 的“决策”部分不仅可以用来说明标准是什么,而且更重要的是说明了为什么需要该标准。这是能够确定一个标准是否应该存在的绝妙方法。如果架构师无法证明一个标准的合理性,那么制定和执行该标准可能并不好。此外,开发人员越明白为什么需要某个标准,他们遵循该标准的可能性就越大(因此也就不会挑战它)。ADR 的“后果”部分是架构师判断一个标准是否有效以及是否应该被制定的另一个好地方。在“后果”中,架构师必须考虑并记录他们正在制定的特定标准的含义和后果。通过分析后果,架构师可能会决定放弃推行该标准。
本书 7.2.1 节中存在许多架构决策。使用事件驱动的微服务、拆分出价者和拍卖者用户界面、在视频捕获中采用实时传输协议( RTP)、使用单个 API 层、使用发布和订阅消息传递,这些只是为这个拍卖系统做出的数十种架构决策中的几个。无论多么的显而易见,系统中的每个架构决策都应记录在案并证明其合理性。
图 19-4 展示了 GGG 拍卖系统中的一个架构决策,即在出价捕获、出价传输和出价跟踪服务之间使用发布和订阅(pub/sub)消息传递。
图 19-4:服务间使用发布和订阅模式
该架构决策的 ADR 可能如图 19-5 所示。
图 19-5:ADR 76. 出价服务之间采用异步发布和订阅消息传递机制
本文摘选自《软件架构:架构模式、特征及实践指南》
本书是美亚广泛好评的英文原书《Fundamentals of Software Architecture》的中文版,是畅销书《卓有成效的程序员》作者Neal Ford的全新力作,NETSTARS CTO 陈斌等资深架构师鼎力推荐。本书全面概述了软件架构的方方面面,涉及架构特征、架构模式、组件识别、图表化和展示架构、演进架构,以及其他许多主题。
读者福利:出版社特别授权“技术琐话”首次刊发本章节内容。
读者福利:本文评论点赞top3的朋友,获得新书一册。
往期推荐:
技术琐话
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。