限界上下文的通信边界会对系统的架构产生直接的影响,在此之前,我们需要理清几个和边界有关的概念。如前所述,我提出了限界上下文的通信边界的概念,并将其分为进程内通信与进程间通信两种方式。在 Toby Clemson 给出的微服务架构中,则将逻辑边界视为整个微服务的边界,而将微服务代码模型中的所有模块视为在同一个网络边界内。但我认为在引入了虚拟化以及容器技术后,仍将这种边界描述为网络边界似乎并不准确,因此我以进程边界来表示前面提到的通信边界。
显然,倘若限界上下文之间采用进程间通信,则每个限界上下文就可以认为是一个微服务——对于微服务,我更愿意用进程边界来界定代码模型的部署与运行。
无论是网络边界,还是进程边界,都可以视为物理边界;而代码模型中对于层以及模块的划分,则属于逻辑边界的范畴。逻辑边界有时候会和物理边界重合,但这仅仅是针对代码模型而言。一个系统多数情况下都会访问其物理边界之外的外部资源,如此看来,一个系统的逻辑边界往往要大于物理边界。
在进行架构设计时,我们往往会将整个系统的架构划分为多个不同的视图,其中最主要的视图就是逻辑视图和物理视图,这是我们看待系统的两种不同视角。前者关注代码结构、层次以及职责的分配,后者关注部署、运行以及资源的分配,这两种视图都需要考虑限界上下文以及它们之间的协作关系。在考虑逻辑视图时,我们会为限界上下文履行的职责所吸引,同时又需得关注它们之间的协作,此时,就该物理视图粉墨登场了。若两个限界上下文的代码模型属于同一个物理边界,就是部署和运行在同一个进程中的好哥俩儿,调用方式变得直接,协作关系较为简单,我们只需要在实现时尽可能维护好逻辑边界即可。如果限界上下文代码模型的逻辑边界与物理边界完全重叠,要考虑的架构要素就变得复杂了。
对于跨进程边界进行协作的限界上下文,我建议为其绘制上下文映射,并通过六边形架构来确定二者之间的通信端口与通信协议。上游限界上下文公开的接口可以是基于 HTTP 的 REST 服务,也可以通过 RPC 访问远程对象,又或者利用消息中间件传递消息。选择的通信协议不同,传递的消息格式以及序列化机制也不同,为下游限界上下文建立的客户端也不相同。由于这种协作关系其实是一种分布式调用,自然存在分布式系统与身俱来的缺陷,例如,网络总是不可靠,维护数据一致性要受到 CAP 原则的约束。这时,就需要考虑服务调用的熔断来及时应对故障,避免因单一故障点带来整个微服务架构的连锁反应。我们还需要权衡数据一致性问题,若不要求严格的数据一致性,则可以引入最终一致性(BASE),如采用可靠事件模式、补偿模式或者 TCC(Try-Confirm-Cancel)模式等。当然我们还需要考虑安全、部署和运维等诸多与分布式系统有关的问题,这些问题已经超出了本课程讨论的范围,这里就略过不提了。
如前所述,倘若我们将单个限界上下文代码模型的边界视为物理边界,则可以认为一个限界上下文就是一个微服务。而在前面介绍六边形架构时,我也提到该架构模式外部的六边形边界实则也是物理边界。基于这些前提,我们得出结论:
显然,在将限界上下文的代码模型边界视为物理边界时,限界上下文、六边形与微服务之间就成了“三位一体”的关系。我们可以将三者的设计原则与思想结合起来,如下图所示:
该图清晰地表达了这种“三位一体”的关系。
我们试以电商系统的购物流程来说明这种“三位一体”的关系。首先,我们通过领域场景分析的用例图来分析该购物流程:
通过对各个用例的语义相关性与功能相关性,结合这些用例的业务能力,可以确定用例的边界。当我们为这些边界进行命名时,就初步获得了如下六个限界上下文:
结合购买流程,电商系统还需要用到第三方物流系统对商品进行配送,这个物流系统可以认为是电商系统的外部系统(External Service)。如果这六个限界上下文之间采用跨进程通信,实际上就是六个微服务,它们应该单独部署在不同节点之上。现在,我们需要站在微服务的角度对其进行思考。需要考虑的内容包括如下。
现在我们可以将六边形架构与限界上下文结合起来,即通过端口确定限界上下文之间的协作关系,绘制上下文映射。如果采用客户方—供应商开发模式,则各个限界上下文六边形的端口就是上游(Upstream,简称 U)或下游(Downstream,简称 D)。由于这些限界上下文都是独立部署的微服务,因此,它们的上游端口应实现为 OHS 模式(下图以绿色端口表示),下游端口应实现为 ACL 模式(下图以蓝色端口表示):
每个微服务都是一个独立的应用,我们可以针对每个微服务规划自己的分层架构,进而确定微服务内的领域建模方式。微服务的协作也有三种机制,分别为命令、查询和事件。Ben Stopford 在文章 Build Services on a Backbone of Events 中总结了这三种机制,具体如下。
发出命令或查询请求的为下游服务,而服务的定义则处于上游。如上图所示,我以菱形端口代表“命令”,矩形端口代表“查询”,这样就能直观地通过上下文映射以及六边形的端口清晰地表达微服务的服务定义以及服务之间的协作方式。例如,Product Context 同时作为 Basket Context 与 Order Context 的上游限界上下文,其查询端口提供的是商品查询服务。Basket Context 作为 Order Context 的上游限界上下文,其命令端口提供了清除购物篮的命令服务。
如果微服务的协作采用事件机制,则上下文映射的模式为发布/订阅事件模式。这时,限界上下文之间的关系有所不同,我们需要识别在这个流程中发生的关键事件。传递关键事件的就是六边形的端口,具体实现为消息队列,适配器则负责发布事件。于是,系统的整体架构就演变为以事件驱动架构(Event-Driven Architecture,EDA)风格构建的微服务系统。Vaughn Vernon 在《实现领域驱动设计》一书中使用六边形架构形象地展现了这一架构风格。
六边形之间传递的三角形就是导致限界上下文切换的关键事件,在领域驱动设计中,作为领域事件(Domain Event)被定义在领域层。为了与限界上下文内部传递的领域事件区分开,我们可以名其为“关键领域事件”,又或者称为“应用事件”,它仍然属于领域模型中的一部分。在前面所示的上下文映射中,我们可以用三角形端口来代表“事件”,事件端口所在的限界上下文为发布者,该事件对应的下游端口则为订阅者。然而,当我们采用“事件”的协作机制时,上下文映射中的上下游语义却发生了变化,原来作为“命令”或“查询”提供者的上游,却成为了“事件”机制下处于下游的订阅者。以购物篮为例,“清除购物篮”命令服务被定义在 Basket Context 中。当提交订单成功后,Order Context 就会发起对该服务的调用。倘若将“提交订单”视为一个内部命令(Command),在订单被提交成功后,就会触发 OrderConfirmed 事件,此时,Order Context 反而成为了该事件的发布者,Basket Context 则会订阅该事件,一旦侦听到该事件触发,就会在 Basket Context 内部执行“清除购物篮”命令。显然,“清除购物篮”不再作为服务发布,而是在事件的 handler 中作为内部功能被调用。
采用“事件”协作机制会改变我们习惯的顺序式服务调用形式,整个调用链会随着事件的发布而产生跳转,尤其是暴露在六边形端口的“关键事件”,更是会产生跨六边形(即限界上下文)的协作。仍以电商系统的购买流程为例,我们只考虑正常流程。在 Basket Context 中,一旦购物篮中的商品准备就绪,买家就会请求下订单,此时开始了事件流。
整个协作过程如下图所示(图中的序号对应事件流的编号):
与订单流程相关的事件包括:
我们注意到这些事件皆以“过去时态”命名,这是因为事件的本质是“事实(Fact)”,意味着它是过去发生的且不可变更的数据,代表了某种动作的发生,并以事件的形式留下了足迹。
正如前面给出的事件驱动架构所示,事件的发布者负责触发输出事件(Outgoing Event),事件的订阅者负责处理输入事件(Incoming Event),它们作为六边形的事件适配器,也就是我所说的网关,被定义在基础设施层。事件适配器的抽象则被定义在应用层。假设电商系统选择 Kafka 作为事件传递的通道,我们就可以为不同的事件类别定义不同的主题(Topic)。此时,Kafka 相当于是连接微服务之间进行协作的事件总线(Event Bus)。Ben Stopford 将采用这种机制实现的微服务称为“事件驱动服务(Event Driven Services)”。
通过电商系统的这个案例,清晰地为我们勾勒出限界上下文、六边形与微服务“三位一体”的设计脉络,即它们的设计思想、设计原则与设计方法是互相促进互相融合的。在架构设计层面上,三者可谓浑然一体。
© 2019 - 2023 Liangliang Lee. Powered by gin and hexo-theme-book.