QCon 全球软件开发大会(北京站)门票 9 折倒计时 4 天,点击立减 ¥880 了解详情
写点什么

SOA 旅程:从了解业务到敏捷架构

2018 年 7 月 10 日

本文要点

  • 如果单体是紧密耦合而不是内聚的,就可以对其进行拆分,以让业务更为敏捷。
  • 有很多错误的方法可以做到这一点。它们将带来同样紧密耦合但没有内聚性的分布式单体。
  • 你肯定不想要这样的结果。你希望你的服务具有内聚性,是松耦合且自治的。
  • 通过业务能力映射和价值链分析技术将你的技术服务和业务能力结合在一起。
  • 如果你对 DDD(Domain Driven Design,领域驱动设计)感兴趣,请把你的边界上下文看作业务功能。
  • 根据康威定律(Conway’s Law)主动采取行动,用它来形成最有凝聚力的组织单元。
  • 高内聚、松耦合和封装是 SOA 的核心特性,同时,这些概念本身已经测试过了,它们真的有用。

我想读者们都很清楚为何、何时和是否应该拆分一个单体。但是,为了以防万一,给大家提个醒:我们的共同目标是业务敏捷。如果这个单体无法满足业务的需求,如果开发的步伐减慢了,那么肯定要做些什么来解决问题。但是,在这之前,很显然,你需要找出原因。从我的经验来看,原因总是相同的:高耦合和低内聚。

如果你的系统属于一个单独的边界上下文,如果它不是足够大(听起来有点模棱两可,后面我会详细说明),那么你要做的就是以正确的方式把系统分解成模块。否则,你需要引入更加自主和独立的概念,我称之为“服务”。这个术语也许已经在整个软件行业中被用得最多的,因此,我要澄清一下我的意思。

我将进一步给出更严格的定义,但就目前而言,我想指出的是,首先,服务具有逻辑边界而非物理边界。它可以包含任何数量的物理服务器,这些服务器可以拥有后端代码和 UI 数据。在这些服务中,可以有任意数量的数据库,它们可以具有不同的模式。

识别服务边界的错误方法

在深入研究什么样才是良好的服务之前,请让我指出在定义它们的边界时最常遇到的错误方法

实体服务

将一个系统拆分为实体服务是一种经常被用到的方法,它被称为反模式。有时候,这个方法源于对重用的痴迷:所有跟某个实体相关的东西,要完全可重用,那是天赐良福!但是,在作为一个主要动机,代码重用其实是个悖论。我相信对于服务来说也是一样的。

因此,以下是这种方法的缺点:

  • 紧密耦合:每个服务都是其他服务的客户端。因此,如果有一项服务发生变化,你就必须测试整个系统。
  • 通信频繁:它们有大量的内部通信,通常是同步的。这让系统变得脆弱,让速度变慢。
  • 大量的服务:难以理解整体情况,也难以跟踪请求。
  • 糟糕的封装:业务规则遍布整个系统。通常,每个客户端在更新其他服务的数据前要做一些检查。同时,更新是与更新查询一起进行的,数据和行为被分开了。
  • 同步:特别是基于 http 时,后面我会仔细讲讲它的缺点。

这是实体服务通常看起来的样子。

盲目将模糊的业务架构映射到技术架构

我常常遇到的一种方法是:“我们的任务就是要写代码交付产品,让我们开干吧!”好吧,对于小型项目,这是可以的,但是,对大一点的项目就不可行。它不可避免地带来业务和 IT 之间的阻抗,从而导致业务敏捷性降低。并且,如果你不够敏捷,那么你就出局了。

下面是与业务功能不一致的技术架构:

这里有两个问题特别引人注目。首先是技术功能 1 的紧密耦合。由于其中有两个业务功能,它们有可能具有不同的开发步骤、不同的可扩展性和可用性需求,而且很难把它们分开。其次,在技术服务之间会有一个间隙(使用“Oops!”标记的地方)。当在这个间隙中出现新的需求时,这两个技术服务会耦合得非常紧密。我会把这个系统称为分布式的整体,尽管这个术语已经被预留给另一个不同的问题

