2.3.1 拆分服务

拆分服务是设计阶段的难点,你需要非常了解应用,然后抽象出领域模型,并且在拆分过程中遵循一定的原则。下面我们来一一介绍。

1.了解应用

深入了解你要拆分的应用是拆分服务的前提条件,你需要了解业务需求、客户使用场景、系统运行机制、旧代码情况等多方面内容。我们团队的做法是回顾需求和设计文档,并对照遗留系统的代码来了解实现细节。了解需求、使用场景可以让你加深对业务的理解,以便后续抽象出合理的领域模型。阅读代码可以让你审视之前的设计,了解技术细节及可能存在的约束。需要注意的是,阅读旧代码的目的是对需求细节进行补充,千万不要被旧代码禁锢设计思维。原有的实现方式在微服务架构下很有可能是不适用的,也可能存在着优化空间,应该重构。

首先需要识别出系统操作,这一步通常可以通过用户故事和应用场景中的动词进行分析。这些操作会成为设计API接口的依据。比如在用户创建订单这一场景中,“创建”这个动作就是一个操作,它最终会被设计成CreateOrder这样的接口。

对于比较复杂的场景,需要通过更有效的工具协助分析。我们团队会使用事件风暴(Event Storming)进行业务流程分析。它是一种轻量级的、容易掌握的领域驱动设计方法。事件风暴的作用是帮助项目参与者对业务有一个统一的认识,比如关键的业务流程、业务规则、业务状态变更、用户行为等。它可以帮助开发人员将业务梳理清楚,发现领域模型和聚合,并且在一定程度上分清业务的边界,为服务拆分做好准备。

事件风暴一般通过工作坊的方式运行,找一个有白板的会议室,准备好各种颜色的即时贴就可以开始了。物料不方便准备也可以使用在线协作工具完成。比如我们一开始用即时贴,但发现贴纸多了之后移动和调整都有点麻烦,后来就选择用lucidspark以在线的方式进行,效率提高了不少。有关事件风暴的内容不是本书重点,这里不再赘述,感兴趣的读者可以去官网了解其实施细节。图2-11展示了事件风暴的基本模型。

图2-11

2.根据业务能力拆分

业务通常指能够为公司或组织产生价值的商业活动,其特定能力取决于业务类型。比如电子商务应用的业务包括商品管理、订单管理、物流管理等;我们所开发的广告平台的业务主要包括客户管理、广告管理、投放管理等。

拆分服务的第一步就是先识别这些业务能力,一般通过组织的目标、结构和商业流程分析可知。下面列举了一个外卖系统可能具有的业务能力。

● 商家管理

● 消费者管理

● 订单管理

● 配送管理

● 其他

识别了业务能力后,接下来要做的就是将业务映射为服务。需要注意的是,业务能力通常都会分层次,即一个业务包含多个子业务。比如订单业务又可以细分为下单、接单、取餐、送达等子业务。因此,要将哪个级别的业务映射为服务需要具体分析。笔者建议遵从高内聚低耦合的原则进行:属于同一个大的业务领域,但彼此相对独立的,可以拆分为不同的服务;反之能力很单一没有细分项的顶级业务可以被定义为一个服务。例如,在订单处理中少不了支付流程,而订单信息和支付显然是非常不同的业务,因此可以分别将它们定义为订单服务和支付服务。再比如,广告投放业务中有广告活动、广告库存、投放标等子业务,既交互又相对独立,适合被拆分为多个服务。

基于业务能力拆分服务的好处是易于分析,业务相对稳定,因此整个架构也比较稳定。但这并不代表服务不会变化,随着业务变更,甚至技术层面的权衡,服务和服务之间的关系都有可能被重新组织。比如某个业务的复杂度增长到一定程度就需要分解对应的服务,或者过多的服务之间频繁通信会导致性能低下,将服务组合在一起反而是更好的选择。

3.根据领域进行拆分

