事件风暴-识别服务边界的另一种技术
事件风暴是识别系统中的聚合(以及微服务)的另一种必不可少的技术。这对于破坏整体结构以及设计复杂的微服务生态系统都是有用的工具。我们已经使用这种技术分解了一个复杂的应用程序,并且打算在单独的博客中介绍Event Storming的经验。对于此博客的范围,我们想给出一个快速的高级概述。如果您有兴趣进一步探索,请观看Alberto Brandelloni的视频。
简而言之,事件风暴是在应用程序团队(在我们的情况下为整体)中进行的头脑风暴,以识别系统中发生的各种领域事件和流程。团队还确定这些事件影响及其后续影响的汇总或模型。在团队进行此练习时,他们会确定不同的重叠概念,模棱两可的领域语言以及相互冲突的业务流程。他们将相关模型分组,重新定义聚合并确定重复的过程。随着这些工作的进行,这些集合所属的有限上下文变得清晰起来。如果所有团队都在同一个房间(物理或虚拟)中,并开始在Scrum风格的白板上绘制事件,命令和过程的映射,那么Event Storming研讨会将非常有用。在本练习结束时,
- 重新定义的聚合列表。这些可能成为新的微服务
- 这些微服务之间需要流动的域事件
- 直接从其他应用程序或用户调用的命令
我们在一场Event Storming研讨会结束时展示了一个示例板。对于团队来说,这是一次很棒的协作活动,以就正确的聚合和有限的上下文达成一致。除了进行出色的团队建设活动外,团队在本次会议中脱颖而出,对领域,通用语言和精确的服务边界有着共同的理解。
微服务之间的通信
一个整体在一个流程边界内托管了多个聚合体。因此,在此边界内可以管理聚合体的事务一致性,例如,如果客户下订单,我们可以减少项目的库存,并向客户发送电子邮件,全部都在一次交易事务中。所有操作都会成功,或者全部都会失败。但是,当我们打破整体并将聚合散布到不同的环境中时,我们将拥有数十甚至数百个微服务。迄今为止,在整体结构的单个边界内存在的流程现在分布在多个分布式系统中。要在所有这些分布式系统上实现事务的完整性和一致性非常困难,而且要付出代价-系统的可用性。
微服务也是分布式系统。因此,CAP定理也适用于它们- “分布式系统只能提供三个所需特征中的两个:一致性,可用性和分区容限(CAP中的“ C”,“ A”和“ P”)。” 在现实世界的系统中,分区容忍度是无法商议的- 网络不可靠,虚拟机可能宕机,区域之间的延迟会变得更糟,等等。
因此,我们可以选择“可用性”或“一致性”。现在,我们知道在任何现代应用程序中,牺牲可用性也不是一个好主意。
围绕最终一致性设计应用程序
如果您尝试跨多个分布式系统构建事务,那么您将再次陷入困境。变成最糟糕的一种分布式整体事务。如果任何一个系统点不可用,则整个流程将不可用,通常会导致令人沮丧的客户体验、失去保障失败的承诺等。
此外,对一项服务的更改通常可能需要对另一项服务进行更改,从而导致复杂而昂贵的部署。因此,我们最好根据自己的用例来设计应用程序,以容忍一点点的不一致性,以提高可用性。对于上面的示例,我们可以使所有进程异步,从而最终保持一致。我们可以异步发送电子邮件,而与其他流程无关。
如果承诺的物品以后在仓库中不可用,该项目可能被延期订购,或者我们可以停止接受超过某个阈值的项目的订单。
有时,您可能会遇到一个场景,该场景可能需要跨越不同流程边界的两个聚合中的强ACID样式事务。这是重新查看这些聚合并将它们合并为一个极好的标志。在我们开始在不同过程边界中分解这些聚合之前,事件风暴和上下文映射将有助于及早识别这些依赖性。将两个微服务合并为一个成本很高,这是我们应该努力避免的事情。
支持事件驱动的架构
微服务可能会在其聚合上发出本质上的变化。这些称为领域事件,并且对这些更改感兴趣的任何服务都可以侦听这些事件并在其域内采取相应的操作。这种方法避免了任何行为上的耦合:一个域不规定其他域应该做什么,以及时间耦合-一个过程的成功完成不依赖于同时可用的所有系统。当然,这将意味着系统最终将保持一致。
在上面的示例中,订单服务发布了一个事件-订单已取消。订阅该事件的其他服务处理其各自的域功能:付款服务退还款项,库存服务调整项目的库存,依此类推。要确保此集成的可靠性和弹性的几点注意事项:
- 生产者应确保至少生产一次事件。如果这样做失败,则应确保存在回退机制以重新触发事件
- 消费者应确保以幂等的方式消费事件。如果再次发生同一事件,那么在用户端不应有任何副作用。事件也可能不按顺序到达。消费者可以使用时间戳记或版本号字段来保证事件的唯一性。
由于某些用例的性质,不一定总是可以使用基于事件的集成。请查看购物车服务和付款服务之间的集成。这是一个同步集成,因此我们需要注意一些事项。这是行为耦合的一个示例-Cart服务可能从Payment服务中调用REST API,并指示其授权订单付款,而时间耦合则需要Payment服务用于Cart服务才能接受订单。这种耦合减少了这些上下文的自治性,并且可能减少了不希望的依赖性。有几种方法可以避免这种耦合,但是使用所有这些选项,我们将失去向客户提供即时反馈的能力。
- 将REST API转换为基于事件的集成。但是,如果支付服务仅公开REST API,则此选项可能不可用
- 购物车服务立即接受订单,并且有一个批处理作业来接管订单并调用支付服务API
- 购物车服务会产生一个本地事件,然后调用付款服务API
在失败和上游依赖项(付款服务)不可用的情况下,将上述内容与重试结合使用可以使设计更具弹性。例如,在发生故障的情况下,可以通过事件或基于批次的重试来备份购物车和付款服务之间的同步集成。这种方法会对客户体验产生额外的影响:客户可能输入了不正确的付款明细,并且当我们离线处理付款时,我们不会将其在线。否则,收回失败的付款可能会增加业务成本。但是,很有可能,购物车服务对于支付服务的不可用性或故障具有弹性,其缺点胜于缺点。例如,如果我们无法离线收款,我们可以通知客户。
避免针对特定消费者数据需求的服务之间的编排
任何面向服务的体系结构中的反模式之一是,这些服务都可以满足消费者的特定访问模式。通常,这发生在消费者团队与服务团队紧密合作时。如果团队正在开发整体应用程序,则他们通常会创建一个跨不同聚合边界的单一API,从而紧密耦合这些聚合。
让我们考虑一个例子。说Web中的“订单详细信息”页面,移动应用程序需要在单个页面上同时显示订单的详细信息和针对该订单处理的退款的详细信息。在整体应用程序中,Order GET API(假设它是REST API)一起查询Orders和Refunds,合并两个聚合,然后将复合响应发送给调用方。由于聚合属于相同的过程边界,因此无需太多开销即可执行此操作。因此,消费者可以在一个调用中获得所有必要的数据。
如果订单和退款是不同上下文的一部分,则数据不再存在于单个微服务或聚合边界内。为消费者保留相同功能的一种选择是使订单服务负责调用退款服务并创建复合响应。此方法引起以下问题:
- 订单服务现在与另一个服务集成在一起,纯粹是为了支持需要退款数据和订单数据的消费者。现在,订单服务的自治性降低了,因为退款总额中的任何更改都将导致订单总额中的更改。
- 订单服务具有另一个集成,因此要考虑另一个故障点-如果退款服务出现故障,订购服务是否仍可以发送部分数据,并且消费者可以正常地故障吗?
- 如果消费者需要更改以从“退款”聚合中获取更多数据,则现在需要两个团队来进行更改
- 如果在整个平台上都遵循这种模式,则可能导致各种域服务之间的依存关系错综复杂,所有这些都是因为这些服务满足了调用者的特定访问模式。
- 前端的后端BFF
减轻这种风险的一种方法是让消费者团队管理各种域服务之间的编排。毕竟,呼叫者会更好地了解访问模式,并且可以完全控制对这些模式的任何更改。这种方法将域服务与表示层分离开来,使它们专注于核心业务流程。但是,如果Web和移动应用程序开始直接调用不同的服务而不是从整体中调用一个复合API,则可能会导致这些应用程序的性能开销–通过较低带宽网络进行多次调用,处理和合并来自不同API的数据,等等。 。
相反,可以使用另一种称为前端的后端的模式。在这种设计模式下,由消费者(在本例中为Web和移动团队)创建和管理的后端服务负责跨多个域服务的集成,纯粹是为了向客户提供前端体验。Web和移动团队现在可以根据他们的用例设计数据合同。他们甚至可以使用GraphQL而不是REST API来灵活地查询并准确获取所需的信息。
重要的是要注意,此服务是由使用者团队拥有和维护的,而不是由拥有域服务的团队拥有和维护的。前端团队现在可以根据自己的需求进行优化-移动应用程序可以请求更小的有效负载,减少来自移动应用程序的呼叫次数等等。查看下面的业务流程的修订视图。
结论
在此博客中,我们触及了各种概念,策略和设计启发法,以便在我们进入微服务领域时,尤其是在尝试将整体式服务拆分为基于域的微服务时,加以考虑。其中许多主题本身就是广阔的主题,我认为我们没有做出足够的公正性来详细解释它们,但是我们想介绍一些关键主题以及我们采用这些主题的经验。进一步阅读(链接)部分提供了一些参考资料和一些有用的内容,供任何希望采用此方法的人使用。
原文:https://medium.com/walmartglobaltech/building-domain-driven-microservices-af688aa1b1b8
译文:jdon.com/54558