微服务 - 定义

微服务中的"微型"一词虽然表示服务的规模,但并不是使应用程序成为微服务的唯一标准。当团队转向基于微服务的架构时,他们的目标是提高其敏捷性 - 自主和频繁地部署功能。很难确定这种建筑风格的简明定义。我喜欢阿德里安·科克克罗夫特的这个简短定义**——"*服务型架构由松散耦合的元素组成,这些元素有边界的上下文*。**

虽然这定义了高水平的设计启发式,但微服务架构具有一些独特的特点,使其与昔日的服务型架构不同。下面有几个这样的特征。这些和几个是有据可查的 -马丁福勒的文章山姆纽曼的建筑微服务,仅举几例。

  1. 服务有明确的边界,以商业环境为中心,而不是以任意的技术抽象为中心
  2. 隐藏实施细节并通过意图揭示界面暴露功能
  3. 服务不会超出其边界共享其内部结构。例如,不共享数据库。
  4. 服务能够抵御故障。
  5. 团队独立拥有其职能,并能够自主发布更改
  6. 团队信奉自动化文化。例如,自动测试、连续集成和连续交付

简而言之,我们可以将此架构风格总结如下:

松散耦合的服务导向架构,其中每个服务都封闭在明确界定的边界环境中,从而能够快速、频繁和可靠地交付应用程序。

域驱动设计和绑定上下文

微服务的力量来自于明确界定它们的责任和划定它们之间的边界。这里的目的是在边界内建立高度的凝聚力,并在边界之外建立低耦合。就是说,往往一起改变的东西应该属于一起。与许多现实生活中的问题一样, 这说起来容易做起来难 , 企业进化了, 假设也改变了。因此,在设计系统时,重构能力是另一件需要考虑的关键。

域驱动设计 (DDD) 是一个关键,我们认为,在设计微服务时,无论是打破整体还是实施绿地项目,都是一个必要的工具。领域驱动的设计,由埃里克·埃文斯在他的成名,是一套想法,原则和模式,帮助设计基于商业领域的基本模型的软件系统。开发人员和域名专家携手合作,以无处不在的共同语言创建商业模式*。*然后,他们将这些模型与有意义的系统绑定,在这些系统和处理这些服务的团队之间建立协作协议。更重要的是,他们设计了系统之间的概念轮廓或边界。

微服务设计从这些概念中汲取灵感,因为所有这些原则都有助于构建模块化系统,这些模块化系统可以相互独立地变化和演变。

在我们进一步前进之前,让我们快速浏览 DDD 的一些基本术语。域驱动设计的完整概述已为此博客提供范围。我们强烈推荐埃里克·埃文斯的书给任何试图建立微服务的人

**域:**表示组织所做的事情。在下面的例子中,它将是零售或电子商务。

**亚多曼:**组织内的组织或业务单位。域由多个子域组成。

**无处不在的语言:**这是用来表达模型的语言。在下面的示例中,项目是属于每个子域的无处不在的语言的模型。开发人员、产品经理、域名专家和业务利益相关者就同一语言达成一致,并在其产品中使用该语言 - 代码、产品文档等。

img

图1。电子商务域中的子域和绑定上下文

**有限制的上下文:*域驱动设计将边界上下文定义为“决定其含义的单词或语句出现的设置”。*简言之,这意味着模型所包含的边界是有意义的。在上述示例中,“项目"在每个上下文中都具有不同的含义。在目录上下文中,项目表示可销售的产品,而在 Cart 上下文中,它意味着客户添加到其购物车中的项目。在履行上下文中,它意味着将运送给客户的仓库项目。每个模型都是不同的,每个模型都有不同的含义,可能包含不同的属性。通过在各自的边界内分离和隔离这些模型,我们可以自由、毫不含糊地表达这些模型。

注意:了解子域和边界上下文之间的区别至关重要。子域属于问题空间,即您的企业如何看待问题,而绑定上下文属于解决方案空间,即我们将如何实现问题的解决方案。从理论上讲,每个子域可能具有多个边界上下文,尽管我们为每个子域争取一个边界上下文。

微服务与绑定上下文有何关联

现在,微服务适合哪里?公平地说,每个有界限的上下文映射到微服务?是和不是。我们将看到原因。在某些情况下,边界上下文的边界或轮廓可能相当大。

