理查森:我是克里斯·理查森。欢迎来到我关于在微服务架构中最小化设计时耦合的演讲。在这次演讲中,我将回答三个问题。什么是设计时耦合?这会造成什么问题?我们如何设计松散耦合的服务?这些年来我做了一些事情。最值得注意的是,我写了一本书《POJOs in Action》我创建了最初的CloudFoundry,它是用于在AWS上部署Java应用程序的PaaS。这些天,我专注于微服务架构。我写了一本书,“微服务模式”我通过咨询和培训帮助世界各地的组织成功地采用和使用微服务。
概述
现在我们来讨论设计时耦合。首先,我将描述微服务架构的基本特征,包括松散的设计时耦合。之后,我将描述一些最小化设计时间耦合的技术。最后,我将使用订购外卖玉米煎饼的问题来说明潜在的耦合问题,然后展示如何消除它们。
微服务架构=架构风格
微服务架构是一种架构样式,它将应用程序构造为一组服务。这些服务是松散耦合的。每个服务都由一个小团队拥有。每个服务都是可独立部署的。每个服务的交付周期(即从提交到部署的时间)必须少于15分钟。
为什么微服务:成功三角
为什么要使用微服务?微服务的采用受到两个重要趋势的推动。第一个趋势是,正如马克·安德森(Marc Andreessen)在2011年所说,软件正在吞噬世界。这个短语的意思是,企业的产品和服务越来越依赖于软件。无论您的公司是金融服务公司、航空公司还是矿业公司,软件都是您业务的核心。第二个趋势是世界变得越来越不稳定、不确定、复杂和模糊。可悲的是,没有比冠状病毒更好的例子了,它是最终的破坏者。由于世界的动态性和不可预测性,企业需要灵活。他们需要敏捷。他们需要更快地创新。因为软件为这些业务提供了动力,所以它必须更快、更频繁、更可靠地交付软件。
要快速、频繁、可靠地交付软件,您需要我所说的成功三角。您需要三件事情的组合:流程、组织和架构。该过程是DevOps,包含了诸如持续交付和部署之类的概念,并经常向生产交付一系列小的更改。您必须将您的组织构建为一个由自主、授权、松散耦合、长寿命的产品团队组成的网络。您需要一个松散耦合和模块化的架构。松耦合再次发挥了作用。如果您有一个开发大型复杂应用程序的大型团队,您通常必须使用微服务。这是因为微服务架构为您提供了进行DevOps所需的可测试性和可部署性,并提供了松散耦合,使您的团队能够松散耦合。
我已经谈了很多关于松耦合的问题,但这到底是什么呢?跨服务的操作会在服务之间创建耦合。服务之间的耦合是连接的程度。例如,在我在整个演讲中使用的客户和订单示例中,createorder操作在客户服务中保留信用,并在订单服务中创建订单。因此,这两个服务之间存在一定程度的耦合。
运行时解耦
解耦主要有两种类型。第一种类型的耦合是运行时耦合。运行时耦合是一个服务的可用性受另一个服务的可用性影响的程度。让我们想象一下,订单服务通过向客户服务发出PUT请求以保留信用来处理CREATEORDER请求。虽然这看起来很简单,但实际上它是一个运行时紧密耦合的示例。在收到客户服务的响应之前,订单服务无法响应POST请求。create order端点的可用性是两个服务可用性的乘积,这两个服务的可用性小于单个服务的可用性。这是分布式应用程序中常见反模式的一个简单示例。消除紧密运行时耦合的一个好方法是使用异步消息传递机制,如saga模式。例如,订单服务可以立即响应创建请求。响应将告诉客户机已收到创建订单的请求,并且需要稍后再进行检查以确定结果。然后,订单服务将与客户服务交换消息,以完成订单的创建。
设计时耦合
第二种类型的耦合是设计时耦合,这是本次演讲的重点。设计时耦合是一个服务因另一个服务的更改而被迫更改的程度。发生耦合是因为一个服务直接或间接地依赖于另一个服务所拥有的概念。让我们假设订单服务使用客户服务的API。它要么调用服务操作,要么订阅其事件。依赖性并非天生就坏。很多时候,这是绝对必要的。但是,这会创建从订单服务到客户服务的设计时耦合。设计时耦合之所以是一个潜在问题,是因为概念可以改变。例如,存在这样一种风险,即对客户服务的更改将强制此API以要求订单服务也更改的方式进行更改。耦合程度取决于客户域的稳定性、客户服务API的设计以及订单服务消耗了多少API。联轴器越紧,锁步变化的风险越大。正如我在后面所描述的,锁定步骤更改需要团队协作,这会降低生产率。
因此,松耦合至关重要。重要的是要记住,松耦合是不能保证的。您必须仔细地将服务设计为松散耦合的。理想情况下,我们应该以避免任何设计时耦合的方式设计服务。例如,我们可以考虑通过将客户和订单子域置于同一服务中,将创建订单转换为本地操作。但是,如果它创建的服务太大,小团队无法维护,那么这可能不是一个好主意。一般来说,虽然我们可以尝试避免设计时耦合,但消除它通常是不现实的。相反,目标是将其最小化。
模块化和低耦合时老想法
这是关于松耦合和微服务的讨论。松耦合是一个跨越整个设计空间的古老概念。例如,帕纳斯在1972年写了一篇关于模块化的著名论文。标题是关于将系统分解为模块时使用的标准。本文中的许多想法与微服务非常相关。另一方面,它们也适用于设计类。
为什么低耦合重要
为什么松耦合很重要?《加速》(Accelerate)是一本必读的书,书的作者发现业务成功与软件开发组织的绩效之间存在着很强的相关性。他们还发现,高绩效组织中的开发人员同意以下说法:,“在不与团队外人员沟通和协调的情况下完成他们的工作。在不依赖其他团队对其系统进行更改或为其他团队创建重要工作的情况下,对其系统的设计进行大规模更改。“能够以这种方式工作需要一种从设计时的角度来看是松散耦合的架构。换句话说,松散的设计时耦合使业务更加有利可图。
锁定步骤更改:添加新冠病毒交付附加费
松散设计时间耦合的反面是紧密设计时间耦合。紧密的设计时耦合是高性能的障碍,因为它会导致锁步更改,这需要团队协调他们的工作。让我们看一个简单的例子。假设订单服务有一个用于检索订单的API端点。订单有四个字段:小计、税费、服务费和送货费。缺少的是订单总数的字段。可能该端点是从数据库模式自动生成的,该模式不存储订单总数。因此,会计服务等客户必须自己计算订单总额。起初,这不是什么大问题,因为这是一个非常简单的计算。然而,在2020年3月,该组织需要实施新冠病毒附加费,以支付个人防护设备的费用。由于计算不是集中的,多个团队需要跟踪并更改代码库中计算订单总数的多个位置。这是一个缓慢且容易出错的过程。这是一个影响多种服务的变化的好例子。更糟糕的是,让我们假设对会计服务进行必要的更改需要对其API进行突破性的更改。这将迫使会计服务的客户也要步调一致地改变,需要更多的会议进行协调。在最坏的情况下,您可以拥有所谓的分布式整体,其中许多或所有服务都在同步地不断变化。这是一种结合了两种建筑风格中最糟糕方面的建筑。
跨团队:单体(巨石) vs 微服务
这种跨团队的协调也发生在整体架构中。这也是不可取的。然而,在单片架构中,更容易构建、测试和部署多个团队所做的更改。您只需在分支上进行必要的更改,然后构建、测试和部署它们。相比之下,部署跨多个服务的更改要困难得多。因为服务是独立部署的,没有停机时间,所以您不能简单地对服务API部署突破性的更改。首先,必须部署支持旧版本和新版本API的服务版本。接下来,必须将所有客户端迁移到该较新的API。最后,您可以删除旧的API版本。这比在一块巨石上做的工作多得多。如您所见,架构级别的耦合导致团队之间的耦合。这是康威定律的一个很好的例子。这里有一个有趣的小提示,梅尔·康韦在推特上,有一些非常有趣的事情要说。
DRY(Do not repeat your self)服务
有几种技术可用于最小化设计时耦合。第一个是应用经典的设计原则,不要重复你自己。该原理表明,每个概念(如订单总数计算器)在应用程序中都有一个表示形式。换句话说,应该有一个地方计算订单总数。您可能会尝试使用传统的方法,在嵌入多个服务的库中实现计算。虽然使用库来实现稳定的实用概念(如金钱)通常是可以的,但包含不断变化的业务逻辑的库还不够干涸。这是因为所有服务都必须使用相同版本的库。当业务逻辑发生变化并且发布了新版本的库时,许多团队必须同时升级到该版本,团队之间还要进行更多的协调和协作。为了在微服务架构中正确应用DRY原则,每个概念都必须在单个服务中表示。例如,订单服务必须计算订单总额。任何需要知道订单总额的服务都必须查询订单服务。这就是干燥原理。
冰山:尽可能少地暴露
另一个有助于实现松散设计时耦合的原则是冰山原则。就像冰山的大部分都在水面下一样,服务API的表面积应该比实现小得多。这是因为隐藏的内容很容易更改,或者相反,通过API公开的内容更难更改,因为它会影响服务客户端。服务API应该封装或隐藏尽可能多的实现。冰山原理的一个很好的例子是简单的API,比如Stripe或Twilio API。Twilio API for SMS允许您向150多个国家/地区的订户发送SMS,但API端点只有三个必需的参数:目的地号码、发件人号码和消息。这个极其简单的API隐藏了将消息路由到适当国家/地区的所有复杂性。我们应努力将同样的原则应用于我们的服务。帕纳斯1972年的论文甚至包含了几句智慧的话。首先,列出最重要和/或最不稳定的设计决策。第二,设计模块,或者在这个场景中,设计封装这些决策的服务。
冰山原理涉及最小化服务表面面积。为了确保松散耦合,服务也应该消耗尽可能少的资源。我们应该尽量减少服务的依赖性数量,因为每一个依赖性都可能触发更改。此外,服务应该尽可能少地使用每个依赖项。此外,应用Postel的健壮性原则并以忽略不需要的响应和事件属性的方式实现每个服务也很重要。这是因为,如果服务选择性地反序列化消息或响应,那么它不会受到对其实际不使用的属性的更改的影响。有趣的是,要记住的一件事是代码生成的反序列化逻辑,通常反序列化所有属性。
每个服务使用一个数据库
促进松耦合的另一个关键原则是每个服务的数据库。例如,让我们想象一下,您将您的巨石折射到服务,但保持数据库不变。在这个部分折射的架构中,订单服务通过直接访问客户表来保留信用。这看起来很简单,但这会导致紧密的设计时耦合。如果负责客户服务的团队更改了客户表,则需要在锁定步骤中更改订单服务。为了确保松散的设计时耦合,服务不能共享表。相反,它们只能通过API进行通信。
外卖玉米饼:设计时耦合的一个案例研究
现在我想讨论一个设计时耦合的例子,它是由我在过去一年中过度食用外卖食品引起的。我有很多时间来彻底研究这个领域。我们将探讨如何改进架构,使其能够更好地处理不断变化的需求。我两本书中的示例应用程序都是Food to Go应用程序。它是一款食品配送应用程序,就像Deliveroo或Dooddash一样,但与这两家公司不同的是,它的虚拟股票自首次公开募股以来实际上已经增值。最初,to Go食品有一个整体式的架构,但随着时间的推移,应用程序团队不断壮大。它被迁移到微服务架构。以下是一些关键服务。订单服务负责创建和管理订单。它使用saga模式实现createorder命令。订单服务首先验证使用餐厅信息的CQRS副本创建订单的请求,该副本由餐厅服务拥有。接下来,它用订单ID响应客户机。然后,订单服务通过与其他服务异步通信完成订单的创建。它调用消费者服务来验证消费者是否可以下订单。接下来,它调用会计服务来授权消费者的信用卡。最后,它创建一个票证。
我想重点讨论订单服务和餐厅服务的设计时耦合。餐厅服务的主要职责是了解有关餐厅的信息。特别是,它的API公开了菜单。在本例中,餐厅服务发布事件,但如果它有一个REST端点,则设计时耦合将是相同的。订单服务使用菜单信息来验证和定价订单。现在,让我们探讨更改对餐厅子域的影响。我想讨论的第一个变化是支持不同大小的菜单项。例如,让我们想象一家餐馆,出售两种不同大小的薯条和萨尔萨酱,一种是小的,一种是大的。我们可以通过引入子菜单项的概念来支持这一要求。薯片和莎莎酱等菜单项可以有两个子菜单项,每种尺寸一个子菜单项。通过向DTO和事件添加父菜单项ID,我们可以向客户端公开菜单项层次结构。这是一个加性变化,因此它是一个非破坏性变化。订单服务可以忽略此属性,因此不受更改的影响。
现在让我们来看另一个变化,它表面上非常相似,但影响更大。有些餐馆有可配置的菜单项。例如,我最喜欢的一家餐馆让你定制你的玉米煎饼。有很多选择,包括付费的附加品,如烤辣椒和鳄梨酱,非常美味。添加对可定制菜单项的支持需要对餐厅订单和厨房域进行大量更改。菜单项需要描述可能的选项。它有一个底价。菜单项具有零个或多个菜单组选项,这些选项已命名,并且具有最小和最大选择属性。每个菜单项组选项都有一个或多个菜单项选项,菜单项选项有名称和价格。为了计算小计,订单行项目需要描述所选选项。订单行项目具有零个或多个描述所选选项的订单行项目选项。同样,为了让厨房准备订单,票据行项目还必须描述所选选项。但是,它只需要知道每个行项目所选选项的名称。这是一个具有广泛影响的变化的例子。拥有三个受影响服务的团队需要花时间规划、实施和部署更改。
理想情况下,最好以避免这种情况的方式构建应用程序。一般来说,隐藏的概念是可以改变的。因此,我们需要一个将菜单结构封装在餐厅服务中的架构。让我们看看如何做到这一点。在本例中,订单服务与餐厅服务耦合,因为它使用菜单项,并且它存储引用菜单项的行项目以记录实际订单。订单服务还使用菜单项验证订单并计算小计。因此,我们可以通过将这些责任转移到餐厅服务来减少耦合。在这种新的设计中,订单服务与餐厅服务的耦合程度明显降低。它仅仅依赖于订单验证和计算小计的概念,它们更简单、更稳定。也许这种方法的一个缺点是餐厅服务现在是订购流程的关键路径的一部分。以前,订单服务有一个菜单项的副本,该菜单项与餐厅服务发布的事件一起维护。在某种意义上,我们减少了设计时耦合,但增加了运行时耦合。这是定义微服务架构时必须进行权衡的一个示例。我们还可以使用API组合将厨房服务与菜单项结构分离。在显示票证时,UI可以动态地从餐厅服务获取票证,而不是存储这些行项目的票证。
基于编舞的协调(Choreography-based Coordination)
我想讨论一下这个传奇的设计,它协调了订单和门票的创建。有两种选择。第一种选择是使用基于编舞的传奇。API网关发布订单创建请求事件。每个服务都订阅该事件。票证服务创建一个票证。订单服务创建订单。餐厅服务也尝试创建订单。如果成功,它将发布一个包含订单小计的订单验证事件。如果失败,餐厅服务将发布订单验证失败事件。其他服务订阅这些事件并做出相应的反应。
基于编排的协调 (orchestration-based Coordination)
另一种选择是使用编排。API网关将创建订单请求路由到业务流程服务。编排服务使用异步请求-响应从餐厅服务开始调用每个服务。编曲和编舞大致相当。然而,它们在耦合的一些细节上有所不同。基于编舞的传奇中的所有参与者都依赖于订单创建请求事件。事实上,团队实际上需要协作来定义该类型。相反,saga编排器依赖于参与者的api。在给定的情况下,一种方法可能比另一种更好。
另一种选择是使用编排。API网关将创建订单请求路由到业务流程服务。编排服务使用异步请求-响应从餐厅服务开始调用每个服务。编曲和编舞大致相当。然而,它们在耦合的一些细节上有所不同。基于编舞的传奇中的所有参与者都依赖于订单创建请求事件。事实上,团队实际上需要协作来定义该类型。相反,saga编排器依赖于参与者的api。在给定的情况下,一种方法可能比另一种更好。
总结
快速频繁的开发需要松散的设计时耦合。您必须仔细设计服务以实现松散耦合。你可以应用干燥原理。您可以将服务设计为冰山。您可以仔细设计服务依赖项。最重要的是,您应该避免共享数据库表。
问答
瓦特:有一个问题在很多事情上都得到了加分,那就是关于您的建议,当API启动异步通信时,您可以将异步API作为入口点来解决问题,但之后仍然需要响应同步请求。也许你想详细说明一下?
Richardson:理想情况下,同步请求只是启动一些事情,然后请求处理程序可以立即返回,就像它返回了一个201,不管它是什么,可以说是创建的。如果它必须等待整个过程完成,那么处理这些请求的服务的每个实例都可以拥有自己对事件的私有订阅,这些事件将指示启动的操作的结果。比如,它可以订阅创建的订单,并订购失败的事件。您可以在反应式接口中想象这一点,其中同步请求处理程序返回一个可完成的未来或您正在使用的任何反应式抽象。然后在请求ID之间有一个HashMap,您需要一些相关ID,这样当一个事件返回说该顺序创建成功或失败时,事件处理程序就可以获取相关ID,查找Mono或CompletableFuture。完成它,这将触发响应的发回。有点乱。它有点进化了。它的缺点是这种架构中存在运行时耦合。我曾与客户合作过,他们刚刚不得不这么做。其中一个甚至有一个SOAPAPI小东西,线程实际上必须阻塞,直到消息处理完成。
瓦特:有时候,用完美的方式去做并不总是那么简单。
如果域驱动的设计正确完成,并且您可以识别聚合、聚合路由和实体以及共享的内核属性,那么设计时耦合可以完全解决吗?
理查森:我想说“是”和“不是”,但其中一件事是如果做得好。如果您能够正确地完成,我认为这确实解决了设计时耦合的一些方面。另一方面,您必须做出关于分解为服务的决策。我想我在演讲中指出了这一点,服务边界就是这些物理边界,因为它们涉及网络通信等等。您需要解决的是一组稍有不同的关注点,这些关注点并不完全与传统的DDD或传统的模块化一致。我认为仅仅做DDD是不够的。
瓦特:当然。我认为有时有很多方法可以决定什么时候需要在边界中使用微服务,这也可以是技术性的,不一定只是领域驱动的。
对于每个服务都有一个数据库,对于在整个企业中创建或维护统一的数据模型,您有什么建议?这是好事吗?
理查森:我知道这是一件非常有趣的事情,因为我认为在企业中,有一种强烈的愿望去做这件事。如果你只看领域驱动设计中的一个关键思想,它是一个有限的环境,这种概念是拥有多个模型而不是一个大的联合体,就像客户是什么的全局视图。即使仅仅从DDD的角度来看,更不用说微服务的角度来看,全局模型通常也不是一件事。从一个角度来看,模型是根据服务公开的API存在的。我认为,是的,你可以在这方面保持一致性。就像客户名称在所有API或地址中以一致的方式表示一样。我认为这是一种更分散的思维方式。
瓦特:我认为,当您通过消息和模式之类的东西使用异步通信时,有时甚至会对消息中的一些耦合缺乏了解。您有什么实用的工具和技巧可以用来最小化服务之间消息中模式更改时的影响吗?
理查森:这很棘手。在理想的世界中,你的事件会发生变化。事件的模式总是以向后兼容的方式发展,因此您所做的更改是可添加的。理论上,某些域对象生命周期中的事件可能以不兼容的方式发生变化。这植根于商业概念,我认为这有一定的稳定性。部分原因是,如果它们以不兼容的方式更改,您必须更新所有使用者,以便他们能够处理旧模式和新模式。升级后,就可以切换到在新模式中发布事件。我认为,任何这样的重大变化都会带来一定程度的痛苦。
瓦特:在你的演讲中,你提到了API的附加更改。你能把你的意思扩大一点吗?
理查森:同步有两个部分,一个是请求,一个是响应。您可以向请求添加可选属性。老客户显然不知道这个属性。他们仍然可以发送旧请求,服务器可以提供默认值。然后,反过来,在响应中,服务器可以返回额外的属性,而客户端可以以一种忽略它不理解的属性的方式编写。有额外的属性,但它们并不相关。
有人评论说团队创建了大量细粒度服务。我的建议是从每个团队提供一项服务开始,除非有充分的理由提供更多服务。虽然我确实看到过相当常见的反模式,但它就像每个开发人员都有一个服务。还有一些公开的更极端的例子,但每个开发人员一个服务似乎很常见。对我来说,是的,看起来您正在创建一个非常细粒度的架构。我会简化事情,因为有一种可能,当发生一些变化时,突然之间,你必须在很多地方做出改变。或者,你最终建立了这个过于复杂的系统,你会发现它在认知上是压倒性的。