从缺点数量上讲,这个反模式绝对是领先的,甚至不知道该从何说起。不管怎样,我们还是试着来讲讲吧:

  • 同步通信是资源密集型的:接受请求的服务等待第 2 个服务,而第 2 个服务等待第 3 个服务,依次类推。
  • 昂贵的扩展:需要扩展每一个服务,而不只是真正有需要的服务。
  • 分布式计算的 8 个谬误:Nuff 在这里讲过。
  • 它是不可靠的:只要有一个服务宕机,整个系统也跟着宕机。
  • 一致性问题:整个系统可能最终处于一个不一致的状态。我认为你不会想使用分布式交易,对吧?如果用到了,那么请记住下面的内容(假设是两阶段事务):
    • 两阶段提交事务本质上是脆弱的。
    • 整体可用性降低:如果一个子事务被拒绝,那么整个请求也就被拒绝。
    • 额外的操作复杂性。真的,去问问你的系统管理员,如果他们在这方面有经验的话。
    • 不断增长的通信延迟。
    • 资源被锁定在各阶段之间。如果第 2 个阶段一直没有发生呢?这代表了严重的扩展问题。无论怎样扩展,等待锁定资源释放的时间一直存在。

为了公平起见,我应该提一下三阶段提交事务和共识协议在解决上述问题上取得了不错的进展。

带有命令消息传递的服务

与 HTTP 相比,从可靠性和资源消耗来看,消息传递向前迈出了巨大的一步。但是,由此产生的服务仍然是耦合的。为什么呢?

首先,当服务 A 通知服务 B 去做某事时,服务 A 显然是知道服务 B 的。因此,如果服务 A 要通知服务 C 去做某事时,我们当然要修改服务 A。

其次,可能是最重要的,服务 A 希望一些来自服务 B 的行为。基于这类通信的特点,服务 B 在服务 A 的上下文中执行某些工作。因此,如果对服务 A 的需求发生变更的话,服务 B 也要做出变更才行。现在,假设服务 D 需要使用服务 B 的功能。但是,服务 D 有其自身的上下文,对服务 B 有自己的需求。很可能服务 B 需要做出一些变更来满足服务 D。在这一切完成之后,我们需要确保这个变更没有破坏服务 A 的功能。这是个经典的紧耦合噩梦。

中心化的数据

在最简单的场景中,“中心化”这个术语是指具有任意数量服务的单个数据库。它的主要缺点是,对以某种方式修改中心化数据的服务逻辑做出变更,有可能会破坏其他请求这些数据的服务。是什么原因呢?因为,现在这个基础数据是根据不同的规则在操作,所以数据流不同了。

但是,一般说来,这个术语适用于不同逻辑的服务访问彼此数据的情形。它常常和一个实体服务反模式一起使用。它们的缺点非常相似。

我相信,一个良好的服务和一个良好的类有一些共同之处。其中一个共性是数据和行为永远在一起。这导致不可能绕过由类接口提供的行为进行随机的数据变更。同样的原则也适用于服务。通过中心化数据,我们通常把服务的接口变成CRUD(C-creat,R-retrieve,U-update,D-delete)。我们把行为和它的数据分开,把中心化的数据服务转变成数据库。

服务编排

这个方法意味着只有一个地方可以路由所有入站的消息或请求。这意味着该服务与其他服务之间的通信是同步的,以及随之而来的所有缺点。此外,业务逻辑必然会渗透进来。因此,它不属于某个单独的服务,而是分布在两个服务之间。它类似于智能管道方法,这个方法已经被证明完全不是个好方法。

这是常见问题的一个例子。有一种名为“进程管理器”的模式被证明在小范围内运行良好。服务编排尝试将这种模式应用在系统范围内。同样, CQRS(Command Query Responsibility Segregation)不应该是高级模式事件源也一样。SOA 应该也是。

根据组织结构定义服务边界

