11月3日,星期三,13:00
Penultimate Electronics公司的首席架构师Logan在自助餐厅打断了一群分布式架构的架构师的讨论。“Austen,你又打石膏了吗?”
“不,只是个夹板,”Austen回答,“我在周末玩极限飞盘高尔夫时扭伤了手腕——差不多要好了。”
“那是?……无所谓了。我刚过来时,你们在讨论什么,讨论得那么激烈?”
“为什么有些人有时不用微服务里的Saga模式把事务串联起来?”Austen问道,“这样架构师就可以把服务拆分到他们想要的那么小。”
“但是你不是还需要在Saga模式中进行编排吗?”Addison问道,“那我们需要异步通信时该怎么办呢?而且,事务会变得多复杂?如果拆得稀碎,我们还能保证数据的准确性吗?”
“你知道,”Austen说,“如果使用企业服务总线(ESB),它可以帮我们管理大部分事情。”“我以为早就没人用ESB了——难道不应该使用Kafka来做这样的事情吗?”
“我以为早就没人用ESB了——难道不应该使用Kafka来做这样的事情吗?”
“它们甚至不是一回事!”Austen说。
Logan打断了愈演愈烈的对话:“它们本来就风马牛不相及,但哪个都不是银弹。像微服务这样的分布式架构是很复杂的,尤其是当架构师无法把所有互相作用的因素梳理清晰的时候。我们需要的是一种方法或框架来解决架构中的难题。”
“当然,”Addison说,“无论我们做什么,都必须尽可能解耦——我看到的所有东西都说架构师必须尽可能地拥抱解耦。”
“如果你那么做,”Logan说,“所有东西都彻底解耦,就没有东西可以和其他部分通信了——这样做软件可太难了!就像很多东西一样,耦合本质上不是坏的,架构师只需要知道如何恰当地应用它。”
在分布式架构中,架构师面对的最困难的任务之一就是厘清作用于其中的各方势力和权衡。发表意见的人们不断赞美“松散耦合”系统的好处,但是架构师怎么可能设计一个互不关联的系统?架构师可以设计粒度非常细的微服务来达到解耦,但是随之而来的编排、事务和异步就成了大问题。泛泛地建议“解耦”,但却没人指导我们如何在达成这个目标的同时构建能用的系统。
粒度和通信上的决策让架构师很为难,因为没有四海皆准的原则帮助他们下定决心——没有已知的最佳实践可以直接应用到现实世界里复杂的系统上。直到现在,在面对每个具体的情况时,架构师还是缺少合适的视角和术语认真地分析并做出最好的(至少不是最糟的)权衡。
为什么架构师在分布式架构中如此难以抉择呢?毕竟从上个世纪开始,我们就用很多相同的机制(例如消息队列、事件等)来开发分布式系统了。为什么到了微服务这里,复杂度增加了这么多?
这和微服务的基本理念息息相关,微服务是由限界上下文的灵感激发而来的。与设计分布式系统不同,开发以限界上下文为模型的服务需要一个微妙而又重要的更改——现在事务性成了架构考量中的头等大事。在微服务之前的很多分布式系统中,事件处理器通常连接同一个关系型数据库,使得它可以处理诸如完整性和事务这样的细节问题。将数据库移动到服务边界内部,使得数据问题变成了架构考量。
如前所述,“软件架构”是你在网上搜不到答案的东西。现代架构师必须掌握的一项技能就是做权衡分析。虽然有几个已经存在了数十年的框架[比如架构权衡分析方法(Architecture Trade-off Analysis Method,ATAM; https://oreil.ly/okbuO )],但是它们都没有关注架构师日常面对的现实问题。
本书重点关注架构师在面对特有的问题时,如何针对任意数量的情景做权衡分析。在架构中,纸上谈兵容易,难点在于细节,尤其是当复杂的部分交织在一起,让人更难去观察和理解其中的每一部分,和图2-1一样。
对于纠缠不清的问题,因为难以拆分各种关注点来独立解决,所以架构师很难进行权衡分析。因此,权衡分析的第一步是厘清问题的维度,分析哪些部分是互相耦合的,以及这些耦合对变化的影响。出于该目的,我们使用最简单的方式来定义耦合。
耦合
如果改变软件系统中的一部分可能带来另一部分的改变,则这两部分是耦合的。
图2-1:把头发编起来,让你分不清其中的某一股
通常,软件架构会产生多维度问题,多种势力以互相依存的方式相互作用。为了做权衡分析,架构师必须首先判断出需要权衡的因素有哪些。
因此,这里我们对软件架构中现代权衡分析的建议是:
1.找出彼此纠缠的部分;
2.分析它们的耦合方式;
3.通过其变化对于相互依存的系统的影响来评估利弊。
步骤看起来简单,难点潜伏在细节中。因此我们用一个分布式系统中最困难的(可能也是最通用的)问题之一作为例子,在实践中讲解这个框架,它和微服务相关。
架构师如何决定微服务的体量和通信方式?
给微服务选择合适的大小,看起来是个普遍问题——太小的服务造成事务性和编排问题,太大的服务会带来规模和分布式问题。
为此,本书接下来的部分在回答上述问题时,会一一解释如何从多方面去考量。我们提供新的术语来区分相似却不同的模式,用现实中的案例来演示如何应用这些模式。
然而,本书的总体目标是提供范例驱动的技巧,帮助你学习如何为领域里的特定问题做权衡分析。我们从定义架构量子以及两种耦合类型(静态和动态)开始。
量子(quantum)这个词是物理学常用的一个术语,即量子力学。作者选用这个词的原因和物理学家的一样。Quantum源自拉丁语Quantus,意思是“多大”或“多少”。在物理学引入它之前,法律行业已经用它来表示“要求或允许的数额”,例如在赔偿金中。这个词也出现在数学领域的拓扑学里,涉及形状族的属性。因为源自拉丁语,所以它的单数形式是quantum,复数是quanta,类似于datum/data。
针对软件架构中各个部分之间的连接和通信方式的拓扑结构和行为方式,架构量子度量其拓扑结构和行为模式的几个方面。
架构量子
架构量子是一个可独立部署的工件,它具有高功能内聚、高静态耦合、同步动态耦合等特征。架构量子的常见例子是工作流中的一个结构良好的微服务。
静态耦合
表示架构如何通过契约解析静态依赖项。这些依赖项包括操作系统、框架和通过传递性依赖项管理发布的库,以及任何启动量子所需的操作需求。
动态耦合
表示量子在运行时如何通信,同步还是异步。因此,对于此类特征的适应度函数必须是持续的,通常要利用监控。
静态耦合和动态耦合看起来相似,架构师必须区分它们。一个简单的区分方式是,静态耦合描述了服务是如何串联起来的,而动态耦合描述了运行时服务如何互相调用。例如,在微服务架构中,一个服务一定会包含其所依赖的组件,例如数据库,这是静态耦合的表现——没有所需的数据,服务是无法运行的。在工作流的流程中,这个服务可能调用其他服务,这是动态耦合的表现。除了这个运行时的工作流以外,这些服务的使用不需要依赖其他服务。因此,静态耦合分析运行依赖,动态耦合分析通信依赖。
这些定义包括重要的特征,我们将详细分析每一个特征。
可独立部署表示架构量子的几个方面——每个量子代表一个在特定架构中可单独部署的单元。因此单体架构——作为一个单元进行部署——可定义为一个架构量子。在分布式架构(例如微服务)中,开发人员倾向于可以独立地部署服务,通常是高度自动化的。因此,从可以独立部署的角度来说,微服务架构中的一个服务代表了一个架构量子(取决于其耦合情况,接下来将讨论)。
在架构中,让每一个架构量子代表一个可部署资产有助于几个目的。首先,架构量子呈现的边界可以帮助在架构师、开发人员和运维人员之间形成实用的通用语言。每个人都理解问题的某些公用部分:架构师理解耦合特性,开发人员理解行为作用域,运维人员理解可部署特性。
其次,当架构师在分布式架构中寻求合适的服务粒度时,架构量子是必须考量的因素之一(静态耦合)。通常,在微服务架构中,开发人员面对一个艰难的问题:什么样的服务粒度可以提供一组最优的权衡呢?一些权衡可以解决可部署性:服务需要什么样的发布周期,还有哪些服务可能受到影响,涉及哪些工程实践,等等。架构师可以从对分布式架构中部署边界的准确理解中获益。我们将在第7章中讨论服务粒度及其权衡问题。
最后,可独立部署性强制架构量子包含通用耦合点,例如数据库。大多数关于架构的讨论都轻易地跳过了这些问题,例如数据库和用户界面(UI),但真实世界的系统大都必须面对这些问题。因此,任何使用共享数据库的系统,由于无法独立部署,都不能形成独立的架构量子,除非数据库部署是与应用同步进行的。许多分布式系统符合多个架构量子的条件,然而就因为它们共享一个公共数据库(有其自己的部署频率),使得它们无法通过可独立部署条件。因此,单考虑部署边界并不能完全提供一个有用的衡量标准。架构师还应该考虑架构量子的第二个条件(高功能内聚)来将架构量子限制在有效范围内。
高功能内聚在结构上意味着关联元素的近似性:类、组件、服务等。有史以来,计算机科学家定义了各种内聚类型,具体到这种情况下的通用模块,可能可以用类或组件代表,取决于平台。从领域的角度来讲,高功能内聚的技术定义和领域驱动设计里限界上下文的目标有一定重合:实现特定领域工作流的行为和数据。
单纯地从可独立部署角度来说,一个巨大的单体架构也有资格称为一个架构量子。然而,它当然不可能称为高功能内聚,因为它包括了整个系统的所有功能。单体越大,功能越不可能内聚。
理想情况下,在微服务架构中,每个服务模型作为一个单独的领域或工作流,因此展示了高功能内聚。内聚在这个上下文中不是关于服务如何交互来完成工作,而是关于服务之间如何彼此独立又相互耦合的。
高静态耦合意味着架构量子中的元素是紧密连接在一起的,是契约中的一部分。架构师认为REST和SOAP等是契约格式,而方法签名和运行依赖项(通过IP地址或URL之类的耦合点)也代表契约。因此,契约是架构中的困难部分。在第13章中,我们会讲到涉及各种契约类型的耦合问题,包括如何选择合适的类型。
在某种程度上,架构量子是度量静态耦合的一种方式,这种度量方式对于大多数架构拓扑来说相对简单。举例来说,图2-2是 Fundamentals of Software Architecture 一书中推荐的架构风格,同时展示了架构量子静态耦合。
任何单体架构风格都必须有一个量子,如图2-2所示。
图2-2:单体架构有且只有一个量子
诚如所见,任何作为单一部署单元且有唯一数据库的架构永远都只有一个量子。这种静态耦合的架构量子度量包括数据库,依赖单一数据库的系统无法拥有超过一个量子。因此,度量架构量子的静态耦合可以帮助我们识别架构中的耦合点,不仅仅是在开发中的软件组件里。大多数单体架构包含一个耦合点(通常是数据库),使得它的量子数量为1。
分布式架构通常以组件级解耦为特征。考虑如图2-3所示的基于服务的架构。
图2-3:基于服务的架构的架构量子
虽然每个服务模型都展现出了微服务中常见的隔离,但该架构只使用了单个关系型数据库,其架构量子分数只得到1分。
基于服务的架构
当说到基于服务的架构时,并不意味着所有基于服务的架构,而是一种特定的混合架构风格,它们遵循分布式宏分层结构,包含独立部署的用户界面、粗粒度的独立部署的远程服务和一个单体数据库。这种架构降低了微服务中的一个复杂度——数据库级别的隔离。服务处于基于服务的架构中时和在微服务里时遵循同样的原则(基于领域驱动设计的限界上下文),只是依赖于同一个关系型数据库,因为架构师没有看到分离数据库的价值(或者是看到了太多的负面影响)。
在重组单体架构时,基于服务的架构是常见的目标,可以在不破坏现有数据库模式和集成点的情况下进行分解。我们会在第5章中介绍分解模式。
到此为止,架构量子的静态耦合度量将所有的拓扑评估为1。然而分布式架构创造了多个量子的可能性,但不能保证全是这样。举例来说,事件驱动架构(Event-Driven Architecture, EDA)的中介者模式的评估结果将始终为单个架构量子,如图2-4所示。
尽管这种风格是一种分布式架构,但有两个耦合点使得它只有一个架构量子:数据库(与前面的单体架构一样)以及请求编排器(Request Orchestrator)——任何让架构整体赖以运行的耦合点都会在其周围形成一个架构量子。
图2-4:经过中介的EDA只有一个架构量子
代理者事件驱动架构(没有中心化的中介者)耦合度要低一些,但也不能保证完全解耦。思考图2-5中的事件驱动架构。
代理者事件驱动架构(没有中心化的中介者)仍然只有一个架构量子,因为所有的服务使用同一个关系型数据库,形成一个公共耦合点。架构量子的静态分析可以回答这个问题:“这个架构依赖对于启动服务是必需的吗?”即使在事件驱动架构的某种场景下,一些服务不访问数据库,如果它们依赖于需要访问数据库的服务,那么它们也将成为架构量子的静态耦合部分。
图2-5:即使像代理者事件驱动架构这类分布式架构,也只能是一个量子
然而,没有公共耦合点的分布式架构是什么情况呢?考虑如图2-6所示的事件驱动架构。
架构师为这个事件驱动系统设计了两个数据存储,并且服务集合之间没有静态依赖项。注意,任意一个架构量子都可以在类似生产环境中运行。它可能不能参与系统所需的每一个工作流,但可以成功地启动和运行——在架构中发送和接受请求。
架构量子的静态耦合度量评估架构和运行组件之间的耦合依赖。因此,操作系统、数据存储、消息代理、容器编排,以及所有其他运行性依赖,通过最严苛的契约和运行性依赖(关于契约在架构量子中扮演的角色详见第13章),都可以构成架构量子的静态耦合。
图2-6:有多个量子的事件驱动架构
微服务架构风格的特点就是服务高度解耦,数据依赖也一样。青睐于它的架构师喜欢高度解耦,并小心翼翼地不在服务之间创造耦合点,结果每个服务变成了独立的量子,如图2-7所示。
图2-7:微服务可能构成自己的量子
每个服务(作为一个限界上下文)都可能有自己的一套架构特征——某个服务可能比其他服务有更高级别的可伸缩性或安全性。这种架构特征作用域级别凸显了微服务架构风格的优势。高度解耦最大限度上允许在一个服务上工作的团队快速前进,且不用担心破坏其他依赖。
然而,如果这个系统和一个用户界面紧密耦合,则使它又成为一个架构量子,如图2-8所示。
图2-8:用户界面的紧密耦合使得微服务架构又降为一个量子
用户界面在前后端之间制造耦合点。如果后端服务有不可用的地方,则大部分用户界面都不能工作。
并且,对于架构师来说,如果这些服务必须在同一个UI下协同使用,他们就很难给每个服务设计不同级别的架构特征(性能、规模、弹性、可靠性等)(尤其是在同步调用的情况下,详见2.1.4节)。
架构师设计异步的用户界面,这样就不会在前后端之间创造耦合点。很多微服务项目开始倾向于使用微前端(micro frontend)框架作为微服务架构里的用户界面。在这种架构里,代表服务交互的用户界面元素是从服务本身发出的。用户界面表面充当了可以显示用户界面元素的画布,并且通常使用事件协助组件之间松散耦合的通信。图2-9正是这样一个架构。
图2-9:在微前端架构中,每个服务+用户界面组件构成一个架构量子
在这个例子中,4个圈起来的服务及其微前端构成了架构量子:每个服务都可以有不同的架构特征。
从量子的角度来说,架构中的任何耦合点都可能创造静态耦合点。如图2-10所示,思考两个系统共享数据库的影响。
即使在含有集成架构的复杂系统中,系统的静态耦合也提供了有价值的洞见。例如绘制静态量子图——架构师近来常用该技术来理解遗留架构——它描述事情是如何纠缠在一起的,有助于判断变更会影响哪些系统,提供了理解(以及可能的解耦)架构的方式。
静态耦合只是分布式系统里的半壁江山,还有一半是动态耦合。
图2-10:两个系统的共享数据库形成耦合点,创造了一个单独的量子
架构量子定义的最后一部分是关于运行时的同步耦合,换句话说,在分布式架构中,架构量子是如何与构成工作流的其他部分交互的。
服务如何互相调用的本质,使得权衡取舍更加困难,因为它代表了一个多维度决策空间,被以下三个因素影响。
通信
指的是连接同步类型:同步或异步。
一致性
描述工作流通信需要原子性还是可以使用最终一致性。
协调
描述工作流是否使用了编排器,还是服务是否通过分散协作的方式通信。
2.1.4.1 通信
当两个服务互相通信时,架构师需要回答一个最基本的问题:通信应该是同步的还是异步的。
同步通信需要请求者等待接收者的响应,如图2-11所示。
图2-11:同步调用等待接收者返回的结果
调用服务发起调用(使用任意一种支持同步调用的协议),并且阻塞直到接收者返回值(或者状态变更、错误条件)。
异步通信发生在两个服务之间,调用者向接收者发出一条信息(常以消息队列之类的机制),当调用者获悉消息将会被处理,它就回去工作。如果请求需要响应值,接收者会使用一个回复队列来异步地通知调用者结果,如图2-12所示。
调用者发送消息到消息队列里,然后继续运行,直到接收者通过返回调用通知它请求信息已经可用。通常,架构师使用消息队列(如图2-12上方的灰色圆柱管)来实现异步通信,但是队列很普遍,而且会在图表中制造干扰,所以很多架构师会省略它们,和下方的图示一样。当然,不用消息队列,架构师也可以用各种库或者框架来实现异步通信。两张图都暗含异步通信,下方的图在视觉上更简明,包含更少的实现细节。
图2-12:异步通信支持并行处理
当选择服务间通信方式时,架构师必须着重考虑其中的利弊。通信方式会对同步性、错误处理、事务性、可伸缩性和性能产生影响。本书的其余部分将对这些问题进行深入探讨。
2.1.4.2 一致性
一致性指的是通信调用必须遵循的事务完整性的严谨程度。原子性事务(全有或全无事务需要在一个请求过程中保持一致性)是一端,而不同程度的最终一致性在另外一端。
事务性——让几个服务参与一个全有或全无的交易——是分布式架构中最难建模的问题之一,因此人们普遍建议尽量避免跨服务事务。我们将在第6、9、10和12章中讨论一致性以及数据和架构的交叉问题。
2.1.4.3 协调
协调是指由通信建模的工作流需要多少协调步骤。微服务的两种常见通用模式是协调和编排,我们将在第11章中介绍。简单的工作流——单一服务对请求的回复——不需要从这个维度进行特别考虑。然而,随着工作流复杂性的增加,对协调的需求会越来越大。
这三个因素(通信、一致性和协调方式)都告诉架构师必须遵循的重要决策。然而,架构师不能分别做这三个选择,每个选择都对其他选择有一定影响。例如,事务性在带有协调者的同步架构里更简单,然而分散协调保持最终一致性的异步架构才能支撑更大的规模。
你可以认为这些相互关联的因素形成一个三维空间,如图2-13所示。
在服务通信期间起作用的每一种力量都表现为一个维度。对于特定的决策,架构师可以绘制代表这些力量的强度的空间位置。
图2-13:动态量子耦合的维度
当架构师对给定情况下的所有因素都有清晰的认识时,就有足够的条件做利弊分析了。对于动态耦合来说,表2-1展示了一个框架,通过8种可能的组合来识别基本模式名称。
表2-1:分布式架构的维度交叉矩阵
为了彻底理解这个矩阵,我们必须先深入理解每个维度。因此接下来的几章会帮你逐一理解不同的通信、一致性和协调方式之间的利弊,然后在第12章重新以整体来看它们。
在第一部分接下来几章里,主要关注静态耦合,并理解分布式架构里的不同维度,包括数据归属、事务性和服务粒度。第二部分侧重于动态耦合和理解微服务里的通信模式。
11月23日,星期二,14:32
Austen一脸费解地来到Addison的办公室:“嘿,Addison,可以打扰你一下吗?”
“当然,怎么了?”
“我一直在看架构量子这个东西,就是不太能理解!”
Addison笑了:“我明白你想说什么。当它是抽象的概念时我也难以消化,但是如果能联系实际,你就会发现它是一套很有用的观点。”
“怎么讲?”
“好吧,”Addison说,“架构量子可以类比DDD中的限界上下文这一术语。”
“那为什么不直接用限界上下文呢?”Austen问道。
“限界上下文在DDD中是一个专有定义,用它去描述架构中的概念会让人们难以区分。它们虽然相似,但的确不是同一个事物。第一部分里讲的功能内聚和独立部署确实符合一个基于限界上下文的服务。但是架构量子这个概念还会区分耦合的类型,这就得说说静态耦合和动态耦合了。”
“那是什么?耦合不就是耦合吗?为什么还要做区分?”
“事实证明,不同的耦合类型有不同的关注点,”Addison说,“让我们先说说静态耦合,我倾向于认为它描述的是事物连接的方式。换句话说,如果考虑在目标架构中去搭建一个服务,那么启动这个服务需要提前准备什么?”
“嗯,这个服务是用Java写的,数据库用的是Postgres,运行在Docker里,就这些,对吗?”
“你遗漏了很多,”Addison说,“如果你必须从头开始搭建该服务怎么办?假设什么都没有。它使用Java,但也用SpringBoot,如果还有15到20个不同的框架和库怎么办?”
“你说得对,那我们可以在Maven POM文件里去找到所有的依赖项,还有什么?”
“静态量子耦合背后的想法是能运作的布线需求。我们用事件进行服务间通信,那需不需要事件代理呢?”
“但那不是动态的部分吗?”
“代理本身的存在不属于这种情况。如果我想启动的服务(或者宽泛点说架构量子)需要消息代理才能工作,那么代理必须出现。当服务通过代理调用另外一个服务时,才到了动态的部分。”
“好吧,有道理,”Austen说,“如果考虑从头开始搭建服务需要什么,那这就是静态量子耦合。”
“正是如此。那个信息非常有用。我们最近为每个服务防御性地构建了静态量子耦合图。”
Austen笑了:“防御性的?你们……”
“我们正在进行可靠性分析,以确定如果我改变了这个东西,哪些可能会被破坏,这个东西可能是架构里的任何事物或者操作。他们正在努力降低风险——他们想知道如果我们改了一个服务,则必须测试什么。”
“我懂了,这就是静态量子耦合,现在我能理解它有用的地方了。它还可以显现出一个团队是怎么影响其他团队的。确实很有用。那有什么可以下载的工具来展示这种静态量子耦合关系吗?”
“你认为它有用真是太好了!”Addison笑道,“不幸的是,还没有人使用我们特有的架构组合来构建和开源我们想要的工具。不过,平台团队的一些人正在给它开发自动化工具,需要针对我们的架构做一些定制。他们使用容器描述文件、POM文件、NPM依赖项,以及其他的依赖工具来构建和维护一个构建依赖项清单。我们还为所有服务建立了可观察性,所以现在我们有持续的日志文件,记录哪些系统相互调用、何时以及多久相互调用。他们正在使用它来构建调用图来查看事物是如何连接的。”
“好吧,所以静态耦合是关于事物是怎么联系在一起的,那动态耦合又是什么?”
“动态耦合是关于量子彼此之间是如何通信的,尤其是同步调用和异步调用,以及它们是如何影响运维架构特征的,比如性能、可伸缩性、弹性、可靠性等。你先想想弹性——还记得可伸缩性和弹性之间的区别吗?”
Austen笑着说:“我没想到还要考试。我想想,可伸缩性是支持大量并发用户的能力,弹性是在短时间内支持大量用户请求的能力。”
“完全正确!你真是太棒了。是的,让我们想想弹性。假设在未来系统里我们需要两个服务(工单服务和分配服务)和两种调用。我们精心设计了彼此高度静态解耦的服务,以便它们可以独立地具有弹性。顺便说一下,这是静态耦合的一个额外作用——它确定了运维架构特征等事物的范围。假设运行工单服务的弹性规模是分配服务的10倍,而我们需要在它们之间进行调用。如果使用同步调用,那么整个流程将会卡住,因为调用方需要等待较慢的服务去处理请求和返回。如果使用异步调用则是另一种情况,用消息队列作为缓冲区,就可以让两个服务独立地运行,从而允许调用者将消息添加到队列之后继续工作,并在工作流完成时收到通知。”
“哦,懂了懂了。架构量子定义了架构特征的范围,所以很明显静态耦合会影响它,但是我现在也发现,因为使用了不同的调用类型,两个服务也可能暂时耦合在一起。”
“是这样的,”Addison说,“在调用过程中,如果调用的性质与性能、响应性、规模和其他一些事情相关,那么架构量子可能暂时相互纠缠。”
“好的,我想我了解架构量子是什么了,以及不同类型的耦合是怎么回事儿了,但是我还没有直观地理解两个量子——quantum(单数)和quanta(复数)的区别。”
“它们就像数据datum和data一样,没有人会用datum,”Addison笑道,“如果你继续深入我们的架构的话,就会发现更多动态耦合对于工作流和事务Saga的影响。”
“我等不及了!”