【编者的话】本文通过一个简单例子,挑战了一个广泛传播的误区:一个微服务正好等于一个REST接口。并以此为契机,指出应该按照业务领域而非技术领域进行团队组织,根据领域驱动设计理念,利用业务词汇,分别从下限和上限判断微服务规模大小。同时建议团队在了解并成功实现了敏捷方法和DevOps原则的基础上,才能尝试大规模采用微服务。
微服务架构终于开始牢固地确立为一种架构模式。Martin Fowler 和 James Lewis 撰写的
关于该主题的开创性论文在上个月已满六年了,现在关于架构的讨论中,微服务即使不是整个讨论话题,至少也占了很大篇幅。
与任何新技术一样,微服务也遵循不可避免的
Gartner技术成熟度曲线。从我们在Twitter中收集到的信息来看,它正位于“幻灭波谷”和“启蒙坡的早期爬升”两个阶段之间。这意味着在过去六年左右的时间里,我们已经学习了很多有关构建微服务的重要课程,其中之一是确保以正确的方式考虑每个微服务的范围。
你会看到,微服务这个名字往往会引导人们朝着他们可能真正要走的路旁的深草丛生的方向前进。当你想到“微服务”一词时,引起你注意的第一件事就是前缀“微”。现在,根据我手头的古典希腊教科书[1]所说,
μικρός对柏拉图或亚里斯多德来说只意味着很少或很小。但是,在日常英语用法中,“微”往往表示惊人的小东西—毕竟,“千分尺”是一米的百万分之一,而你使用“显微镜”可以看到肉眼看不见的东西,因为它们的尺寸非常小。
正是因为人们对问题的理解存在差异。与之前的单体应用相比,微服务应该“小”。但是,它不应该
太小-试图使你的微服务太小可能是团队在尝试实现微服务架构时遇到的最常见的错误之一。
这种“微服务必须很小”的想法,使我们遇到了第一个问题。我们听到的关于微服务的常见投诉之一是,将它们用于银行等复杂领域太难了,因为它们所需的REST或消息传递接口无法提供跨多个微服务进行两阶段提交的方法。每当我们听到这类投诉时,我们的脑海中就会发出警钟–该投诉通常是团队将微服务视为很小的服务的征兆。为了解决这个问题,让我们从一个简单的样例开始,寻找一种解决方法,然后回头思考更多有关该解决方案对整个微服务体系结构的影响。
我们将从一个简单的帐户服务设计开始,从一个不包含微服务实现的设计开始,以便我们了解它的发展。假设该团队正在应用
领域驱动设计(稍后会详细介绍),并且在他们的第一轮研究中,他们发现需要一个帐户实体,来引用几个相关的依赖
实体(条目和所有者)。
团队很快发现该帐户上有一些定义明确的操作;借方,贷方,打开和关闭。幸运的是,这些操作可以很好地映射到REST接口,因此采用这种简单的基于实体的设计并将其映射到微服务实现相对容易。但是,问题很快就来了,他们尚未弄清楚如何在帐户之间进行转帐,如下图所示:
事实是,帐户之间转移的整个想法需要一个全新的REST接口-这很明显。问题是你如何实现,以及这是否代表了全新的微服务?
最简单的假设(有很多团队就是这么做的),假定微服务和REST接口之间应该有1-1的映射。但是,当我们将该等价应用于我们的示例时,我们很快就会遇到问题。我们将如何实施新的微服务?最简单的方法似乎是让Transfer微服务两次调用Account微服务。一次是从“发件人”帐户中扣除,一次是贷记到“收件者”帐户中。因此,我们的实现可能看起来像这样:
但是,这使我们陷入了前面提到的两阶段提交问题!如果借方成功而贷方失败,则可怜的客户的帐户中的钱将比以前少,但没有任何追索权。这种情况显然是不可接受的。许多团队改为尝试以下解决方案:
这解决了两阶段提交问题,但是现在我们要做的是在数据库中引入耦合。这违反了微服务设计的原则之一,那就是服务应该拥有其数据,并且不能通过共享数据库“隐藏”耦合。那么正确的答案是什么?不幸的是,许多团队继续(我认为)走得很远,开始寻求涉及Saga模式的解决方案,以便通过补偿交易来解决问题。但是,虽然Saga模式确实存在,但对于像这样的简单情况,它并不是正确的默认解决方案。
让我们考虑以下解决方案,它挑战了先前的假设之一:
在此解决方案中,我们要做的是重新考虑我们所认为的约束-一个微服务正好等于一个REST接口。这个假设已被编码到网上的数十个微服务教程中。但是,如果你仔细阅读Fowler的原始论文,将会发现从未指定此假设。可以在微服务边界公开多个服务。但这并不是这个小练习的重点。相反,我们想缩小范围,然后借用此练习来阐明更广泛的问题。
之前,我们说过示例中的团队已将领域驱动设计(DDD)作为其微服务设计过程的一部分[2]。在这方面,团队的目标是正确的。在团队构建微服务设计过程中,我们在该领域看到的最大问题之一是,他们通常不会从领域驱动设计之类的技术开始。相反,他们从其他地方开始,例如从现有系统的设计开始,然后尝试从那里派生各自的微服务。或者,它们以架构(通常根据工具和框架来指定)开始,并试图让微服务“有机地”发展。在这两种情况下,最终得到的都不是我所说的微服务-它们往往非常注重技术,并且与业务可以识别的术语完全无关。取而代之的是,我们倾向于用那些被企业识别为“业务微服务”的词语来指称那些框架,并尝试在设计中找到它们。设计不足的症状是解决方案中几乎没有“业务微服务”,因为你不是从业务词汇开始的。从业务词汇开始是至关重要的第一步,这就是为什么我们建议所有构建微服务的团队在其设计过程中都应用领域驱动设计。
如果你不首先从业务词汇开始,那么你通常会得到一个可能如下所示的体系结构:
现在,你们中的许多人可能会看着这个说:“这到底有什么问题?这看起来像我们自己的微服务架构!”为了回答这个问题,我们必须一直追溯到Fowler在其原始的Microservices论文中提出的观点。他和Lewis提出的观点之一是,当你拥有由技术专家组成的团队时,你所生产的软件也将按技术领域进行组织,这就是
康韦法则。他的解决方案是,你应该让按业务职能组成的跨职能团队代替。
但是,当你开发类似于之前的微服务架构时,你实际上已经回到了微服务原本要解决的问题!你不仅重新创建了单体应用,而且因为创建了
分布式的单体应用而使情况变得更糟。这违反了马丁有史以来最重要的一句话:
福勒的“分布式对象第一定律”:请勿分布式处理你的对象。
那么,你的架构应该是什么样?好吧,在顶层,只需要想象绘制垂直而非水平线即可。
简而言之,微服务架构在50,000英尺的范围内应该是什么样子。该体系结构应包括一组独立的服务,这些服务按业务领域进行组织,并且不面向由技术领域分开规定的复杂的端到端网络。既然我们已经重新设置了,那么我们将面对最初在本文开头提出的问题-每个微服务应该有多大?让我们先从下限开始,然后逐步提高到建议的上限。
下限:微服务应至少包含一个集合(或至少一个独立实体)以及在该集合的实体上运行的关联服务。
为了理解该定义,我们必须确保你熟悉我们所指的两个术语。集合是领域驱动设计(DDD)中的一个概念,我们已经看到了一个具体的示例。
聚合是一组相关的实体,它们的生命周期捆绑在一起,可以将它们视为一个单元。一个典型的例子是Order/LineItem关系,我们的Account/Entry示例只是其中的一种。
第二个术语也是一个熟悉的术语,但可能不是你想的那样。我们以DDD定义
服务的方式使用该术语。服务是功能的“验证”,我们在前面的示例中调用的“转移”就是该思想的规范示例。Evans在他的书中建议我们将这些对象建模为称为服务的独立接口,这些接口是无状态的,并且其接口是根据领域模型的其他元素(实体和值对象)定义的。关键是这里的服务指的是自然不对应于任何特定实体或值对象的领域概念。
这里最重要的设计要点是,在考虑将微服务制作得很小时,你必须非常非常仔细地考虑事务边界。首先,你必须考虑微服务中所涉及实体的生命周期,即我们始终在持久性方面考虑的创建/读取/更新/删除周期。然后你必须将思想扩展到这些实体的组可能发生的所有更新-这些都是Services将识别的事情。但是,你不仅需要考虑简单的一对一事务(例如转移),还需要在域中更广泛地考虑实体组上的其他操作,尤其是在批处理更新和复杂查询等方面。
在这一点上,一些纯粹主义者可能会大喊:“等等!这将使我的微服务需要10个(也许20个)单独的REST接口!”可能是这样。如果域的特定区域足够复杂,以至于不能保证对一组实体进行如此多的操作,那么这就是你应作为微服务发布的最小单元。它看起来更像是一个微型整体,但它比尝试解决将其拆分成较小的问题时要好得多。
我们发现,最初最好使微服务太大而不是太小。与采用两个细粒度的微服务并将其组合起来相比,采用较大的(粗粒度的)微服务将其拆分为两个要容易得多。
找到正确的抽象级别
如果这是微服务设计的正确下限,那么你实际上如何确定所有这些聚合,尤其是与这些聚合一起提供的服务?幸运的是,“领域驱动设计”社区最近(在过去的几年中)提出了一个很好的答案—通过执行
事件风暴开始你的设计过程。
Event Storming(
这里和
这里都进行了描述)是Alberto Brandolini发明的研讨会和流程,使团队可以使用便签和白板快速识别业务领域中最重要的
事件,从而将这些事件按时间顺序排列,然后确定启动事件的
命令,执行这些命令所需的
数据以及代表一个事件从另一个事件开始遵循的逻辑的
策略。
在与客户一起使用事件风暴过程时,我们发现,它为你提供了一种可重复的,易于理解的方法,用于识别与实体专家讨论领域词汇时出现的实体和集合。但是最重要的是,它不仅向你显示实体和聚合的数据结构,而且还非常重要地显示了用户在这些实体上进行操作以创建事件的命令,以及通常是将系统各部分与业务逻辑联系在一起的隐藏“策略”。我们不会按照上面的链接介绍你的整个过程(但是你可以在
此处看到该过程的完整示例),但是我们将向你展示开展研讨会的示例结果。
在事件风暴中,黄色即时贴是在系统上运行的用户(表示为角色)。绿色便笺是用户通过命令与之交互的数据元素(聚合或偶发的孤立实体)(蓝色便笺)。橙色便笺是事件,这些事件是由于执行命令,通过接收来自其他外部实体的某种通信,或者可能通过执行策略形式的业务逻辑而生成的。
但是重要的是最终安排。如你所见,对一组特定的数据进行操作并生成一组特定的事件的命令全部组合在一起。在正确的粒度级别上,这形成了一个非常好的微服务首过(first-pass)设计。这是因为流程本身往往会在很早的时候就将不同的参与者及其与系统交互的事件分开。你最终得到的是一种非常由内而外的设计,并且避免了当我们只关注数据结构或微服务之间交互的纯技术方面时导致的许多过早优化。但是,你应该记住,这只是第一步。第一次尝试很难获得微服务的粒度。你需要计划对设计进行几次迭代以获得正确的粒度。
因此,如果集合及其关联的服务对象是微服务大小的正确下限,那么正确的上限是多少?在这里,我们将完全摆脱关于领域驱动设计的讨论,而转向一个完全不同的问题:
上限:微服务的大小不得超过允许两个比萨饼的团队在一天之内发布单个完整且大小合适的用户故事到生产中的微服务。
现在,我们真的使你们中的一些人备受鼓舞。你们中有些人已经大喊“但是,这取决于你拥有哪种CI/CD工具,测试过程,质量检查过程,用户故事的大小,甚至团队的速度!”我们的回应是你说的
完全正确。
看起来,行业采用微服务的全部原因是为了使团队可以更快地发布软件,并且问题更少。到目前为止,你们大多数人已经读过Nicole Forsgren等人的著作《
加速:精益软件和DevOps的科学:建立和扩展高性能技术组织》。在那本书中,团队从研究中得出的最重要的统计数据之一是,他们将软件交付性能的节奏(团队可以发布的频率)与其他许多因素相联系,这些因素决定了一支高绩效团队。本质上,表现最好的团队也是发布频率最高的团队。这正好成为上述问题的核心。
微服务比单体服务更具吸引力的全部原因是,它们减少了我们在单体服务中遇到的一系列问题-单体服务的测试周期可能长达数天或数周,并且单体服务太复杂以至于它导致修改或扩展它们具有挑战性或不可能。但是,如果你的团队可以在一天之内添加新功能,那么按照定义,你不会受到测试周期或修改或更改复杂性的阻碍。
但这导致了一系列稍微不同的抱怨-当我们向团队建议这种节奏时,他们开始想出了他们永远无法满足这种快速发布周期的原因。他们开始谈论其“敏捷”过程的开销,例如持续几个小时的日常站立,或者自上而下要求使用诸如Jira之类的工具来减慢它们的运行速度,甚至更糟的是,在发布任何内容之前,都必须经过变更委员会投入生产。如果这是你的问题,那么我们有不同的甚至更具争议性的建议:
除非你已经了解并成功实现了敏捷方法和DevOps原则(例如自动测试和持续集成),否则请勿尝试大规模采用微服务。在讲话之前,请确保可以走路。
应用在上限上的另一项试金石是,如果你的微服务失败,那么哪些业务功能将会下降。如果你失去了多个业务功能,或者你的微服务粒度太粗,你应该对其进行重构,或者你遇到另一个问题,那就是取决于该特定微服务的业务流程太多(例如,你将它位于几种不同流程的关键路径上)。
这就是我们必须结束这篇特定文章的地方。微服务是一项奇妙的架构创新,具有逆转多年来出现的单片架构所遇到问题的潜力。如果在正确的粒度级别上实施,它们在使系统更灵活和减小决策的爆炸半径方面具有实质性优势。
但是微服务不是万能药。他们要求团队在DevOps和Agile实践中已经相当成熟,然后才能成功应用它们。微服务设计应始终是一个迭代过程。如果你的团队无法采用该迭代过程,那么该技术不是适合你团队的技术,或者你的团队需要进行更改。我们发现,在你请求更改系统之前,你通常并不真正知道一组微服务的粒度是否正确。如果发现实施新业务功能的更改需要更改多个业务微服务,则你可能没有正确的粒度(你的微服务粒度太细)。另一方面,如果发布变更/新功能所需的测试时间与编码工作相比不成比例地过长,则你的设计可能过于粗糙。在这两种情况下,解决方案都是进行适当的更改,然后从经验中学习并更改你的流程,以避免将来出现其他微服务问题。
[1] Crosby and Schaeffer, “An Introduction to Greek”, Allyn and Bacon, Boston MA, 1928
[2] For more on DDD, see Eric Evans’ book “Domain-Driven Design: Tackling Complexity in the Heart of Software”, Addison-Wesley Professional, Boston MA, 2004
原文链接: What’s the right size for a Microservice?(翻译:池剑锋)