领域驱动设计(DDD)的理念已经出现近20年了,但一直不温不火。微服务架构让它焕发了第二春,使之成为非常热门的设计方法。原因很简单,微服务架构非常适合使用DDD理论进行设计,子域、界限上下文的概念可以与微服务完美匹配。子域也为服务拆分提供了很好的方法。子域是一个个独立的领域模型,用来描述应用程序的问题子域。识别子域和识别业务能力类似:分析业务并了解业务中不同的问题域,定义出领域模型的边界,即界限上下文(Bounded Context),每一个子问题就是一个子域,对应一个服务。还以外卖业务为例,它涉及商品服务、订单服务、派送服务,包括商品管理、订单管理、派送管理等问题子域,如图2-12所示。

图2-12

使用领域驱动方法拆分服务一般包括以下两个步骤。

(1)业务场景转变为领域模型

这一步需要抽象出领域模型,并根据模型之间的关系划分界限上下文,即一组具有清晰边界的、高内聚的业务模型。可以认为这就是我们要定义的微服务。

我们团队使用上面提到的事件风暴去识别领域模型。这种方法从系统事件的角度出发,将由系统状态的变化而产生的事件作为关注点,然后根据时间顺序串联出整个业务流程中发生的事件,分析出和事件相关的执行者、指令、读模型、策略等内容,最终识别出聚合模型,划分出界限上下文。例如外卖系统的核心业务流程中的事件有订单已创建、用户已支付、商家已接单、外卖已派送等,这些事件对应的执行者有消费者、商家、派送员等。通过分析,我们也能很清晰地识别出事件对应的领域模型有订单、支付、商家、用户等,再根据模型的交互关系定义聚合,整个业务建模的过程就基本完成了。

分析过程难免有偏差,对于识别出来的领域模型,可以基于低耦合高内聚的原则来评判模型的合理性。比如,划分出来的子域之间的依赖关系是不是做到了尽可能少?是不是出现了不合理的相互依赖?另外,可以在脑海中将业务流程映射到模型中快速浏览一遍,看看模型是否能满足业务的需要,并且思考在业务变化时模型是否可以方便地扩展。当然我们还是要强调一点,设计往往要为现实妥协,通常我们会综合考虑性能、复杂度、成本等因素,并找到一个可接受的设计平衡点。

(2)领域模型转变为微服务

分析出领域模型后就可以根据它来设计服务、系统架构、接口等,这些都是典型的软件设计方面的内容,读者可以基于自己的习惯使用合适的方法进行设计,比如敏捷方法。对于微服务架构而言,还需要在设计上注意以下两点。

● 服务依赖:合理的微服务架构,其依赖关系应该很清晰。我们首先要注意的就是,不能出现两个服务相互依赖的情况,也就是通常所说的循环引用。一个合理的依赖拓扑结构应该是树或者森林,或者有向无环图。服务提供者不需要知道是谁调用了它,即上游服务不需要知道下游服务,这样才能保证系统是局部依赖的,而不是全局依赖的。全局依赖的应用不满足微服务架构要求,而是所谓的分布式单体应用。

● 服务交互:拆分后的微服务不能独立实现业务功能,必然要彼此交互。在交互方式的选择上要注意使用场景,一般情况下有下面几种交互方式。

● RPC远程调用:RPC是笔者首推的服务间交互方式,通信效率相对更高,且易于集成。RPC会产生一些额外的依赖,需要根据使用的接口描述语言(IDL)生成对应的服务端和客户端。我们的微服务应用使用的就是gRPC调用方式。

● HTTP/JSON调用:通过定义RESTful API以HTTP方式进行通信,数据为JSON格式。它最大的优势是轻量,服务间的耦合也极低,只需要知道接口就可以调用,但在传输性能上通常比RPC稍逊一筹。

● 异步消息调用:即“发布-订阅”交互方式。这种方式非常适合用在基于事件驱动设计的系统中。服务生产者发布一条消息,监听的一方消费这条消息并完成自己的业务逻辑。消息交互可以消除命令式调用带来的依赖,让整个系统更有弹性。

服务交互的方式并不必须只选择一种,针对不同场景使用不同的方式更加合理。读者可根据自身情况自由选择。上述两个步骤的示意图如图2-13所示。

图2-13