img

图2。有界限的上下文和微服务

请考虑上面的示例。定价范围分为三种不同的型号 - 价格、定价项目和折扣,每个模型负责目录项目的价格,计算项目列表的总价格,并分别应用折扣。我们可以创建一个包含上述所有模型的单个系统,但它可能会成为一个不合理的大应用程序。如前所述,每个数据模型都有其不变和业务规则。随着时间的推移,如果我们不小心,这个系统可能会变成一团泥球,边界模糊,责任重叠,并可能回到我们开始的地方——一块巨石。

此系统建模的另一种方法是将相关模型分离或组合成单独的微服务。在 DDD 中, 这些型号价格、 定价项目和折扣 - 称为聚合。聚合是一个自成一体的模型,构成相关的模型。您只能通过已发布的界面更改聚合状态,聚合确保一致性,并确保不变符保持良好状态。

从形式上讲,聚合是作为数据更改单元处理的关联对象的集合。外部引用仅限于被指定为根源的聚合体的一个成员。一组一致性规则适用于聚合的边界内。

img

图3。定价上下文中的微服务

同样,没有必要将每个聚合建模为独特的微服务。事实证明,对于图 3 中的服务(聚合)来说,情况并非如此,但这不一定是一条规则。在某些情况下,在单个服务中托管多个聚合可能很有意义,尤其是当我们不完全了解业务领域时。需要注意的一个重要问题是,仅在单个聚合体内才能保证一致性,并且聚合只能通过已发布的界面进行修改。任何违反这些规定的行为都有可能变成一团泥球。

上下文地图 - 划出精确微服务边界的方法

武器库中的另一个基本工具包是上下文地图的概念再次 - 从域驱动设计。巨石通常由不同的模型组成,大部分是紧密耦合的 - 模型也许知道彼此的亲密细节,改变一个可能会对另一个造成副作用,等等。当您分解整体时,识别这些模型 (本例中的聚合) 及其关系至关重要。上下文地图帮助我们做到这一点。它们用于识别和定义各种边界上下文和聚合之间的关系。虽然在上面的示例中,有界限的上下文定义了模型的边界 - 价格、折扣等,但上下文地图定义了这些模型之间和不同上下文之间的关系。在确定这些依赖性后,我们可以确定实施这些服务的团队之间的正确协作模式。

对上下文地图的全面探索超出了此博客的范围,但我们将以示说明。下图表示处理电子商务订单付款的各种应用程序。

  1. 购物车上下文负责在线授权订单;订单上下文流程履行后付款流程(如结算);联系中心处理任何例外情况,如重复付款和更改订单使用的付款方式
  2. 为了简单起见,让我们假设所有这些上下文都是作为单独的服务实现的
  3. 所有这些上下文都封装了相同的模型。
  4. 请注意,这些模型在逻辑上是相同的。即,它们都遵循相同的无处不在的域语言 - 付款方法、授权和结算。只是它们是不同上下文的一部分。

同一模式分布在不同上下文中的另一个迹象是,所有这些模式都直接与单一支付网关集成,并彼此执行相同的操作

img

图4。定义错误的上下文地图

重新定义服务边界 将聚合图映射到正确的上下文

在上述设计中,有一些问题非常明显(图4)。付款聚合是多个上下文的一部分。不可能在各种服务中强制执行不变和一致性,更不用说这些服务之间的并发问题了。例如,当订单服务尝试发布先前提交的付款方法的结算时,联系中心更改了与订单相关的付款方式,会发生什么情况。此外,请注意,支付网关的任何更改将强制更改多个服务和可能众多的团队,因为不同的组可以拥有这些上下文。