严格地说,这样做不一定是错误的。在一个理想世界中,企业以最有效的方式组织而成,它运作良好。但是,在现实生活中,政治悄然而至。因此,要非常谨慎地使用这种方式。不过它可以为我们带来一些好的线索,至少有一个好的开始。

围绕层次定义服务

层次本质上是紧密耦合的。因此,围绕层次而组织的服务在本质上也是紧密耦合的。可以考虑一下垂直切片

我希望我的服务具备什么样的性质

因此,在讨论识别服务边界的方法之前,让我们先快速勾勒出要达到的目标

在深入研究构成我对服务理解的价值观(遵循 Kent Beck 的极限编程方法论),也即定义我对服务边界思考的主要动力之前,我想提出一些我的服务一直在遵守的原则。我把它们分为主要和推断两类。让我们从主要原则开始。事实上有两个主要原则。

松散耦合

几乎所有服务都存在的一个问题是紧密耦合。你无法自由地修改任何服务的实现,因为你永远不知道哪个服务依赖于此服务。这就是所谓的松散耦合,这也正是我一直想摆脱的,不只是在高级架构层面,也包括在代码层面。

高度内聚

在“识别服务边界的错误方法”一节中所描述服务都具有低内聚性。这些服务的数据和行为遍布整个系统。内聚性都跟功能有关:当我提到“内聚”时,我的意思是“暴露非常具体的行为”。维基百科也是这么。而且服务还具有封装性,封装通常也指信息隐藏,但是这种概念并没有被普遍接受

推断原则如下:

正确的粒度

当粒度太粗时,一个服务可以被分为若干个其他内聚服务。当粒度太细时,一个服务迫切需要其他服务数据或功能来进行操作,因此,耦合变得紧密起来。

高度自治

松散耦合带来了服务自治的概念。自治是指一个服务的可用性不依赖其他服务的可用性。为了完成自己的工作,一个服务不需要其他服务的功能或数据。此外,一个服务甚至可能不知道其他服务(在大多数情况下不应该知道)。

通过事件进行通信的服务

服务并不是存在于真空中,因此,它们之间需要通信。如何实现呢?我主张使用以行为为中心和基于业务驱动事件消息类型,反对使用同步请求和命令消息。这样的架构被称为基于事件驱动的架构。发布的事件应该反映业务概念,在领域中已经发生过的真实事件:订单已完成、交易已处理、票据已付款。

分散的数据

当服务是松散耦合、高度内聚的,那么也就是自治的,它们根本不需要彼此的数据,数据自然而然地变得分散了。

服务编排

服务编排是拒绝使用同步通信、采用业务事件和拒绝使用中心化数据存储的自然产物。

如何定义服务边界

那么,如何识别服务,让它们最终可以成为松散耦合和高度内聚的呢?换句话说,可维护的、可靠的和具有业务一致性的架构的核心价值是什么呢?

业务能力和业务服务

首先,我们来介绍业务能力的概念。业务能力是组织为保持自身运作和运营能力而做的事。它是对整个企业功能的具体贡献。它是组织为实现其目标而拥有的具体功能或能力。采购经理购买货物,货仓保存货物,销售员销售货物,财务人员计算利润。因此,业务能力对涉及相同业务的不同公司来说几乎是一样的。它们的实现是不同的。业务能力实现所在的逻辑边界称为业务服务。那么,这其中有些什么呢?有业务策略、业务规则、业务流程、参与其中并做出具体决定的人员、这些人员使用的应用程序。意象图看起来是这样的:

在业务能力和相应的业务服务之间应该始终存在一个双向关系。

我认为,比较业务服务边界和接口是非常容易的。这两者都是通过功能的声明性描述进行定义的:它们不说它们会如何完成它们的工作,而是告诉我们它们能做什么。

总而言之,业务服务边界和业务能力这两者都是声明性的概念,很少会发生变化。

业务服务如何与其他服务交互

业务服务的通信,或者,更具体地说,这些业务服务的业务流程的交互是通过业务领域事件来定义的。业务服务和其他业务交换的事件形成了它们之间的接口或合约。这些事件可以以电话、电邮或简单对话的形式实现。

