如何设计一个复杂的业务系统?从对领域设计、云原生、微服务、中台的理解出发,回想自己这几年做的一些大型企业数字化转型项目。关于《软件工程》这期,趁着春节长假,写下自己对架构设计的一些思考和学习随笔。草草写下,希望能引起大家一些启发和讨论。当然,本文所说的软件开发主要侧重于业务应用软件的开发,而中间件、数据库等技术组件的开发侧重于其他方面,这里不做讨论。01如何解决复杂的业务设计Aliware软件架构设计本身就是一件复杂的事情,但其实业界有一个共识,就是“通过组件化实现关注点分离,降低局部复杂度”。其实无论我们使用容器、中间件、消息、数据库等,从某种意义上说,它们都是组件。这样做的好处是可以在不同的系统中重复使用。在云原生兴起的今天,我们更容易以通用化、组件化服务的形式使用它,所以如果你现在不享受云原生技术的红利,你就会被时代抛弃。云原生满足非功能性质量需求云原生技术可以最大程度解决很多非功能性质量和技术需求(如上图所示),因此作为企业级应用架构,自然会转移其自身专注于业务应用程序功能设计本身出现了。现在,对于一个复杂的业务架构的设计,要想做得又快又好,无外乎两种情况:一种是架构师本身对业务理解深刻,能力超强,精通;另一个是原来的业务。系统本身有清晰的模型和足够的“高内聚低耦合”,可以基于它快速分析业务变化,形成新的业务架构设计。我们应该追求的是第二种情况,即从企业级模型建设的一开始,就要慎重对待模型设计和业务流程。只有扎实的基础,才能有后续的“快速迭代”。让我们回到架构设计的本质,即为什么要先设计再代码实现。设计首先是解决问题的复杂性。于是有人做了一个架构交给一个团队去实现,很快就发现架构和实现的设计是完全不同的东西。当然,原因很明确——缺乏交流和沟通;其次,要建立团队协作和沟通的共识。即使我们做了很好的架构设计,团队达成了共识,大家也都努力把设计变成了现实,但是一个长期困扰软件行业的问题还是出现了。无论前期设计多么“精确”,需求总是在变化。发现下一个坑已经不远了,结果是情况越来越糟,也就是我们常说的架构“腐败”,最后大家不得不接受重写。这些经历让我们逐渐明白,软件架构设计的本质是通过分离核心问题来降低复杂度,使系统能够更快地响应外部业务变化,使系统能够不断演进。遇到变更无需从头开始,确保实施成本得到有效控制。所以,我觉得从架构设计的角度来说,以下三点是最关键的:让我们的模型、组件、业务划分尽可能的贴近变化的本质,比如对于一般的电商系统,它就是用户、产品、交易、支付等等,这样的划分可以让我们在一定范围内(业务模块)“隔离”变化,从而帮助我们有效减少变化点。在设计上,业务模型内部内聚度高,模型间耦合度低,即各自完成的业务相对独立,不会因为一方离线而牵扯到另一方。例如商品推荐功能暂停,但交易和支付业务应该继续正常提供服务,可能会提示用户暂时无法提供推荐服务,或者干脆降级为自下而上的策略.模型和组件在业务中尽可能重用。正是这种可重用性成就了今天的互联网级架构。我们不会每次搭建电子商务系统都从头开始。最“复用”的业务模块显然会专注于设计和运营,成为核心业务模块。当然,这样的电子商务系统在架构上必然会更加健壮。以上三点无疑指向了业务。从业务出发,面对业务变化,是我们现代架构设计成功的关键。因此,复杂业务架构设计的核心本质就是保证我们在面对业务变化时能够有足够快的响应。响应能力。02DomaindesignAliware提到了业务软件开发的通病:从小项目到大型业务系统,但随着新需求的不断增加,最终演变成开发团队的噩梦。而这些噩梦大多源于对软件概念完整性的破坏(“概念完整性”一词出自软件工程经典书籍《人月神话》)。这些业务代码可能是几代开发人员各自为政(我们也称之为“屎山”)堆叠起来的,在这个过程中没有人有意识地维护软件的概念完整性。而DDD领域设计,尤其是DDD提供的战略建模层次的概念,是维护软件概念完整性的一剂良药。“技术服务于业务,业务驱动技术”是大多数人的共识,尤其是对于商业公司而言。DDD领域设计主张业务领域本身应该是软件设计中关注的焦点(换句话说,软件开发人员必须了解业务)与这种思想非常吻合;此外,DDD还为复杂的商业软件设计提供了实用的解决方案。解决方案,这也是我作为架构师极力提倡的去研究和讨论DDD领域设计的相关知识。战略建模在战略层面,DDD非常强调对业务问题的分析和分解,通过识别核心问题来降低问题的复杂度。DDD在战略层面维护模型的概念完整性。两个最重要的概念是限界上下文和反腐败层。定义有界上下文。Boundedcontext的定义在任何有关DDD的书籍中都会有详细的解释。在这里我只是想分享一些我的理解。这时候可能有人会问:限界上下文有多大,划分上下文有没有什么规律可循?划分上下文的规则无非是普遍适用的“高内聚低耦合”,说起来可能太空洞了。其实真正让大家困惑的是,那些不知道怎么划分的事物之间的关系,有的甚至被归入了一个上下文中。其实我觉得与其关注语境的“大小”,不如关注模型的“质量”,概念的完整性是否容易被破坏。在我看来,判断大小是否合适取决于应用开发团队的能力,看开发团队能在多大程度上控制软件的概念完整性。只要开发团队没问题,再大的范围也是可以的。如果开发团队的水平在行业上游,维护上下文的范围往往非常大;有些公司的开发团队水平参差不齐,因此在项目实施过程中,可能需要划分比较小的上下文,尽量减少“狗屎山”不断堆积。做好防腐层边界上下文需要时刻保护其维护的边界和边界内概念的完整性。这时,需要将某个上下文的概念转化为另一个上下文概念的地方称为“防腐层”。防腐层的实现有很多种,典型的实现为适配器Adapter。此外,从广义上讲,Gateway也是一种典型的防腐层构件。当然,防腐层的代码与其他内部业务模型之间肯定存在明显的物理差异。边界(当然,并不一定意味着防腐层应该作为一个独立的进程来部署),至少我们可以考虑将防腐层作为一个独立的类库来构建和维护。阿里巴巴的内部系统比如星环其实就是这样的思路。典型防腐层的设计和战术建模。DDD战术的核心概念是实体和聚合。为了更好的理解什么是聚合、聚合根、聚合内部实体,下面举例说明。想象一下电子商务系统的订单相关模型。我们可能会得到三个相互关联的概念:订单Order、订单头OrderHeader、订单行项OrderItem:一个叫做Order的聚合。这个订单聚合的聚合根是一个叫做OrderHeader的实体,实体OrderHeader的ID叫做OrderId(订单号)。通过OrderHeader实体,我们可以访问OrderItem实体的聚合。实体OrderItem的部分ID称为ProductId(产品ID)。因为业务变更不允许同一个商品出现在同一个订单的不同订单项中,所以我们可以选择商品ID作为订单项的部分ID。“聚合是数据修改的单位”。基于这个原则,我们可以做到“聚合内强一致性,聚合外最终一致性”。例如,我们不能接受一个订单中所有订单项的金额之和不等于订单表头,我们必须将订单表头和订单订单项这两个实体划分到同一个聚合中。设计聚合的原理我们先来看看《实现领域驱动设计》一书中对聚合设计原理的描述。原文有点难懂。让我稍微解释一下:在一致性边界内对真实的不变条件进行建模。聚合用于封装真正的不变性,而不是简单地将对象分组在一起。聚合中有一组不变的业务规则。每个实体和值对象都按照统一的业务规则进行操作,实现对象数据的一致性。边界外的任何东西都与聚合无关。这就是聚合如何能够实现高业务内聚的原因。设计尽可能小的聚集体。如果聚合设计的太大,聚合中包含的实体太多,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或数据库锁,最终导致系统可用性差。小聚合设计可以减少因为业务过大导致聚合重构的可能性,让领域模型更适应业务变化。通过唯一标识符引用其他聚合。通过关联外部聚合根ID而非直接对象引用来引用聚合。外部聚合的对象在聚合边界内进行管理,容易导致聚合边界不清晰,增加聚合之间的耦合度。在边界外使用最终一致性。聚合内部的数据强一致,聚合之间的数据最终一致。在单个事务中最多可以更改一个聚合的状态。如果一个业务操作涉及到多个聚合状态的变化,应该使用领域事件异步修改相关聚合,实现聚合之间的解耦(我会在领域事件部分详细讲解相关内容)。跨聚合服务调用是通过应用层实现的。为了实现微服务中聚合之间的解耦,以及未来微服务以聚合为单位的组合和拆分,应该避免跨聚合域服务调用和跨聚合数据库表关联。以上原则是DDD的一些通用设计原则,还是那句话:“适合自己的才是最好的。“在系统设计过程中,必须考虑项目的具体情况,如果面临使用方便、性能要求高、技术能力欠缺、全局事务管理等因素,这些原则并非不可打破解决实际问题是出发点,设计聚合DDD领域建模的步骤通常使用类似的事件风暴,一般通过用例分析、场景分析和用户旅程分析等,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,梳理领域对象之间的关系,找出聚合根,找出与聚合根业务密切相关的实体和值对象,然后组合聚合根,entities,valueobjects来构建聚合。业务系统大家都很熟悉,有很多可直接借鉴的电子商务业务成熟模式;再以另外一个场景——保险业务为例,看看聚合的构建过程有哪些步骤,当然这个例子是我从其他学习资料上看到的。比较典型,可以举个例子来说明:insurance保险业务简单示例(来自学习资料)第一步:使用用例分析或者事件风暴等,根据业务行为,梳理出所有的实体以及这些行为过程中发生的价值对象,如保单、目标、客户、被保险人等。第二步:从众多实体中选择一个适合作为对象管理器的根实体,即聚合根。判断一个实体是否为聚合根,如上一章所述,可以结合以下场景分析:是否存在独立的生命周期?是否有全球唯一的ID?可以创建或修改其他对象吗??是否有专门的模块来管理这个实体。图中的聚合根分别是保单和客户实体。第三步:根据上一章的聚合设计原则,找出所有与聚合根Entities和值对象相关联的紧密依赖关系。构造包含聚合根、多个实体和值对象的对象集合。这个集合是聚合。在图中,我们构建了客户和保险的两个聚合。Step4:在聚合中,根据Aggregateroot,entity,valueobjects的dependencies,画出对象的引用和依赖模型。这里需要说明一下:投保人和被保险人的数据是通过关联客户ID从客户聚合中获取的。在保险聚合中,它们是保险申请表的价值对象。这些价值对象的数据就是客户的冗余数据。即使以后客户聚合的数据发生变化,也不会影响申请表的值对象数据。从图中我们也可以看出实体之间的引用关系,比如在保险聚合中,保单聚合根指的是报价实体,报价实体指的是报价规则子实体。第五步:多个聚合根据业务语义和语境划分到同一个limit中。那么以上就是一个聚合诞生的完整过程。03不同场景下的领域建模策略Aliware由于企业中情况的多样性,有着不同的发展历程。有遗留单体系统的微服务改造,新未知领域的业务建模和系统设计,遗留系统的局部优化。案件。在不同的场景下,领域建模的策略也会有所不同。下面我们来看看在几种场景下如何进行领域建模。新系统新系统对于复杂的业务领域,在开始领域建模之前,可能需要将领域划分为多个层次。域被拆分成子域,子域也需要进一步拆分。例如,保险需要拆分为承保、理赔、收付、再保险等子域,而承保子域又进一步细分为投保、保单管理等子域。复杂领域如果不进一步细分,由于问题领域大,领域建模的工程量会非常大。你通过事件风暴完成一个大领域的建模并不容易,即使勉强完成,效果也未必好。对于复杂的领域,我们可以分三步完成领域建模和微服务设计。拆分子域建立域模型,根据业务域的特点,参考流程节点边界或功能聚合模块等边界因素。结合领域专家和项目组的讨论,将领域逐步分解为合适大小的子领域,对子领域使用事件风暴,划分聚合和限界上下文,领域模型在子域是初步确定的。领域模型微调梳理领域内所有子领域的领域模型,对每个子领域的领域模型进行微调。微调的过程侧重于考虑不同领域模型中聚合的重组。同步考虑领域模型和聚合边界,服务和事件之间的依赖关系,确定最终的领域模型。微服务的设计与拆分根据领域模型和微服务的拆分原则,完成了微服务的拆分与设计。单体遗留系统如果我们面对的是单体遗留系统,我们只需要将部分功能分离成微服务,而其余的仍然是单体的,整体保持不变,比如将面临性能瓶颈的模块拆分成微服务。我们只需要将这个具体的功能理解为一个简单的子域,参考简单域建模的方式即可。在微服务设计中,我们还需要考虑新旧系统之间的服务和业务兼容性,必要时引入防腐层。04云原生时代的挑战Aliware随着云原生技术的兴起,现在企业级架构更加云化,云化架构风格有了新的侧重点:弹性边界。弹性边界是云原生企业级应用架构的一个核心概念。它是指以弹性为最重要考虑因素划定的系统边界,决定着我们能否充分发挥云原生平台的全部能力。所以我们需要一种新的方式来弥补之前商业模式的不足,来满足新的云原生的需求。现在可以说,微服务基本都是基于云原生架构,在固定弹性的平台上使用微服务架构,实现成本极高。可以说,云原生其实应该是微服务的前提条件。在云原生时代,我们需要将弹性作为首要考虑因素,纳入建模考虑。那么弹性边界就是我们划分系统的重要依据。此外,还需要考虑弹性边界之间的依赖关系,尽量避免弹性耦合。对于业务建模,为了配合云原生时代的架构,我觉得应该做到以下几点:建立一个能够体现弹性边界的模型结构,这时候需要考虑原则不同的弹性边界来划分边界上下文;if两个上下文明显有不同的弹性需求,所以应该拆分。而如果对灵活性有一致的需求,可以考虑先不拆。那么此时拆分微服务可以有多“微”呢?简单来说就是足够“微”,可以更好的利用弹性来控制成本的大小。从异步模型的角度,优化业务逻辑;典型的就是MQ消息队列系统,因为有了broker,所以生产者和消费者不必同时保持可用性和相同的吞吐量,生产者也不需要立即等待回复。位置松耦合:典型的例子是服务注册中心。消费者不需要直接知道提供者的具体位置,而是通过注册中心来查找和访问服务。当弹性边界拆分业务上下文时,同一个弹性边界保持业务的强一致性。当异步调用产生中间异常时,需要维护业务的最终一致性。05不要忽视组织结构的影响Aliware的“康威定律”告诉我们,组织结构将决定团队沟通结构和产品结构。梳理组织架构,往往是在需求调研的时候做的。就信息采集而言,这里的业务架构设计没有什么特别之处。不同的是,业务架构的目标是企业级的容量规划,希望突破壁垒,形成合力。正是由于这个原因,组织结构对业务架构的设计有很大的反应。企业级数字化转型方案必须与组织架构相匹配,否则难以落地。可以说,部门利益是企业级架构的最大障碍之一,跨越这个障碍也是对架构师能力的要求之一。当然,在某些情况下,当没有更好的解决办法时,不动也是一种选择。以我的经验,这种问题没有特别好的解决办法。无非两种:一种是被能力超强的人主导,在最高层的支持下,大力推动这种决策。但是,企业规模越大,尤其是在业务中占据主导地位的企业,就越难形成这样的结构;二是加强企业内部业务架构人员的能力和数量(最好所有部门的角色相似),让这些企业组织人员作为合作伙伴参与项目的全过程,在实施过程中构建协作网络项目,提高决策效率,让组织架构不再是企业数字化转型的瓶颈。06SOA-微服务-中台:妥协的艺术Aliware很多年前,这些传统的大型ERP业务软件实际上在很大范围内保持了业务概念的完整性。安装ERP后,数据库在同一个限界上下文中有七八百个表(即七八百个实体)。不过,令人钦佩的是,这些ERP在如此庞大的限界上下文中,依然保持了业务理念的完整性。实现它非常困难,但破坏它却非常容易。实施一套ERP定制项目后,数据库中可能有上百张表,更何况命名不规则看起来也很奇怪。这些厂商的ERP实施顾问和开发人员日夜维护着这座巨大的“屎山”。我们不能让这些庞大的“单体应用”无限制地增长,所以我们又举起了“分而治之”的大旗。SOA等软件组件化技术为我们提供了拆分的工具。我们根据优势将一个大的有界上下文拆分为几个相对较小的有界上下文;在物理上,我们将一个大型的单个应用程序拆分为多个服务。一般来说,我们基本上会将服务的物理边界和限界上下文的域边界叠加起来。一个限界上下文对应一个或多个可以独立部署的服务应用,服务应用包括限界上下文核心业务逻辑的实现。SOA的服务组件的物理边界给服务之间的调用增加了一些困难,这使得开发人员简化了对象之间的关系,编写了更多“高内聚、低耦合”的代码。当服务组件不多时,构建防腐层的工作量不会太大。我们只需要处理组件之间的代码就搞定了。但是,我们的架构师和开发人员太喜欢“分而治之”了。微服务的广泛使用甚至被滥用。让我们看到很多微服务真的很“微”,几乎一个DDD的聚合就可以对应一个可以独立部署的微服务。这样的微服务不能自己做太多的业务,这就需要越来越多的微服务“聚合”在一起对外提供业务服务。当然,微服务技术基础设施的发展也为服务之间的调用提供了更多的便利,跨越微服务的边界已经成为常态;这个时候,业务开发人员区分“同一个上下文中的服务调用”和“上下文之间的防腐层”需要时刻保持清醒的头脑。这时候往往很难将有界上下文与物理对齐微服务的边界,这不可避免地增加了维护每个有界上下文的概念完整性的难度。既然越来越难以维护“微小”的独立限界上下文的概念完整性,那么让我们重新聚合它们?将它们集成到一个中等规模的有界上下文中,就是所谓的企业级业务架构,也就是我们现在所说的业务中台。最终的目标可以说是想要实现“企业级”的和谐。所以在某种程度上,软件工程是妥协的艺术,是“中间道路”。无论是做中台还是大中台,无论企业规模大小,我们都应该结合自身的经营目标和自身所拥有的资源,在“保持更大范围的理念完整性”和“保持更多的防腐layercodes”,在它们之间做一个平衡,这也是一个企业架构师要做的核心事情之一。我们团队这些年确实对“业务中台方法论”做了一些积累和实践,在一些项目中也做过实践。当然,最有灵魂的部分之一是上面提到的域设计。以前很多人说DDD领域设计和业务中台方法论最难的是没有合适的工具和平台去实践。今天,其实阿里开源的COLA、Transwarp、内部使用的BizWorks,都是非常好的工具和平台。07结语Aliware企业级应用架构在不断演化迭代,但我总觉得企业应用架构的形成过程是在一个看似科学的方法论下实现的,而不是一个完全科学的过程。仔细想想,做软件架构的其实很羡慕做架构架构的,因为架构架构有严谨的机械基础做基础,有很多东西可以精确计算,软件架构却没有可以精确计算的部件很多,因此,上述的“不断妥协”是一种可行的设计思想和设计艺术;其实,这也应验了“没有灵丹妙药”这句话。由于时间仓促,部分内容简单提及,一些需要举例说明的地方,本文也简单提及。如果以后有时间,我会结合更多的实际案例来补充本文中提到的观点。也希望这篇文章能够启发大家对当前云原生时代企业级应用架构设计的思考和探讨,相互学习,共同进步。