通过一些调整,并将聚合与正确的上下文对齐,我们得到了这些子域的更好表示 - 图 5。发生了很大的变化。让我们回顾一下这些变化:

  1. 付款总额有一个新的 - 家庭支付服务。此服务还从需要支付服务的其他服务中提取支付网关。由于单一的边界上下文现在拥有聚合,不变因素易于管理:所有交易都发生在相同的服务边界内,有助于避免任何并发问题。
  2. 支付聚合使用反腐败层 (ACL) 将核心域模型与支付网关的数据模型隔离,后者通常是第三方提供商,并且可能必然会发生变化。我们将深入探讨应用设计,例如在以后的帖子中使用端口和适配器模式的服务。ACL 层通常包含将支付网关的数据模型转换为付款聚合数据模型的适配器。
  3. 推车服务通过直接 API 呼叫呼叫支付服务,因为推车服务可能需要在客户访问网站时完成付款授权
  4. 记下订单和付款服务之间的互动。订单服务会发出域事件(稍后在此博客中将对此进行更多关注)。支付服务收听此事件并完成订单结算
  5. 联系中心服务可能有很多聚合,但我们只对此使用案例的订单集合感兴趣。当付款方式发生变化时,此服务会发出事件,而付款服务会通过倒转之前使用的信用卡并处理新信用卡来响应该事件。

img

图5。重新定义上下文映射

通常,单体或旧应用程序具有许多聚合,通常具有重叠的边界。创建这些聚合及其依赖性的上下文图有助于我们了解我们将从这些巨石中夺回的任何新微服务的轮廓。请记住,微服务架构的成败取决于这些集合中集合与高凝聚力之间的低耦合。

同样重要的是要注意,有界限的上下文本身就是合适的有凝聚力的单位。即使上下文具有多个聚合,整个上下文及其聚合可以组合成一个微型服务。我们发现这种启发式特别有用的领域是有点晦涩 - 想想一个新的业务线,组织正在冒险进入。您可能对分离的正确边界没有足够的洞察力,任何过早分解聚合物都可能导致昂贵的重构。想象一下,必须将两个数据库合并为一个数据库,以及数据迁移,因为我们碰巧发现两个聚合体属于一起。但是,请确保这些聚合体通过接口进行充分隔离,以便它们不会知道彼此复杂的细节。

事件风暴 - 识别服务边界的另一种技术

事件风暴是另一种基本技术,以识别集合(因此微服务)在系统中。它是一个有用的工具,既打破巨石,当设计一个复杂的微服务生态系统。我们使用这种技术来分解我们的复杂应用程序之一,我们打算在一个单独的博客中涵盖我们与事件风暴的经验。对于此博客的范围,我们希望给出一个快速的高级别概述。如果您有兴趣进一步探索,请观看阿尔贝托·布兰德罗尼关于该主题的视频。

简言之,事件风暴是处理应用程序的团队之间的头脑风暴练习 - 在我们的例子中,是一个整体 - 识别系统中发生的各种域事件和过程。团队还确定这些事件影响的聚合或模型及其产生的任何后续影响。当团队进行此练习时,他们会识别不同的重叠概念、模棱两可的域语言和相互冲突的业务流程。他们组组相关模型,重新定义聚合并识别重复的过程。随着这项工作的进展,这些聚合体所属的边界上下文变得清晰。如果所有团队都在一个单间 - 物理或虚拟 ,并开始在 Scrum 风格的白板上绘制事件、命令和流程的地图,则事件风暴研讨会是有用的。在本次练习结束时,以下是通常的结果:

  1. 重新定义的聚合列表。这些可能成为新的微服务
  2. 需要在这些微服务之间流动的域事件
  3. 来自其他应用程序或用户的直接调用命令

我们在下面的活动风暴研讨会结束时展示了一个示例板。对于团队来说,就正确的聚合点和边界上下文达成一致是一次很好的协作练习。除了是一个伟大的团队建设练习,团队走出这个会议与领域,无处不在的语言和精确的服务边界的共同理解。

img

图 6.事件风暴板

微服务之间的通信

要快速回顾,单块在单个过程边界内承载多个聚合。因此,在此边界内管理聚合物的一致性是可能的。例如,如果客户下订单,我们可以减损项目库存,在单个交易中向客户发送电子邮件 。所有操作都将成功,或者所有操作都将失败。但是,当我们打破整体,将聚合扩展到不同的上下文时,我们将有数十个甚至数百个微服务。迄今存在于单体边界内的进程现在分布在多个分布式系统中。在所有这些分布式系统中实现交易完整性和一致性是非常困难的,而且成本很高 - 系统的可用性。