业务服务和技术服务

因为业务服务边界在整个企业中是最稳定的,所以围绕它们构建技术服务就很有意义了。电话用RabbitMQ 事件替代,纸质文档以web 界面替代,计算由HTTP 调用替代,而其他一些乏味的工作则由ML 来做。通过这种方法,我们获得稳定的服务合约:业务服务很少发生变化,因此,由相应的技术部门实现的合约也很少会发生变化。

以这种方式确定的技术服务充分尊从单一事实来源的概念。这个来源代表了特定的事件,也代表存储在单个位置的数据。这很重要:在技术服务中,应该有单个逻辑位置来发布具体事件。这些事件不应该包含大量数据,重点不是要通过这些事件来获得数据。它们只是通知,告知我们某个事件发生了。此外,如果你的事件包含大量的数据,那么,你的服务边界可能出错了。事实上,这些有着大量数据交换的服务应该单独成一个服务。另一方面,这些事件应该包含足够的数据,即它们应该是完全自足的。但是,它们不应该有任何引用,因为这样会引入紧密耦合的共享数据库。

Udi Dahan 恰如其分地总结了这一点

“服务是具体业务能力的技术实现,任何数据或规则都只能由一个服务拥有。”

这让一个常见的错误浮出水面——物理和逻辑架构必须是一样的。实际上,它们不必一样。如果有个需要我们去集成的 web 服务,它不一定是一个成熟的业务服务。

业务能力映射

这是一种旨在促进服务边界识别的技术。

首先,你应该确定你的高级功能,它们一般是组织的核心功能。由此产生的服务可能是单独的业务。因此,它们可以被外包出去,或者相反,被收购。在定义这个服务时,你应该问“什么”和“如何”这两个问题,并自己确定声明性的功能。要做到这一点,必须和不同的利益相关者进行大量的沟通,以听取所有观点。和那些做底层工作的人沟通,找出他们所参与的主要流程以及他们实际上在做什么。

每个服务应该对应单独的短语,就像“我 < 动词 >< 名词 >”这样。比如,“我处理付款”,或者“我检查欺诈付款”。每个这样的服务都是一个交付业务价值的步骤。

了解企业如何赚钱,可以为我们提供一些线索。如果你对一个组织如何寻找潜在客户、这些潜在客户如何成为真正的客户、他们何时以及如何购买何物有一个清楚的认识,那么,你也许已经完成了高级服务识别。

组织结构虽然可能具有误导性,但也有可能会带来帮助。只是不要期望一个企业能够安排得完美无缺,但更高层次的整体组织视图肯定是有用的。

寻找能够对某些功能进行自动化的方法也是有帮助的。这样可以让我们远离实施细节,有助于找到有用的抽象

不要忘了找出业务服务之间的交互方式。因此,正如我已经提到的,每件事都重要:电话、电邮、消息、普通的对话。

在完成这个更高层次的概览之后,深入到每个服务。这个过程本质上是一样的。但是,我有个建议,A 永远不能将整个服务边界识别过程看成是一个主要步骤或阶段,因为这样就变成瀑布模型了。这个过程与开发很接近。尽管在编码前都需要做一些初始的工作来识别顶层服务,但如果没有参与编码,我就不会深入其中。

因此,业务服务交互的总体情况(用箭头表示事件)看起来如下所示:

价值链分析

我用这种方法来处理业务能力映射。基本上,它可以归结为以下几个方面。首先,将你的组织看成是一组从具体实现中抽象出来的业务功能。其次是我已经提到过的:这些功能是实现企业主要业务目标的步骤。

可以沿着这条线进行发现服务的对话(按照相反的顺序,可以从业务目标回到最初开始的地方):

  • 你的主要业务目标是什么?(或者,换句话说,你怎样赚钱?)
  • 我们销售家具。
  • 你给客户送货吗?
  • 没有。
  • 你从哪里获得这些家具?从哪里购买或自己生产的吗?
  • 我们生产家具。我们在米兰有工厂。
  • 原材料从哪里来?
  • 我们的家具是用我们自己种植的树木制作的,用到的紧固件是从别人那里采购的。