领域驱动设计是一种比较复杂的设计方法,想要解释得很清楚可能需要写一本书才行。在这里我们只做简单引导,具有领域驱动设计经验的团队可以尝试使用它去完成服务拆分,最终的拆分结果和依据业务能力进行拆分的结果应该是一致的。

4.拆分原则

拆分的服务需要足够小,以便让小团队进行开发和测试。但服务的范围到底多小才合适呢?这常常是一个经验化的结论,特别是“微”这个形容词在一定程度上会误导人。我们可以借鉴一些面向对象的设计原则,这些原则对于定义服务依然适用。

第一个就是单一职责原则(SRP)。在面向对象设计中,单一职责原则要求改变一个类时一般只有一个理由:定义的类的职责应该单一,只负责一件事情。定义服务其实也如此,就是要保证服务聚焦于单一的某个业务,做到高度内聚,这样做的好处是可以提升服务的稳定性。

另外一个在服务拆分中可以参考的原则是闭包原则(CCP)。它本来的含义是,对包做出的修改应该都在包之内。这意味着如果某项更新导致了多个不同的类都要修改,那么这些类应该被放在同一个包内。在出现变更时,我们只需要对一个包进行修改即可。在微服务架构下同样需要将因为同一个原因被改变的部分放在同一个服务中,减少依赖,让变更和部署更容易。对于某个业务规则的变更,应该尽可能修改更少的服务,理想情况下最好只修改一个服务。

除了上面提到的两个原则,服务拆分之后,我们可以再依据下面的规则检查服务设计是否合理。

● 服务必须是内聚的,服务内部应该实现强相关的功能。

● 服务应该遵守公共闭包原则,同时更改的内容应该放在一起,以确保每次更改只影响一个服务。

● 服务应该松散耦合,每个服务都封装完好,对外暴露提供业务能力的API,可以在不影响外部访问的情况下进行内部修改。

● 服务应该是可测试的。

● 每个服务都要足够小,可以由小团队开发完成。

● 服务的开发团队应该是自治的,即能够以最少的协作或依赖开发和部署自己的服务。

5.服务拆分的难点

软件设计中没有完美的方案,更多的时候是对现实的妥协。服务拆分也如此,有些时候没办法保证拆分完美,比如可能会遇到以下常见的难点和问题。

● 过多的跨进程通信:服务拆分会导致网络间问题。第一个是跨进程通信导致了网络延迟,在传输的数据量比较大的时候会更加明显。另外,如果业务调用链路比较长,比如要跨好几个服务才能完成一次业务流程,这势必降低应用的可用性。介入的节点越多,出现问题的概率就越大。为了应对这一问题,一方面需要在应用中引入分布式追踪这样的能力,以便在出现问题时可以快速追踪到根源;另一方面,服务治理也应该成为架构中不可或缺的能力,使用服务网格以透明的方式引入治理能力是一个不错的选择,我们在第4章中会具体介绍。

● 分布式事务:将单体应用改造成微服务架构所要面对的一个数据层面的痛点就是分布式事务。传统的解决方案是使用两阶段提交这样的机制,但对于并发和流量比较大的Web应用来说,这种方案并不合适。互联网业界中比较常用的办法是使用补偿方案,优先考虑应用的性能,保证数据的最终一致性。在第4章中,我们会介绍我们团队基于Saga理论实现的一个分布式事务解决方案。除此之外,在业务流程上通过一些顺序调整消除分布式事务也是一种思路。

● 上帝类难以被拆分:在面向对象编程领域,上帝类是指具有过多责任的类,应用程序的很多功能都被编写到一个单一的“了解全部”的对象中,这个对象维护了大部分信息并且提供了操作数据的大部分方法。因此这个对象持有过多的数据,承担了过多的责任,它的角色如同上帝一般。其他对象都依赖于上帝类并获得信息。由于被过多引用且持有多种不同领域的数据,拆分上帝类尤其困难。领域驱动设计为拆分上帝类提供了一个比较好的方法,那就是为各自的领域模型实现上帝类的不同版本,这些模型只涵盖自己领域内的数据和职责。