微服务也是分布式系统。因此,CAP 定理也适用于它们 - *“分布式系统只能提供三个所需特征中的两个:一致性、可用性和分区容差(CAP 中的"C”、“A"和"P”)。*在现实世界中,分区容差是不容谈判的——网络不可靠,虚拟机器会下降,区域之间的延迟会变坏,等等。

因此,这让我们可以选择可用性或一致性。现在,我们知道,在任何现代应用程序中,牺牲可用性也不是一个好主意。

img

图7。CAP 定理

围绕最终一致性设计应用程序

如果您尝试跨多个分布式系统构建交易,则最终将再次位于单片土地上。只有这一次,这将是最糟糕的类型,分布式巨石。如果任何系统不可用,整个流程将不可用,这通常会导致客户体验令人沮丧、承诺失败等。此外,对一项服务的更改通常会导致对另一项服务的更改,从而导致复杂且昂贵的部署。因此,我们最好设计适合我们使用案例的应用程序,以容忍一些有利于可用性的不一致性。例如,我们可以使所有过程异步进行,从而最终保持一致。我们可以异步发送电子邮件,独立于其他流程;如果以后仓库中没有承诺的物品,则项目可能会被退订,或者我们可以停止接受超出一定阈值的商品订单。 有时,您可能会遇到一个场景,该场景可能需要在不同流程边界的两个聚合体中进行强 ACID 式交易。这是一个很好的迹象,重新审视这些集合,也许把它们合并成一个。事件风暴和上下文地图将有助于在我们开始在不同的过程边界中分解这些聚合之前尽早识别这些依赖性。将两个微服务合并为一个服务是昂贵的,这是我们应该努力避免的。

喜欢事件驱动的架构

微服务可以发出发生在其聚合物上的基本变化。这些称为域事件,任何对这些更改感兴趣的服务都可以收听这些事件并在其域内采取各自的行动。此方法避免了任何行为耦合 - 一个域没有规定其他域应该做什么,而时间耦合过程的成功完成并不取决于同时可用的所有系统。当然,这将意味着系统最终将保持一致。

img

图8。事件驱动的架构

在上面的示例中,订单服务发布事件 - 订单已取消。已订阅活动的其他服务处理各自的域功能:付款服务退还资金、库存服务调整项目库存等。要确保这种集成的可靠性和弹性,需要注意的事项很少:

  1. 制作人应确保他们至少制作一次事件。如果这样做有失败,他们应该确保有一个回退机制存在,以重新触发事件
  2. 消费者应确保以不应有的方式消费事件。如果再次发生同一事件,则不应在消费者端产生任何副作用。事件也可能出序。消费者可以使用时间戳或版本数字字段来保证事件的独特性。

由于某些使用案例的性质,可能并不总是能够使用基于事件的集成。请看推车服务和支付服务之间的集成。这是一个同步集成,因此有一些事情我们应该注意。这是行为耦合的一个例子 - 推车服务可能从支付服务调用 REST API,并指示它授权支付订单,和时间耦合 - 支付服务需要为Cart服务接受订单。这种耦合会降低这些上下文的自主性,甚至可能是一种不良的依赖。有几种方法可以避免这种耦合,但有了所有这些选项,我们将失去向客户提供即时反馈的能力。

  1. 将 REST API 转换为基于事件的集成。但是,如果支付服务只暴露了 REST API,则此选项可能不可用
  2. 推车服务可即时接受订单,并且有一批工作可接听订单并致电付款服务 API
  3. 推车服务产生本地事件,然后调用支付服务 API

如果上游依赖性 (付款 ) 服务失败和不可用,上述与重述相结合,可导致更具弹性的设计。例如,在发生故障时,Cart 和支付服务之间的同步集成可以通过事件或基于批次的重述来备份。此方法对客户体验有额外的影响 - 客户可能输入了不正确的付款详细信息,当我们离线处理付款时,我们不会将其在线。或者,收回失败的付款可能会给企业增加成本。但是,Cart 服务对支付服务的不可用或故障具有弹性的好处很可能大于缺点。例如,如果无法离线收取付款,我们可以通知客户。简言之,用户体验、弹性和运营成本之间存在权衡,设计系统时明智的做法是牢记这些折衷方案。

避免针对特定消费者数据需求的服务之间协调

任何以服务为导向的架构中的反模式之一是,服务迎合了消费者的具体访问模式。通常,当消费者团队与服务团队密切合作时,就会发生这种情况。如果团队正在处理单片应用程序,他们通常会创建跨越不同聚合边界的单个 API,从而将这些聚合物紧密耦合。让我们考虑一个例子。在 Web 中显示订单详细信息页面,移动应用程序需要在单个页面上显示订单的详细信息和处理订单的退款详细信息。在单体应用程序中,订单获取 API 假设它是 REST API - 查询订单和退款在一起,合并两个集合,并向呼叫者发送综合响应。由于聚合属于相同的过程边界,因此无需大量开销即可做到这一点。因此,消费者可以在一次通话中获得所有必要的数据。

如果订单和退款是不同上下文的一部分,则数据将不再存在于单个微服务或聚合边界内。为消费者保留相同功能的一个选项是让订单服务负责呼叫退款服务并创建综合响应。此方法引起若干关注:

\1. 订购服务现在与另一项服务集成,纯粹是为了支持需要退款数据以及订单数据的消费者。订单服务现在不太自主,因为退款聚合的任何更改都将导致订单总和的更改。

\2. 订单服务有另一个集成,因此另一个故障点要考虑 - 如果退款服务下降,订单服务仍然可以发送部分数据,消费者是否可以优雅地失败?

\3. 如果消费者需要更改才能从退款汇总中获取更多数据,则现在有两个团队参与此更改

\4. 如果在整个平台上遵循此模式,则可能导致各种域名服务之间错综复杂的依赖网络,所有这些服务都迎合了呼叫者特定的访问模式。

前端的后端 (BFFs)

降低此风险的一种方法是让消费者团队管理各种域服务之间的协调。毕竟,来电者更了解访问模式,并且可以完全控制这些模式的任何更改。此方法将域服务与演示文稿层脱钩,使其专注于核心业务流程。但是,如果 Web 和移动应用开始直接调用不同的服务,而不是单体的复合 API,则可能会导致这些应用的性能开销 - 通过较低的带宽网络进行多次呼叫,处理和合并来自不同 API 的数据,等等。

相反,人们可以使用另一种模式称为[后端前端](https://www.boldare.com/blog/event-storming-guide/Backend for Frontends - https://www.thoughtworks.com/insights/blog/bff-soundcloud)。在这种设计模式下,由消费者创建和管理的后端服务 ( 在这种情况下, Web 和移动 - 团队负责跨多个域名服务的集成,纯粹是为了向客户提供前端体验。网络和移动团队现在可以根据他们所迎合的使用案例设计数据合同。他们甚至可以使用 GraphQL 而不是 REST API 灵活查询并准确获取所需的内容。请务必注意,此服务由消费者团队拥有和维护,而不是由拥有域服务的团队拥有和维护。前端团队现在可以根据自己的需求进行优化 - 移动应用可以请求较小的有效载荷,减少来自移动应用的呼叫次数,等等。请看下面的编排修订视图。BFF 服务现在为其使用案例同时调用订单和退款域服务。

img

图9。前端的后端

在打破单体中的大量服务之前,尽早构建 BFF 服务也很有用。否则,要么域名服务必须支持域间协调,要么 Web 和移动应用必须从前端直接调用多个服务。这两种选择都会导致绩效开销、放弃工作以及团队之间缺乏自主性。

结论

在这个博客中,我们触及了各种概念、策略和设计启发式,当我们冒险进入微服务世界时,更具体地说,当我们试图将一块巨石分解成多个基于域的微服务时。其中许多是巨大的主题本身,我不认为我们已经做了足够的正义来解释他们全部详细,但我们想介绍一些关键的主题和我们的经验,采用这些。进一步阅读(链接)部分有一些参考和一些有用的内容,任何人谁想要追求这条道路。

**更新:**该系列的后两个博客都出来了。这两个博客讨论使用域驱动设计原理以及端口和适配器设计模式实现 Cart 微服务,并举例说明代码。这些博客的主要重点是演示这两个原则/模式如何帮助我们构建敏捷、可测试和可重量化的模块化应用程序 - 简言之,能够响应我们都在运营的快节奏环境。