这里的主要能力可能是下面这几个:“提供原材料”、“制造家具”、“销售家具”。我已经省略了“家具仓储”和“市场营销”,因为它们已经无处不在。如果公司提供送货服务,那么还需要加入“家具送货”服务。

在这些类型的业务中存在两个非常普遍的链:供应链需求链。这两个名词本身已经很直观了。此外,还有一个方法混合了业务能力映射和价值链。毫无疑问,它就是能力链,它主要体现在成果图表上。这里有个很好的例子

SOA 和 DDD

我假设你很知道什么是界上下文。但让我感到困惑的是,我不知道如何定义它们。例如众人皆知的产品目录总是让我一头雾水。我想知道这概念背后的理由。现在我能够回答这个问题了:边界上下文就是一个业务服务。需要补充说明一下,只显示某些商品的产品目录很少会是一个成熟的服务,它不具备任何行为。业务服务肯定要实现一些行为或一些业务功能。目录实际上只是 Backend for Frontend 模式一个例子。它只是从其他服务中获取数据,并展示出来。

Backend for Frontend 示例。在 SOA 领域中,它不是一个成熟的服务。

SOA 和康威定律

在尝试影响组织结构时,我把康威定律当做目标。该定律表明,我们注定要产生一种设计结果,也就是组织沟通结构的副本,因此,我的目标是识别并优化那些沟通路径。所谓最优结构,就是指最具内聚性的结构。理想情况下,这些沟通路径应该属于同一业务服务中关系非常密切的一组人。因此,围绕业务服务形成组织结构就完全合理了。这个方法根本不是什么新东西,但很少会遇到。按这种方法构建的组织,其内部单元会是内聚的、自治的和可替换的。同时,企业会变得更敏捷。

高度内聚、松散耦合和封装是自然的基本特征

我在OOP 领域工作了很长一段时间,实践XP,并创建SOA 架构,我总是觉得它们有一些共通的东西。

瀑布模型中的阶段对应软件中层,这些层之间需要依赖彼此的数据,因此,它们天生是紧密耦合的。相反,XP 放弃了阶段的概念,使用了非常短的周期,把所有的活动结合在一起,其中包括和领域专家的交流、开发、单元测试、功能测试、客户反馈。Sprint 是高内聚的,而且可以做到不那么紧耦合。它们持续形成了bizDevOps 文化。

过程编程是指实现一系列计算步骤的程序,并操作数据:进行这个操作,然后进行那个操作,接着进行另一个操作。这种方法无视数据封装。整个概念意味着数据和程序是分离的。这种方法与实体服务反模式产生了共鸣,不是吗?相反,OOP 就是封装。适当的对象就像有责任能力的成年人一样,具备完成工作所需要的一切。我的业务服务也一样。它们不暴露数据,而是暴露行为,并通过事件来通知他们的工作进展。

康威定律意味着我们要围绕业务服务创建内聚的沟通结构,而不是由开发人员、QA、业务分析人员等组成的层级组织单元。

所有这些特征都是与生俱来的。原子由质子、中子和电子组成,它们都有自己的行为和需要遵守的规律,但是只作用于一个原子,这样形成了非常内聚的封闭微系统,与其它东西之间只有松散的耦合。而XP 是一种持续的改进,可以用像OOP 和SOA 这样的工具来加强,进化是大自然的本质。

最后,关于软件的核心价值,我认为关键的是构建正确的抽象。它表现在各个层面、各个阶段上。也就是从高级别的SOA 开始,通过XP 的持续改进和反馈,识别主要的业务能力,并围绕它们形成技术服务, 获得能够反映域驱动设计(Domain-Driven Design)和 OOP 的正确抽象。这就是如何在日常工作中通过业务 -IT 的一致性来达到业务敏捷的方式。

例子?

第一个例子属于支付服务提供商领域。这篇博文包含了一些对伸缩性、sagas、聚合边界、组合UI 和CQRS 的一些想法。第二个例子属于比较普遍的电子商务领域,提供了一些正确识别边界的技巧。第三个例子是更加底层的示例,以RabbitMQ 为主。

随着时间的流逝,我注意到我的服务的粒度变得越来越粗,saga 包含了实体的整个生命周期。对于我的第一个例子中的金融交易,我可能会做一些与众不同的事。我会创建一个saga,它属于一个单独的服务,具有以下高级的阶段或状态:交易已注册、已通过交易欺诈检测、交易已处理、交易已协调、交易已完成。可能有一天,我甚至会写一篇属于我自己的“保卫战”博文。

作者简介

Vadim Samokhin 是俄罗斯领先的临床研究公司 Gemotest 开发部门的负责人。他曾经涉及的领域包括电子商务、支付解决方案和卡片采集。他热衷于 OOP、SOA、敏捷方法、通过业务 -IT 的协调实现业务敏捷。他偶尔会在 medium.com/@wrong.about 上分享跟上述主题有关的想法。

阅读英文原文: The SOA Journey: From Understanding Business to Agile Architectur

感谢无明对本文的审校。

2018 年 7 月 10 日 17:0013645
用户头像

发布了 199 篇内容, 共 66.9 次阅读, 收获喜欢 275 次。

关注

评论

发布
暂无评论
发现更多内容

第三周总结

Karl

架构师训练营第三周学习总结

CATTY

重学 Java 设计模式:实战命令模式「模拟高档餐厅八大菜系,小二点单厨师烹饪场景」

小傅哥

设计模式 小傅哥 重构 代码优化 命令模式

架构师训练营 - 学习笔记 - 第三周

心在飞

极客大学架构师训练营

第三周课后作业

iHai

极客大学架构师训练营

Redis系列(二): 连集合底层实现原理都不知道,你敢说Redis用的很溜?

z小赵

Java redis 高并发 高并发系统设计

我们是如何做go语言系统测试覆盖率收集的?

大卡尔

go 测试覆盖率 精准测试

week3.课后作业

个人练习生niki

单例模式 组合模式

架构师训练营 - 第三周命题作业

牛牛

极客大学架构师训练营 命题作业

ARTS-week-4

youngitachi

ARTS 打卡计划 arts

618 我们狂欢的是什么?

Neco.W

拼多多 电商 京东 活动专区

锦囊篇|一文摸懂EventBus

ClericYi

关于多线程,你必须知道的那些玩意儿

ClericYi

每周学习总结 - 架构师培训 3 期

Damon

极客时间架构师训练营 - week3 - 作业 2

jjn0703

极客大学架构师训练营

ARTS-03 -- ARTS-04

NIMO

ARTS 打卡计划 ARTS活动

学习总结 - 第3周

饶军

再谈云原生:我的看法

lidaobing

云原生 k8s 中间件

故障演练利器之ChaosBlade介绍

心平气和

故障演练 故障注入

极客时间 - 架构师培训 -3 期作业

Damon

week3.学习总结

个人练习生niki

springboot整合Quartz实现定时任务(api使用篇)

北漂码农有话说

架构师训练营 -week3- 作业

晓-Michelle

极客大学架构师训练营

第三周学习总结

iHai

极客大学架构师训练营

锦囊篇|一文摸懂LeakCanary

ClericYi

还有比二分查找更快的算法,面向接口编程Protocol,John 易筋 ARTS 打卡 Week 05

John(易筋)

swift ARTS 打卡计划 二分查找 binary search protocol

锦囊篇|一文摸懂ButterKnife

ClericYi

Prometheus 2.19.0 新特性

耳东

Prometheus

游戏夜读 | 《FPS关卡设计》

game1night

区块链系列教程之:比特币中的网络和区块链

程序那些事

比特币 区块链 网络 p2p

关于JVM,你必须知道的那些玩意儿

ClericYi

边缘计算隔离技术的挑战与实践

边缘计算隔离技术的挑战与实践

SOA旅程:从了解业务到敏捷架构-InfoQ